From 3e194d7c8a20e384efe3679d7a4884e1f611f461 Mon Sep 17 00:00:00 2001 From: alyssa Date: Sat, 28 Dec 2024 21:08:26 +0000 Subject: [PATCH] feat(models): basic rust impl --- Cargo.lock | 107 +++++++++++++++- Cargo.toml | 2 + lib/model_macros/Cargo.toml | 13 ++ lib/model_macros/src/lib.rs | 241 ++++++++++++++++++++++++++++++++++++ lib/models/Cargo.toml | 13 ++ lib/models/src/lib.rs | 2 + lib/models/src/system.rs | 103 +++++++++++++++ 7 files changed, 479 insertions(+), 2 deletions(-) create mode 100644 lib/model_macros/Cargo.toml create mode 100644 lib/model_macros/src/lib.rs create mode 100644 lib/models/Cargo.toml create mode 100644 lib/models/src/lib.rs create mode 100644 lib/models/src/system.rs diff --git a/Cargo.lock b/Cargo.lock index 23e0fcab..faf1c5f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -623,6 +624,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.66", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -890,9 +925,9 @@ dependencies = [ [[package]] name = "fred" -version = "9.3.0" +version = "9.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ac76d6e24da83723c1d118a1d3b794d883eec94715eeaa611698558d5547048" +checksum = "3cdd5378252ea124b712e0ac55147d26ae3af575883b34b8423091a4c719606b" dependencies = [ "arc-swap", "async-trait", @@ -1493,6 +1528,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -1539,6 +1580,17 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "ipnet" version = "2.7.1" @@ -1860,6 +1912,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "model_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -2188,6 +2249,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "pluralkit_models" +version = "0.1.0" +dependencies = [ + "chrono", + "model_macros", + "sea-query", + "serde", + "serde_json", + "sqlx", + "uuid", +] + [[package]] name = "png" version = "0.17.14" @@ -2860,6 +2934,30 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sea-query" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085e94f7d7271c0393ac2d164a39994b1dff1b06bc40cd9a0da04f3d672b0fee" +dependencies = [ + "inherent", + "sea-query-derive", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9834af2c4bd8c5162f00c89f1701fb6886119a88062cf76fe842ea9e232b9839" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.66", + "thiserror", +] + [[package]] name = "security-framework" version = "2.11.0" @@ -3034,6 +3132,7 @@ version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -3285,6 +3384,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -3367,6 +3467,7 @@ dependencies = [ "bitflags 2.5.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -3410,6 +3511,7 @@ dependencies = [ "base64 0.22.1", "bitflags 2.5.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -3447,6 +3549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index f598c431..f164d00a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ members = [ "./services/gateway", "./services/avatars", "./services/scheduled_tasks", + + "./lib/models", ] [workspace.dependencies] diff --git a/lib/model_macros/Cargo.toml b/lib/model_macros/Cargo.toml new file mode 100644 index 00000000..d2d0e009 --- /dev/null +++ b/lib/model_macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "model_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0" +proc-macro2 = "1.0" +syn = "2.0" + diff --git a/lib/model_macros/src/lib.rs b/lib/model_macros/src/lib.rs new file mode 100644 index 00000000..fe5f5193 --- /dev/null +++ b/lib/model_macros/src/lib.rs @@ -0,0 +1,241 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Expr, Ident, Meta, Type}; + +#[derive(Clone, Debug)] +enum ElemPatchability { + None, + Private, + Public, +} + +#[derive(Clone, Debug)] +struct ModelField { + name: Ident, + ty: Type, + patch: ElemPatchability, + json: Option, + is_privacy: bool, +} + +fn parse_field(field: syn::Field) -> ModelField { + let mut f = ModelField { + name: field.ident.expect("field missing ident"), + ty: field.ty, + patch: ElemPatchability::None, + json: None, + is_privacy: false, + }; + + for attr in field.attrs.iter() { + match &attr.meta { + Meta::Path(path) => { + let ident = path.segments[0].ident.to_string(); + match ident.as_str() { + "private_patchable" => match f.patch { + ElemPatchability::None => { + f.patch = ElemPatchability::Private; + } + _ => { + panic!("cannot have multiple patch tags on same field"); + } + }, + "patchable" => match f.patch { + ElemPatchability::None => { + f.patch = ElemPatchability::Public; + } + _ => { + panic!("cannot have multiple patch tags on same field"); + } + }, + "privacy" => f.is_privacy = true, + _ => panic!("unknown attribute"), + } + } + Meta::NameValue(nv) => match nv.path.segments[0].ident.to_string().as_str() { + "json" => { + if f.json.is_some() { + panic!("cannot set json multiple times for same field"); + } + f.json = Some(nv.value.clone()); + } + _ => panic!("unknown attribute"), + }, + Meta::List(_) => panic!("unknown attribute"), + } + } + + if matches!(f.patch, ElemPatchability::Public) && f.json.is_none() { + panic!("must have json name to be publicly patchable"); + } + + f +} + +#[proc_macro_attribute] +pub fn pk_model( + _args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let model_type = match ast.data { + syn::Data::Struct(struct_data) => struct_data, + _ => panic!("pk_model can only be used on a struct"), + }; + + let tname = Ident::new(&format!("PK{}", ast.ident), Span::call_site()); + let patchable_name = Ident::new(&format!("PK{}Patch", ast.ident), Span::call_site()); + + let fields = if let syn::Fields::Named(fields) = model_type.fields { + fields + .named + .iter() + .map(|f| parse_field(f.clone())) + .collect::>() + } else { + panic!("fields of a struct must be named"); + }; + + println!("{}: {:#?}", tname, fields); + + let tfields = mk_tfields(fields.clone()); + let from_json = mk_tfrom_json(fields.clone()); + let from_sql = mk_tfrom_sql(fields.clone()); + let to_json = mk_tto_json(fields.clone()); + + let fields: Vec = fields + .iter() + .filter(|f| !matches!(f.patch, ElemPatchability::None)) + .cloned() + .collect(); + + let patch_fields = mk_patch_fields(fields.clone()); + let patch_from_json = mk_patch_from_json(fields.clone()); + let patch_validate = mk_patch_validate(fields.clone()); + let patch_to_json = mk_patch_to_json(fields.clone()); + let patch_to_sql = mk_patch_to_sql(fields.clone()); + + return quote! { + #[derive(sqlx::FromRow, Debug, Clone)] + pub struct #tname { + #tfields + } + + impl #tname { + pub fn from_json(input: String) -> Self { + #from_json + } + + pub fn to_json(self) -> String { + #to_json + } + } + + #[derive(Debug, Clone)] + pub struct #patchable_name { + #patch_fields + } + + impl #patchable_name { + pub fn from_json(input: String) -> Self { + #patch_from_json + } + + pub fn validate(self) -> bool { + #patch_validate + } + + pub fn to_sql(self) -> sea_query::UpdateStatement { + // sea_query::Query::update() + #patch_to_sql + } + + pub fn to_json(self) -> String { + #patch_to_json + } + } + } + .into(); +} + +fn mk_tfields(fields: Vec) -> TokenStream { + fields + .iter() + .map(|f| { + let name = f.name.clone(); + let ty = f.ty.clone(); + quote! { + #name: #ty, + } + }) + .collect() +} +fn mk_tfrom_json(fields: Vec) -> TokenStream { + quote! { unimplemented!(); } +} +fn mk_tfrom_sql(fields: Vec) -> TokenStream { + quote! { unimplemented!(); } +} +fn mk_tto_json(fields: Vec) -> TokenStream { + // todo: check privacy access + let fielddefs: TokenStream = fields + .iter() + .filter_map(|f| { + f.json.as_ref().map(|v| { + let tname = f.name.clone(); + quote! { + #v: self.#tname, + } + }) + }) + .collect(); + + let privacyfielddefs: TokenStream = fields + .iter() + .filter_map(|f| { + if f.is_privacy { + let tname = f.name.clone(); + let tnamestr = f.name.clone().to_string(); + Some(quote! { + #tnamestr: self.#tname, + }) + } else { + None + } + }) + .collect(); + + quote! { + serde_json::to_string(&serde_json::json!({ + #fielddefs + "privacy": { + #privacyfielddefs + } + })).expect("json serializing generated models should not fail") + } +} + +fn mk_patch_fields(fields: Vec) -> TokenStream { + fields + .iter() + .map(|f| { + let name = f.name.clone(); + let ty = f.ty.clone(); + quote! { + #name: Option<#ty>, + } + }) + .collect() +} +fn mk_patch_validate(_fields: Vec) -> TokenStream { + quote! { true } +} +fn mk_patch_from_json(fields: Vec) -> TokenStream { + quote! { unimplemented!(); } +} +fn mk_patch_to_sql(fields: Vec) -> TokenStream { + quote! { unimplemented!(); } +} +fn mk_patch_to_json(fields: Vec) -> TokenStream { + quote! { unimplemented!(); } +} diff --git a/lib/models/Cargo.toml b/lib/models/Cargo.toml new file mode 100644 index 00000000..2e40d30c --- /dev/null +++ b/lib/models/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pluralkit_models" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { workspace = true, features = ["serde"] } +model_macros = { path = "../model_macros" } +sea-query = "0.32.1" +serde = { workspace = true } +serde_json = { workspace = true, features = ["preserve_order"] } +sqlx = { workspace = true, default-features = false, features = ["chrono"] } +uuid = { workspace = true } diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs new file mode 100644 index 00000000..8ca1df21 --- /dev/null +++ b/lib/models/src/lib.rs @@ -0,0 +1,2 @@ +mod system; +pub use system::*; diff --git a/lib/models/src/system.rs b/lib/models/src/system.rs new file mode 100644 index 00000000..56b25070 --- /dev/null +++ b/lib/models/src/system.rs @@ -0,0 +1,103 @@ +use std::error::Error; + +use model_macros::pk_model; + +use chrono::NaiveDateTime; +use sqlx::{postgres::PgTypeInfo, Database, Decode, Postgres, Type}; +use uuid::Uuid; + +// todo: fix this +pub type SystemId = i32; + +// // todo: move this +#[derive(serde::Serialize, Debug, Clone)] +pub enum PrivacyLevel { + #[serde(rename = "public")] + Public = 1, + #[serde(rename = "private")] + Private = 2, +} + +impl Type for PrivacyLevel { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("INT4") + } +} + +impl From for i32 { + fn from(enum_value: PrivacyLevel) -> Self { + enum_value as i32 + } +} + +impl From for PrivacyLevel { + fn from(value: i32) -> Self { + match value { + 1 => PrivacyLevel::Public, + 2 => PrivacyLevel::Private, + _ => unimplemented!(), + } + } +} + +struct MyType; + +impl<'r, DB: Database> Decode<'r, DB> for PrivacyLevel +where + i32: Decode<'r, DB>, +{ + fn decode( + value: ::ValueRef<'r>, + ) -> Result> { + let value = >::decode(value)?; + Ok(Self::from(value)) + } +} + +#[pk_model] +struct System { + id: SystemId, + #[json = "id"] + #[private_patchable] + hid: String, + #[json = "uuid"] + uuid: Uuid, + #[json = "name"] + name: Option, + #[json = "description"] + description: Option, + #[json = "tag"] + tag: Option, + #[json = "pronouns"] + pronouns: Option, + #[json = "avatar_url"] + avatar_url: Option, + #[json = "banner_image"] + banner_image: Option, + #[json = "color"] + color: Option, + token: Option, + #[json = "webhook_url"] + webhook_url: Option, + webhook_token: Option, + #[json = "created"] + created: NaiveDateTime, + #[privacy] + name_privacy: PrivacyLevel, + #[privacy] + avatar_privacy: PrivacyLevel, + #[privacy] + description_privacy: PrivacyLevel, + #[privacy] + banner_privacy: PrivacyLevel, + #[privacy] + member_list_privacy: PrivacyLevel, + #[privacy] + front_privacy: PrivacyLevel, + #[privacy] + front_history_privacy: PrivacyLevel, + #[privacy] + group_list_privacy: PrivacyLevel, + #[privacy] + pronoun_privacy: PrivacyLevel, +}