Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
29 changes: 16 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>) -> Option<<Self as StructDiff>::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<usize>) -> Option<<Self as StructDiff>::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.
Expand Down
49 changes: 36 additions & 13 deletions derive/src/difference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
Expand Down Expand Up @@ -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<String> =
struct_.generics.iter().map(|x| x.full()).collect();

Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions derive/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ pub fn attrs_map_strategy(attributes: &[crate::parse::Attribute]) -> Option<MapS
}
})
}

pub fn attrs_expose(attributes: &[crate::parse::Attribute]) -> Option<Option<String>> {
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,
})
}
49 changes: 14 additions & 35 deletions src/collections/ordered_array_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = usize>,
Box<dyn Fn() -> Box<dyn Iterator<Item = usize>>>,
Expand Down Expand Up @@ -750,9 +751,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}
}
Expand All @@ -765,18 +764,14 @@ mod test {
let s1_vec = s1.chars().collect::<Vec<_>>();
let s2_vec = s2.chars().collect::<Vec<_>>();

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::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}
}
Expand All @@ -786,10 +781,8 @@ mod test {
let s1: Vec<char> = "abc".chars().collect();
let s2: Vec<char> = "".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;
Expand Down Expand Up @@ -870,9 +863,7 @@ mod test {
continue;
};

let changed = apply(changes, s2_vec.clone())
.into_iter()
.collect::<Vec<char>>();
let changed = apply(changes, s2_vec.clone()).collect::<Vec<char>>();
assert_eq!(&s1_vec, &changed);
}
}
Expand Down Expand Up @@ -1005,9 +996,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}

Expand All @@ -1024,9 +1013,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}

Expand All @@ -1043,9 +1030,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}
}
Expand All @@ -1063,9 +1048,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}
}
Expand All @@ -1083,9 +1066,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}
}
Expand All @@ -1103,9 +1084,7 @@ mod test {
return;
};

let changed = apply(changes, s2.chars().collect::<Vec<_>>())
.into_iter()
.collect::<String>();
let changed = apply(changes, s2.chars().collect::<Vec<_>>()).collect::<String>();
assert_eq!(s1, changed)
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/collections/unordered_array_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
),
));
Expand Down Expand Up @@ -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)),
)
}

Expand Down
4 changes: 2 additions & 2 deletions src/collections/unordered_map_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)));
}
Expand Down Expand Up @@ -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::<Vec<_>>()
.into_iter(),
)
Expand Down
2 changes: 1 addition & 1 deletion tests/derives.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(unused_imports)]
#![allow(unused_imports, clippy::type_complexity)]

use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
Expand Down
Loading
Loading