diff --git a/CHANGELOG.md b/CHANGELOG.md index c84abe7..ab3932b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## Unreleased - ReleaseDate +### Added + +- New `IntoChanges` trait and a parallel `*Changes` type per diff, projecting a diff to the subset of nodes that actually changed. Built-in implementations cover `Leaf`, the map/set diffs, and tuples. +- The `Diffable` derive macro now recognizes `#[daft(changes)]` on structs as opt-in to generating a parallel `FooChanges` type and an `IntoChanges` impl. Opting in is required because the projection needs `Eq` (and, with the new `serde` feature, `Serialize`) on every leaf type — constraints existing users may not satisfy. +- New `serde` feature emits `Serialize` impls for `Leaf` and every `*Changes` type, so a projected diff can be written to JSON (or any other serde format) with unchanged subtrees omitted entirely. + ## [0.1.6] - 2026-05-14 ### Added diff --git a/Cargo.lock b/Cargo.lock index 5b98dc2..b623cd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,8 @@ dependencies = [ "newtype-uuid", "oxnet", "paste", + "serde", + "serde_json", "uuid", ] @@ -169,6 +171,8 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", + "serde", + "serde_json", "syn", "trybuild", "uuid", @@ -269,6 +273,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", + "serde", + "serde_core", ] [[package]] @@ -400,12 +406,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -441,18 +441,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -472,14 +482,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -717,3 +728,9 @@ checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" dependencies = [ "memchr", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index a248453..7fbcd64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ paste = "1.0.15" prettyplease = "0.2.29" proc-macro2 = "1.0" quote = "1.0" +serde = { version = "1.0.219", default-features = false } +serde_json = "1.0.140" syn = "2.0" trybuild = "1.0.103" uuid = "1.12.0" diff --git a/daft-derive/Cargo.toml b/daft-derive/Cargo.toml index cdb8dd6..0f8f61e 100644 --- a/daft-derive/Cargo.toml +++ b/daft-derive/Cargo.toml @@ -21,11 +21,19 @@ proc-macro2.workspace = true quote.workspace = true syn = { workspace = true, features = ["full", "visit"] } +[features] +# Mirror of daft's `serde` feature: emits the hand-written `Serialize` impl +# on `*Changes` structs alongside the `IntoChanges` impl. Enabled +# transitively when a downstream crate turns on `daft/serde`. +serde = [] + [dev-dependencies] -daft = { workspace = true, features = ["derive"] } +daft = { workspace = true, features = ["derive", "serde"] } datatest-stable.workspace = true expectorate.workspace = true prettyplease.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true trybuild.workspace = true uuid = { workspace = true, features = ["v4"] } diff --git a/daft-derive/src/internals/imp.rs b/daft-derive/src/internals/imp.rs index dddf87e..5f1ed66 100644 --- a/daft-derive/src/internals/imp.rs +++ b/daft-derive/src/internals/imp.rs @@ -270,12 +270,19 @@ fn make_struct_impl( StructMode::Default => make_diff_struct(input, s, errors.new_child()) .map(|(generated_struct, diff_fields)| { let diff_impl = make_diff_impl(input, &diff_fields); + let changes_items = if struct_config.changes { + make_changes_items(input, s, &diff_fields) + } else { + TokenStream::new() + }; // Uncomment for some debugging // eprintln!("{generated_struct}"); // eprintln!("{diff_impl}"); + // eprintln!("{changes_items}"); quote! { #generated_struct #diff_impl + #changes_items } }), StructMode::Leaf => { @@ -284,6 +291,27 @@ fn make_struct_impl( } } +/// Emit the `*Changes` struct, its inherent impls, the `IntoChanges` impl +/// on the diff, and — when the `serde` feature is on — a hand-written +/// `Serialize` impl that skips `None` fields. +fn make_changes_items( + input: &DeriveInput, + s: &DataStruct, + diff_fields: &DiffFields, +) -> TokenStream { + let changes_struct = make_changes_struct(input, s, diff_fields); + let into_changes_impl = make_into_changes_impl(input, s, diff_fields); + #[cfg(feature = "serde")] + let serialize_impl = make_serialize_impl(input, diff_fields); + #[cfg(not(feature = "serde"))] + let serialize_impl = TokenStream::new(); + quote! { + #changes_struct + #into_changes_impl + #serialize_impl + } +} + /// Create the `Diff` struct fn make_diff_struct( input: &DeriveInput, @@ -370,96 +398,94 @@ fn make_diff_struct( } }; - // Generate PartialEq, Eq, and Debug implementations for the diff struct. We - // can't rely on `#[derive] because we want to put bounds on the - // Diffable::Diff types, not on the original types. - let (impl_gen, ty_gen, _) = &new_generics.split_for_impl(); - - let debug_impl = { - let where_clause = diff_fields.where_clause_with_trait_bound( - &parse_quote! { ::core::fmt::Debug }, - ); - let members = diff_fields.fields.members(); + // We can't `#[derive]` Debug/PartialEq/Eq here because the rustc-emitted + // bounds would apply to the original type parameters; we need bounds on + // the projected `::Diff<'__daft>` types instead. + let trait_impls = make_projected_trait_impls( + &name, + &new_generics, + &diff_fields.fields, + non_exhaustive.is_some(), + |bound| diff_fields.where_clause_with_trait_bound(bound), + ); + + Some((quote! { #struct_def #trait_impls }, diff_fields)) +} - let finish = if non_exhaustive.is_some() { - quote! { .finish_non_exhaustive() } - } else { - quote! { .finish() } - }; +/// Emit `Debug`, `PartialEq`, and `Eq` impls for a generated `*Diff` or +/// `*Changes` struct. +/// +/// The two callers differ only in how field-type bounds are projected onto +/// the where clause — the diff impls bound `::Diff<'__daft>` +/// while the changes impls bound `< as IntoChanges>::Changes`. The +/// `where_clause_for` closure encapsulates that difference; everything else +/// (debug body shape, partial-eq fold, non-exhaustive finish) is shared. +fn make_projected_trait_impls( + name: &Path, + generics: &Generics, + fields: &Fields, + non_exhaustive: bool, + mut where_clause_for: impl FnMut(&syn::TraitBound) -> WhereClause, +) -> TokenStream { + let (impl_gen, ty_gen, _) = generics.split_for_impl(); + let finish = if non_exhaustive { + quote! { .finish_non_exhaustive() } + } else { + quote! { .finish() } + }; - let debug_body = match &s.fields { - Fields::Named(_) => { - quote! { - f.debug_struct(stringify!(#name)) - #( - .field(stringify!(#members), &self.#members) - )* - #finish - } - } + let debug_impl = { + let where_clause = + where_clause_for(&parse_quote! { ::core::fmt::Debug }); + let members = fields.members(); + let body = match fields { + Fields::Named(_) => quote! { + f.debug_struct(stringify!(#name)) + #( .field(stringify!(#members), &self.#members) )* + #finish + }, Fields::Unnamed(_) => quote! { f.debug_tuple(stringify!(#name)) - #( - .field(&self.#members) - )* + #( .field(&self.#members) )* #finish }, Fields::Unit => quote! { - f.debug_struct(stringify!(#name)) - #finish + f.debug_struct(stringify!(#name)) #finish }, }; quote! { impl #impl_gen ::core::fmt::Debug for #name #ty_gen #where_clause { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - #debug_body + #body } } } }; let partial_eq_impl = { - let where_clause = diff_fields.where_clause_with_trait_bound( - &parse_quote! { ::core::cmp::PartialEq }, - ); - let members = diff_fields.fields.members(); - - // Return true if there aren't any fields to compare. - let partial_eq_body: Expr = if diff_fields.fields.is_empty() { + let where_clause = + where_clause_for(&parse_quote! { ::core::cmp::PartialEq }); + let members = fields.members(); + let body: Expr = if fields.is_empty() { parse_quote! { true } } else { - parse_quote! { - #(self.#members == other.#members) && * - } + parse_quote! { #(self.#members == other.#members) && * } }; - quote! { impl #impl_gen ::core::cmp::PartialEq for #name #ty_gen #where_clause { - fn eq(&self, other: &Self) -> bool { - #partial_eq_body - } + fn eq(&self, other: &Self) -> bool { #body } } } }; let eq_impl = { - let where_clause = diff_fields - .where_clause_with_trait_bound(&parse_quote! { ::core::cmp::Eq }); - + let where_clause = where_clause_for(&parse_quote! { ::core::cmp::Eq }); quote! { impl #impl_gen ::core::cmp::Eq for #name #ty_gen #where_clause {} } }; - Some(( - quote! { - #struct_def - #debug_impl - #partial_eq_impl - #eq_impl - }, - diff_fields, - )) + quote! { #debug_impl #partial_eq_impl #eq_impl } } /// Impl `Diffable` for the original struct @@ -512,6 +538,312 @@ fn make_diff_impl( } } +/// Emit the `*Changes` struct definition and its `Debug`/`PartialEq`/`Eq` +/// impls. Each field is wrapped in `Option<...>` so unchanged subtrees can +/// be represented as `None`. +fn make_changes_struct( + input: &DeriveInput, + s: &DataStruct, + diff_fields: &DiffFields, +) -> TokenStream { + let vis = &input.vis; + let name = parse_str::(&format!("{}Changes", input.ident)).unwrap(); + let non_exhaustive = + input.attrs.iter().find(|attr| attr.path().is_ident("non_exhaustive")); + + let daft_lt = daft_lifetime(); + let daft_crate = daft_crate(); + let new_generics = add_lifetime_to_generics(input, &daft_lt); + let where_clause = diff_fields.where_clause_with_trait_bound( + &parse_quote! { #daft_crate::IntoChanges }, + ); + + let phantom_ty = { + let ident = &input.ident; + let (_, orig_ty_gen, _) = input.generics.split_for_impl(); + quote! { + ::core::marker::PhantomData &#daft_lt #ident #orig_ty_gen> + } + }; + + let changes_field_ty = |ty: &syn::Type| -> TokenStream { + quote_spanned! {ty.span()=> + ::core::option::Option<<#ty as #daft_crate::IntoChanges>::Changes> + } + }; + + // The struct *shape* (named/unnamed/unit) comes from `s.fields`, but + // field types come from `diff_fields.fields` since those carry the + // projected `::Diff<'__daft>` form. + let struct_def = if diff_fields.fields.is_empty() { + match &s.fields { + Fields::Named(_) | Fields::Unit => quote! { + #non_exhaustive + #vis struct #name #new_generics #where_clause { + _phantom: #phantom_ty, + } + }, + Fields::Unnamed(_) => quote! { + #non_exhaustive + #vis struct #name #new_generics (#phantom_ty) #where_clause; + }, + } + } else { + match &diff_fields.fields { + Fields::Named(fields) => { + let entries = fields.named.iter().map(|f| { + let field_vis = &f.vis; + let field_name = f.ident.as_ref().unwrap(); + let ty = changes_field_ty(&f.ty); + quote_spanned! {f.span()=> + #field_vis #field_name: #ty, + } + }); + quote! { + #non_exhaustive + #vis struct #name #new_generics #where_clause { + #(#entries)* + } + } + } + Fields::Unnamed(fields) => { + let entries = fields.unnamed.iter().map(|f| { + let field_vis = &f.vis; + let ty = changes_field_ty(&f.ty); + quote_spanned! {f.span()=> + #field_vis #ty + } + }); + quote! { + #non_exhaustive + #vis struct #name #new_generics (#(#entries),*) #where_clause; + } + } + Fields::Unit => unreachable!( + "Fields::Unit always produces an empty changes struct" + ), + } + }; + + let trait_impls = make_projected_trait_impls( + &name, + &new_generics, + &diff_fields.fields, + non_exhaustive.is_some(), + |bound| diff_fields.changes_where_clause_with_trait_bound(bound), + ); + + quote! { #struct_def #trait_impls } +} + +/// Implement `IntoChanges` on the generated `*Diff` struct. Projects every +/// field to its changes-only representation and returns `None` if every +/// projection was itself `None` (no leaf changed). +fn make_into_changes_impl( + input: &DeriveInput, + s: &DataStruct, + diff_fields: &DiffFields, +) -> TokenStream { + let ident = &input.ident; + let diff_name = parse_str::(&format!("{ident}Diff")).unwrap(); + let changes_name = parse_str::(&format!("{ident}Changes")).unwrap(); + let daft_crate = daft_crate(); + let daft_lt = daft_lifetime(); + let new_generics = add_lifetime_to_generics(input, &daft_lt); + let (impl_gen, ty_gen, _) = &new_generics.split_for_impl(); + let where_clause = diff_fields.where_clause_with_trait_bound( + &parse_quote! { #daft_crate::IntoChanges }, + ); + + if diff_fields.fields.is_empty() { + return quote! { + impl #impl_gen #daft_crate::IntoChanges for #diff_name #ty_gen + #where_clause + { + type Changes = #changes_name #ty_gen; + + fn into_changes(self) -> ::core::option::Option { + ::core::option::Option::None + } + } + }; + } + + // One `__daft_` binding per field so the construction + // step below can name them positionally even for tuple structs. + let bindings: Vec = diff_fields + .fields + .iter() + .enumerate() + .map(|(i, f)| { + let binding = binding_ident(f, i); + let access = match &f.ident { + Some(ident) => quote! { self.#ident }, + None => { + let idx: Index = i.into(); + quote! { self.#idx } + } + }; + quote_spanned! {f.span()=> + let #binding = #daft_crate::IntoChanges::into_changes(#access); + } + }) + .collect(); + + let binding_names: Vec = diff_fields + .fields + .iter() + .enumerate() + .map(|(i, f)| binding_ident(f, i)) + .collect(); + + let constructor = match &s.fields { + Fields::Named(_) => { + let entries = diff_fields.fields.iter().zip(&binding_names).map( + |(f, binding)| { + let name = f.ident.as_ref().unwrap(); + quote! { #name: #binding } + }, + ); + quote! { #changes_name { #(#entries),* } } + } + Fields::Unnamed(_) => { + quote! { #changes_name(#(#binding_names),*) } + } + Fields::Unit => unreachable!( + "Fields::Unit handled in the empty-fields branch above" + ), + }; + + quote! { + impl #impl_gen #daft_crate::IntoChanges for #diff_name #ty_gen + #where_clause + { + type Changes = #changes_name #ty_gen; + + fn into_changes(self) -> ::core::option::Option { + #(#bindings)* + if #(#binding_names.is_some())||* { + ::core::option::Option::Some(#constructor) + } else { + ::core::option::Option::None + } + } + } + } +} + +/// Build a `__daft_` binding identifier. The prefix keeps tuple +/// indices valid as idents and avoids colliding with user field names. +fn binding_ident(f: &Field, index: usize) -> syn::Ident { + match &f.ident { + Some(ident) => { + syn::Ident::new(&format!("__daft_{ident}"), ident.span()) + } + None => syn::Ident::new(&format!("__daft_{index}"), f.span()), + } +} + +/// Implement `serde::Serialize` on the generated `*Changes` struct. +/// +/// Hand-written instead of `#[derive(Serialize)]` because serde's +/// auto-bound generator cannot follow projected associated types like +/// `<::Diff<'__daft> as IntoChanges>::Changes`, and the +/// derived impl fails to type-check. We pair an explicit where clause with +/// per-field `if let Some(...)` so unchanged subtrees serialize to nothing. +/// +/// This is never called for empty `*Changes` because `into_changes` on the +/// corresponding `*Diff` returns `None` in that case — no value of the +/// empty struct ever reaches `Serialize`. +#[cfg(feature = "serde")] +fn make_serialize_impl( + input: &DeriveInput, + diff_fields: &DiffFields, +) -> TokenStream { + if diff_fields.fields.is_empty() { + return TokenStream::new(); + } + + let name = parse_str::(&format!("{}Changes", input.ident)).unwrap(); + let daft_crate = daft_crate(); + let daft_lt = daft_lifetime(); + let new_generics = add_lifetime_to_generics(input, &daft_lt); + let (impl_gen, ty_gen, _) = &new_generics.split_for_impl(); + + let where_clause = diff_fields.changes_where_clause_with_trait_bound( + &parse_quote! { #daft_crate::__private_serde::Serialize }, + ); + + let members: Vec<_> = diff_fields.fields.members().collect(); + let is_tuple = matches!(&diff_fields.fields, Fields::Unnamed(_)); + + let count_expr = quote! {{ + let mut __count = 0usize; + #( if self.#members.is_some() { __count += 1; } )* + __count + }}; + + let (begin, end_trait) = if is_tuple { + ( + quote! { + #daft_crate::__private_serde::Serializer::serialize_tuple_struct( + serializer, stringify!(#name), #count_expr, + )? + }, + quote! { #daft_crate::__private_serde::ser::SerializeTupleStruct }, + ) + } else { + ( + quote! { + #daft_crate::__private_serde::Serializer::serialize_struct( + serializer, stringify!(#name), #count_expr, + )? + }, + quote! { #daft_crate::__private_serde::ser::SerializeStruct }, + ) + }; + + let serialize_fields = members.iter().enumerate().map(|(i, member)| { + let call = if is_tuple { + quote! { #end_trait::serialize_field(&mut __state, __value)? } + } else { + // Named-field key matches the derived struct's field name. + let key = match member { + syn::Member::Named(id) => quote! { stringify!(#id) }, + syn::Member::Unnamed(_) => { + let s = i.to_string(); + quote! { #s } + } + }; + quote! { #end_trait::serialize_field(&mut __state, #key, __value)? } + }; + quote! { + if let ::core::option::Option::Some(__value) = &self.#member { + #call; + } + } + }); + + quote! { + #[automatically_derived] + impl #impl_gen #daft_crate::__private_serde::Serialize for #name #ty_gen + #where_clause + { + fn serialize<__DaftS>( + &self, + serializer: __DaftS, + ) -> ::core::result::Result<__DaftS::Ok, __DaftS::Error> + where + __DaftS: #daft_crate::__private_serde::Serializer, + { + let mut __state = #begin; + #( #serialize_fields )* + #end_trait::end(__state) + } + } + } +} + /// For a `Diff` struct generated by this derive macro, tracks the fields that /// will be put into that struct. /// @@ -652,6 +984,37 @@ impl DiffFields { where_clause } + + /// Returns an iterator over the *changes*-projected field types — i.e. + /// `::Changes` for each field. Used by + /// the Changes-side struct definition and its inherent trait impls. + fn changes_types(&self) -> impl Iterator + '_ { + let daft_crate = daft_crate(); + self.types().map(move |ty| -> syn::Type { + parse_quote_spanned! {ty.span()=> + <#ty as #daft_crate::IntoChanges>::Changes + } + }) + } + + /// Like [`Self::where_clause_with_trait_bound`], but each predicate is + /// applied to the corresponding changes-projected type rather than the + /// diff type itself. + fn changes_where_clause_with_trait_bound( + &self, + trait_bound: &syn::TraitBound, + ) -> WhereClause { + let predicates = self.changes_types().map(|ty| -> WherePredicate { + parse_quote_spanned! {ty.span()=> + #ty: #trait_bound + } + }); + + let mut where_clause = self.where_clause.clone(); + where_clause.predicates.extend(predicates); + + where_clause + } } impl ToTokens for DiffFields { @@ -699,6 +1062,12 @@ fn generate_field_diffs( #[derive(Debug)] struct StructConfig { mode: StructMode, + /// `#[daft(changes)]`: opt in to emitting the `*Changes` struct, the + /// `IntoChanges` impl, and (with the `serde` feature) a `Serialize` impl + /// on the changes type. Off by default to keep the derive's contract + /// minimal — users with non-`Eq` fields or unbounded type parameters can + /// continue to derive `Diffable` without picking up extra constraints. + changes: bool, } impl StructConfig { @@ -707,6 +1076,7 @@ impl StructConfig { errors: ErrorSink<'_, syn::Error>, ) -> Option { let mut mode = StructMode::Default; + let mut changes = false; for attr in attrs { { @@ -723,10 +1093,17 @@ impl StructConfig { )); } } + } else if meta.path.is_ident("changes") { + if changes { + errors.push_warning(meta.error( + "#[daft(changes)] specified multiple times", + )); + } + changes = true; } else { errors.push_critical(meta.error( "unknown attribute \ - (supported attributes: leaf)", + (supported attributes: leaf, changes)", )); } @@ -740,10 +1117,25 @@ impl StructConfig { } } + // `leaf` already makes the diff a `Leaf`, which has its own + // `IntoChanges` impl. Combining the two attributes is a user error. + if changes && matches!(mode, StructMode::Leaf) { + let span = attrs + .iter() + .find(|a| a.path().is_ident("daft")) + .map(|a| a.to_token_stream()) + .unwrap_or_default(); + errors.push_critical(syn::Error::new_spanned( + span, + "#[daft(changes)] is redundant on `#[daft(leaf)]` structs \ + (their `Leaf` diff already implements `IntoChanges`)", + )); + } + if errors.has_critical_errors() { None } else { - Some(Self { mode }) + Some(Self { mode, changes }) } } } diff --git a/daft-derive/tests/fixtures/invalid/struct-unknown-attribute-multiple.stderr b/daft-derive/tests/fixtures/invalid/struct-unknown-attribute-multiple.stderr index 30d9c42..5dd3f6a 100644 --- a/daft-derive/tests/fixtures/invalid/struct-unknown-attribute-multiple.stderr +++ b/daft-derive/tests/fixtures/invalid/struct-unknown-attribute-multiple.stderr @@ -1,4 +1,4 @@ -error: unknown attribute (supported attributes: leaf) +error: unknown attribute (supported attributes: leaf, changes) --> tests/fixtures/invalid/struct-unknown-attribute-multiple.rs:4:8 | 4 | #[daft(ignore, leaf, leaf)] diff --git a/daft-derive/tests/fixtures/invalid/struct-unknown-attribute.stderr b/daft-derive/tests/fixtures/invalid/struct-unknown-attribute.stderr index c0068bd..55e096d 100644 --- a/daft-derive/tests/fixtures/invalid/struct-unknown-attribute.stderr +++ b/daft-derive/tests/fixtures/invalid/struct-unknown-attribute.stderr @@ -1,4 +1,4 @@ -error: unknown attribute (supported attributes: leaf) +error: unknown attribute (supported attributes: leaf, changes) --> tests/fixtures/invalid/struct-unknown-attribute.rs:4:8 | 4 | #[daft(ignore)] diff --git a/daft-derive/tests/fixtures/serde/leaf_field.json b/daft-derive/tests/fixtures/serde/leaf_field.json new file mode 100644 index 0000000..af15c51 --- /dev/null +++ b/daft-derive/tests/fixtures/serde/leaf_field.json @@ -0,0 +1,10 @@ +{ + "inner": { + "before": { + "value": 1 + }, + "after": { + "value": 2 + } + } +} diff --git a/daft-derive/tests/fixtures/serde/map_field.json b/daft-derive/tests/fixtures/serde/map_field.json new file mode 100644 index 0000000..14ed535 --- /dev/null +++ b/daft-derive/tests/fixtures/serde/map_field.json @@ -0,0 +1,16 @@ +{ + "entries": { + "common": { + "2": { + "before": "beta", + "after": "BETA" + } + }, + "added": { + "4": "delta" + }, + "removed": { + "3": "gamma" + } + } +} diff --git a/daft-derive/tests/fixtures/serde/mixed_fields.json b/daft-derive/tests/fixtures/serde/mixed_fields.json new file mode 100644 index 0000000..9c69917 --- /dev/null +++ b/daft-derive/tests/fixtures/serde/mixed_fields.json @@ -0,0 +1,31 @@ +{ + "version": { + "before": 1, + "after": 2 + }, + "profile": { + "weight": { + "before": 70, + "after": 71 + } + }, + "attrs": { + "common": { + "b": { + "before": 2, + "after": 20 + } + }, + "added": { + "c": 3 + } + }, + "tags": { + "added": [ + "z" + ], + "removed": [ + "y" + ] + } +} diff --git a/daft-derive/tests/fixtures/serde/nested_struct.json b/daft-derive/tests/fixtures/serde/nested_struct.json new file mode 100644 index 0000000..a1cd2dc --- /dev/null +++ b/daft-derive/tests/fixtures/serde/nested_struct.json @@ -0,0 +1,8 @@ +{ + "inner": { + "value": { + "before": 1, + "after": 2 + } + } +} diff --git a/daft-derive/tests/fixtures/serde/set_field.json b/daft-derive/tests/fixtures/serde/set_field.json new file mode 100644 index 0000000..d6dd981 --- /dev/null +++ b/daft-derive/tests/fixtures/serde/set_field.json @@ -0,0 +1,10 @@ +{ + "labels": { + "added": [ + "delta" + ], + "removed": [ + "alpha" + ] + } +} diff --git a/daft-derive/tests/fixtures/serde/simple_struct.json b/daft-derive/tests/fixtures/serde/simple_struct.json new file mode 100644 index 0000000..81eac1b --- /dev/null +++ b/daft-derive/tests/fixtures/serde/simple_struct.json @@ -0,0 +1,6 @@ +{ + "retries": { + "before": 3, + "after": 5 + } +} diff --git a/daft-derive/tests/fixtures/serde/tuple_struct.json b/daft-derive/tests/fixtures/serde/tuple_struct.json new file mode 100644 index 0000000..80dbe8f --- /dev/null +++ b/daft-derive/tests/fixtures/serde/tuple_struct.json @@ -0,0 +1,10 @@ +[ + { + "before": 1, + "after": 2 + }, + { + "before": false, + "after": true + } +] diff --git a/daft-derive/tests/fixtures/valid/changes.rs b/daft-derive/tests/fixtures/valid/changes.rs new file mode 100644 index 0000000..7908d1d --- /dev/null +++ b/daft-derive/tests/fixtures/valid/changes.rs @@ -0,0 +1,42 @@ +//! Exercises `#[daft(changes)]` opt-in: emission of the `*Changes` struct, the +//! `IntoChanges` impl, and (under the `serde` feature) the hand-written +//! `Serialize` impl. Covers named, tuple, empty, and `#[daft(ignore)]`-only +//! shapes plus a `#[daft(leaf)]` field, since the leaf path threads through +//! `Leaf::IntoChanges` rather than the generated `*Changes` type. + +use daft::Diffable; +use std::collections::BTreeMap; + +#[derive(Debug, Eq, PartialEq, Diffable)] +#[daft(changes)] +struct Config { + name: String, + retries: u32, + tags: BTreeMap, +} + +#[derive(Debug, Eq, PartialEq, Diffable)] +#[daft(changes)] +struct Pair(u32, String); + +#[derive(Debug, Eq, PartialEq, Diffable)] +#[daft(changes)] +struct OnlyIgnored { + #[daft(ignore)] + _scratch: u32, +} + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct Inner { + value: u32, +} + +#[derive(Debug, Eq, PartialEq, Diffable)] +#[daft(changes)] +struct Wrapper { + #[daft(leaf)] + inner: Inner, + label: String, +} + +fn main() {} diff --git a/daft-derive/tests/fixtures/valid/output/changes.output.rs b/daft-derive/tests/fixtures/valid/output/changes.output.rs new file mode 100644 index 0000000..4ad14fd --- /dev/null +++ b/daft-derive/tests/fixtures/valid/output/changes.output.rs @@ -0,0 +1,583 @@ +struct ConfigDiff<'__daft> { + name: ::Diff<'__daft>, + retries: ::Diff<'__daft>, + tags: as ::daft::Diffable>::Diff<'__daft>, +} +impl<'__daft> ::core::fmt::Debug for ConfigDiff<'__daft> +where + ::Diff<'__daft>: ::core::fmt::Debug, + ::Diff<'__daft>: ::core::fmt::Debug, + as ::daft::Diffable>::Diff<'__daft>: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(ConfigDiff)) + .field(stringify!(name), &self.name) + .field(stringify!(retries), &self.retries) + .field(stringify!(tags), &self.tags) + .finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for ConfigDiff<'__daft> +where + ::Diff<'__daft>: ::core::cmp::PartialEq, + ::Diff<'__daft>: ::core::cmp::PartialEq, + as ::daft::Diffable>::Diff<'__daft>: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.retries == other.retries + && self.tags == other.tags + } +} +impl<'__daft> ::core::cmp::Eq for ConfigDiff<'__daft> +where + ::Diff<'__daft>: ::core::cmp::Eq, + ::Diff<'__daft>: ::core::cmp::Eq, + as ::daft::Diffable>::Diff<'__daft>: ::core::cmp::Eq, +{} +impl ::daft::Diffable for Config { + type Diff<'__daft> = ConfigDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> ConfigDiff<'__daft> { + Self::Diff { + name: ::daft::Diffable::diff(&self.name, &other.name), + retries: ::daft::Diffable::diff(&self.retries, &other.retries), + tags: ::daft::Diffable::diff(&self.tags, &other.tags), + } + } +} +struct ConfigChanges<'__daft> +where + ::Diff<'__daft>: ::daft::IntoChanges, + ::Diff<'__daft>: ::daft::IntoChanges, + as ::daft::Diffable>::Diff<'__daft>: ::daft::IntoChanges, +{ + name: ::core::option::Option< + <::Diff<'__daft> as ::daft::IntoChanges>::Changes, + >, + retries: ::core::option::Option< + <::Diff<'__daft> as ::daft::IntoChanges>::Changes, + >, + tags: ::core::option::Option< + < as ::daft::Diffable>::Diff<'__daft> as ::daft::IntoChanges>::Changes, + >, +} +impl<'__daft> ::core::fmt::Debug for ConfigChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, + < as ::daft::Diffable>::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(ConfigChanges)) + .field(stringify!(name), &self.name) + .field(stringify!(retries), &self.retries) + .field(stringify!(tags), &self.tags) + .finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for ConfigChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, + < as ::daft::Diffable>::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.retries == other.retries + && self.tags == other.tags + } +} +impl<'__daft> ::core::cmp::Eq for ConfigChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, + < as ::daft::Diffable>::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, +{} +impl<'__daft> ::daft::IntoChanges for ConfigDiff<'__daft> +where + ::Diff<'__daft>: ::daft::IntoChanges, + ::Diff<'__daft>: ::daft::IntoChanges, + as ::daft::Diffable>::Diff<'__daft>: ::daft::IntoChanges, +{ + type Changes = ConfigChanges<'__daft>; + fn into_changes(self) -> ::core::option::Option { + let __daft_name = ::daft::IntoChanges::into_changes(self.name); + let __daft_retries = ::daft::IntoChanges::into_changes(self.retries); + let __daft_tags = ::daft::IntoChanges::into_changes(self.tags); + if __daft_name.is_some() || __daft_retries.is_some() || __daft_tags.is_some() { + ::core::option::Option::Some(ConfigChanges { + name: __daft_name, + retries: __daft_retries, + tags: __daft_tags, + }) + } else { + ::core::option::Option::None + } + } +} +#[automatically_derived] +impl<'__daft> ::daft::__private_serde::Serialize for ConfigChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, + < as ::daft::Diffable>::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, +{ + fn serialize<__DaftS>( + &self, + serializer: __DaftS, + ) -> ::core::result::Result<__DaftS::Ok, __DaftS::Error> + where + __DaftS: ::daft::__private_serde::Serializer, + { + let mut __state = ::daft::__private_serde::Serializer::serialize_struct( + serializer, + stringify!(ConfigChanges), + { + let mut __count = 0usize; + if self.name.is_some() { + __count += 1; + } + if self.retries.is_some() { + __count += 1; + } + if self.tags.is_some() { + __count += 1; + } + __count + }, + )?; + if let ::core::option::Option::Some(__value) = &self.name { + ::daft::__private_serde::ser::SerializeStruct::serialize_field( + &mut __state, + stringify!(name), + __value, + )?; + } + if let ::core::option::Option::Some(__value) = &self.retries { + ::daft::__private_serde::ser::SerializeStruct::serialize_field( + &mut __state, + stringify!(retries), + __value, + )?; + } + if let ::core::option::Option::Some(__value) = &self.tags { + ::daft::__private_serde::ser::SerializeStruct::serialize_field( + &mut __state, + stringify!(tags), + __value, + )?; + } + ::daft::__private_serde::ser::SerializeStruct::end(__state) + } +} +struct PairDiff<'__daft>( + ::Diff<'__daft>, + ::Diff<'__daft>, +); +impl<'__daft> ::core::fmt::Debug for PairDiff<'__daft> +where + ::Diff<'__daft>: ::core::fmt::Debug, + ::Diff<'__daft>: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple(stringify!(PairDiff)).field(&self.0).field(&self.1).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for PairDiff<'__daft> +where + ::Diff<'__daft>: ::core::cmp::PartialEq, + ::Diff<'__daft>: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 && self.1 == other.1 + } +} +impl<'__daft> ::core::cmp::Eq for PairDiff<'__daft> +where + ::Diff<'__daft>: ::core::cmp::Eq, + ::Diff<'__daft>: ::core::cmp::Eq, +{} +impl ::daft::Diffable for Pair { + type Diff<'__daft> = PairDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> PairDiff<'__daft> { + Self::Diff { + 0: ::daft::Diffable::diff(&self.0, &other.0), + 1: ::daft::Diffable::diff(&self.1, &other.1), + } + } +} +struct PairChanges<'__daft>( + ::core::option::Option< + <::Diff<'__daft> as ::daft::IntoChanges>::Changes, + >, + ::core::option::Option< + <::Diff<'__daft> as ::daft::IntoChanges>::Changes, + >, +) +where + ::Diff<'__daft>: ::daft::IntoChanges, + ::Diff<'__daft>: ::daft::IntoChanges; +impl<'__daft> ::core::fmt::Debug for PairChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple(stringify!(PairChanges)).field(&self.0).field(&self.1).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for PairChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 && self.1 == other.1 + } +} +impl<'__daft> ::core::cmp::Eq for PairChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, +{} +impl<'__daft> ::daft::IntoChanges for PairDiff<'__daft> +where + ::Diff<'__daft>: ::daft::IntoChanges, + ::Diff<'__daft>: ::daft::IntoChanges, +{ + type Changes = PairChanges<'__daft>; + fn into_changes(self) -> ::core::option::Option { + let __daft_0 = ::daft::IntoChanges::into_changes(self.0); + let __daft_1 = ::daft::IntoChanges::into_changes(self.1); + if __daft_0.is_some() || __daft_1.is_some() { + ::core::option::Option::Some(PairChanges(__daft_0, __daft_1)) + } else { + ::core::option::Option::None + } + } +} +#[automatically_derived] +impl<'__daft> ::daft::__private_serde::Serialize for PairChanges<'__daft> +where + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, +{ + fn serialize<__DaftS>( + &self, + serializer: __DaftS, + ) -> ::core::result::Result<__DaftS::Ok, __DaftS::Error> + where + __DaftS: ::daft::__private_serde::Serializer, + { + let mut __state = ::daft::__private_serde::Serializer::serialize_tuple_struct( + serializer, + stringify!(PairChanges), + { + let mut __count = 0usize; + if self.0.is_some() { + __count += 1; + } + if self.1.is_some() { + __count += 1; + } + __count + }, + )?; + if let ::core::option::Option::Some(__value) = &self.0 { + ::daft::__private_serde::ser::SerializeTupleStruct::serialize_field( + &mut __state, + __value, + )?; + } + if let ::core::option::Option::Some(__value) = &self.1 { + ::daft::__private_serde::ser::SerializeTupleStruct::serialize_field( + &mut __state, + __value, + )?; + } + ::daft::__private_serde::ser::SerializeTupleStruct::end(__state) + } +} +struct OnlyIgnoredDiff<'__daft> { + _phantom: ::core::marker::PhantomData &'__daft OnlyIgnored>, +} +impl<'__daft> ::core::fmt::Debug for OnlyIgnoredDiff<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(OnlyIgnoredDiff)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for OnlyIgnoredDiff<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for OnlyIgnoredDiff<'__daft> {} +impl ::daft::Diffable for OnlyIgnored { + type Diff<'__daft> = OnlyIgnoredDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> OnlyIgnoredDiff<'__daft> { + Self::Diff { + _phantom: ::core::marker::PhantomData, + } + } +} +struct OnlyIgnoredChanges<'__daft> { + _phantom: ::core::marker::PhantomData &'__daft OnlyIgnored>, +} +impl<'__daft> ::core::fmt::Debug for OnlyIgnoredChanges<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(OnlyIgnoredChanges)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for OnlyIgnoredChanges<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for OnlyIgnoredChanges<'__daft> {} +impl<'__daft> ::daft::IntoChanges for OnlyIgnoredDiff<'__daft> { + type Changes = OnlyIgnoredChanges<'__daft>; + fn into_changes(self) -> ::core::option::Option { + ::core::option::Option::None + } +} +struct InnerDiff<'__daft> { + value: ::Diff<'__daft>, +} +impl<'__daft> ::core::fmt::Debug for InnerDiff<'__daft> +where + ::Diff<'__daft>: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(InnerDiff)) + .field(stringify!(value), &self.value) + .finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for InnerDiff<'__daft> +where + ::Diff<'__daft>: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} +impl<'__daft> ::core::cmp::Eq for InnerDiff<'__daft> +where + ::Diff<'__daft>: ::core::cmp::Eq, +{} +impl ::daft::Diffable for Inner { + type Diff<'__daft> = InnerDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> InnerDiff<'__daft> { + Self::Diff { + value: ::daft::Diffable::diff(&self.value, &other.value), + } + } +} +struct WrapperDiff<'__daft> { + inner: ::daft::Leaf<&'__daft Inner>, + label: ::Diff<'__daft>, +} +impl<'__daft> ::core::fmt::Debug for WrapperDiff<'__daft> +where + ::daft::Leaf<&'__daft Inner>: ::core::fmt::Debug, + ::Diff<'__daft>: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(WrapperDiff)) + .field(stringify!(inner), &self.inner) + .field(stringify!(label), &self.label) + .finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for WrapperDiff<'__daft> +where + ::daft::Leaf<&'__daft Inner>: ::core::cmp::PartialEq, + ::Diff<'__daft>: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner && self.label == other.label + } +} +impl<'__daft> ::core::cmp::Eq for WrapperDiff<'__daft> +where + ::daft::Leaf<&'__daft Inner>: ::core::cmp::Eq, + ::Diff<'__daft>: ::core::cmp::Eq, +{} +impl ::daft::Diffable for Wrapper { + type Diff<'__daft> = WrapperDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> WrapperDiff<'__daft> { + Self::Diff { + inner: ::daft::Leaf { + before: &self.inner, + after: &other.inner, + }, + label: ::daft::Diffable::diff(&self.label, &other.label), + } + } +} +struct WrapperChanges<'__daft> +where + ::daft::Leaf<&'__daft Inner>: ::daft::IntoChanges, + ::Diff<'__daft>: ::daft::IntoChanges, +{ + inner: ::core::option::Option< + <::daft::Leaf<&'__daft Inner> as ::daft::IntoChanges>::Changes, + >, + label: ::core::option::Option< + <::Diff<'__daft> as ::daft::IntoChanges>::Changes, + >, +} +impl<'__daft> ::core::fmt::Debug for WrapperChanges<'__daft> +where + <::daft::Leaf<&'__daft Inner> as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::fmt::Debug, +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(WrapperChanges)) + .field(stringify!(inner), &self.inner) + .field(stringify!(label), &self.label) + .finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for WrapperChanges<'__daft> +where + <::daft::Leaf< + &'__daft Inner, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner && self.label == other.label + } +} +impl<'__daft> ::core::cmp::Eq for WrapperChanges<'__daft> +where + <::daft::Leaf<&'__daft Inner> as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::core::cmp::Eq, +{} +impl<'__daft> ::daft::IntoChanges for WrapperDiff<'__daft> +where + ::daft::Leaf<&'__daft Inner>: ::daft::IntoChanges, + ::Diff<'__daft>: ::daft::IntoChanges, +{ + type Changes = WrapperChanges<'__daft>; + fn into_changes(self) -> ::core::option::Option { + let __daft_inner = ::daft::IntoChanges::into_changes(self.inner); + let __daft_label = ::daft::IntoChanges::into_changes(self.label); + if __daft_inner.is_some() || __daft_label.is_some() { + ::core::option::Option::Some(WrapperChanges { + inner: __daft_inner, + label: __daft_label, + }) + } else { + ::core::option::Option::None + } + } +} +#[automatically_derived] +impl<'__daft> ::daft::__private_serde::Serialize for WrapperChanges<'__daft> +where + <::daft::Leaf< + &'__daft Inner, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, + <::Diff< + '__daft, + > as ::daft::IntoChanges>::Changes: ::daft::__private_serde::Serialize, +{ + fn serialize<__DaftS>( + &self, + serializer: __DaftS, + ) -> ::core::result::Result<__DaftS::Ok, __DaftS::Error> + where + __DaftS: ::daft::__private_serde::Serializer, + { + let mut __state = ::daft::__private_serde::Serializer::serialize_struct( + serializer, + stringify!(WrapperChanges), + { + let mut __count = 0usize; + if self.inner.is_some() { + __count += 1; + } + if self.label.is_some() { + __count += 1; + } + __count + }, + )?; + if let ::core::option::Option::Some(__value) = &self.inner { + ::daft::__private_serde::ser::SerializeStruct::serialize_field( + &mut __state, + stringify!(inner), + __value, + )?; + } + if let ::core::option::Option::Some(__value) = &self.label { + ::daft::__private_serde::ser::SerializeStruct::serialize_field( + &mut __state, + stringify!(label), + __value, + )?; + } + ::daft::__private_serde::ser::SerializeStruct::end(__state) + } +} diff --git a/daft-derive/tests/integration_test.rs b/daft-derive/tests/integration_test.rs index d2f5395..06fd113 100644 --- a/daft-derive/tests/integration_test.rs +++ b/daft-derive/tests/integration_test.rs @@ -273,3 +273,122 @@ fn diff_pair_lifetimes() { assert_eq!(owned.before, "hello"); assert_eq!(owned.after, "world"); } + +#[cfg(feature = "serde")] +mod changes_serde { + //! End-to-end coverage of `#[daft(changes)]` plus the `serde` feature: + //! the projected diff must round-trip through `serde_json` with every + //! unchanged leaf omitted. + + use daft::{Diffable, IntoChanges}; + use serde_json::json; + use std::collections::BTreeMap; + + #[test] + fn struct_with_changes_serializes_only_modified_fields() { + #[derive(Debug, Eq, PartialEq, Diffable)] + #[daft(changes)] + struct Config { + name: String, + retries: u32, + } + + let before = Config { name: "alpha".to_owned(), retries: 3 }; + let after = Config { name: "alpha".to_owned(), retries: 5 }; + + let changes = + before.diff(&after).into_changes().expect("retries changed"); + let json = serde_json::to_value(&changes).unwrap(); + assert_eq!(json, json!({ "retries": { "before": 3, "after": 5 } })); + + // Two equal values produce no changes at all. + assert!(before.diff(&before).into_changes().is_none()); + } + + #[test] + fn nested_struct_changes_serialize_recursively() { + #[derive(Debug, Eq, PartialEq, Diffable)] + #[daft(changes)] + struct Inner { + value: u32, + note: String, + } + + #[derive(Debug, Eq, PartialEq, Diffable)] + #[daft(changes)] + struct Outer { + inner: Inner, + tag: u32, + } + + let before = + Outer { inner: Inner { value: 1, note: "a".to_owned() }, tag: 7 }; + let after = + Outer { inner: Inner { value: 2, note: "a".to_owned() }, tag: 7 }; + + let changes = + before.diff(&after).into_changes().expect("inner.value changed"); + let json = serde_json::to_value(&changes).unwrap(); + + // `note` is unchanged so it's dropped; `tag` is unchanged so it's + // dropped; only `inner.value` survives. + assert_eq!( + json, + json!({ "inner": { "value": { "before": 1, "after": 2 } } }), + ); + } + + #[test] + fn map_changes_filter_unchanged_entries() { + #[derive(Debug, Eq, PartialEq, Diffable)] + #[daft(changes)] + struct Cache { + entries: BTreeMap, + } + + let before = Cache { + entries: [(1, "alpha"), (2, "beta"), (3, "gamma")] + .into_iter() + .collect(), + }; + let after = Cache { + entries: [(1, "alpha"), (2, "BETA"), (4, "delta")] + .into_iter() + .collect(), + }; + + let changes = before.diff(&after).into_changes().expect("map changed"); + let json = serde_json::to_value(&changes).unwrap(); + + assert_eq!( + json, + json!({ + "entries": { + "common": { + "2": { "before": "beta", "after": "BETA" }, + }, + "added": { "4": "delta" }, + "removed": { "3": "gamma" }, + }, + }), + ); + } + + #[test] + fn tuple_struct_changes_serialize_positionally() { + #[derive(Debug, Eq, PartialEq, Diffable)] + #[daft(changes)] + struct Pair(u32, &'static str); + + let before = Pair(1, "same"); + let after = Pair(2, "same"); + + let changes = + before.diff(&after).into_changes().expect("first changed"); + // Tuple structs serialize as a sequence with `null` for skipped + // positions in formats that preserve nulls; we rely on + // `serialize_tuple_struct` to elide positions entirely. + let json = serde_json::to_value(&changes).unwrap(); + assert_eq!(json, json!([{ "before": 1, "after": 2 }])); + } +} diff --git a/daft-derive/tests/serde_snapshot.rs b/daft-derive/tests/serde_snapshot.rs new file mode 100644 index 0000000..4ed9fbd --- /dev/null +++ b/daft-derive/tests/serde_snapshot.rs @@ -0,0 +1,212 @@ +//! Snapshot tests for the JSON form of projected diffs. +//! +//! Each test builds two values, computes their diff, projects it through +//! `IntoChanges`, serializes the result to pretty-printed JSON, and compares +//! the result against a file in `tests/fixtures/serde/`. Regenerate the +//! expected files with `EXPECTORATE=overwrite`. +//! +//! These tests complement the unit-style ones in `integration_test.rs` by +//! producing reviewable artifacts: the pretty JSON makes it easy to inspect +//! exactly which subtrees survive the projection and which are elided. + +#![cfg(feature = "serde")] + +use daft::{Diffable, IntoChanges}; +use expectorate::assert_contents; +use serde::Serialize; +use std::collections::{BTreeMap, BTreeSet}; + +/// Compare `value` serialized as pretty JSON against +/// `tests/fixtures/serde/{name}.json`. +fn assert_json_snapshot(name: &str, value: &T) { + let mut pretty = serde_json::to_string_pretty(value) + .expect("serializing changes to JSON"); + pretty.push('\n'); + let path = format!("tests/fixtures/serde/{name}.json"); + assert_contents(&path, &pretty); +} + +#[test] +fn simple_struct() { + #[derive(Diffable)] + #[daft(changes)] + struct Config { + name: String, + retries: u32, + timeout_ms: u32, + } + + let before = + Config { name: "alpha".to_owned(), retries: 3, timeout_ms: 1000 }; + let after = + Config { name: "alpha".to_owned(), retries: 5, timeout_ms: 1000 }; + + let changes = before.diff(&after).into_changes().expect("retries changed"); + assert_json_snapshot("simple_struct", &changes); +} + +#[test] +fn nested_struct() { + #[derive(Diffable)] + #[daft(changes)] + struct Inner { + value: u32, + note: String, + } + + #[derive(Diffable)] + #[daft(changes)] + struct Outer { + inner: Inner, + tag: u32, + } + + let before = + Outer { inner: Inner { value: 1, note: "stable".to_owned() }, tag: 7 }; + let after = + Outer { inner: Inner { value: 2, note: "stable".to_owned() }, tag: 7 }; + + let changes = + before.diff(&after).into_changes().expect("inner.value changed"); + assert_json_snapshot("nested_struct", &changes); +} + +#[test] +fn map_field() { + #[derive(Diffable)] + #[daft(changes)] + struct Cache { + entries: BTreeMap, + version: u32, + } + + let before = Cache { + entries: [(1, "alpha"), (2, "beta"), (3, "gamma")] + .into_iter() + .collect(), + version: 1, + }; + let after = Cache { + entries: [(1, "alpha"), (2, "BETA"), (4, "delta")] + .into_iter() + .collect(), + version: 1, + }; + + let changes = before.diff(&after).into_changes().expect("entries changed"); + assert_json_snapshot("map_field", &changes); +} + +#[test] +fn set_field() { + #[derive(Diffable)] + #[daft(changes)] + struct Tags { + labels: BTreeSet<&'static str>, + } + + let before = + Tags { labels: ["alpha", "beta", "gamma"].into_iter().collect() }; + let after = + Tags { labels: ["beta", "gamma", "delta"].into_iter().collect() }; + + let changes = before.diff(&after).into_changes().expect("labels changed"); + assert_json_snapshot("set_field", &changes); +} + +#[test] +fn tuple_struct() { + #[derive(Diffable)] + #[daft(changes)] + struct Pair(u32, String, bool); + + let before = Pair(1, "same".to_owned(), false); + let after = Pair(2, "same".to_owned(), true); + + let changes = before.diff(&after).into_changes().expect("0 and 2 changed"); + assert_json_snapshot("tuple_struct", &changes); +} + +#[test] +fn leaf_field() { + // `#[daft(leaf)]` short-circuits a struct field to a `Leaf<&Inner>` even + // when `Inner` itself implements `Diffable`. The Changes projection + // surfaces it as `{ "before": ..., "after": ... }`. + #[derive(Diffable, Serialize, Eq, PartialEq)] + struct Inner { + value: u32, + } + + #[derive(Diffable)] + #[daft(changes)] + struct Wrapper { + #[daft(leaf)] + inner: Inner, + label: String, + } + + let before = + Wrapper { inner: Inner { value: 1 }, label: "unchanged".to_owned() }; + let after = + Wrapper { inner: Inner { value: 2 }, label: "unchanged".to_owned() }; + + let changes = before.diff(&after).into_changes().expect("inner changed"); + assert_json_snapshot("leaf_field", &changes); +} + +#[test] +fn mixed_fields() { + // A single struct exercising every shape at once: a `Leaf` field, a map, + // a set, a nested struct, and an unchanged field that should drop out. + #[derive(Diffable)] + #[daft(changes)] + struct Profile { + name: String, + weight: u32, + } + + #[derive(Diffable)] + #[daft(changes)] + struct Snapshot { + version: u32, + profile: Profile, + attrs: BTreeMap, + tags: BTreeSet, + unchanged: u32, + } + + let before = Snapshot { + version: 1, + profile: Profile { name: "alice".to_owned(), weight: 70 }, + attrs: [("a".to_owned(), 1), ("b".to_owned(), 2)].into_iter().collect(), + tags: ["x".to_owned(), "y".to_owned()].into_iter().collect(), + unchanged: 42, + }; + let after = Snapshot { + version: 2, + profile: Profile { name: "alice".to_owned(), weight: 71 }, + attrs: [("a".to_owned(), 1), ("b".to_owned(), 20), ("c".to_owned(), 3)] + .into_iter() + .collect(), + tags: ["x".to_owned(), "z".to_owned()].into_iter().collect(), + unchanged: 42, + }; + + let changes = before.diff(&after).into_changes().expect("multiple changes"); + assert_json_snapshot("mixed_fields", &changes); +} + +#[test] +fn no_changes_yields_none() { + // Diffing a value against itself must yield `None` so callers can skip + // serialization entirely instead of emitting an empty payload. + #[derive(Diffable)] + #[daft(changes)] + struct Config { + name: String, + retries: u32, + } + + let value = Config { name: "alpha".to_owned(), retries: 3 }; + assert!(value.diff(&value).into_changes().is_none()); +} diff --git a/daft-derive/tests/snapshot_test.rs b/daft-derive/tests/snapshot_test.rs index 0453514..f7d4925 100644 --- a/daft-derive/tests/snapshot_test.rs +++ b/daft-derive/tests/snapshot_test.rs @@ -20,6 +20,14 @@ fn daft_snapshot( path: &Utf8Path, input: String, ) -> datatest_stable::Result<()> { + // The `changes.rs` fixture exercises `#[daft(changes)]`, whose output + // includes a `Serialize` impl only when daft-derive's `serde` feature is + // on. The checked-in snapshot reflects that case, so we skip the + // fixture when serde is off rather than maintain a second snapshot. + if !cfg!(feature = "serde") && path.file_name() == Some("changes.rs") { + return Ok(()); + } + let data = syn::parse_str::(&input)?; let output = run_derive_macro(&data); diff --git a/daft/Cargo.toml b/daft/Cargo.toml index f2ca73d..443813b 100644 --- a/daft/Cargo.toml +++ b/daft/Cargo.toml @@ -19,17 +19,28 @@ indexmap = { workspace = true, optional = true } newtype-uuid = { workspace = true, optional = true } oxnet = { workspace = true, optional = true } paste.workspace = true +serde = { workspace = true, optional = true, features = ["alloc", "derive"] } uuid = { workspace = true, optional = true, features = ["v4"] } +[dev-dependencies] +serde_json.workspace = true + [features] default = ["std"] -std = ["alloc"] +# Enabling `std` flips on `serde`'s `std` feature too when serde is in use, +# so the `*Changes` types for `HashMap`/`HashSet` get the right Serialize +# impls. The `?` form is a no-op when `serde` is disabled. +std = ["alloc", "serde?/std"] alloc = [] derive = ["dep:daft-derive"] newtype-uuid1 = ["dep:newtype-uuid"] oxnet01 = ["dep:oxnet"] uuid1 = ["dep:uuid"] indexmap = ["dep:indexmap", "alloc"] +# `serde` adds `Serialize` impls to `Leaf` and to the `*Changes` types so +# that a diff projected via `IntoChanges` can be emitted to JSON or any +# other serde format with unchanged subtrees omitted entirely. +serde = ["dep:serde", "daft-derive?/serde", "indexmap?/serde"] [package.metadata.docs.rs] all-features = true diff --git a/daft/README.md b/daft/README.md index 5dbf284..fdbb467 100644 --- a/daft/README.md +++ b/daft/README.md @@ -408,6 +408,48 @@ struct BorrowedDataDiff<'daft, 'a: 'daft, 'b: 'daft, T: ?Sized + 'daft> { } ```` +### Serializing only the changed parts + +Daft’s diff types preserve the full *before*/*after* structure, which is +the right shape for in-memory analysis but a poor fit for writing diffs +to a file: a diff of two large structures with one altered field still +serializes both sides in full. + +The [`IntoChanges`](https://docs.rs/daft/0.1.6/daft/changes/trait.IntoChanges.html) trait and the parallel `*Changes` type per diff +handle this. Calling [`into_changes`](https://docs.rs/daft/0.1.6/daft/changes/IntoChanges/fn.into_changes.html) drops +every unchanged subtree and returns \[`None`\] if nothing changed at all. +Built-in implementations cover [`Leaf`](https://docs.rs/daft/0.1.6/daft/leaf/struct.Leaf.html), the map and set diffs, and +tuples; the [`Diffable`](https://docs.rs/daft-derive/0.1.6/daft_derive/derive.Diffable.html) derive macro emits the +corresponding `*Changes` type and impl when a struct is annotated with +`#[daft(changes)]`. + +The `serde` feature layers on top: it derives [`Serialize`] for [`Leaf`](https://docs.rs/daft/0.1.6/daft/leaf/struct.Leaf.html) +and every `*Changes` type. The derive’s emitted `Serialize` impl on the +generated `*Changes` skips `None` fields, so the serialized output +contains *only* the modified subtree. + +#### Example + +````rust +use daft::{Diffable, IntoChanges}; + +#[derive(Diffable)] +#[daft(changes)] +struct Config { + name: String, + retries: u32, +} + +let before = Config { name: "alpha".to_owned(), retries: 3 }; +let after = Config { name: "alpha".to_owned(), retries: 5 }; + +let changes = before.diff(&after).into_changes().expect("retries changed"); + +// `name` is unchanged, so it's `None`; `retries` is `Some(...)`. +assert!(changes.name.is_none()); +assert!(changes.retries.is_some()); +```` + ## Optional features * `derive`: Enable the `Diffable` derive macro: **disabled** by default. @@ -419,6 +461,12 @@ Implementations for standard library types, all **enabled** by default: (With `default-features = false`, daft is no-std compatible.) +Serialization, **disabled** by default: + +* `serde`: Add `Serialize` impls to [`Leaf`](https://docs.rs/daft/0.1.6/daft/leaf/struct.Leaf.html) and to every `*Changes` + type so a projected diff can be written to JSON or any other serde + format. + Implementations for third-party types, all **disabled** by default: * `uuid1`: Enable diffing for [`uuid::Uuid`](https://docs.rs/uuid/1.12.1/uuid/struct.Uuid.html). @@ -468,6 +516,7 @@ this crate and a great alternative. Daft diverges from diffus in a few ways: [`HashMap`]: https://doc.rust-lang.org/nightly/std/collections/hash/map/struct.HashMap.html [`BTreeSet`]: https://doc.rust-lang.org/nightly/alloc/collections/btree/set/struct.BTreeSet.html [`HashSet`]: https://doc.rust-lang.org/nightly/std/collections/hash/set/struct.HashSet.html +[`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html [GAT]: https://blog.rust-lang.org/2021/08/03/GATs-stabilization-push.html diff --git a/daft/src/alloc_impls.rs b/daft/src/alloc_impls.rs index 47ecfde..64641eb 100644 --- a/daft/src/alloc_impls.rs +++ b/daft/src/alloc_impls.rs @@ -109,7 +109,7 @@ map_diff!( /// assert_eq!(modified, [(&2, Leaf { before: &"dolor", after: &"sit" })]); /// # } /// ``` - BTreeMap, Ord + BTreeMap, Ord, "::alloc::collections::BTreeMap::is_empty" ); set_diff!( /// A diff of two [`BTreeSet`] instances. @@ -141,7 +141,7 @@ set_diff!( /// assert_eq!(changes, expected); /// # } /// ``` - BTreeSet, Ord + BTreeSet, Ord, "::alloc::collections::BTreeSet::is_empty" ); /// Treat Vecs as Leafs diff --git a/daft/src/changes.rs b/daft/src/changes.rs new file mode 100644 index 0000000..556d3cb --- /dev/null +++ b/daft/src/changes.rs @@ -0,0 +1,177 @@ +//! Projecting a diff to the subset of nodes that actually changed. +//! +//! See [`IntoChanges`] for an overview. + +use crate::Leaf; + +/// Project a diff to the subset of nodes that have changed. +/// +/// Daft's diff types preserve the full *before*/*after* structure regardless +/// of whether anything changed. That is the right representation for +/// in-memory analysis, but a poor fit for storage and transport: a diff of +/// two large structures with one altered field still serializes both sides +/// in full. +/// +/// `IntoChanges` describes the dual representation. For each diff type +/// there is a corresponding *changes* type — `Leaf<&T>` maps to itself, +/// [`BTreeMapDiff`](crate::BTreeMapDiff) maps to +/// [`BTreeMapChanges`](crate::BTreeMapChanges), a derived `FooDiff` maps to a +/// generated `FooChanges`, and so on. Calling [`into_changes`][Self::into_changes] +/// drops every unchanged subtree and returns [`Some`] only if anything +/// remained. [`None`] is reserved for "no changes at all". +/// +/// The trait requires `Eq` at the leaves: there is no way to tell whether a +/// `Leaf<&T>` represents a change without comparing the two sides. +/// +/// With the `serde` feature, the `*Changes` types implement [`Serialize`]. +/// `None` fields are skipped at serialization time, so unchanged subtrees +/// are omitted entirely from the serialized output. +/// +/// [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html +/// +/// # Example +/// +/// ``` +/// # #[cfg(feature = "std")] { +/// use daft::{Diffable, IntoChanges, Leaf}; +/// use std::collections::BTreeMap; +/// +/// let before: BTreeMap = +/// [(1, "alpha"), (2, "beta"), (3, "gamma")].into_iter().collect(); +/// let after: BTreeMap = +/// [(1, "alpha"), (2, "BETA"), (4, "delta")].into_iter().collect(); +/// +/// let changes = +/// before.diff(&after).into_changes().expect("the map changed"); +/// +/// // `common` only retains entries whose value differs. +/// assert_eq!( +/// changes.common, +/// [(&2, Leaf { before: &"beta", after: &"BETA" })] +/// .into_iter() +/// .collect(), +/// ); +/// assert_eq!(changes.added, [(&4, &"delta")].into_iter().collect()); +/// assert_eq!(changes.removed, [(&3, &"gamma")].into_iter().collect()); +/// +/// // Diffing a map against itself yields no changes at all. +/// assert!(before.diff(&before).into_changes().is_none()); +/// # } +/// ``` +pub trait IntoChanges { + /// The "changes-only" representation of this diff. + /// + /// For terminal types (`Leaf<&T>`, primitive diffs) this is typically + /// `Self`. For composite diffs, it is a parallel struct in which each + /// field is the corresponding child's [`Changes`][Self::Changes] wrapped + /// in [`Option`] so unchanged subtrees can be skipped. + type Changes; + + /// Drop unchanged subtrees from the diff. + /// + /// Returns [`None`] if every leaf is unchanged, and [`Some`] containing + /// the projected representation otherwise. + fn into_changes(self) -> Option; +} + +impl IntoChanges for Leaf<&T> { + type Changes = Self; + + #[inline] + fn into_changes(self) -> Option { + if self.before == self.after { + None + } else { + Some(self) + } + } +} + +impl IntoChanges for Leaf> { + type Changes = Self; + + #[inline] + fn into_changes(self) -> Option { + if self.before == self.after { + None + } else { + Some(self) + } + } +} + +impl IntoChanges for Leaf> { + type Changes = Self; + + #[inline] + fn into_changes(self) -> Option { + if self.before == self.after { + None + } else { + Some(self) + } + } +} + +macro_rules! tuple_into_changes { + ($(($($name:ident $ix:tt),+)),+ $(,)?) => { + $( + impl<$($name: IntoChanges),+> IntoChanges for ($($name,)+) { + type Changes = ($(Option<$name::Changes>,)+); + + fn into_changes(self) -> Option { + let tup = ($(self.$ix.into_changes(),)+); + if $(tup.$ix.is_none())&&+ { + None + } else { + Some(tup) + } + } + } + )+ + } +} + +tuple_into_changes! { + (A 0), + (A 0, B 1), + (A 0, B 1, C 2), + (A 0, B 1, C 2, D 3), + (A 0, B 1, C 2, D 3, E 4), + (A 0, B 1, C 2, D 3, E 4, F 5), + (A 0, B 1, C 2, D 3, E 4, F 5, G 6), + (A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7), + (A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8), + (A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Diffable; + + #[test] + fn leaf_changes() { + let leaf: Leaf<&i32> = 1.diff(&1); + assert_eq!(leaf.into_changes(), None); + + let leaf: Leaf<&i32> = 1.diff(&2); + assert_eq!(leaf.into_changes(), Some(Leaf { before: &1, after: &2 })); + } + + #[test] + fn tuple_changes() { + let before = (1, 2); + let after = (1, 2); + let diff = before.diff(&after); + assert_eq!(diff.into_changes(), None); + + let before = (1, 2); + let after = (1, 3); + let diff = before.diff(&after); + assert_eq!( + diff.into_changes(), + Some((None, Some(Leaf { before: &2, after: &3 }))), + ); + } +} diff --git a/daft/src/leaf.rs b/daft/src/leaf.rs index f3afc73..f622551 100644 --- a/daft/src/leaf.rs +++ b/daft/src/leaf.rs @@ -7,6 +7,7 @@ use core::ops::{Deref, DerefMut}; /// /// For more information, see the [crate-level documentation](crate). #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Leaf { /// The value on the before side. pub before: T, diff --git a/daft/src/lib.rs b/daft/src/lib.rs index 304f7f3..f113691 100644 --- a/daft/src/lib.rs +++ b/daft/src/lib.rs @@ -428,6 +428,52 @@ //! # } //! ``` //! +//! ## Serializing only the changed parts +//! +//! Daft's diff types preserve the full *before*/*after* structure, which is +//! the right shape for in-memory analysis but a poor fit for writing diffs +//! to a file: a diff of two large structures with one altered field still +//! serializes both sides in full. +//! +//! The [`IntoChanges`] trait and the parallel `*Changes` type per diff +//! handle this. Calling [`into_changes`][IntoChanges::into_changes] drops +//! every unchanged subtree and returns [`None`] if nothing changed at all. +//! Built-in implementations cover [`Leaf`], the map and set diffs, and +//! tuples; the [`Diffable`][macro@Diffable] derive macro emits the +//! corresponding `*Changes` type and impl when a struct is annotated with +//! `#[daft(changes)]`. +//! +//! The `serde` feature layers on top: it derives [`Serialize`] for [`Leaf`] +//! and every `*Changes` type. The derive's emitted `Serialize` impl on the +//! generated `*Changes` skips `None` fields, so the serialized output +//! contains *only* the modified subtree. +//! +//! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html +//! +//! ### Example +//! +//! ``` +//! # #[cfg(all(feature = "derive", feature = "std"))] { +//! use daft::{Diffable, IntoChanges}; +//! +//! #[derive(Diffable)] +//! #[daft(changes)] +//! struct Config { +//! name: String, +//! retries: u32, +//! } +//! +//! let before = Config { name: "alpha".to_owned(), retries: 3 }; +//! let after = Config { name: "alpha".to_owned(), retries: 5 }; +//! +//! let changes = before.diff(&after).into_changes().expect("retries changed"); +//! +//! // `name` is unchanged, so it's `None`; `retries` is `Some(...)`. +//! assert!(changes.name.is_none()); +//! assert!(changes.retries.is_some()); +//! # } +//! ``` +//! //! # Optional features //! //! * `derive`: Enable the `Diffable` derive macro: **disabled** by default. @@ -439,6 +485,12 @@ //! //! (With `default-features = false`, daft is no-std compatible.) //! +//! Serialization, **disabled** by default: +//! +//! * `serde`: Add `Serialize` impls to [`Leaf`] and to every `*Changes` +//! type so a projected diff can be written to JSON or any other serde +//! format. +//! //! Implementations for third-party types, all **disabled** by default: //! //! * `uuid1`: Enable diffing for [`uuid::Uuid`]. @@ -493,11 +545,18 @@ #[cfg(feature = "alloc")] extern crate alloc; +// Re-export of `serde` for the derive macro, so downstream crates can pick up +// serde transitively through `daft` instead of having to add it directly. +#[cfg(feature = "serde")] +#[doc(hidden)] +pub use serde as __private_serde; + #[macro_use] mod macros; #[cfg(feature = "alloc")] mod alloc_impls; +mod changes; mod core_impls; mod diffable; mod leaf; @@ -507,6 +566,7 @@ mod third_party; #[cfg(feature = "alloc")] pub use alloc_impls::*; +pub use changes::*; /// Derive macro for the [`Diffable`] trait. /// /// The behavior of this macro varies by type: diff --git a/daft/src/macros.rs b/daft/src/macros.rs index be2ebe9..5f01aaa 100644 --- a/daft/src/macros.rs +++ b/daft/src/macros.rs @@ -37,10 +37,18 @@ macro_rules! leaf_deref { /// Create a type `Diff` and `impl Diffable` on it. /// -/// This is supported for `BTreeMap` and `HashMap` +/// This is supported for `BTreeMap` and `HashMap`. The `$is_empty:literal` +/// argument is the qualified path to the map's `is_empty` method, used to +/// drive `#[serde(skip_serializing_if = ...)]` on the changes type so +/// empty `common`/`added`/`removed` maps drop out of the serialized form. #[cfg(feature = "alloc")] macro_rules! map_diff { - ($(#[$doc:meta])* $typ:ident, $key_constraint:ident) => { + ( + $(#[$doc:meta])* + $typ:ident, + $key_constraint:ident, + $is_empty:literal $(,)? + ) => { paste::paste! { $(#[$doc])* #[derive(Debug, PartialEq, Eq)] @@ -195,16 +203,69 @@ macro_rules! map_diff { diff } } + + #[doc = "Changes-only projection of `" $typ "Diff`."] + /// + /// Returned by [`IntoChanges::into_changes`](crate::IntoChanges::into_changes). + /// `common` is filtered to entries whose values actually changed; + /// `added` and `removed` carry through from the diff unmodified. + #[derive(Debug, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(::serde::Serialize))] + pub struct [<$typ Changes>]<'daft, K: $key_constraint + Eq, V> { + /// Entries present in both maps whose values differ between + /// `before` and `after`. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = $is_empty))] + pub common: $typ<&'daft K, $crate::Leaf<&'daft V>>, + + /// Entries present in the `after` map, but not in `before`. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = $is_empty))] + pub added: $typ<&'daft K, &'daft V>, + + /// Entries present in the `before` map, but not in `after`. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = $is_empty))] + pub removed: $typ<&'daft K, &'daft V>, + } + + impl<'daft, K: $key_constraint + Eq, V: Eq> $crate::IntoChanges + for [<$typ Diff>]<'daft, K, V> + { + type Changes = [<$typ Changes>]<'daft, K, V>; + + fn into_changes(self) -> Option { + let mut common = self.common; + common.retain(|_, leaf| leaf.is_modified()); + if common.is_empty() + && self.added.is_empty() + && self.removed.is_empty() + { + None + } else { + Some([<$typ Changes>] { + common, + added: self.added, + removed: self.removed, + }) + } + } + } } } } /// Create a type `Diff` and `impl Diffable` on it. /// -/// This is supported for `BTreeSet` and `HashSet`. +/// This is supported for `BTreeSet` and `HashSet`. The `$is_empty:literal` +/// argument is the qualified path to the set's `is_empty` method, used to +/// drive `#[serde(skip_serializing_if = ...)]` on the changes type so +/// empty `added`/`removed` sets drop out of the serialized form. #[cfg(feature = "alloc")] macro_rules! set_diff { - ($(#[$doc:meta])* $typ:ident, $key_constraint:ident) => { + ( + $(#[$doc:meta])* + $typ:ident, + $key_constraint:ident, + $is_empty:literal $(,)? + ) => { paste::paste! { $(#[$doc])* #[derive(Debug, PartialEq, Eq)] @@ -251,6 +312,40 @@ macro_rules! set_diff { diff } } + + #[doc = "Changes-only projection of `" $typ "Diff`."] + /// + /// Returned by [`IntoChanges::into_changes`](crate::IntoChanges::into_changes). + /// Set diffs have no notion of "modified" elements, so `common` is + /// omitted entirely — only the asymmetric difference is recorded. + #[derive(Debug, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(::serde::Serialize))] + pub struct [<$typ Changes>]<'daft, K: $key_constraint + Eq> { + /// Entries present in the `after` set, but not in `before`. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = $is_empty))] + pub added: $typ<&'daft K>, + + /// Entries present in the `before` set, but not in `after`. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = $is_empty))] + pub removed: $typ<&'daft K>, + } + + impl<'daft, K: $key_constraint + Eq> $crate::IntoChanges + for [<$typ Diff>]<'daft, K> + { + type Changes = [<$typ Changes>]<'daft, K>; + + fn into_changes(self) -> Option { + if self.added.is_empty() && self.removed.is_empty() { + None + } else { + Some([<$typ Changes>] { + added: self.added, + removed: self.removed, + }) + } + } + } } } } diff --git a/daft/src/std_impls.rs b/daft/src/std_impls.rs index 0ab0a6f..1cbfa4c 100644 --- a/daft/src/std_impls.rs +++ b/daft/src/std_impls.rs @@ -65,7 +65,7 @@ map_diff!( /// assert_eq!(modified, [(&2, Leaf { before: &"dolor", after: &"sit" })]); /// # } /// ``` - HashMap, Hash + HashMap, Hash, "::std::collections::HashMap::is_empty" ); set_diff!( /// A diff of two [`HashSet`] instances. @@ -97,7 +97,7 @@ set_diff!( /// assert_eq!(changes, expected); /// # } /// ``` - HashSet, Hash + HashSet, Hash, "::std::collections::HashSet::is_empty" ); #[cfg(test)] diff --git a/daft/src/third_party/indexmap.rs b/daft/src/third_party/indexmap.rs index 3955f0a..4fd3a5a 100644 --- a/daft/src/third_party/indexmap.rs +++ b/daft/src/third_party/indexmap.rs @@ -2,8 +2,8 @@ use crate::Diffable; use core::hash::Hash; use indexmap::{IndexMap, IndexSet}; -map_diff!(IndexMap, Hash); -set_diff!(IndexSet, Hash); +map_diff!(IndexMap, Hash, "::indexmap::IndexMap::is_empty"); +set_diff!(IndexSet, Hash, "::indexmap::IndexSet::is_empty"); #[cfg(test)] mod tests {