From 72c97102832843708ae1ad01e3979ccc9f980593 Mon Sep 17 00:00:00 2001 From: fluo10 Date: Fri, 27 Jun 2025 08:42:45 +0900 Subject: [PATCH] Add test for derive and new trait method about author id --- Cargo.toml | 2 + lazy-supplements-core/Cargo.toml | 12 ++-- .../src/data/entity/record_deletion.rs | 7 +- lazy-supplements-core/src/data/syncable.rs | 33 ++++++--- lazy-supplements-macros/Cargo.toml | 6 ++ lazy-supplements-macros/src/lib.rs | 72 ++++++++++--------- .../tests/derive_syncable.rs | 29 ++++++++ 7 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 lazy-supplements-macros/tests/derive_syncable.rs diff --git a/Cargo.toml b/Cargo.toml index ec43140..da87ed2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,13 @@ license = "MIT OR Apache-2.0" repository = "https://forgejo.fireturlte.net/lazy-supplements" [workspace.dependencies] +chrono = "0.4.41" ciborium = "0.2.2" clap = { version = "4.5.38", features = ["derive"] } dioxus = { version = "0.6.0", features = [] } lazy-supplements-core.path = "lazy-supplements-core" libp2p = { version = "0.55.0", features = ["macros", "mdns", "noise", "ping", "tcp", "tokio", "yamux" ] } +sea-orm = { version = "1.1.11", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros", "with-chrono", "with-uuid"] } sea-orm-migration = { version = "1.1.0", features = ["runtime-tokio-rustls", "sqlx-postgres"] } serde = { version = "1.0.219", features = ["derive"] } thiserror = "2.0.12" diff --git a/lazy-supplements-core/Cargo.toml b/lazy-supplements-core/Cargo.toml index 32480d0..ed99ba4 100644 --- a/lazy-supplements-core/Cargo.toml +++ b/lazy-supplements-core/Cargo.toml @@ -8,21 +8,23 @@ repository.workspace = true [features] default = [] -desktop = ["dep:clap"] -test = ["dep:tempfile"] +desktop = ["dep:clap", "macros"] +mobile = ["macros"] +macros = ["dep:lazy-supplements-macros"] +test = ["dep:tempfile", "macros"] [dependencies] base64 = "0.22.1" -chrono = "0.4.41" +chrono.workspace = true chrono-tz = "0.10.3" ciborium.workspace = true clap = {workspace = true, optional = true} futures = "0.3.31" -lazy-supplements-macros.path = "../lazy-supplements-macros" +lazy-supplements-macros = { path = "../lazy-supplements-macros", optional = true } libp2p.workspace = true libp2p-core = { version = "0.43.0", features = ["serde"] } libp2p-identity = { version = "0.2.11", features = ["ed25519", "peerid", "rand", "serde"] } -sea-orm = { version = "1.1.11", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros", "with-chrono", "with-uuid"] } +sea-orm.workspace = true sea-orm-migration.workspace = true serde.workspace = true tempfile = { version = "3.20.0", optional = true } diff --git a/lazy-supplements-core/src/data/entity/record_deletion.rs b/lazy-supplements-core/src/data/entity/record_deletion.rs index 33ed387..cc06d28 100644 --- a/lazy-supplements-core/src/data/entity/record_deletion.rs +++ b/lazy-supplements-core/src/data/entity/record_deletion.rs @@ -7,14 +7,15 @@ use serde::{Deserialize, Serialize}; use crate::data::syncable::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SyncableModel)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[cfg_attr(feature="macros", derive(SyncableModel))] #[sea_orm(table_name = "record_deletion")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - #[syncable(uuid)] + #[cfg_attr(feature="macros", syncable(uuid))] pub id: Uuid, #[sea_orm(indexed)] - #[syncable(timestamp)] + #[cfg_attr(feature="macros", syncable(timestamp))] pub created_at: DateTimeUtc, pub table_name: String, pub record_id: Uuid, diff --git a/lazy-supplements-core/src/data/syncable.rs b/lazy-supplements-core/src/data/syncable.rs index dac0a1e..9c6e885 100644 --- a/lazy-supplements-core/src/data/syncable.rs +++ b/lazy-supplements-core/src/data/syncable.rs @@ -1,9 +1,11 @@ -use sea_orm::{*, prelude::*, query::*}; +use sea_orm::{prelude::*, query::*, sea_query::SimpleExpr, *}; +#[cfg(feature="macros")] pub use lazy_supplements_macros::SyncableModel; pub trait SyncableModel: ModelTrait { type SyncableEntity: SyncableEntity; fn get_timestamp(&self) -> DateTimeUtc; - fn get_uuid(&self) -> Uuid; + fn get_id(&self) -> Uuid; + fn get_author_id(&self) -> Uuid; } pub trait SyncableEntity: EntityTrait< @@ -15,9 +17,17 @@ pub trait SyncableEntity: EntityTrait< type SyncableActiveModel: SyncableActiveModel; type SyncableColumn: SyncableColumn; - async fn get_updated_after(date: DateTimeUtc, db: &DatabaseConnection) -> Result::Model>, SyncableError> { + async fn get_updated(from: DateTimeUtc,until: DateTimeUtc, db: &DatabaseConnection) -> Result::Model>, SyncableError> { let result: Vec = ::find() - .filter(Self::SyncableColumn::updated_at().gte(date)) + .filter(Self::SyncableColumn::timestamp_between(from, until)) + .all(db) + .await.unwrap(); + Ok(result) + } + async fn get_updated_by_author(from: DateTimeUtc, author: Uuid, db: &DatabaseConnection) -> Result::Model>, SyncableError> { + let result: Vec = ::find() + .filter(Self::SyncableColumn::timestamp_between(from, until)) + .filter(Self::SyncableColumn::author_eq(author)) .all(db) .await.unwrap(); Ok(result) @@ -30,15 +40,18 @@ pub trait SyncableEntity: EntityTrait< pub trait SyncableActiveModel: ActiveModelTrait { type SyncableEntity: SyncableEntity; - fn get_uuid(&self) -> Option; + fn get_id(&self) -> Option; fn get_timestamp(&self) -> Option; + fn get_author_id(&self) -> Option; fn try_merge(&mut self, other: ::SyncableModel) -> Result<(), SyncableError> { if self.get_uuid().ok_or(SyncableError::MissingField("uuid"))? != other.get_uuid() { return Err(SyncableError::MismatchUuid) } if self.get_timestamp().ok_or(SyncableError::MissingField("updated_at"))? < other.get_timestamp() { for column in <<::Entity as EntityTrait>::Column as Iterable>::iter() { - self.take(column).set_if_not_equals(other.get(column)); + if column.should_sync(){ + self.take(column).set_if_not_equals(other.get(column)); + } } } Ok(()) @@ -47,10 +60,12 @@ pub trait SyncableActiveModel: ActiveModelTrait { } pub trait SyncableColumn: ColumnTrait { - fn is_uuid(&self) -> bool; + fn is_id(&self) -> bool; fn is_timestamp(&self) -> bool; - fn updated_at() -> Self; - fn should_skipped(&self); + fn should_sync(&self) -> bool; + fn timestamp_between(from: DateTimeUtc, to: DateTimeUtc) -> SimpleExpr; + fn author_eq(author_id: Uuid) -> SimpleExpr; + fn is_author_id(&self) -> bool; } diff --git a/lazy-supplements-macros/Cargo.toml b/lazy-supplements-macros/Cargo.toml index 8e8862c..5643f97 100644 --- a/lazy-supplements-macros/Cargo.toml +++ b/lazy-supplements-macros/Cargo.toml @@ -14,3 +14,9 @@ heck = "0.5.0" proc-macro2 = "1.0.95" quote = "1.0.40" syn = { version = "2.0.104", features = ["full"] } + +[dev-dependencies] +chrono.workspace = true +lazy-supplements-core.workspace = true +sea-orm.workspace = true +uuid.workspace = true \ No newline at end of file diff --git a/lazy-supplements-macros/src/lib.rs b/lazy-supplements-macros/src/lib.rs index 1a2fbd4..aae46e4 100644 --- a/lazy-supplements-macros/src/lib.rs +++ b/lazy-supplements-macros/src/lib.rs @@ -4,25 +4,30 @@ use proc_macro2::Span; use quote::{format_ident, quote, ToTokens}; use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprTuple, Field, Fields, FieldsNamed, Ident}; -#[proc_macro_derive(SyncableModel)] +#[proc_macro_derive(SyncableModel, attributes(syncable))] pub fn syncable_model(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; assert_eq!(format_ident!("{}", struct_name), "Model"); let fields = extract_fields(&input.data); - let uuid_field = extract_uuid_field(&fields); - let uuid_field_camel = Ident::new(&uuid_field.to_string().to_upper_camel_case(), Span::call_site()); - let timestamp_field = extract_timestamp_field(&fields); - let timestamp_field_camel = Ident::new(×tamp_field.to_string().to_upper_camel_case(), Span::call_site()); - let skip_fields = extract_skip_fields(&fields); + let id_snake = extract_unique_field_ident(&fields, "id"); + let id_camel = Ident::new(&id_snake.to_string().to_upper_camel_case(), Span::call_site()); + let timestamp_snake = extract_unique_field_ident(&fields, "timestamp"); + let timestamp_camel = Ident::new(×tamp_snake.to_string().to_upper_camel_case(), Span::call_site()); + let author_id_snake = extract_unique_field_ident(&fields, "author_id"); + let author_id_camel = Ident::new(&author_id_snake.to_string().to_upper_camel_case(), Span::call_site()); + let skips_snake = extract_field_idents(&fields, "skip"); let output = quote!{ impl SyncableModel for #struct_name { type SyncableEntity = Entity; - fn get_uuid(&self) -> Uuid { - self.#uuid_field + fn get_id(&self) -> Uuid { + self.#id_snake } fn get_timestamp() -> DateTimeUtc { - self.#timestamp_field + self.#timestamp_snake + } + fn get_author_id() -> Uuid { + self.#timestamp_snake } } impl SyncableEntity for Entity { @@ -33,44 +38,45 @@ pub fn syncable_model(input: TokenStream) -> TokenStream { impl SyncableActiveModel for ActiveModel { type SyncableEntity = Entity; - fn get_uuid(&self) -> Option { - self.#uuid_field.into_value() + fn get_id(&self) -> Option { + self.#id_snake.into_value() } fn get_timestamp(&self) -> Option { - self.#timestamp_field.into_value() + self.#timestamp_snake.into_value() } - } + fn get_author_id(&self) -> Option { + self.#author_id_snake.into_value() + } } impl SyncableColumn for Column { - fn is_uuid(&self) -> bool { - self == &Column::#uuid_field_camel + fn is_id(&self) -> bool { + matches!(self, Column::#id_camel) } fn is_timestamp(&self) -> bool { - self == &Column::#timestamp_field_camel + matches!(self, Column::#timestamp_camel) + } + fn is_author_id(&self) -> bool { + matches!(self, Column::#author_id_camel) + } + fn should_sync(&self) -> bool { + todo!() + } + fn timestamp_between(from: DateTimeUtc, until: DateTimeUtc) -> SimpleExpr { + todo!() } } }; output.into() } -fn extract_skip_fields(fields: &FieldsNamed) -> Vec<&Ident> { - extract_fields_with_attribute(fields, "skip") -} -fn extract_timestamp_field(fields: &FieldsNamed) -> &Ident { - let mut timestamp_fields = extract_fields_with_attribute(fields, "timestamp"); - if timestamp_fields.len() == 1 { - timestamp_fields.pop().unwrap() +fn extract_unique_field_ident<'a>(fields: &'a FieldsNamed, attribute_arg: &'static str) -> &'a Ident { + let mut fields = extract_field_idents(fields, attribute_arg); + if fields.len() == 1 { + fields.pop().unwrap() } else { - panic!("Model must need one timestamp field attribute") + panic!("Model must need one {} field attribute", attribute_arg) } } -fn extract_uuid_field(fields: &FieldsNamed) -> &Ident { - let mut uuid_fields = extract_fields_with_attribute(fields, "uuid"); - if uuid_fields.len() == 1 { - uuid_fields.pop().unwrap() - } else { - panic!("Model must need one uuid field attribute") - } -} -fn extract_fields_with_attribute<'a>(fields: &'a FieldsNamed, attribute_arg: &'static str) -> Vec<&'a Ident>{ + +fn extract_field_idents<'a>(fields: &'a FieldsNamed, attribute_arg: &'static str) -> Vec<&'a Ident>{ fields.named.iter() .filter_map(|field| { field.attrs.iter() diff --git a/lazy-supplements-macros/tests/derive_syncable.rs b/lazy-supplements-macros/tests/derive_syncable.rs new file mode 100644 index 0000000..813fe39 --- /dev/null +++ b/lazy-supplements-macros/tests/derive_syncable.rs @@ -0,0 +1,29 @@ +use chrono::Local; +use sea_orm::entity::{ + *, + prelude::* +}; +use lazy_supplements_core::data::syncable::*; +use lazy_supplements_macros::SyncableModel; + + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SyncableModel)] +#[sea_orm(table_name = "syncable")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + #[syncable(id)] + pub id: Uuid, + #[sea_orm(indexed)] + #[syncable(timestamp)] + pub created_at: DateTimeUtc, + pub table_name: String, + + #[syncable(author_id)] + pub updated_by: Uuid, + pub record_id: Uuid, +} + +#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] +pub enum Relation{} + +impl ActiveModelBehavior for ActiveModel {}