From 5fedceb38321b9c349fd9cd7f5efe765063d84b2 Mon Sep 17 00:00:00 2001 From: CanadianBaconBoi Date: Wed, 18 Feb 2026 10:06:51 +0100 Subject: [PATCH] Use derive macro to create TableRow/PartialTableRow --- .idea/cove-chat.iml | 1 + Cargo.lock | 14 +- Cargo.toml | 14 +- bin-test/Cargo.toml | 4 +- cove-db/Cargo.toml | 1 + cove-db/cove-db-macros/Cargo.toml | 12 ++ cove-db/cove-db-macros/src/lib.rs | 257 ++++++++++++++++++++++++++++++ cove-db/src/query/text.rs | 2 +- cove-db/src/rows/attachment.rs | 71 +-------- cove-db/src/rows/channel.rs | 126 +-------------- cove-db/src/rows/guild.rs | 160 +------------------ cove-db/src/rows/guild_member.rs | 100 +----------- cove-db/src/rows/message.rs | 126 +-------------- cove-db/src/rows/mod.rs | 6 + cove-db/src/rows/nonce.rs | 72 +-------- cove-db/src/rows/user.rs | 182 +-------------------- cove-net/common/Cargo.toml | 2 +- 17 files changed, 328 insertions(+), 822 deletions(-) create mode 100644 cove-db/cove-db-macros/Cargo.toml create mode 100644 cove-db/cove-db-macros/src/lib.rs diff --git a/.idea/cove-chat.iml b/.idea/cove-chat.iml index 838d228..c59cf18 100644 --- a/.idea/cove-chat.iml +++ b/.idea/cove-chat.iml @@ -9,6 +9,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index fd142e5..d579358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,11 +191,21 @@ name = "cove-db" version = "0.1.0" dependencies = [ "anyhow", + "cove-db-macros", "cove-net-common", "serde_json", "sqlx", ] +[[package]] +name = "cove-db-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cove-net-client" version = "0.1.0" @@ -1863,9 +1873,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 16a3319..da846b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,10 @@ resolver = "3" members = [ "cove-net/common", "cove-net/client", - "cove-net/server" -, "bin-test", "cove-db"] + "cove-net/server", + "bin-test", + "cove-db", + "cove-db/cove-db-macros"] [workspace.dependencies] @@ -13,6 +15,7 @@ async-trait = "0.1.89" anyhow = "1.0.101" hyper = { version = "1", features = ["full"] } http-body-util = { version = "0.1.3", features = ["full"] } +rand = "0.10.0" cove-net-common = {path = "cove-net/common"} serde_json = "1.0.149" @@ -25,4 +28,9 @@ tokio = { version = "1", features = ["full"] } hyper-util = { version = "0.1", features = ["full"] } cove-db = {path = "cove-db"} -sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid", "json", "derive" ]} \ No newline at end of file +sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid", "json", "derive" ]} + +cove-db-macros = {path = "cove-db/cove-db-macros" } +syn = {version = "2.0.116", features = ["full"]} +quote = {version = "1.0.44"} +proc-macro2 = "1.0.106" \ No newline at end of file diff --git a/bin-test/Cargo.toml b/bin-test/Cargo.toml index 9bb4d34..1c4c886 100644 --- a/bin-test/Cargo.toml +++ b/bin-test/Cargo.toml @@ -9,5 +9,5 @@ cove-net-common.workspace = true tokio.workspace = true cove-db.workspace = true sqlx.workspace = true -anyhow = "1.0.101" -scc = "3.5.6" \ No newline at end of file +anyhow.workspace = true +scc.workspace = true \ No newline at end of file diff --git a/cove-db/Cargo.toml b/cove-db/Cargo.toml index 5415769..3a36aee 100644 --- a/cove-db/Cargo.toml +++ b/cove-db/Cargo.toml @@ -7,5 +7,6 @@ edition = "2024" sqlx.workspace = true anyhow.workspace = true serde_json.workspace = true +cove-db-macros.workspace = true cove-net-common.workspace = true \ No newline at end of file diff --git a/cove-db/cove-db-macros/Cargo.toml b/cove-db/cove-db-macros/Cargo.toml new file mode 100644 index 0000000..a9a94f5 --- /dev/null +++ b/cove-db/cove-db-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cove-db-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn.workspace = true +quote.workspace = true +proc-macro2.workspace = true \ No newline at end of file diff --git a/cove-db/cove-db-macros/src/lib.rs b/cove-db/cove-db-macros/src/lib.rs new file mode 100644 index 0000000..4166e02 --- /dev/null +++ b/cove-db/cove-db-macros/src/lib.rs @@ -0,0 +1,257 @@ +use proc_macro::{TokenStream}; +use quote::{format_ident, quote}; +use syn::{parse_str, Data, DeriveInput, Fields}; + +#[proc_macro_derive(DeriveTableRow, attributes(table_name))] +pub fn derive_table_row(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + impl_derive_table_row(ast) +} + +fn impl_derive_table_row(ast: DeriveInput) -> TokenStream { + let table_name: String = { + if ast.attrs.len() != 1 { + return quote!{compile_error!("The DeriveTableRow macro expects exactly one (1) table_name attribute")}.into(); + } + let attr = ast.attrs.first().unwrap(); + if attr.path().is_ident("table_name") { + if let Ok(args) = attr.parse_args::() { + args.value() + } else { + return quote!{compile_error!("The DeriveTableRow macro's attribute table_name should only consist of a string")}.into(); + } + } else { + return quote!{compile_error!("Incorrect attribute name for table_name attribute")}.into(); + } + }; + + let name = ast.ident; + let partial_name = format_ident!("Partial{}", name); + + let data = ast.data; + + let fields = match data { + Data::Struct(data) => { + match data.fields { + Fields::Named(named_fields) => { + named_fields + }, + _ => return quote!{compile_error!("The DeriveTableRow macro can only apply to structs solely consisting of named fields")}.into() + } + }, + _ => return quote!{compile_error!("The DeriveTableRow macro can only be applied to structs.")}.into() + }; + + + + let struct_partial_row = { + let struct_partial_row_body = { + let mut ret = quote!(); + for field in fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + let ty = &field.ty; + ret.extend(quote! { + pub #name: Option<#ty>, + }); + } + ret + }; + quote! { + #[derive(Default)] + pub struct #partial_name { + #struct_partial_row_body + } + + + } + }; + + let impl_table_row = quote! { + impl crate::rows::TableRow for #name { + type Error = anyhow::Error; + type PartialRow = #partial_name; + + fn get_table_name() -> &'static str { + #table_name + } + } + + + }; + + let impl_partial_table_row = quote! { + impl crate::rows::PartialTableRow for #partial_name { + type Error = anyhow::Error; + type FullTableRow = #name; + } + + + }; + + let impl_insertable_row = { + let insert_body = { + let mut ret = quote!( + crate::part::insert::SqlInsert::with_table(::get_table_name()) + ); + + for field in fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + let quoted_name = format!("{}", name); + let ty = &field.ty; + ret.extend( + quote! { + .col::<#ty>(#quoted_name) + } + ) + } + + ret + }; + + let bind_body = { + let mut ret = quote! { + query + }; + + let mut idx = 0; + let max_idx = fields.named.len(); + for field in fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + ret.extend(quote! { + .bind(&self.#name) + }); + if idx < max_idx-1 { + ret.extend(parse_str::("?")); + } + idx += 1; + } + + ret + }; + + quote! { + impl crate::rows::InsertableRow for #name { + fn insert(&'_ self) -> crate::part::insert::SqlInsert { + #insert_body + } + + fn bind<'a>(&'a self, query: crate::part::BindQuery<'a>) -> Result, Self::Error> { + #bind_body + } + } + + + } + }; + + let impl_selectable_row = quote! { + impl crate::rows::SelectableRow for #partial_name {} + }; + + let impl_where_row = { + let match_body = { + let mut ret = quote!(); + for field in fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + let quoted_name = format!("{}", name); + ret.extend( + quote! { + #quoted_name => query.bind(self.#name.as_ref().unwrap())?, + } + ) + } + ret + }; + + quote! { + impl crate::rows::WhereRow for #partial_name { + fn bind<'a>(&'a self, wheres: crate::part::sql_where::SqlWhere, query: crate::part::BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + #match_body + _ => return Err(anyhow::anyhow!("No column {col} exists for table users")) + } + } + Ok(query) + } + } + + + } + }; + + let impl_try_from_pgrow_for_full = { + let try_from_body = { + let mut ret = quote!(); + + for field in fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + let quoted_name = format!("{}", name); + ret.extend( + quote! { + #name: row.try_get(#quoted_name)?, + } + ) + } + + ret + }; + + quote! { + impl TryFrom for #name { + type Error = anyhow::Error; + + fn try_from(row: sqlx::postgres::PgRow) -> Result { + use sqlx::Row; + Ok(Self { + #try_from_body + }) + } + } + + + } + }; + + let impl_from_pgrow_for_partial = { + let from_body = { + let mut ret = quote!(); + for field in fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + let quoted_name = format!("{}", name); + ret.extend( + quote! { + #name: row.try_get(#quoted_name).ok(), + } + ) + } + ret + }; + + quote! { + impl From for #partial_name { + fn from(row: sqlx::postgres::PgRow) -> Self { + use sqlx::Row; + Self { + #from_body + } + } + } + + + } + }; + + let mut ret = quote!(); + ret.extend(struct_partial_row); + ret.extend(impl_table_row); + ret.extend(impl_partial_table_row); + ret.extend(impl_insertable_row); + ret.extend(impl_selectable_row); + ret.extend(impl_where_row); + ret.extend(impl_try_from_pgrow_for_full); + ret.extend(impl_from_pgrow_for_partial); + + ret.into() +} \ No newline at end of file diff --git a/cove-db/src/query/text.rs b/cove-db/src/query/text.rs index e821bef..ae6eb84 100644 --- a/cove-db/src/query/text.rs +++ b/cove-db/src/query/text.rs @@ -1,5 +1,5 @@ use anyhow::{Error}; -use sqlx::{Execute, Executor}; +use sqlx::{Execute}; use sqlx::postgres::{ PgQueryResult}; use cove_net_common::id::message_type::MessageType; use cove_net_common::id::SnowflakeID; diff --git a/cove-db/src/rows/attachment.rs b/cove-db/src/rows/attachment.rs index 13558a2..040c1bf 100644 --- a/cove-db/src/rows/attachment.rs +++ b/cove-db/src/rows/attachment.rs @@ -1,77 +1,12 @@ -use anyhow::anyhow; -use sqlx::postgres::PgRow; -use sqlx::Row; +use cove_db_macros::DeriveTableRow; use cove_net_common::id::SnowflakeID; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::select::SqlSelect; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; +#[derive(DeriveTableRow)] +#[table_name("attachments")] pub struct AttachmentRow { pub id: SnowflakeID, pub uploader_id: SnowflakeID, pub channel_id: Option, pub file_size: i32, pub cdn_url: String -} - -impl TableRow for AttachmentRow { - type Error = anyhow::Error; - - fn get_table_name() -> &'static str { - "attachments" - } -} - -impl InsertableRow for AttachmentRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("id") - .col::("uploader_id") - .opt_col::("channel_id") - .col::("file_size") - .col::("cdn_url") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.id)? - .bind(&self.uploader_id)? - .bind(&self.channel_id)? - .bind(&self.file_size)? - .bind(&self.cdn_url) - } -} - -impl SelectableRow for AttachmentRow {} - -impl WhereRow for AttachmentRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "id" => query.bind(&self.id)?, - "uploader_id" => query.bind(&self.uploader_id)?, - "channel_id" => query.bind(&self.channel_id)?, - "file_size" => query.bind(&self.file_size)?, - "cdn_url" => query.bind(&self.cdn_url)?, - _ => return Err(anyhow!("No column {col} exists for table attachments")) - } - } - Ok(query) - } -} - -impl TryFrom for AttachmentRow { - type Error = anyhow::Error; - - fn try_from(value: PgRow) -> Result { - Ok(Self { - id: value.try_get("id")?, - uploader_id: value.try_get("uploader_id")?, - channel_id: value.try_get("channel_id")?, - file_size: value.try_get("file_size")?, - cdn_url: value.try_get("cdn_url")?, - }) - } } \ No newline at end of file diff --git a/cove-db/src/rows/channel.rs b/cove-db/src/rows/channel.rs index ad14308..4a6c6bb 100644 --- a/cove-db/src/rows/channel.rs +++ b/cove-db/src/rows/channel.rs @@ -1,15 +1,11 @@ -use anyhow::anyhow; use serde_json::Value; -use sqlx::postgres::PgRow; -use sqlx::Row; use sqlx::types::time::OffsetDateTime; +use cove_db_macros::DeriveTableRow; use cove_net_common::id::SnowflakeID; use cove_net_common::id::types::channel::ChannelMessageType; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; +#[derive(DeriveTableRow)] +#[table_name("channels")] pub struct ChannelRow { pub id: SnowflakeID, pub guild_id: Option, @@ -37,120 +33,4 @@ pub struct ChannelRow { pub user_limit: i32, pub region: Option -} - -impl TableRow for ChannelRow { - type Error = anyhow::Error; - - fn get_table_name() -> &'static str { - "channels" - } -} - -impl InsertableRow for ChannelRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("id") - .opt_col::("guild_id") - .opt_col::("parent_id") - .col::("name") - .col::("channel_type") - .col::("position") - .col::("permission_overwrites") - .col::("rate_limit_per_user") - .col::("nsfw") - .col::("loud") - .opt_col::("thread_metadata") - .col::("member_count") - .col::("message_count") - .opt_col::("thread_owner_id") - .col::("created_at") - .col::("updated_at") - .opt_col::("topic") - .col::("user_limit") - .opt_col::("region") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.id)? - .bind(&self.guild_id)? - .bind(&self.parent_id)? - .bind(&self.name)? - .bind(&self.channel_type)? - .bind(&self.position)? - .bind(&self.permission_overwrites)? - .bind(&self.rate_limit_per_user)? - .bind(&self.nsfw)? - .bind(&self.loud)? - .bind(&self.thread_metadata)? - .bind(&self.member_count)? - .bind(&self.message_count)? - .bind(&self.thread_owner_id)? - .bind(&self.created_at)? - .bind(&self.updated_at)? - .bind(&self.topic)? - .bind(&self.user_limit)? - .bind(&self.region) - } -} - -impl SelectableRow for ChannelRow {} - -impl WhereRow for ChannelRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "id" => query.bind(&self.id)?, - "guild_id" => query.bind(&self.guild_id)?, - "parent_id" => query.bind(&self.parent_id)?, - "name" => query.bind(&self.name)?, - "channel_type" => query.bind(&self.channel_type)?, - "position" => query.bind(&self.position)?, - "permission_overwrites" => query.bind(&self.permission_overwrites)?, - "rate_limit_per_user" => query.bind(&self.rate_limit_per_user)?, - "nsfw" => query.bind(&self.nsfw)?, - "loud" => query.bind(&self.loud)?, - "thread_metadata" => query.bind(&self.thread_metadata)?, - "member_count" => query.bind(&self.member_count)?, - "message_count" => query.bind(&self.message_count)?, - "thread_owner_id" => query.bind(&self.thread_owner_id)?, - "created_at" => query.bind(&self.created_at)?, - "updated_at" => query.bind(&self.updated_at)?, - "topic" => query.bind(&self.topic)?, - "user_limit" => query.bind(&self.user_limit)?, - "region" => query.bind(&self.region)?, - _ => return Err(anyhow!("No column {col} exists for table attachments")) - } - } - Ok(query) - } -} - -impl TryFrom for ChannelRow { - type Error = anyhow::Error; - - fn try_from(value: PgRow) -> Result { - Ok(Self{ - id: value.try_get("id")?, - guild_id: value.try_get("guild_id")?, - parent_id: value.try_get("parent_id")?, - name: value.try_get("name")?, - channel_type: value.try_get("channel_type")?, - position: value.try_get("position")?, - permission_overwrites: value.try_get("permission_overwrites")?, - rate_limit_per_user: value.try_get("rate_limit_per_user")?, - nsfw: value.try_get("nsfw")?, - loud: value.try_get("loud")?, - thread_metadata: value.try_get("thread_metadata")?, - member_count: value.try_get("member_count")?, - message_count: value.try_get("message_count")?, - thread_owner_id: value.try_get("thread_owner_id")?, - created_at: value.try_get("created_at")?, - updated_at: value.try_get("updated_at")?, - topic: value.try_get("topic")?, - user_limit: value.try_get("user_limit")?, - region: value.try_get("region")?, - }) - } } \ No newline at end of file diff --git a/cove-db/src/rows/guild.rs b/cove-db/src/rows/guild.rs index e147dcf..f318933 100644 --- a/cove-db/src/rows/guild.rs +++ b/cove-db/src/rows/guild.rs @@ -1,17 +1,13 @@ -use anyhow::anyhow; -use sqlx::postgres::PgRow; -use sqlx::Row; use sqlx::types::time::OffsetDateTime; +use cove_db_macros::DeriveTableRow; use cove_net_common::guild::component::default_notification::DefaultMessageNotificationSetting; use cove_net_common::guild::component::explicit_filter::ExplicitContentFilterSetting; use cove_net_common::guild::component::verification_level::VerificationLevelSetting; use cove_net_common::id::SnowflakeID; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; -struct GuildRow { +#[derive(DeriveTableRow)] +#[table_name("guilds")] +pub struct GuildRow { pub id: SnowflakeID, pub name: String, @@ -49,152 +45,4 @@ struct GuildRow { pub updated_at: OffsetDateTime, pub large: bool, pub member_count: i32 -} - -impl TableRow for GuildRow { - type Error = anyhow::Error; - - fn get_table_name() -> &'static str { - "guilds" - } -} - -impl InsertableRow for GuildRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("id") - .col::("name") - .opt_col::("description") - .opt_col::("icon") - .opt_col::("banner") - .opt_col::("splash") - .col::("owner_id") - .opt_col::<[u8; 8]>("owner_permissions") - .col::("region") - .col::("features") - .opt_col::("afk_channel_id") - .col::("afk_timeout") - .col::("verification_level") - .col::("default_message_notifications") - .col::("explicit_content_filter") - .opt_col::("system_channel_id") - .opt_col::("system_channel_flags") - .arr_col::("premium_boosters") - .col::("premium_tier") - .col::("premium_booster_count") - .col::("widget_enabled") - .opt_col::("widget_channel_id") - .col::("preferred_locale") - .col::("created_at") - .col::("updated_at") - .col::("large") - .col::("member_count") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.id)? - .bind(&self.name)? - .bind(&self.description)? - .bind(&self.icon)? - .bind(&self.banner)? - .bind(&self.splash)? - .bind(&self.owner_id)? - .bind(&self.owner_permissions)? - .bind(&self.region)? - .bind(&self.features)? - .bind(&self.afk_channel_id)? - .bind(&self.afk_timeout)? - .bind(&self.verification_level)? - .bind(&self.default_message_notifications)? - .bind(&self.explicit_content_filter)? - .bind(&self.system_channel_id)? - .bind(&self.system_channel_flags)? - .bind(&self.premium_boosters)? - .bind(&self.premium_tier)? - .bind(&self.premium_booster_count)? - .bind(&self.widget_enabled)? - .bind(&self.widget_channel_id)? - .bind(&self.preferred_locale)? - .bind(&self.created_at)? - .bind(&self.updated_at)? - .bind(&self.large)? - .bind(&self.member_count) - } -} - -impl SelectableRow for GuildRow {} - -impl WhereRow for GuildRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "id" => query.bind(&self.id)?, - "name" => query.bind(&self.name)?, - "description" => query.bind(&self.description)?, - "icon" => query.bind(&self.icon)?, - "banner" => query.bind(&self.banner)?, - "splash" => query.bind(&self.splash)?, - "owner_id" => query.bind(&self.owner_id)?, - "owner_permissions" => query.bind(&self.owner_permissions)?, - "region" => query.bind(&self.region)?, - "features" => query.bind(&self.features)?, - "afk_channel_id" => query.bind(&self.afk_channel_id)?, - "afk_timeout" => query.bind(&self.afk_timeout)?, - "verification_level" => query.bind(&self.verification_level)?, - "default_message_notifications" => query.bind(&self.default_message_notifications)?, - "explicit_content_filter" => query.bind(&self.explicit_content_filter)?, - "system_channel_id" => query.bind(&self.system_channel_id)?, - "system_channel_flags" => query.bind(&self.system_channel_flags)?, - "premium_boosters" => query.bind(&self.premium_boosters)?, - "premium_tier" => query.bind(&self.premium_tier)?, - "premium_booster_count" => query.bind(&self.premium_booster_count)?, - "widget_enabled" => query.bind(&self.widget_enabled)?, - "widget_channel_id" => query.bind(&self.widget_channel_id)?, - "preferred_locale" => query.bind(&self.preferred_locale)?, - "created_at" => query.bind(&self.created_at)?, - "updated_at" => query.bind(&self.updated_at)?, - "large" => query.bind(&self.large)?, - "member_count" => query.bind(&self.member_count)?, - _ => return Err(anyhow!("No column {col} exists for table attachments")) - } - } - Ok(query) - } -} - -impl TryFrom for GuildRow { - type Error = anyhow::Error; - - fn try_from(value: PgRow) -> Result { - Ok(Self { - id: value.try_get("id")?, - name: value.try_get("name")?, - description: value.try_get("description")?, - icon: value.try_get("icon")?, - banner: value.try_get("banner")?, - splash: value.try_get("splash")?, - owner_id: value.try_get("owner_id")?, - owner_permissions: value.try_get("owner_permissions")?, - region: value.try_get("region")?, - features: value.try_get("features")?, - afk_channel_id: value.try_get("afk_channel_id")?, - afk_timeout: value.try_get("afk_timeout")?, - verification_level: value.try_get("verification_level")?, - default_message_notifications: value.try_get("default_message_notifications")?, - explicit_content_filter: value.try_get("explicit_content_filter")?, - system_channel_id: value.try_get("system_channel_id")?, - system_channel_flags: value.try_get("system_channel_flags")?, - premium_boosters: value.try_get("premium_boosters")?, - premium_tier: value.try_get("premium_tier")?, - premium_booster_count: value.try_get("premium_booster_count")?, - widget_enabled: value.try_get("widget_enabled")?, - widget_channel_id: value.try_get("widget_channel_id")?, - preferred_locale: value.try_get("preferred_locale")?, - created_at: value.try_get("created_at")?, - updated_at: value.try_get("updated_at")?, - large: value.try_get("large")?, - member_count: value.try_get("member_count")?, - }) - } } \ No newline at end of file diff --git a/cove-db/src/rows/guild_member.rs b/cove-db/src/rows/guild_member.rs index c5d7333..a9b7fcd 100644 --- a/cove-db/src/rows/guild_member.rs +++ b/cove-db/src/rows/guild_member.rs @@ -1,14 +1,10 @@ -use anyhow::anyhow; -use sqlx::postgres::PgRow; -use sqlx::Row; use sqlx::types::time::OffsetDateTime; +use cove_db_macros::DeriveTableRow; use cove_net_common::id::SnowflakeID; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; -struct GuildMemberRow { +#[derive(DeriveTableRow)] +#[table_name("guild_members")] +pub struct GuildMemberRow { pub guild_id: SnowflakeID, pub user_id: SnowflakeID, @@ -26,92 +22,4 @@ struct GuildMemberRow { pub timed_out_until: Option, pub updated_at: OffsetDateTime, -} - -impl TableRow for GuildMemberRow { - type Error = anyhow::Error; - - fn get_table_name() -> &'static str { - "guild_members" - } -} - -impl InsertableRow for GuildMemberRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("guild_id") - .col::("user_id") - .opt_col::("nick") - .col::("joined_at") - .arr_col::("roles") - .opt_col::("boosting_since") - .opt_col::("voice_channel_id") - .col::("deafened") - .col::("muted") - .col::("pending") - .opt_col::("timed_out_until") - .col::("updated_at") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.guild_id)? - .bind(&self.user_id)? - .bind(&self.nick)? - .bind(&self.joined_at)? - .bind(&self.roles)? - .bind(&self.boosting_since)? - .bind(&self.voice_channel_id)? - .bind(&self.deafened)? - .bind(&self.muted)? - .bind(&self.pending)? - .bind(&self.timed_out_until)? - .bind(&self.updated_at) - } -} - -impl SelectableRow for GuildMemberRow {} - -impl WhereRow for GuildMemberRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "guild_id" => query.bind(&self.guild_id)?, - "user_id" => query.bind(&self.user_id)?, - "nick" => query.bind(&self.nick)?, - "joined_at" => query.bind(&self.joined_at)?, - "roles" => query.bind(&self.roles)?, - "boosting_since" => query.bind(&self.boosting_since)?, - "voice_channel_id" => query.bind(&self.voice_channel_id)?, - "deafened" => query.bind(&self.deafened)?, - "muted" => query.bind(&self.muted)?, - "pending" => query.bind(&self.pending)?, - "timed_out_until" => query.bind(&self.timed_out_until)?, - "updated_at" => query.bind(&self.updated_at)?, - _ => return Err(anyhow!("No column {col} exists for table attachments")) - } - } - Ok(query) - } -} - -impl TryFrom for GuildMemberRow { - type Error = anyhow::Error; - - fn try_from(value: PgRow) -> Result { - Ok(Self { - guild_id: value.try_get("guild_id")?, - user_id: value.try_get("user_id")?, - nick: value.try_get("nick")?, - joined_at: value.try_get("joined_at")?, - roles: value.try_get("roles")?, - boosting_since: value.try_get("boosting_since")?, - voice_channel_id: value.try_get("voice_channel_id")?, - deafened: value.try_get("deafened")?, - muted: value.try_get("muted")?, - pending: value.try_get("pending")?, - timed_out_until: value.try_get("timed_out_until")?, - updated_at: value.try_get("updated_at")?, - }) - } } \ No newline at end of file diff --git a/cove-db/src/rows/message.rs b/cove-db/src/rows/message.rs index a6be473..7cc7003 100644 --- a/cove-db/src/rows/message.rs +++ b/cove-db/src/rows/message.rs @@ -1,15 +1,11 @@ -use anyhow::anyhow; -use sqlx::postgres::PgRow; -use sqlx::Row; use sqlx::types::time::OffsetDateTime; +use cove_db_macros::DeriveTableRow; use cove_net_common::id::SnowflakeID; use cove_net_common::id::types::text::TextMessageType; use cove_net_common::message::c2s::text::text::TextEmbed; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; +#[derive(DeriveTableRow)] +#[table_name("messages")] pub struct MessageRow { pub id: SnowflakeID, pub channel_id: SnowflakeID, @@ -36,120 +32,4 @@ pub struct MessageRow { pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, -} - -impl TableRow for MessageRow { - type Error = anyhow::Error; - - fn get_table_name() -> &'static str { - "messages" - } -} - -impl InsertableRow for MessageRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("id") - .col::("channel_id") - .opt_col::("guild_id") - .col::("author_id") - .col::("content") - .col::("timestamp") - .opt_col::("edited_timestamp") - .col::("tts") - .opt_arr_col::("mentions") - .col::("mention_everyone") - .opt_arr_col::("embeds") - .opt_arr_col::("attachments") - .opt_col::("reply_message_id") - .opt_col::("application_id") - .col::("message_type") - .opt_col::("thread_name") - .opt_col::("auto_archive_duration") - .col::("created_at") - .col::("updated_at") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.id)? - .bind(&self.channel_id)? - .bind(&self.guild_id)? - .bind(&self.author_id)? - .bind(&self.content)? - .bind(&self.timestamp)? - .bind(&self.edited_timestamp)? - .bind(&self.tts)? - .bind(&self.mentions)? - .bind(&self.mention_everyone)? - .bind(&self.embeds)? - .bind(&self.attachments)? - .bind(&self.reply_message_id)? - .bind(&self.application_id)? - .bind(&self.message_type)? - .bind(&self.thread_name)? - .bind(&self.auto_archive_duration)? - .bind(&self.created_at)? - .bind(&self.updated_at) - } -} - -impl SelectableRow for MessageRow {} - -impl WhereRow for MessageRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "id" => query.bind(&self.id)?, - "channel_id" => query.bind(&self.channel_id)?, - "guild_id" => query.bind(&self.guild_id)?, - "author_id" => query.bind(&self.author_id)?, - "content" => query.bind(&self.content)?, - "timestamp" => query.bind(&self.timestamp)?, - "edited_timestamp" => query.bind(&self.edited_timestamp)?, - "tts" => query.bind(&self.tts)?, - "mentions" => query.bind(&self.mentions)?, - "mention_everyone" => query.bind(&self.mention_everyone)?, - "embeds" => query.bind(&self.embeds)?, - "attachments" => query.bind(&self.attachments)?, - "reply_message_id" => query.bind(&self.reply_message_id)?, - "application_id" => query.bind(&self.application_id)?, - "message_type" => query.bind(&self.message_type)?, - "thread_name" => query.bind(&self.thread_name)?, - "auto_archive_duration" => query.bind(&self.auto_archive_duration)?, - "created_at" => query.bind(&self.created_at)?, - "updated_at" => query.bind(&self.updated_at)?, - _ => return Err(anyhow!("No column {col} exists for table attachments")) - } - } - Ok(query) - } -} - -impl TryFrom for MessageRow { - type Error = anyhow::Error; - - fn try_from(value: PgRow) -> Result { - Ok(Self { - id: value.try_get("id")?, - channel_id: value.try_get("channel_id")?, - guild_id: value.try_get("guild_id")?, - author_id: value.try_get("author_id")?, - content: value.try_get("content")?, - timestamp: value.try_get("timestamp")?, - edited_timestamp: value.try_get("edited_timestamp")?, - tts: value.try_get("tts")?, - mentions: value.try_get("mentions")?, - mention_everyone: value.try_get("mention_everyone")?, - embeds: value.try_get("embeds")?, - attachments: value.try_get("attachments")?, - reply_message_id: value.try_get("reply_message_id")?, - application_id: value.try_get("application_id")?, - message_type: value.try_get("message_type")?, - thread_name: value.try_get("thread_name")?, - auto_archive_duration: value.try_get("auto_archive_duration")?, - created_at: value.try_get("created_at")?, - updated_at: value.try_get("updated_at")?, - }) - } } \ No newline at end of file diff --git a/cove-db/src/rows/mod.rs b/cove-db/src/rows/mod.rs index 5366f3c..22b97fb 100644 --- a/cove-db/src/rows/mod.rs +++ b/cove-db/src/rows/mod.rs @@ -3,6 +3,12 @@ use crate::part::insert::SqlInsert; use crate::part::select::SqlSelect; use crate::part::sql_where::SqlWhere; +pub mod attachment; +pub mod channel; +pub mod guild; +pub mod guild_member; +pub mod message; +pub mod nonce; pub mod user; pub trait TableRow { diff --git a/cove-db/src/rows/nonce.rs b/cove-db/src/rows/nonce.rs index 56d4bfc..137d6fe 100644 --- a/cove-db/src/rows/nonce.rs +++ b/cove-db/src/rows/nonce.rs @@ -1,77 +1,13 @@ -use anyhow::anyhow; -use sqlx::postgres::PgRow; -use sqlx::Row; use sqlx::types::time::OffsetDateTime; +use cove_db_macros::DeriveTableRow; use cove_net_common::id::SnowflakeID; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; -struct NonceRow { +#[derive(DeriveTableRow)] +#[table_name("pending_nonces")] +pub struct NonceRow { pub nonce: String, pub channel_id: SnowflakeID, pub author_id: SnowflakeID, pub created_at: OffsetDateTime, pub expires_at: OffsetDateTime, -} - -impl TableRow for NonceRow { - type Error = anyhow::Error; - - fn get_table_name() -> &'static str { - "pending_nonces" - } -} - -impl InsertableRow for NonceRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("nonce") - .col::("channel_id") - .col::("author_id") - .col::("created_at") - .col::("expires_at") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.nonce)? - .bind(&self.channel_id)? - .bind(&self.author_id)? - .bind(&self.created_at)? - .bind(&self.expires_at) - } -} - -impl SelectableRow for NonceRow {} - -impl WhereRow for NonceRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "nonce" => query.bind(&self.nonce)?, - "channel_id" => query.bind(&self.channel_id)?, - "author_id" => query.bind(&self.author_id)?, - "created_at" => query.bind(&self.created_at)?, - "expires_at" => query.bind(&self.expires_at)?, - _ => return Err(anyhow!("No column {col} exists for table attachments")) - } - } - Ok(query) - } -} - -impl TryFrom for NonceRow { - type Error = anyhow::Error; - - fn try_from(value: PgRow) -> Result { - Ok(Self { - nonce: value.try_get("nonce")?, - channel_id: value.try_get("channel_id")?, - author_id: value.try_get("author_id")?, - created_at: value.try_get("created_at")?, - expires_at: value.try_get("expires_at")?, - }) - } } \ No newline at end of file diff --git a/cove-db/src/rows/user.rs b/cove-db/src/rows/user.rs index 6c15bae..e925bef 100644 --- a/cove-db/src/rows/user.rs +++ b/cove-db/src/rows/user.rs @@ -1,15 +1,11 @@ -use anyhow::anyhow; use serde_json::Value; -use sqlx::postgres::PgRow; -use sqlx::Row; use sqlx::types::time::OffsetDateTime; +use cove_db_macros::DeriveTableRow; use cove_net_common::id::SnowflakeID; -use crate::part::BindQuery; -use crate::part::insert::SqlInsert; -use crate::part::sql_where::SqlWhere; -use crate::rows::{InsertableRow, PartialTableRow, SelectableRow, TableRow, WhereRow}; use crate::types::user_status::UserStatus; +#[derive(DeriveTableRow)] +#[table_name("users")] pub struct UserRow { pub id: SnowflakeID, pub username: String, @@ -30,176 +26,4 @@ pub struct UserRow { pub preferences: Value, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, -} - -#[derive(Default)] -pub struct PartialUserRow { - pub id: Option, - pub username: Option, - pub discriminator: Option, - pub avatar_hash: Option>, - pub email: Option, - pub email_verified: Option, - pub password_hash: Option, - pub mfa_enabled: Option, - pub mfa_secret: Option>, - pub status: Option, - pub public_flags: Option, - pub locale: Option, - pub premium_since: Option>, - pub premium_end: Option>, - pub bot: Option, - pub bot_oauth_scopes: Option, - pub preferences: Option, - pub created_at: Option, - pub updated_at: Option, -} - -impl TableRow for UserRow { - type Error = anyhow::Error; - type PartialRow = PartialUserRow; - - fn get_table_name() -> &'static str { - "users" - } -} - -impl PartialTableRow for PartialUserRow { - type Error = anyhow::Error; - type FullTableRow = UserRow; - -} - -impl InsertableRow for UserRow { - fn insert(&'_ self) -> SqlInsert { - SqlInsert::with_table(Self::get_table_name()) - .col::("id") - .col::("username") - .col::("discriminator") - .opt_col::("avatar_hash") - .col::("email") - .col::("email_verified") - .col::("password_hash") - .col::("mfa_enabled") - .opt_col::("mfa_secret") - .col::("status") - .col::("public_flags") - .col::("locale") - .opt_col::("premium_since") - .opt_col::("premium_end") - .col::("bot") - .col::("bot_oauth_scopes") - .col::("preferences") - .col::("created_at") - .col::("updated_at") - } - - fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { - query.bind(&self.id)? - .bind(&self.username)? - .bind(&self.discriminator)? - .bind(&self.avatar_hash)? - .bind(&self.email)? - .bind(&self.email_verified)? - .bind(&self.password_hash)? - .bind(&self.mfa_enabled)? - .bind(&self.mfa_secret)? - .bind(&self.status)? - .bind(&self.public_flags)? - .bind(&self.locale)? - .bind(&self.premium_since)? - .bind(&self.premium_end)? - .bind(&self.bot)? - .bind(&self.bot_oauth_scopes)? - .bind(&self.preferences)? - .bind(&self.created_at)? - .bind(&self.updated_at) - } -} - -impl SelectableRow for PartialUserRow {} - -impl WhereRow for PartialUserRow { - fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { - let mut query = query; - for (_, col) in wheres.indexed_placeholders { - query = match col.as_str() { - "id" => query.bind(self.id.as_ref().unwrap())?, - "username" => query.bind(self.username.as_ref().unwrap())?, - "discriminator" => query.bind(self.discriminator.as_ref().unwrap())?, - "avatar_hash" => query.bind(self.avatar_hash.as_ref().unwrap())?, - "email" => query.bind(self.email.as_ref().unwrap())?, - "email_verified" => query.bind(self.email_verified.as_ref().unwrap())?, - "password_hash" => query.bind(self.password_hash.as_ref().unwrap())?, - "mfa_enabled" => query.bind(self.mfa_enabled.as_ref().unwrap())?, - "mfa_secret" => query.bind(self.mfa_secret.as_ref().unwrap())?, - "status" => query.bind(self.status.as_ref().unwrap())?, - "public_flags" => query.bind(self.public_flags.as_ref().unwrap())?, - "locale" => query.bind(self.locale.as_ref().unwrap())?, - "premium_since" => query.bind(self.premium_since.as_ref().unwrap())?, - "premium_end" => query.bind(self.premium_end.as_ref().unwrap())?, - "bot" => query.bind(self.bot.as_ref().unwrap())?, - "bot_oauth_scopes" => query.bind(self.bot_oauth_scopes.as_ref().unwrap())?, - "preferences" => query.bind(self.preferences.as_ref().unwrap())?, - "created_at" => query.bind(self.created_at.as_ref().unwrap())?, - "updated_at" => query.bind(self.updated_at.as_ref().unwrap())?, - _ => return Err(anyhow!("No column {col} exists for table users")) - } - } - Ok(query) - } -} - -impl TryFrom for UserRow { - type Error = anyhow::Error; - - fn try_from(row: PgRow) -> Result { - Ok(Self { - id: row.try_get("id")?, - username: row.try_get("username")?, - discriminator: row.try_get("discriminator")?, - avatar_hash: row.try_get("avatar_hash")?, - email: row.try_get("email")?, - email_verified: row.try_get("email_verified")?, - password_hash: row.try_get("password_hash")?, - mfa_enabled: row.try_get("mfa_enabled")?, - mfa_secret: row.try_get("mfa_secret")?, - status: row.try_get("status")?, - public_flags: row.try_get("public_flags")?, - locale: row.try_get("locale")?, - premium_since: row.try_get("premium_since")?, - premium_end: row.try_get("premium_end")?, - bot: row.try_get("bot")?, - bot_oauth_scopes: row.try_get("bot_oauth_scopes")?, - preferences: row.try_get("preferences")?, - created_at: row.try_get("created_at")?, - updated_at: row.try_get("updated_at")?, - }) - } -} - -impl From for PartialUserRow { - fn from(row: PgRow) -> Self { - Self { - id: row.try_get("id").ok(), - username: row.try_get("username").ok(), - discriminator: row.try_get("discriminator").ok(), - avatar_hash: row.try_get("avatar_hash").ok(), - email: row.try_get("email").ok(), - email_verified: row.try_get("email_verified").ok(), - password_hash: row.try_get("password_hash").ok(), - mfa_enabled: row.try_get("mfa_enabled").ok(), - mfa_secret: row.try_get("mfa_secret").ok(), - status: row.try_get("status").ok(), - public_flags: row.try_get("public_flags").ok(), - locale: row.try_get("locale").ok(), - premium_since: row.try_get("premium_since").ok(), - premium_end: row.try_get("premium_end").ok(), - bot: row.try_get("bot").ok(), - bot_oauth_scopes: row.try_get("bot_oauth_scopes").ok(), - preferences: row.try_get("preferences").ok(), - created_at: row.try_get("created_at").ok(), - updated_at: row.try_get("updated_at").ok(), - } - } } \ No newline at end of file diff --git a/cove-net/common/Cargo.toml b/cove-net/common/Cargo.toml index 3ed980b..af15d4d 100644 --- a/cove-net/common/Cargo.toml +++ b/cove-net/common/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] anyhow.workspace = true hex.workspace = true -rand = "0.10.0" +rand.workspace = true serde.workspace = true serde_with.workspace = true serde_json.workspace = true