struct-patch-derive-0.9.3/.cargo_vcs_info.json0000644000000001610000000000100147400ustar { "git": { "sha1": "40a7f25e49fb76822ed01740b1bf23399f2140fe" }, "path_in_vcs": "struct-patch-derive" }struct-patch-derive-0.9.3/Cargo.lock0000644000000101720000000000100127160ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "darrentsung_debug_parser" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf488eca7807ce3c8e64bee95c3fbf8f1935c905b3b73835e75db16fc458fdc4" dependencies = [ "anyhow", "html-escape", "nom", "ordered-float", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "html-escape" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ "utf8-width", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "ordered-float" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" dependencies = [ "num-traits", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "pretty_assertions_sorted" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95d32882f2adbdfd30312733271b83c527ee8007bf78dc21afe510463ac6a0" dependencies = [ "darrentsung_debug_parser", "pretty_assertions", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "struct-patch-derive" version = "0.9.3" dependencies = [ "pretty_assertions_sorted", "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" struct-patch-derive-0.9.3/Cargo.toml0000644000000024420000000000100127420ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "struct-patch-derive" version = "0.9.3" authors = ["Antonio Yang "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A library that helps you implement partial updates for your structs." readme = "README.md" keywords = [ "struct", "patch", "macro", "derive", "overlay", ] categories = ["development-tools"] license = "MIT" repository = "https://github.com/yanganto/struct-patch/" [features] merge = [] op = [] status = [] [lib] name = "struct_patch_derive" path = "src/lib.rs" proc-macro = true [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1.0" [dependencies.syn] version = "2.0" features = ["parsing"] [dev-dependencies.pretty_assertions_sorted] version = "1.2.3" struct-patch-derive-0.9.3/Cargo.toml.orig000064400000000000000000000007621046102023000164260ustar 00000000000000[package] name = "struct-patch-derive" authors.workspace = true version.workspace = true edition.workspace = true categories.workspace = true keywords.workspace = true repository.workspace = true description.workspace = true license.workspace = true readme.workspace = true [lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["parsing"] } [features] status = [] op = [] merge = [] [dev-dependencies] pretty_assertions_sorted = "1.2.3" struct-patch-derive-0.9.3/LICENSE000064400000000000000000000017771046102023000145530ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. struct-patch-derive-0.9.3/README.md000064400000000000000000000141621046102023000150150ustar 00000000000000# Struct Patch [![Crates.io][crates-badge]][crate-url] [![MIT licensed][mit-badge]][mit-url] [![Docs][doc-badge]][doc-url] A lib help you patch Rust instance, and easy to partial update configures. ## Introduction This crate provides the `Patch`, `Filler` traits and accompanying derive macro. If the any field in `Patch` is some then it will overwrite the field of instance when apply. If the any field in the instance is none then it will try to fill the field with the `Filler`. Currently, `Filler` only support `Option` and `Vec` fields. The other fields and operator for Filler will implement later. The detail discussion is in [issue #81](https://github.com/yanganto/struct-patch/issues/81) ## Quick Example Deriving `Patch` on a struct will generate a struct similar to the original one, but with all fields wrapped in an `Option`. An instance of such a patch struct can be applied onto the original struct, replacing values only if they are set to `Some`, leaving them unchanged otherwise. ```rust use struct_patch::Patch; use serde::{Deserialize, Serialize}; #[derive(Default, Debug, PartialEq, Patch)] #[patch(attribute(derive(Debug, Default, Deserialize, Serialize)))] struct Item { field_bool: bool, field_int: usize, field_string: String, } fn patch_json() { let mut item = Item { field_bool: true, field_int: 42, field_string: String::from("hello"), }; let data = r#"{ "field_int": 7 }"#; let patch: ItemPatch = serde_json::from_str(data).unwrap(); item.apply(patch); // You can do // `let new_item = item << patch;` // For multiple patches, // you can do this // `let new_item = item << patch_1 << patch_2;` // or make an aggregated one, but please make sure the patch fields do not conflict, else will panic // ``` // let overall_patch = patch_1 + patch_2 + patch_3; // let new_item = item << overall_patch; // ``` assert_eq!( item, Item { field_bool: true, field_int: 7, field_string: String::from("hello") } ); } ``` Deriving `Filler` on a struct will generate a struct similar to the original one with the field with `Option`. Unlike `Patch`, the `Filler` only work on the empty fields of instance. ```rust use struct_patch::Filler; #[derive(Filler)] struct Item { field_int: usize, maybe_field_int: Option, list: Vec, } let mut item = Item { field_int: 0, maybe_field_int: None, list: Vec::new(), }; let filler_1 = ItemFiller{ maybe_field_int: Some(7), list: Vec::new() }; item.apply(filler_1); assert_eq!(item.maybe_field_int, Some(7)); let filler_2 = ItemFiller{ maybe_field_int: Some(100), list: Vec::new() }; // The field is not empty, so the filler has not effect. item.apply(filler_2); assert_eq!(item.maybe_field_int, Some(7)); let filler_3 = ItemFiller{ maybe_field_int: Some(100), list: vec![7] }; item.apply(filler_3); assert_eq!(item.maybe_field_int, Some(7)); assert_eq!(item.list, vec![7]); ``` ## Documentation and Examples Also, you can modify the patch structure by defining `#[patch(...)]` or `#[filler(...)]` attributes on the original struct or fields. Struct attributes: - `#[patch(name = "...")]`: change the name of the generated patch struct. - `#[patch(attribute(...))]`: add attributes to the generated patch struct. - `#[patch(attribute(derive(...)))]`: add derives to the generated patch struct. Field attributes: - `#[patch(skip)]`: skip the field in the generated patch struct. - `#[patch(name = "...")]`: change the type of the field in the generated patch struct. - `#[patch(attribute(...))]`: add attributes to the field in the generated patch struct. - `#[patch(attribute(derive(...)))]`: add derives to the field in the generated patch struct. - `#[filler(extendable)]`: use the struct of field for Filler, the struct needs implement `Default`, `Extend`, `IntoIterator` and `is_empty`. Please check the [traits][doc-traits] of document to learn more. The [examples][examples] demo following scenarios. - diff two instance for a patch - create a patch from json string - rename the patch structure - check a patch is empty or not - add attribute to patch struct - show option field behavior - show operators about patches - show example with serde crates, ex: `humantime_serde` for duration - show filler with all possible types ## Features This crate also includes the following optional features: - `status`(default): implements the `Status` trait for the patch struct, which provides the `is_empty` method. - `op` (default): provide operators `<<` between instance and patch, and `+` for patches - default: when there is a field conflict between patches, `+` will add together if the `#[patch(addable)]` or `#[patch(add=fn)]` is provided, else it will panic. - `merge` (optional): implements the `Merge` trait for the patch struct, which provides the `merge` method, and `<<` between patches. - `std`(optional): - `box`: implements the `Patch>` trait for `T` where `T` implements `Patch

`. This let you patch a boxed (or not) struct with a boxed patch. - `option`: implements the `Patch>` trait for `Option` where `T` implements `Patch

`, please take a look at the example to learn more. - default: `T` needs to implement `From

`. When patching on None, it will based on `from

` to cast T, and this let you patch structs containing fields with optional values. - `none_as_default`: `T` needs to implement `Default`. When patching on None, it will patch on a default instance, and this also let you patch structs containing fields with optional values. - `keep_none`: When patching on None, it is still None. [crates-badge]: https://img.shields.io/crates/v/struct-patch.svg [crate-url]: https://crates.io/crates/struct-patch [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg [mit-url]: https://github.com/yanganto/struct-patch/blob/readme/LICENSE [doc-badge]: https://img.shields.io/badge/docs-rs-orange.svg [doc-url]: https://docs.rs/struct-patch/ [doc-traits]: https://docs.rs/struct-patch/latest/struct_patch/traits/trait.Patch.html#container-attributes [examples]: /struct-patch/examples struct-patch-derive-0.9.3/src/filler.rs000064400000000000000000000321661046102023000161540ustar 00000000000000use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; use syn::meta::ParseNestedMeta; use syn::spanned::Spanned; use syn::{parenthesized, DeriveInput, Error, Lit, LitStr, Result, Type}; const FILLER: &str = "filler"; const ATTRIBUTE: &str = "attribute"; const EXTENDABLE: &str = "extendable"; const EMPTY_VALUE: &str = "empty_value"; pub(crate) struct Filler { visibility: syn::Visibility, struct_name: Ident, filler_struct_name: Ident, generics: syn::Generics, attributes: Vec, fields: Vec, } enum FillerType { Option, /// The type with `Default`, `Extend`, `IntoIterator` and `is_empty` implementations Extendable(Ident), /// The type with a value defined for empty NativeValue(Lit), } impl FillerType { fn inner(&self) -> &Ident { if let FillerType::Extendable(ident) = self { ident } else { panic!("Only FillerType::Extendable has inner indent") } } fn value(&self) -> &Lit { if let FillerType::NativeValue(lit) = self { lit } else { panic!("Only FillerType::NativeValue has value") } } } struct Field { ident: Option, ty: Type, attributes: Vec, fty: FillerType, } impl Filler { /// Generate the token stream for the filler struct and it resulting implementations pub fn to_token_stream(&self) -> Result { let Filler { visibility, struct_name, filler_struct_name: name, generics, attributes, fields, } = self; let filler_struct_fields = fields .iter() .map(|f| f.to_token_stream()) .collect::>>()?; let option_field_names = fields .iter() .filter(|f| matches!(f.fty, FillerType::Option)) .map(|f| f.ident.as_ref()) .collect::>(); let extendable_field_names = fields .iter() .filter(|f| matches!(f.fty, FillerType::Extendable(_))) .map(|f| f.ident.as_ref()) .collect::>(); let extendable_field_types = fields .iter() .filter(|f| matches!(f.fty, FillerType::Extendable(_))) .map(|f| f.fty.inner()) .collect::>(); let native_value_field_names = fields .iter() .filter(|f| matches!(f.fty, FillerType::NativeValue(_))) .map(|f| f.ident.as_ref()) .collect::>(); let native_value_field_values = fields .iter() .filter(|f| matches!(f.fty, FillerType::NativeValue(_))) .map(|f| f.fty.value()) .collect::>(); let mapped_attributes = attributes .iter() .map(|a| { quote! { #[#a] } }) .collect::>(); let filler_struct = quote! { #(#mapped_attributes)* #visibility struct #name #generics { #(#filler_struct_fields)* } }; let where_clause = &generics.where_clause; #[cfg(feature = "status")] let status_impl = quote!( impl #generics struct_patch::traits::Status for #name #generics #where_clause { fn is_empty(&self) -> bool { #( if self.#option_field_names.is_some() { return false } )* #( if !self.#extendable_field_names.is_empty() { return false } )* #( if self.#native_value_field_names != #native_value_field_values { return false } )* true } } ); #[cfg(not(feature = "status"))] let status_impl = quote!(); let filler_impl = quote! { impl #generics struct_patch::traits::Filler< #name #generics > for #struct_name #generics #where_clause { fn apply(&mut self, filler: #name #generics) { #( if self.#native_value_field_names == #native_value_field_values { self.#native_value_field_names = filler.#native_value_field_names; } )* #( if self.#extendable_field_names.is_empty() { self.#extendable_field_names.extend(filler.#extendable_field_names.into_iter()); } )* #( if let Some(v) = filler.#option_field_names { if self.#option_field_names.is_none() { self.#option_field_names = Some(v); } } )* } fn new_empty_filler() -> #name #generics { #name { #(#option_field_names: None,)* #(#extendable_field_names: #extendable_field_types::default(),)* #(#native_value_field_names: #native_value_field_values,)* } } } }; Ok(quote! { #filler_struct #status_impl #filler_impl }) } /// Parse the filler struct pub fn from_ast( DeriveInput { ident, data, generics, attrs, vis, }: syn::DeriveInput, ) -> Result { let original_fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = data { fields } else { return Err(syn::Error::new( ident.span(), "Filler derive only use for struct", )); }; let mut attributes = vec![]; let mut fields = vec![]; for attr in attrs { if attr.path().to_string().as_str() != FILLER { continue; } if let syn::Meta::List(meta) = &attr.meta { if meta.tokens.is_empty() { continue; } } attr.parse_nested_meta(|meta| { let path = meta.path.to_string(); match path.as_str() { ATTRIBUTE => { // #[filler(attribute(derive(Deserialize)))] // #[filler(attribute(derive(Deserialize, Debug), serde(rename = "foo"))] let content; parenthesized!(content in meta.input); let attribute: TokenStream = content.parse()?; attributes.push(attribute); } _ => { return Err(meta.error(format_args!( "unknown filler container attribute `{}`", path.replace(' ', "") ))); } } Ok(()) })?; } for field in original_fields { if let Some(f) = Field::from_ast(field)? { fields.push(f); } } let ts = TokenStream::from_str(&format!("{}Filler", &ident,)).unwrap(); let lit = LitStr::new(&ts.to_string(), Span::call_site()); let filler_struct_name = lit.parse()?; Ok(Filler { visibility: vis, filler_struct_name, struct_name: ident, generics, attributes, fields, }) } } impl Field { /// Generate the token stream for the Filler struct fields pub fn to_token_stream(&self) -> Result { let Field { ident, ty, attributes, .. } = self; let attributes = attributes .iter() .map(|a| { quote! { #[#a] } }) .collect::>(); match ident { Some(ident) => Ok(quote! { #(#attributes)* pub #ident: #ty, }), None => Ok(quote! { #(#attributes)* pub #ty, }), } } /// Parse the filler struct field pub fn from_ast( syn::Field { ident, ty, attrs, .. }: syn::Field, ) -> Result> { let mut fty = filler_type(&ty); let mut attributes = vec![]; for attr in attrs { if attr.path().to_string().as_str() != FILLER { continue; } if let syn::Meta::List(meta) = &attr.meta { if meta.tokens.is_empty() { continue; } } attr.parse_nested_meta(|meta| { let path = meta.path.to_string(); match path.as_str() { ATTRIBUTE => { // #[filler(attribute(serde(alias = "my-field")))] let content; parenthesized!(content in meta.input); let attribute: TokenStream = content.parse()?; attributes.push(attribute); } EXTENDABLE => { // #[filler(extendable)] if fty.is_some() { return Err(meta .error("The field is already the field of filler, we can't defined more than once")); } fty = Some(FillerType::Extendable(none_option_filler_type(&ty))); } EMPTY_VALUE => { // #[filler(empty_value=some value)] if fty.is_some() { return Err(meta .error("The field is already the field of filler, we can't defined more than once")); } if let Some(lit) = get_lit(path, &meta)? { fty = Some(FillerType::NativeValue(lit)); } else { return Err(meta .error("empty_value needs a clear value to define empty")); } } _ => { return Err(meta.error(format_args!( "unknown filler field attribute `{}`", path.replace(' ', "") ))); } } Ok(()) })?; } Ok(fty.map(|fty| Field { ident, ty, attributes, fty, })) } } trait ToStr { fn to_string(&self) -> String; } impl ToStr for syn::Path { fn to_string(&self) -> String { self.to_token_stream().to_string() } } fn filler_type(ty: &Type) -> Option { if let Type::Path(type_path) = ty { let segments = &type_path.path.segments; if segments.len() == 1 && segments[0].ident == "Option" { if let syn::PathArguments::AngleBracketed(args) = &segments[0].arguments { if args.args.len() == 1 { return Some(FillerType::Option); } } } else if segments.len() == 1 && segments[0].ident == "Vec" || segments[0].ident == "VecDeque" || segments[0].ident == "LinkedList" || segments[0].ident == "HashMap" || segments[0].ident == "BTreeMap" || segments[0].ident == "HashSet" || segments[0].ident == "BTreeSet" || segments[0].ident == "BinaryHeap" { if let syn::PathArguments::AngleBracketed(args) = &segments[0].arguments { if args.args.len() == 1 { return Some(FillerType::Extendable(segments[0].ident.clone())); } } } } None } fn none_option_filler_type(ty: &Type) -> Ident { if let Type::Path(type_path) = ty { type_path.path.segments[0].ident.clone() } else { panic!("#[filler(extendable)] should use on a type") } } fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { let expr: syn::Expr = meta.value()?.parse()?; let mut value = &expr; while let syn::Expr::Group(e) = value { value = &e.expr; } if let syn::Expr::Lit(syn::ExprLit { lit, .. }) = value { Ok(Some(lit.clone())) } else { Err(Error::new( expr.span(), format!( "expected serde {} attribute to be lit: `{} = \"...\"`", attr_name, attr_name ), )) } } struct-patch-derive-0.9.3/src/lib.rs000064400000000000000000000012131046102023000154320ustar 00000000000000extern crate proc_macro; mod filler; mod patch; use filler::Filler; use patch::Patch; #[proc_macro_derive(Patch, attributes(patch))] pub fn derive_patch(item: proc_macro::TokenStream) -> proc_macro::TokenStream { Patch::from_ast(syn::parse_macro_input!(item as syn::DeriveInput)) .unwrap() .to_token_stream() .unwrap() .into() } #[proc_macro_derive(Filler, attributes(filler))] pub fn derive_filler(item: proc_macro::TokenStream) -> proc_macro::TokenStream { Filler::from_ast(syn::parse_macro_input!(item as syn::DeriveInput)) .unwrap() .to_token_stream() .unwrap() .into() } struct-patch-derive-0.9.3/src/patch.rs000064400000000000000000000470351046102023000157770ustar 00000000000000extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; use syn::{ meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, LitStr, Result, Type, }; const PATCH: &str = "patch"; const NAME: &str = "name"; const ATTRIBUTE: &str = "attribute"; const SKIP: &str = "skip"; const ADDABLE: &str = "addable"; const ADD: &str = "add"; pub(crate) struct Patch { visibility: syn::Visibility, struct_name: Ident, patch_struct_name: Ident, generics: syn::Generics, attributes: Vec, fields: Vec, } #[cfg(feature = "op")] pub(crate) enum Addable { Disable, AddTriat, #[cfg(feature = "op")] AddFn(Ident), } struct Field { ident: Option, ty: Type, attributes: Vec, retyped: bool, #[cfg(feature = "op")] addable: Addable, } impl Patch { /// Generate the token stream for the patch struct and it resulting implementations pub fn to_token_stream(&self) -> Result { let Patch { visibility, struct_name, patch_struct_name: name, generics, attributes, fields, } = self; let patch_struct_fields = fields .iter() .map(|f| f.to_token_stream()) .collect::>>()?; let field_names = fields.iter().map(|f| f.ident.as_ref()).collect::>(); let renamed_field_names = fields .iter() .filter(|f| f.retyped) .map(|f| f.ident.as_ref()) .collect::>(); let original_field_names = fields .iter() .filter(|f| !f.retyped) .map(|f| f.ident.as_ref()) .collect::>(); let mapped_attributes = attributes .iter() .map(|a| { quote! { #[#a] } }) .collect::>(); let patch_struct = quote! { #(#mapped_attributes)* #visibility struct #name #generics { #(#patch_struct_fields)* } }; let where_clause = &generics.where_clause; #[cfg(feature = "status")] let patch_status_impl = quote!( impl #generics struct_patch::traits::Status for #name #generics #where_clause { fn is_empty(&self) -> bool { #( if self.#field_names.is_some() { return false } )* true } } ); #[cfg(not(feature = "status"))] let patch_status_impl = quote!(); #[cfg(feature = "merge")] let patch_merge_impl = quote!( impl #generics struct_patch::traits::Merge for #name #generics #where_clause { fn merge(self, other: Self) -> Self { Self { #( #renamed_field_names: match (self.#renamed_field_names, other.#renamed_field_names) { (Some(a), Some(b)) => Some(a.merge(b)), (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, }, )* #( #original_field_names: other.#original_field_names.or(self.#original_field_names), )* } } } ); #[cfg(not(feature = "merge"))] let patch_merge_impl = quote!(); #[cfg(feature = "op")] let addable_handles = fields .iter() .map(|f| { match &f.addable { Addable::AddTriat => quote!( Some(a + b) ), Addable::AddFn(f) => { quote!( Some(#f(a, b)) ) } , Addable::Disable => quote!( panic!("There are conflict patches, please use `#[patch(addable)]` if you want to add these values.") ) } }) .collect::>(); #[cfg(all(feature = "op", not(feature = "merge")))] let op_impl = quote! { impl #generics core::ops::Shl<#name #generics> for #struct_name #generics #where_clause { type Output = Self; fn shl(mut self, rhs: #name #generics) -> Self { struct_patch::traits::Patch::apply(&mut self, rhs); self } } impl #generics core::ops::Add for #name #generics #where_clause { type Output = Self; fn add(mut self, rhs: Self) -> Self { Self { #( #renamed_field_names: match (self.#renamed_field_names, rhs.#renamed_field_names) { (Some(a), Some(b)) => { #addable_handles }, (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, }, )* #( #original_field_names: match (self.#original_field_names, rhs.#original_field_names) { (Some(a), Some(b)) => { #addable_handles }, (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, }, )* } } } }; #[cfg(feature = "merge")] let op_impl = quote! { impl #generics core::ops::Shl<#name #generics> for #struct_name #generics #where_clause { type Output = Self; fn shl(mut self, rhs: #name #generics) -> Self { struct_patch::traits::Patch::apply(&mut self, rhs); self } } impl #generics core::ops::Shl<#name #generics> for #name #generics #where_clause { type Output = Self; fn shl(mut self, rhs: Self) -> Self { struct_patch::traits::Merge::merge(self, rhs) } } impl #generics core::ops::Add for #name #generics #where_clause { type Output = Self; fn add(mut self, rhs: Self) -> Self { Self { #( #renamed_field_names: match (self.#renamed_field_names, rhs.#renamed_field_names) { (Some(a), Some(b)) => { #addable_handles }, (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, }, )* #( #original_field_names: match (self.#original_field_names, rhs.#original_field_names) { (Some(a), Some(b)) => { #addable_handles }, (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, }, )* } } } }; #[cfg(not(feature = "op"))] let op_impl = quote!(); let patch_impl = quote! { impl #generics struct_patch::traits::Patch< #name #generics > for #struct_name #generics #where_clause { fn apply(&mut self, patch: #name #generics) { #( if let Some(v) = patch.#renamed_field_names { self.#renamed_field_names.apply(v); } )* #( if let Some(v) = patch.#original_field_names { self.#original_field_names = v; } )* } fn into_patch(self) -> #name #generics { #name { #( #renamed_field_names: Some(self.#renamed_field_names.into_patch()), )* #( #original_field_names: Some(self.#original_field_names), )* } } fn into_patch_by_diff(self, previous_struct: Self) -> #name #generics { #name { #( #renamed_field_names: if self.#renamed_field_names != previous_struct.#renamed_field_names { Some(self.#renamed_field_names.into_patch_by_diff(previous_struct.#renamed_field_names)) } else { None }, )* #( #original_field_names: if self.#original_field_names != previous_struct.#original_field_names { Some(self.#original_field_names) } else { None }, )* } } fn new_empty_patch() -> #name #generics { #name { #( #field_names: None, )* } } } }; Ok(quote! { #patch_struct #patch_status_impl #patch_merge_impl #patch_impl #op_impl }) } /// Parse the patch struct pub fn from_ast( DeriveInput { ident, data, generics, attrs, vis, }: syn::DeriveInput, ) -> Result { let original_fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = data { fields } else { return Err(syn::Error::new( ident.span(), "Patch derive only use for struct", )); }; let mut name = None; let mut attributes = vec![]; let mut fields = vec![]; for attr in attrs { if attr.path().to_string().as_str() != PATCH { continue; } if let syn::Meta::List(meta) = &attr.meta { if meta.tokens.is_empty() { continue; } } attr.parse_nested_meta(|meta| { let path = meta.path.to_string(); match path.as_str() { NAME => { // #[patch(name = "PatchStruct")] if let Some(lit) = get_lit_str(path, &meta)? { if name.is_some() { return Err(meta .error("The name attribute can't be defined more than once")); } name = Some(lit.parse()?); } } ATTRIBUTE => { // #[patch(attribute(derive(Deserialize)))] // #[patch(attribute(derive(Deserialize, Debug), serde(rename = "foo"))] let content; parenthesized!(content in meta.input); let attribute: TokenStream = content.parse()?; attributes.push(attribute); } _ => { return Err(meta.error(format_args!( "unknown patch container attribute `{}`", path.replace(' ', "") ))); } } Ok(()) })?; } for field in original_fields { if let Some(f) = Field::from_ast(field)? { fields.push(f); } } Ok(Patch { visibility: vis, patch_struct_name: name.unwrap_or({ let ts = TokenStream::from_str(&format!("{}Patch", &ident,)).unwrap(); let lit = LitStr::new(&ts.to_string(), Span::call_site()); lit.parse()? }), struct_name: ident, generics, attributes, fields, }) } } impl Field { /// Generate the token stream for the Patch struct fields pub fn to_token_stream(&self) -> Result { let Field { ident, ty, attributes, .. } = self; let attributes = attributes .iter() .map(|a| { quote! { #[#a] } }) .collect::>(); match ident { Some(ident) => Ok(quote! { #(#attributes)* pub #ident: Option<#ty>, }), None => Ok(quote! { #(#attributes)* pub Option<#ty>, }), } } /// Parse the patch struct field pub fn from_ast( syn::Field { ident, ty, attrs, .. }: syn::Field, ) -> Result> { let mut attributes = vec![]; let mut field_type = None; let mut skip = false; #[cfg(feature = "op")] let mut addable = Addable::Disable; for attr in attrs { if attr.path().to_string().as_str() != PATCH { continue; } if let syn::Meta::List(meta) = &attr.meta { if meta.tokens.is_empty() { continue; } } attr.parse_nested_meta(|meta| { let path = meta.path.to_string(); match path.as_str() { SKIP => { // #[patch(skip)] skip = true; } ATTRIBUTE => { // #[patch(attribute(serde(alias = "my-field")))] let content; parenthesized!(content in meta.input); let attribute: TokenStream = content.parse()?; attributes.push(attribute); } NAME => { // #[patch(name = "ItemPatch")] let expr: LitStr = meta.value()?.parse()?; field_type = Some(expr.parse()?) } #[cfg(feature = "op")] ADDABLE => { // #[patch(addable)] addable = Addable::AddTriat; } #[cfg(not(feature = "op"))] ADDABLE => { return Err(syn::Error::new( ident.span(), "`addable` needs `op` feature", )); } #[cfg(feature = "op")] ADD => { // #[patch(add=fn)] let f: Ident = meta.value()?.parse()?; addable = Addable::AddFn(f); } #[cfg(not(feature = "op"))] ADD => { return Err(syn::Error::new(ident.span(), "`add` needs `op` feature")); } _ => { return Err(meta.error(format_args!( "unknown patch field attribute `{}`", path.replace(' ', "") ))); } } Ok(()) })?; if skip { return Ok(None); } } Ok(Some(Field { ident, retyped: field_type.is_some(), ty: field_type.unwrap_or(ty), attributes, #[cfg(feature = "op")] addable, })) } } trait ToStr { fn to_string(&self) -> String; } impl ToStr for syn::Path { fn to_string(&self) -> String { self.to_token_stream().to_string() } } fn get_lit_str(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { let expr: syn::Expr = meta.value()?.parse()?; let mut value = &expr; while let syn::Expr::Group(e) = value { value = &e.expr; } if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }) = value { let suffix = lit.suffix(); if !suffix.is_empty() { return Err(Error::new( lit.span(), format!("unexpected suffix `{}` on string literal", suffix), )); } Ok(Some(lit.clone())) } else { Err(Error::new( expr.span(), format!( "expected serde {} attribute to be a string: `{} = \"...\"`", attr_name, attr_name ), )) } } #[cfg(test)] mod tests { use pretty_assertions_sorted::assert_eq_sorted; use syn::token::Pub; use super::*; #[test] fn parse_patch() { // Test case 1: Valid patch with attributes and fields let input = quote! { #[derive(Patch)] #[patch(name = "MyPatch", attribute(derive(Debug, PartialEq, Clone, Serialize, Deserialize)))] pub struct Item { #[patch(name = "SubItemPatch")] pub field1: SubItem, #[patch(skip)] pub field2: Option, } }; let expected = Patch { visibility: syn::Visibility::Public(Pub::default()), struct_name: syn::Ident::new("Item", Span::call_site()), patch_struct_name: syn::Ident::new("MyPatch", Span::call_site()), generics: syn::Generics::default(), attributes: vec![quote! { derive(Debug, PartialEq, Clone, Serialize, Deserialize) }], fields: vec![Field { ident: Some(syn::Ident::new("field1", Span::call_site())), ty: LitStr::new("SubItemPatch", Span::call_site()) .parse() .unwrap(), attributes: vec![], retyped: true, #[cfg(feature = "op")] addable: Addable::Disable, }], }; let result = Patch::from_ast(syn::parse2(input).unwrap()).unwrap(); assert_eq_sorted!( format!("{:?}", result.to_token_stream()), format!("{:?}", expected.to_token_stream()) ); } }