feat(models): basic rust impl

This commit is contained in:
alyssa 2024-12-28 21:08:26 +00:00
parent 0862964305
commit 3e194d7c8a
7 changed files with 479 additions and 2 deletions

View file

@ -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"

241
lib/model_macros/src/lib.rs Normal file
View file

@ -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<Expr>,
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::<Vec<ModelField>>()
} 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<ModelField> = 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<ModelField>) -> 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<ModelField>) -> TokenStream {
quote! { unimplemented!(); }
}
fn mk_tfrom_sql(fields: Vec<ModelField>) -> TokenStream {
quote! { unimplemented!(); }
}
fn mk_tto_json(fields: Vec<ModelField>) -> 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<ModelField>) -> 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<ModelField>) -> TokenStream {
quote! { true }
}
fn mk_patch_from_json(fields: Vec<ModelField>) -> TokenStream {
quote! { unimplemented!(); }
}
fn mk_patch_to_sql(fields: Vec<ModelField>) -> TokenStream {
quote! { unimplemented!(); }
}
fn mk_patch_to_json(fields: Vec<ModelField>) -> TokenStream {
quote! { unimplemented!(); }
}

13
lib/models/Cargo.toml Normal file
View file

@ -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 }

2
lib/models/src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
mod system;
pub use system::*;

103
lib/models/src/system.rs Normal file
View file

@ -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<Postgres> for PrivacyLevel {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("INT4")
}
}
impl From<PrivacyLevel> for i32 {
fn from(enum_value: PrivacyLevel) -> Self {
enum_value as i32
}
}
impl From<i32> 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: <DB as Database>::ValueRef<'r>,
) -> Result<Self, Box<dyn Error + 'static + Send + Sync>> {
let value = <i32 as Decode<DB>>::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<String>,
#[json = "description"]
description: Option<String>,
#[json = "tag"]
tag: Option<String>,
#[json = "pronouns"]
pronouns: Option<String>,
#[json = "avatar_url"]
avatar_url: Option<String>,
#[json = "banner_image"]
banner_image: Option<String>,
#[json = "color"]
color: Option<String>,
token: Option<String>,
#[json = "webhook_url"]
webhook_url: Option<String>,
webhook_token: Option<String>,
#[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,
}