From e84e5c14927323ac61c122c813e72d9aee656701 Mon Sep 17 00:00:00 2001 From: kirk Date: Sat, 22 Feb 2025 20:38:03 +0000 Subject: [PATCH] add #[difference(export)] attribute to allow pub diff types --- Cargo.toml | 1 + README.md | 29 +++++----- derive/src/difference.rs | 49 ++++++++++++----- derive/src/lib.rs | 3 +- derive/src/shared.rs | 8 +++ src/collections/ordered_array_like.rs | 49 +++++------------ src/collections/unordered_array_like.rs | 4 +- src/collections/unordered_map_like.rs | 4 +- tests/derives.rs | 2 +- tests/expose.rs | 72 +++++++++++++++++++++++++ tests/integration.rs | 14 +++-- tests/types.rs | 6 +-- 12 files changed, 163 insertions(+), 78 deletions(-) create mode 100644 tests/expose.rs diff --git a/Cargo.toml b/Cargo.toml index 30e4bd8..079ec70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/knickish/structdiff" description = """zero-dependency crate for generating and applying partial diffs between struct instances""" keywords = ["delta-compression", "difference"] categories = ["compression"] +rust-version = "1.82.0" [dependencies] nanoserde = { version = "^0.1.37", optional = true } diff --git a/README.md b/README.md index 1c28a20..101c8f4 100644 --- a/README.md +++ b/README.md @@ -48,19 +48,22 @@ assert_ne!(diffed, second); For more examples take a look at [integration tests](/tests) ## Derive macro attributes -- `#[difference(skip)]` - Do not consider this field when creating a diff -- `#[difference(recurse)]` - Generate a StructDiff for this field when creating a diff -- `#[difference(collection_strategy = {})]` - - `"ordered_array_like"` - Generates a minimal changeset for ordered, array-like collections of items which implement `PartialEq`. (uses levenshtein difference) - - `"unordered_array_like"` - Generates a minimal changeset for unordered, array-like collections of items which implement `Hash + Eq`. - - `"unordered_map_like"` - Generates a minimal changeset for unordered, map-like collections for which the key implements `Hash + Eq`. -- `#[difference(map_equality = {})]` - Used with `unordered_map_like` - - `"key_only"` - only replace a key-value pair for which the key has changed - - `"key_and_value"` - replace a key-value pair if either the key or value has changed -- `#[difference(setters)]` - Generate setters for all fields in the struct (used on struct) - - Example: for the `field1` of the `Example` struct used above, a function with the signature `set_field1_with_diff(&mut self, value: Option) -> Option<::Diff>` will be generated. Useful when a single field will be changed in a struct with many fields, as it saves the comparison of all other fields. -- `#[difference(setter)]` - Generate setters for this struct field (used on field) -- `#[difference(setter_name = {})]` - Use this name instead of the default value when generating a setter for this field (used on field) +- Field level + - `#[difference(skip)]` - Do not consider this field when creating a diff + - `#[difference(recurse)]` - Generate a StructDiff for this field when creating a diff + - `#[difference(collection_strategy = {})]` + - `"ordered_array_like"` - Generates a minimal changeset for ordered, array-like collections of items which implement `PartialEq`. (uses levenshtein difference) + - `"unordered_array_like"` - Generates a minimal changeset for unordered, array-like collections of items which implement `Hash + Eq`. + - `"unordered_map_like"` - Generates a minimal changeset for unordered, map-like collections for which the key implements `Hash + Eq`. + - `#[difference(map_equality = {})]` - Used with `unordered_map_like` + - `"key_only"` - only replace a key-value pair for which the key has changed + - `"key_and_value"` - replace a key-value pair if either the key or value has changed + - `#[difference(setter)]` - Generate setters for this struct field + - `#[difference(setter_name = {})]` - Use this name instead of the default value when generating a setter for this field (used on field) +- Struct Level + - `#[difference(setters)]` - Generate setters for all fields in the struct + - Example: for the `field1` of the `Example` struct used above, a function with the signature `set_field1_with_diff(&mut self, value: Option) -> Option<::Diff>` will be generated. Useful when a single field will be changed in a struct with many fields, as it saves the comparison of all other fields. + - `#[difference(expose)]`/`#[difference(expose = "MyDiffTypeName")]` - expose the generated difference type (optionally, with the specified name) ## Optional features - [`nanoserde`, `serde`] - Serialization of `Difference` derived associated types. Allows diffs to easily be sent over network. diff --git a/derive/src/difference.rs b/derive/src/difference.rs index 156ccd2..4898384 100644 --- a/derive/src/difference.rs +++ b/derive/src/difference.rs @@ -6,7 +6,7 @@ use alloc::string::String; use crate::parse::{Category, ConstValType, Enum, Generic, Struct, Type}; #[cfg(feature = "generated_setters")] use crate::shared::{attrs_all_setters, attrs_setter}; -use crate::shared::{attrs_collection_type, attrs_recurse, attrs_skip}; +use crate::shared::{attrs_collection_type, attrs_expose, attrs_recurse, attrs_skip}; use proc_macro::TokenStream; fn get_used_lifetimes(ty: &Type) -> Vec { @@ -108,8 +108,16 @@ pub(crate) fn derive_struct_diff_struct(struct_: &Struct) -> TokenStream { #[cfg(feature = "generated_setters")] let mut setters_body = String::new(); - let enum_name = - String::from("__".to_owned() + struct_.name.as_ref().unwrap().as_str() + "StructDiffEnum"); + let exposed = attrs_expose(&struct_.attributes); + + let enum_name = match exposed.clone() { + Some(Some(name)) => name, + Some(None) => String::from(struct_.name.as_ref().unwrap().to_string() + "StructDiffEnum"), + _ => String::from( + "__".to_owned() + struct_.name.as_ref().unwrap().as_str() + "StructDiffEnum", + ), + }; + let struct_generics_names_hash: HashSet = struct_.generics.iter().map(|x| x.full()).collect(); @@ -1000,14 +1008,13 @@ pub(crate) fn derive_struct_diff_struct(struct_: &Struct) -> TokenStream { "" }; + let const_start = "#[allow(non_camel_case_types)]\nconst _: () = {"; + format!( - "#[allow(non_camel_case_types)] - const _: () = {{ - use structdiff::collections::*; + "{non_exposed_const_start} {type_aliases} {ref_type_aliases} {nanoserde_hack} - #[allow(non_camel_case_types)] /// Generated type from StructDiff #[derive({owned_derives})]{serde_bounds} @@ -1027,6 +1034,9 @@ pub(crate) fn derive_struct_diff_struct(struct_: &Struct) -> TokenStream { {{ {diff_ref_enum_body} }} + {exposed_const_start} + + impl{ref_enum_def_generics} Into<{enum_name}{owned_enum_impl_generics}> for {enum_name}Ref{ref_enum_impl_generics} where @@ -1070,6 +1080,8 @@ pub(crate) fn derive_struct_diff_struct(struct_: &Struct) -> TokenStream { {setters} }};", + non_exposed_const_start = if exposed.is_some() { "" } else { const_start }, + exposed_const_start = if exposed.is_some() { const_start } else { "" }, type_aliases = owned_type_aliases, ref_type_aliases = ref_type_aliases, nanoserde_hack = nanoserde_hack, @@ -1253,7 +1265,14 @@ pub(crate) fn derive_struct_diff_enum(enum_: &Enum) -> TokenStream { let mut type_aliases = String::new(); let mut used_generics: Vec<&Generic> = Vec::new(); - let enum_name = String::from("__".to_owned() + enum_.name.as_str() + "StructDiffEnum"); + let exposed = attrs_expose(&enum_.attributes); + + let enum_name = match exposed.clone() { + Some(Some(name)) => name, + Some(None) => String::from(enum_.name.clone() + "StructDiffEnum"), + _ => String::from("__".to_owned() + &enum_.name + "StructDiffEnum"), + }; + let ref_into_owned_body = format!( "Self::Replace(variant) => {}::Replace(variant.clone()),", &enum_name @@ -1425,12 +1444,10 @@ pub(crate) fn derive_struct_diff_enum(enum_: &Enum) -> TokenStream { #[cfg(not(feature = "serde"))] let serde_bound = ""; - format!( - "const _: () = {{ - use structdiff::collections::*; - {type_aliases} - {nanoserde_hack} + let const_start = "#[allow(non_camel_case_types)]\nconst _: () = {"; + format!( + "{non_exposed_const_start} /// Generated type from StructDiff #[derive({owned_derives})]{serde_bounds} #[allow(non_camel_case_types)] @@ -1450,6 +1467,10 @@ pub(crate) fn derive_struct_diff_enum(enum_: &Enum) -> TokenStream { {{ Replace(&'__diff_target {struct_name}{struct_generics}) }} + {exposed_const_start} + + {type_aliases} + {nanoserde_hack} impl{ref_enum_def_generics} Into<{enum_name}{enum_impl_generics}> for {enum_name}Ref{ref_enum_impl_generics} where @@ -1500,6 +1521,8 @@ pub(crate) fn derive_struct_diff_enum(enum_: &Enum) -> TokenStream { }} }} }};", + non_exposed_const_start = if exposed.is_some() { "" } else { const_start }, + exposed_const_start = if exposed.is_some() { const_start } else { "" }, type_aliases = type_aliases, nanoserde_hack = nanoserde_hack, owned_derives = owned_derives, diff --git a/derive/src/lib.rs b/derive/src/lib.rs index f57e897..095a408 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -16,11 +16,10 @@ mod parse; pub fn derive_struct_diff(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse::parse_data(input); - // ok we have an ident, hopefully it's a struct let ts = match &input { parse::Data::Struct(struct_) if struct_.named => derive_struct_diff_struct(struct_), parse::Data::Enum(enum_) => derive_struct_diff_enum(enum_), - _ => unimplemented!("Only structs are supported"), + _ => unimplemented!("Only structs and enums are supported"), }; ts diff --git a/derive/src/shared.rs b/derive/src/shared.rs index 5339091..2e890c9 100644 --- a/derive/src/shared.rs +++ b/derive/src/shared.rs @@ -100,3 +100,11 @@ pub fn attrs_map_strategy(attributes: &[crate::parse::Attribute]) -> Option Option> { + attributes.iter().find_map(|attr| match attr.tokens.len() { + 1 if attr.tokens[0].starts_with("expose") => Some(None), + 2.. if attr.tokens[0] == "expose" => Some(Some((&attr.tokens[1]).to_string())), + _ => return None, + }) +} diff --git a/src/collections/ordered_array_like.rs b/src/collections/ordered_array_like.rs index 9e3fc2c..fe00dd7 100644 --- a/src/collections/ordered_array_like.rs +++ b/src/collections/ordered_array_like.rs @@ -237,6 +237,7 @@ fn create_last_change_row<'src, 'target: 'src, T: Clone + PartialEq + 'target>( let mut target_forward = target_start..target_end; let mut target_rev = (target_end..target_start).rev(); + #[allow(clippy::type_complexity)] let (target_range, source_range): ( &mut dyn Iterator, Box Box>>, @@ -750,9 +751,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } } @@ -765,18 +764,14 @@ mod test { let s1_vec = s1.chars().collect::>(); let s2_vec = s2.chars().collect::>(); - for diff_type in [ - // levenshtein, - hirschberg, - ] { + { + let diff_type = hirschberg; let Some(changes) = diff_type(&s1_vec, &s2_vec) else { assert_eq!(&s1_vec, &s2_vec); return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } } @@ -786,10 +781,8 @@ mod test { let s1: Vec = "abc".chars().collect(); let s2: Vec = "".chars().collect(); - for diff_type in [ - // levenshtein, - hirschberg, - ] { + { + let diff_type = hirschberg; let Some(changes) = diff_type(&s1, &s2) else { assert_eq!(s1, s2); return; @@ -870,9 +863,7 @@ mod test { continue; }; - let changed = apply(changes, s2_vec.clone()) - .into_iter() - .collect::>(); + let changed = apply(changes, s2_vec.clone()).collect::>(); assert_eq!(&s1_vec, &changed); } } @@ -1005,9 +996,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } @@ -1024,9 +1013,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } @@ -1043,9 +1030,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } } @@ -1063,9 +1048,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } } @@ -1083,9 +1066,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } } @@ -1103,9 +1084,7 @@ mod test { return; }; - let changed = apply(changes, s2.chars().collect::>()) - .into_iter() - .collect::(); + let changed = apply(changes, s2.chars().collect::>()).collect::(); assert_eq!(s1, changed) } } diff --git a/src/collections/unordered_array_like.rs b/src/collections/unordered_array_like.rs index b45e80a..b088f77 100644 --- a/src/collections/unordered_array_like.rs +++ b/src/collections/unordered_array_like.rs @@ -166,7 +166,7 @@ pub fn unordered_hashcmp< UnorderedArrayLikeDiffInternal::Replace( current .into_iter() - .flat_map(|(k, v)| std::iter::repeat(k).take(v)) + .flat_map(|(k, v)| std::iter::repeat_n(k, v)) .collect(), ), )); @@ -333,7 +333,7 @@ where Box::new( list_hash .into_iter() - .flat_map(|(k, v)| std::iter::repeat(k).take(v)), + .flat_map(|(k, v)| std::iter::repeat_n(k, v)), ) } diff --git a/src/collections/unordered_map_like.rs b/src/collections/unordered_map_like.rs index b247cd1..4589f5f 100644 --- a/src/collections/unordered_map_like.rs +++ b/src/collections/unordered_map_like.rs @@ -167,7 +167,7 @@ pub fn unordered_hashcmp< return Some(UnorderedMapLikeDiff(UnorderedMapLikeDiffInternal::Replace( current .into_iter() - .flat_map(|(k, (v, count))| std::iter::repeat((k, v)).take(count)) + .flat_map(|(k, (v, count))| std::iter::repeat_n((k, v), count)) .collect(), ))); } @@ -319,7 +319,7 @@ pub fn apply_unordered_hashdiffs< Box::new( list_hash .into_iter() - .flat_map(|(k, (v, count))| std::iter::repeat((k.clone(), v.clone())).take(count)) + .flat_map(|(k, (v, count))| std::iter::repeat_n((k.clone(), v.clone()), count)) .collect::>() .into_iter(), ) diff --git a/tests/derives.rs b/tests/derives.rs index c655ed1..2c684e6 100644 --- a/tests/derives.rs +++ b/tests/derives.rs @@ -1,4 +1,4 @@ -#![allow(unused_imports)] +#![allow(unused_imports, clippy::type_complexity)] use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, diff --git a/tests/expose.rs b/tests/expose.rs new file mode 100644 index 0000000..11413c9 --- /dev/null +++ b/tests/expose.rs @@ -0,0 +1,72 @@ +#![allow(unused_imports)] + +use assert_unordered::{assert_eq_unordered, assert_eq_unordered_sort}; + +use std::f64::consts::PI; +use std::hash::Hash; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet, LinkedList}, + fmt::Debug, + num::Wrapping, +}; +use structdiff::{Difference, StructDiff}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "nanoserde")] +use nanoserde::{DeBin, SerBin}; + +#[test] +fn test_expose() { + #[derive(Debug, PartialEq, Clone, Difference)] + #[difference(expose)] + struct Example { + field1: f64, + } + + let first = Example { field1: 0.0 }; + + let second = Example { field1: PI }; + + for diff in first.diff(&second) { + match diff { + ExampleStructDiffEnum::field1(v) => { + dbg!(&v); + } + } + } + + for diff in first.diff_ref(&second) { + match diff { + ExampleStructDiffEnumRef::field1(v) => { + dbg!(&v); + } + } + } +} + +#[test] +fn test_expose_rename() { + #[derive(Debug, PartialEq, Clone, Difference)] + #[difference(expose = "Cheese")] + struct Example { + field1: f64, + } + + let first = Example { field1: 0.0 }; + + let second = Example { field1: PI }; + + for diff in first.diff(&second) { + match diff { + Cheese::field1(_v) => {} + } + } + + for diff in first.diff_ref(&second) { + match diff { + CheeseRef::field1(_v) => {} + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 8434c06..ee11973 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2,10 +2,12 @@ mod derives; mod enums; +mod expose; mod types; use assert_unordered::{assert_eq_unordered, assert_eq_unordered_sort}; pub use types::{RandValue, Test, TestEnum, TestSkip}; +use std::f32::consts::PI; use std::hash::Hash; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet, LinkedList}, @@ -49,7 +51,7 @@ fn test_example() { }; let second = Example { - field1: 3.14, + field1: PI as f64, field2: vec![1], field3: vec![2, 3, 4].into_iter().collect(), }; @@ -80,7 +82,7 @@ fn test_derive() { test1: first.test1, test2: String::from("Hello Diff"), test3: vec![1], - test4: 3.14, + test4: PI, test5: Some(12), }; @@ -104,7 +106,7 @@ fn test_derive_with_skip() { test1: first.test1, test2: String::from("Hello Diff"), test3skip: vec![1], - test4: 3.14, + test4: PI, }; let diffs = first.diff(&second); @@ -283,6 +285,8 @@ fn test_enums() { } mod derive_inner { + use std::f32::consts::PI; + use super::{StructDiff, Test}; //tests that the associated type does not need to be exported manually @@ -300,7 +304,7 @@ mod derive_inner { test1: first.test1, test2: String::from("Hello Diff"), test3: vec![1], - test4: 3.14, + test4: PI, test5: Some(13), }; @@ -354,7 +358,7 @@ fn test_recurse() { test1: 2, test2: String::new(), test3: Vec::new(), - test4: 3.14, + test4: PI, test5: None, }, test3: Some(Test::default()), diff --git a/tests/types.rs b/tests/types.rs index 7322d61..2b8f676 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -145,11 +145,7 @@ mod generators { pub(super) fn rand_bool(rng: &mut WyRand) -> bool { let base = rng.generate::() as usize; - if base % 2 == 0 { - true - } else { - false - } + base % 2 == 0 } pub(super) fn rand_string(rng: &mut WyRand) -> String {