diff --git a/.gitignore b/.gitignore index 1096b27..0f0e72d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target Cargo.lock -test.db \ No newline at end of file +*test.db \ No newline at end of file diff --git a/patch-db-macro-internals/src/lib.rs b/patch-db-macro-internals/src/lib.rs index fa44b5b..94f1e69 100644 --- a/patch-db-macro-internals/src/lib.rs +++ b/patch-db-macro-internals/src/lib.rs @@ -1,50 +1,77 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{ - AttributeArgs, Fields, Ident, Item, ItemEnum, ItemStruct, Lit, LitStr, Meta, MetaNameValue, - NestedMeta, Type, + Data, DataEnum, DataStruct, DeriveInput, Fields, Ident, Lit, LitStr, MetaNameValue, Type, }; -pub fn build_model(attr: &AttributeArgs, item: &Item) -> TokenStream { +pub fn build_model(item: &DeriveInput) -> TokenStream { let mut model_name = None; - for arg in attr { + for arg in item + .attrs + .iter() + .filter(|attr| attr.path.is_ident("model")) + .map(|attr| attr.parse_args::().unwrap()) + { match arg { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { + MetaNameValue { path, lit: Lit::Str(s), .. - })) if path.is_ident("name") => model_name = Some(s.parse().unwrap()), + } if path.is_ident("name") => model_name = Some(s.parse().unwrap()), _ => (), } } - match item { - Item::Struct(struct_ast) => build_model_struct(struct_ast, model_name), - Item::Enum(enum_ast) => build_model_enum(enum_ast, model_name), + match &item.data { + Data::Struct(struct_ast) => build_model_struct(item, struct_ast, model_name), + Data::Enum(enum_ast) => build_model_enum(item, enum_ast, model_name), _ => panic!("Models can only be created for Structs and Enums"), } } -fn build_model_struct(ast: &ItemStruct, model_name: Option) -> TokenStream { +fn build_model_struct( + base: &DeriveInput, + ast: &DataStruct, + model_name: Option, +) -> TokenStream { let model_name = model_name.unwrap_or_else(|| { Ident::new( - &format!("{}Model", ast.ident), + &format!("{}Model", base.ident), proc_macro2::Span::call_site(), ) }); - let base_name = &ast.ident; - let model_vis = &ast.vis; + let base_name = &base.ident; + let model_vis = &base.vis; let mut child_fn_name: Vec = Vec::new(); let mut child_model: Vec = Vec::new(); let mut child_path: Vec = Vec::new(); - let serde_rename_all = todo!(); + let serde_rename_all = base + .attrs + .iter() + .filter(|attr| attr.path.is_ident("serde")) + .filter_map(|attr| attr.parse_args::().ok()) + .filter(|nv| nv.path.is_ident("rename_all")) + .find_map(|nv| match nv.lit { + Lit::Str(s) => Some(s.value()), + _ => None, + }); match &ast.fields { Fields::Named(f) => { for field in &f.named { let ident = field.ident.clone().unwrap(); child_fn_name.push(ident.clone()); - child_model.push(field.ty.clone()); - let serde_rename = todo!(); - match (serde_rename, serde_rename_all) { + let ty = &field.ty; + child_model.push(syn::parse2(quote! { patch_db::Model<#ty> }).unwrap()); // TODO: check attr + let serde_rename = field + .attrs + .iter() + .filter(|attr| attr.path.is_ident("serde")) + .filter_map(|attr| syn::parse2::(attr.tokens.clone()).ok()) + .filter(|nv| nv.path.is_ident("rename")) + .find_map(|nv| match nv.lit { + Lit::Str(s) => Some(s), + _ => None, + }); + match (serde_rename, serde_rename_all.as_ref().map(|s| s.as_str())) { (Some(a), _) => child_path.push(a), (None, Some("lowercase")) => child_path.push(LitStr::new( &heck::CamelCase::to_camel_case(ident.to_string().as_str()).to_lowercase(), @@ -78,10 +105,16 @@ fn build_model_struct(ast: &ItemStruct, model_name: Option) -> TokenStrea } } } - Fields::Unnamed(f) => {} + Fields::Unnamed(f) => { + if f.unnamed.len() == 1 { + } else if f.unnamed.len() > 1 { + } + todo!() + } Fields::Unit => (), } quote! { + #[derive(Debug, Clone)] #model_vis struct #model_name(patch_db::Model<#base_name>); impl core::ops::Deref for #model_name { type Target = patch_db::Model<#base_name>; @@ -90,6 +123,9 @@ fn build_model_struct(ast: &ItemStruct, model_name: Option) -> TokenStrea } } impl #model_name { + pub fn new(ptr: json_ptr::JsonPointer) -> Self { + #model_name(patch_db::Model::new(ptr)) + } // foreach element, create accessor fn #( pub fn #child_fn_name(&self) -> #child_model { @@ -97,9 +133,12 @@ fn build_model_struct(ast: &ItemStruct, model_name: Option) -> TokenStrea } )* } + impl patch_db::HasModel for #base_name { + type Model = #model_name; + } } } -fn build_model_enum(ast: &ItemEnum, model_name: Option) -> TokenStream { +fn build_model_enum(base: &DeriveInput, ast: &DataEnum, model_name: Option) -> TokenStream { todo!() } diff --git a/patch-db-macro/src/lib.rs b/patch-db-macro/src/lib.rs index d6d4dbb..8af4780 100644 --- a/patch-db-macro/src/lib.rs +++ b/patch-db-macro/src/lib.rs @@ -1,13 +1,10 @@ extern crate proc_macro; use proc_macro::TokenStream; -use syn::{parse_macro_input, AttributeArgs, Item}; +use syn::{parse_macro_input, DeriveInput}; -#[proc_macro_attribute] -pub fn model(attr_in: TokenStream, item_in: TokenStream) -> TokenStream { - let mut res = proc_macro2::TokenStream::from(item_in.clone()); - let attr = parse_macro_input!(attr_in as AttributeArgs); - let item = parse_macro_input!(item_in as Item); - res.extend(patch_db_macro_internals::build_model(&attr, &item)); - res.into() +#[proc_macro_derive(HasModel, attributes(model))] +pub fn model(item: TokenStream) -> TokenStream { + let item = parse_macro_input!(item as DeriveInput); + patch_db_macro_internals::build_model(&item).into() } diff --git a/patch-db/Cargo.toml b/patch-db/Cargo.toml index b8a480f..0085fd3 100644 --- a/patch-db/Cargo.toml +++ b/patch-db/Cargo.toml @@ -19,6 +19,7 @@ serde_json = "1.0.61" serde_cbor = { path = "../cbor" } thiserror = "1.0.23" tokio = { version = "1.0.1", features = ["sync", "fs", "rt", "io-util", "macros"] } +patch-db-macro = { path = "../patch-db-macro" } [dev-dependencies] proptest = "1.0.0" diff --git a/patch-db/src/lib.rs b/patch-db/src/lib.rs index 5d23d21..fc7aafa 100644 --- a/patch-db/src/lib.rs +++ b/patch-db/src/lib.rs @@ -1002,13 +1002,14 @@ impl Deserialize<'de>> DerefMut for ModelDataMut { } } +#[derive(Debug)] pub struct Model Deserialize<'de>> { ptr: JsonPointer, phantom: PhantomData, } impl Model where - T: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, + T: Serialize + for<'de> Deserialize<'de>, { pub fn new(ptr: JsonPointer) -> Self { Self { @@ -1045,3 +1046,18 @@ where } } } +impl std::clone::Clone for Model +where + T: Serialize + for<'de> Deserialize<'de>, +{ + fn clone(&self) -> Self { + Model { + ptr: self.ptr.clone(), + phantom: PhantomData, + } + } +} + +pub trait HasModel { + type Model; +} diff --git a/patch-db/src/test.rs b/patch-db/src/test.rs index 8f1a761..6e3c147 100644 --- a/patch-db/src/test.rs +++ b/patch-db/src/test.rs @@ -1,6 +1,8 @@ use super::*; +use crate as patch_db; use proptest::prelude::*; use tokio::{fs, runtime::Builder}; +use patch_db_macro::HasModel; async fn init_db(db_name: String) -> PatchDb { cleanup_db(&db_name).await; @@ -35,26 +37,26 @@ async fn put_string_into_root(db: PatchDb, s: String) -> Arc { #[tokio::test] async fn basic() { - let db = init_db("basic".to_string()).await; + let db = init_db("basic.test.db".to_string()).await; let ptr: JsonPointer = "/b/b".parse().unwrap(); let mut get_res: Value = db.get(&ptr).await.unwrap(); assert_eq!(get_res, 1); db.put(&ptr, &"hello").await.unwrap(); get_res = db.get(&ptr).await.unwrap(); assert_eq!(get_res, "hello"); - cleanup_db(&"basic".to_string()).await; + cleanup_db(&"basic.test.db".to_string()).await; } #[tokio::test] async fn transaction() { - let db = init_db("transaction".to_string()).await; + let db = init_db("transaction.test.db".to_string()).await; let mut tx = db.begin(); let ptr: JsonPointer = "/b/b".parse().unwrap(); tx.put(&ptr, &(2 as usize)).await.unwrap(); tx.put(&ptr, &(1 as usize)).await.unwrap(); let _res = tx.commit().await.unwrap(); println!("res = {:?}", _res); - cleanup_db(&"transaction".to_string()).await; + cleanup_db(&"transaction.test.db".to_string()).await; } proptest! { @@ -67,32 +69,23 @@ proptest! { .thread_stack_size(3 * 1024 * 1024) .build() .unwrap(); - let db = runtime.block_on(init_db("doesnt_crash".to_string())); + let db = runtime.block_on(init_db("doesnt_crash.test.db".to_string())); let put_future = put_string_into_root(db, s); runtime.block_on(put_future); - runtime.block_on(cleanup_db(&"doesnt_crash".to_string())); + runtime.block_on(cleanup_db(&"doesnt_crash.test.db".to_string())); } } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, HasModel)] pub struct Sample { a: String, + #[model(name = ChildModel)] b: Child, } -pub struct SampleModel(Model); -impl core::ops::Deref for SampleModel { - type Target = Model; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct Child { a: String, b: usize, } - -pub struct ChildModel(Model);