diff --git a/Cargo.lock b/Cargo.lock index ae2c641..e5f397d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,8 @@ version = "1.1.0" dependencies = [ "bullet_stream", "cache_diff_derive", + "serde", + "trybuild", ] [[package]] @@ -170,18 +172,18 @@ dependencies = [ [[package]] name = "strum" -version = "0.26.3" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ "heck", "proc-macro2", @@ -252,9 +254,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.101" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" +checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" dependencies = [ "glob", "serde", @@ -271,17 +273,6 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" -[[package]] -name = "usage" -version = "0.1.0" -dependencies = [ - "cache_diff", - "indoc", - "pretty_assertions", - "serde", - "trybuild", -] - [[package]] name = "winapi-util" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 0796f4a..5664130 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = [ - "usage", "cache_diff", "cache_diff_derive", ] diff --git a/cache_diff/Cargo.toml b/cache_diff/Cargo.toml index 7692603..3c72bc9 100644 --- a/cache_diff/Cargo.toml +++ b/cache_diff/Cargo.toml @@ -13,6 +13,10 @@ documentation.workspace = true cache_diff_derive = { version = "1" , optional = true, path = "../cache_diff_derive" } bullet_stream = { version = "0", optional = true } +[dev-dependencies] +trybuild = "1.0.104" +serde.workspace = true + [features] default = ["derive"] derive = ["dep:cache_diff_derive"] diff --git a/cache_diff/src/lib.rs b/cache_diff/src/lib.rs index 1fc37d4..a801622 100644 --- a/cache_diff/src/lib.rs +++ b/cache_diff/src/lib.rs @@ -252,4 +252,5 @@ pub trait CacheDiff { format!("`{}`", value) } } +#[cfg(feature = "derive")] pub use cache_diff_derive::CacheDiff; diff --git a/usage/tests/compliation_tests.rs b/cache_diff/tests/compliation_tests.rs similarity index 100% rename from usage/tests/compliation_tests.rs rename to cache_diff/tests/compliation_tests.rs diff --git a/usage/tests/fails/accidental_custom_field.rs b/cache_diff/tests/fails/accidental_custom_field.rs similarity index 100% rename from usage/tests/fails/accidental_custom_field.rs rename to cache_diff/tests/fails/accidental_custom_field.rs diff --git a/usage/tests/fails/accidental_custom_field.stderr b/cache_diff/tests/fails/accidental_custom_field.stderr similarity index 69% rename from usage/tests/fails/accidental_custom_field.stderr rename to cache_diff/tests/fails/accidental_custom_field.stderr index 33f66cd..f576df3 100644 --- a/usage/tests/fails/accidental_custom_field.stderr +++ b/cache_diff/tests/fails/accidental_custom_field.stderr @@ -1,4 +1,10 @@ error: Unknown cache_diff attribute: `custom`. Must be one of `rename`, `display`, `ignore` + --> tests/fails/accidental_custom_field.rs:5:18 + | +5 | #[cache_diff(custom = function)] + | ^^^^^^ + +error: The cache_diff attribute `custom` is available on the struct, not the field --> tests/fails/accidental_custom_field.rs:5:18 | diff --git a/cache_diff/tests/fails/duplicate_field.rs b/cache_diff/tests/fails/duplicate_field.rs new file mode 100644 index 0000000..aba00dc --- /dev/null +++ b/cache_diff/tests/fails/duplicate_field.rs @@ -0,0 +1,9 @@ +use cache_diff::CacheDiff; + +#[derive(CacheDiff)] +struct CustomDiffFn { + #[cache_diff(rename = "foo", rename = "bar")] + name: String, +} + +fn main() {} diff --git a/cache_diff/tests/fails/duplicate_field.stderr b/cache_diff/tests/fails/duplicate_field.stderr new file mode 100644 index 0000000..93c6edc --- /dev/null +++ b/cache_diff/tests/fails/duplicate_field.stderr @@ -0,0 +1,11 @@ +error: CacheDiff duplicate attribute: `rename` + --> tests/fails/duplicate_field.rs:5:34 + | +5 | #[cache_diff(rename = "foo", rename = "bar")] + | ^^^^^^ + +error: previously `rename` defined here + --> tests/fails/duplicate_field.rs:5:18 + | +5 | #[cache_diff(rename = "foo", rename = "bar")] + | ^^^^^^ diff --git a/usage/tests/fails/missing_custom.rs b/cache_diff/tests/fails/missing_custom.rs similarity index 100% rename from usage/tests/fails/missing_custom.rs rename to cache_diff/tests/fails/missing_custom.rs diff --git a/cache_diff/tests/fails/missing_custom.stderr b/cache_diff/tests/fails/missing_custom.stderr new file mode 100644 index 0000000..675d5b1 --- /dev/null +++ b/cache_diff/tests/fails/missing_custom.stderr @@ -0,0 +1,13 @@ +error: `Expected `MissingCustom` to implement the `custom` attribute `#[cache_diff(custom = )]`, but it does not + --> tests/fails/missing_custom.rs:3:10 + | +3 | #[derive(CacheDiff)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `CacheDiff` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Field `i_am_a_custom_field` is ignored and requires `MissingCustom` to implement `custom` + --> tests/fails/missing_custom.rs:6:5 + | +6 | i_am_a_custom_field: String, + | ^^^^^^^^^^^^^^^^^^^ diff --git a/usage/tests/fails/missing_display.rs b/cache_diff/tests/fails/missing_display.rs similarity index 100% rename from usage/tests/fails/missing_display.rs rename to cache_diff/tests/fails/missing_display.rs diff --git a/usage/tests/fails/missing_display.stderr b/cache_diff/tests/fails/missing_display.stderr similarity index 95% rename from usage/tests/fails/missing_display.stderr rename to cache_diff/tests/fails/missing_display.stderr index 11d79c2..7f2d6e6 100644 --- a/usage/tests/fails/missing_display.stderr +++ b/cache_diff/tests/fails/missing_display.stderr @@ -8,7 +8,7 @@ error[E0277]: `NotDisplay` doesn't implement `std::fmt::Display` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead = note: required for `&NotDisplay` to implement `std::fmt::Display` note: required by a bound in `fmt_value` - --> $WORKSPACE/cache_diff/src/lib.rs + --> src/lib.rs | | fn fmt_value(&self, value: &T) -> String { | ^^^^^^^^^^^^^^^^^ required by this bound in `CacheDiff::fmt_value` diff --git a/usage/tests/pass/custom_with_serde_attribute.rs b/cache_diff/tests/pass/custom_with_serde_attribute.rs similarity index 100% rename from usage/tests/pass/custom_with_serde_attribute.rs rename to cache_diff/tests/pass/custom_with_serde_attribute.rs diff --git a/cache_diff/tests/pass/struct_with_generics.rs b/cache_diff/tests/pass/struct_with_generics.rs new file mode 100644 index 0000000..312f269 --- /dev/null +++ b/cache_diff/tests/pass/struct_with_generics.rs @@ -0,0 +1,22 @@ +use cache_diff::CacheDiff; + +#[derive(CacheDiff)] +struct Example +where + T: std::fmt::Display + Eq, +{ + name: String, + other: T, +} + +fn main() { + let now = Example:: { + name: "Richard".to_string(), + other: "John Jacob Jingleheimer Schmidt (his name is my name too)".to_string(), + }; + + let _ = now.diff(&Example:: { + name: "Richard".to_string(), + other: "schneems".to_string(), + }); +} diff --git a/cache_diff_derive/Cargo.toml b/cache_diff_derive/Cargo.toml index e03078d..1309b20 100644 --- a/cache_diff_derive/Cargo.toml +++ b/cache_diff_derive/Cargo.toml @@ -14,7 +14,7 @@ quote = "1.0" syn = { version = "2.0", features = ["extra-traits"] } proc-macro2 = "1.0" bullet_stream = { version = "0", optional = true } -strum = {version = "0.26", features = ["derive"] } +strum = {version = "0.27", features = ["derive"] } [lib] proc-macro = true diff --git a/cache_diff_derive/src/cache_diff_container.rs b/cache_diff_derive/src/cache_diff_container.rs deleted file mode 100644 index 077c813..0000000 --- a/cache_diff_derive/src/cache_diff_container.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Represents a named struct i.e. `struct Metadat { version: String }` for implementing the CacheDiff trait -//! -//! In syn terminology a "container" is a named struct, un-named (tuple) struct, or an enum. In the -//! case of CacheDiff, it's always a named struct. A container can have zero or more attributes: -//! -//! ```text -//! #[cache_diff(custom = custom_diff)] -//! struct Metadata { -//! // ... -//! } -//! ``` -//! -//! This looks similar to, but is differnt than a field attribute: -//! -//! ```text -//! #[cache_diff(rename = "Ruby Version")] -//! name: String -//! ``` -//! -//! Field attributes are handled by [CacheDiffField] and associated functions. -//! -//! One or more comma-separated attributes is parsed into a [ParsedAttribute] for the container. -//! Then one or more named fields are parsed into one or more [ActiveField]-s. Finally this information -//! is brought together to create a fully formed [CacheDiffContainer]. - -use crate::cache_diff_field::{ActiveField, ParsedField}; -use std::str::FromStr; -use syn::parse::Parse; -use syn::Data::Struct; -use syn::Fields::Named; -use syn::{DataStruct, FieldsNamed, Ident}; - -/// Represents the fully parsed Struct, it's attributes and all of it's parsed fields -#[derive(Debug, PartialEq)] -pub(crate) struct CacheDiffContainer { - /// The identifier of a struct e.g. `struct Metadata {version: String}` would be `Metadata` - pub(crate) identifier: Ident, - /// An optional path to a custom diff function - pub(crate) custom: Option, // #[cache_diff(custom = )] - /// One or more named fields - pub(crate) fields: Vec, -} - -impl CacheDiffContainer { - pub(crate) fn from_ast(input: &syn::DeriveInput) -> syn::Result { - let identifier = input.ident.clone(); - let mut container_custom = None; - - for attribute in input - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cache_diff")) - { - match attribute.parse_args_with(ParsedAttribute::parse)? { - ParsedAttribute::custom(path) => container_custom = Some(path), - } - } - - let mut fields = Vec::new(); - for ast_field in match input.data { - Struct(DataStruct { - fields: Named(FieldsNamed { ref named, .. }), - .. - }) => named, - _ => unimplemented!("CacheDiff derive macro can only be used on named structs"), - } - .to_owned() - .iter() - { - match ParsedField::from_field(ast_field)? { - ParsedField::IgnoredCustom => { - if container_custom.is_none() { - return Err(syn::Error::new( - identifier.span(), - format!( - "field `{field}` on {container} marked ignored as custom, but no `#[cache_diff(custom = )]` found on `{container}`", - field = ast_field.clone().ident.expect("named structs only"), - container = &identifier, - ) - )); - } - } - ParsedField::IgnoredOther => {} - ParsedField::Active(active_field) => fields.push(active_field), - } - } - - if fields.is_empty() { - Err(syn::Error::new( - identifier.span(), - "No fields to compare for CacheDiff, ensure struct has at least one named field that isn't `cache_diff(ignore)`-d", - )) - } else { - Ok(CacheDiffContainer { - identifier, - custom: container_custom, - fields, - }) - } - } -} - -/// Holds one macro configuration attribute for a field (i.e. `name: String`) -/// -/// Enum variants match configuration attribute keys exactly, this allows us to guarantee our error -/// messages are correct. -/// -/// Zero or more of these are used to build a [MuhField] -#[derive(Debug, strum::EnumDiscriminants)] -#[strum_discriminants(derive(strum::EnumIter, strum::Display, strum::EnumString))] -#[strum_discriminants(name(KnownAttribute))] -enum ParsedAttribute { - #[allow(non_camel_case_types)] - custom(syn::Path), -} - -/// List all valid attributes for a field, mostly for error messages -fn known_attributes() -> String { - use strum::IntoEnumIterator; - - KnownAttribute::iter() - .map(|k| format!("`{k}`")) - .collect::>() - .join(", ") -} - -impl syn::parse::Parse for ParsedAttribute { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let name: Ident = input.parse()?; - let name_str = name.to_string(); - match KnownAttribute::from_str(&name_str).map_err(|_| { - syn::Error::new( - name.span(), - format!( - "Unknown cache_diff attribute: `{name_str}`. Must be one of {valid_keys}", - valid_keys = known_attributes() - ), - ) - })? { - KnownAttribute::custom => { - input.parse::()?; - Ok(ParsedAttribute::custom(input.parse()?)) - } - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use syn::DeriveInput; - - #[test] - fn test_custom_all_ignored() { - let input: DeriveInput = syn::parse_quote! { - struct Metadata { - #[cache_diff(ignore)] - version: String - } - }; - - let result = CacheDiffContainer::from_ast(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()), - r#"No fields to compare for CacheDiff, ensure struct has at least one named field that isn't `cache_diff(ignore)`-d"# - ); - } - - #[test] - fn test_no_fields() { - let input: DeriveInput = syn::parse_quote! { - struct Metadata {} - }; - - let result = CacheDiffContainer::from_ast(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()), - r#"No fields to compare for CacheDiff, ensure struct has at least one named field that isn't `cache_diff(ignore)`-d"# - ); - } - - #[test] - fn test_custom_missing_on_container() { - let input: DeriveInput = syn::parse_quote! { - struct Metadata { - #[cache_diff(ignore = "custom")] - version: String - } - }; - - let result = CacheDiffContainer::from_ast(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()), - r#"field `version` on Metadata marked ignored as custom, but no `#[cache_diff(custom = )]` found on `Metadata`"# - ); - } - - #[test] - fn test_custom_on_container() { - let input: DeriveInput = syn::parse_quote! { - #[cache_diff(custom = my_function)] - struct Metadata { - version: String - } - }; - - let container = CacheDiffContainer::from_ast(&input).unwrap(); - assert!(container.custom.is_some()); - } - - #[test] - fn test_no_custom_on_container() { - let input: DeriveInput = syn::parse_quote! { - struct Metadata { - version: String - } - }; - - let container = CacheDiffContainer::from_ast(&input).unwrap(); - assert!(container.custom.is_none()); - } -} diff --git a/cache_diff_derive/src/cache_diff_field.rs b/cache_diff_derive/src/cache_diff_field.rs deleted file mode 100644 index 711e040..0000000 --- a/cache_diff_derive/src/cache_diff_field.rs +++ /dev/null @@ -1,387 +0,0 @@ -//! Represents a field on a struct i.e. `version: String` for implementing the CacheDiff trait -//! -//! A `syn::Field`` is can be a named or un-named value (in the case of a tuple struct) or -//! also represent an Enum. In the case of CacheDiff, we only deal in named structs. -//! -//! A field can have zero or more `#[cache_diff()]` attribute annotations for example: -//! -//! ```text -//! #[cache_diff(rename = "Ruby Version")] -//! name: String -//! ``` -//! -//! These attributes look similar to "container" attributes but are different: -//! -//! ```text -//! #[cache_diff(custom = custom_diff)] -//! struct Metadata { -//! // ... -//! } -//! ``` -//! -//! Container attributes are handled by [CacheDiffContainer] and associated files. -//! -//! Each comma separated attribute is parsed into a [ParsedAttribute] enum and that information is -//! combined to form a full [ParsedField]. -//! -//! A one or more [ParsedField::Active]-s lives inside of a [CacheDiffContainer]. - -use std::str::FromStr; -use strum::IntoEnumIterator; -use syn::{punctuated::Punctuated, spanned::Spanned, Field, Ident, PathArguments, Token}; - -#[derive(Debug, PartialEq)] -pub(crate) enum ParsedField { - IgnoredCustom, - IgnoredOther, - Active(ActiveField), -} - -#[derive(Debug, PartialEq)] -pub(crate) struct ActiveField { - /// What the user will see when this field differs and invalidates the cache - /// i.e. `age: usize` will be `"age"`` - pub(crate) name: String, - /// The function to use when rendering values on the field - /// i.e. `age: 42` will be `"42"` - pub(crate) display_fn: syn::Path, - /// The proc-macro identifier for a field i.e. `name: String` would be a programatic - /// reference to `name` that can be used along with `quote!` to produce code - pub(crate) field_identifier: Ident, -} - -impl ParsedField { - pub(crate) fn from_field(field: &Field) -> syn::Result { - let mut rename = None; - let mut display = None; - let mut ignored = None; - let field_identifier = field.ident.clone().ok_or_else(|| { - syn::Error::new( - field.span(), - "CacheDiff can only be used on structs with named fields", - ) - })?; - - if let Some(attributes) = field - .attrs - .iter() - .find(|&attr| attr.path().is_ident("cache_diff")) - { - match &attributes.meta { - syn::Meta::List(meta_list) => { - for attr in meta_list.parse_args_with( - Punctuated::::parse_terminated, - )? { - match attr { - ParsedAttribute::rename(name) => { - rename = Some(name); - } - ParsedAttribute::display(path) => { - display = Some(path); - } - ParsedAttribute::ignore(field_status) => { - // - match field_status { - Ignored::IgnoreCustom => { - ignored = Some(ParsedField::IgnoredCustom) - } - Ignored::IgnoreOther => { - ignored = Some(ParsedField::IgnoredOther) - } - } - } - } - } - } - _ => { - return Err(syn::Error::new( - attributes.pound_token.span, - "Expected a list of attributes", - )) - } - } - } - - if let Some(ignored) = ignored { - if display.is_some() || rename.is_some() { - Err(syn::Error::new(field_identifier.span(), format!("The cache_diff attribute `{}` renders other attributes useless, remove additional attributes", KnownAttribute::ignore))) - } else { - Ok(ignored) - } - } else { - Ok(ParsedField::Active(ActiveField { - name: rename.unwrap_or_else(|| field_identifier.to_string().replace("_", " ")), - display_fn: display.unwrap_or_else(|| { - if is_pathbuf(&field.ty) { - syn::parse_str("std::path::Path::display") - .expect("PathBuf::display parses as a syn::Path") - } else { - syn::parse_str("std::convert::identity") - .expect("std::convert::identity parses as a syn::Path") - } - }), - field_identifier, - })) - } - } -} - -/// Holds one macro configuration attribute for a field (i.e. `name: String`) -/// -/// Enum variants match configuration attribute keys exactly, this allows us to guarantee our error -/// messages are correct. -/// -/// Zero or more of these are used to build a [MuhField] -#[derive(Debug, strum::EnumDiscriminants)] -#[strum_discriminants(derive(strum::EnumIter, strum::Display, strum::EnumString))] -#[strum_discriminants(name(KnownAttribute))] -enum ParsedAttribute { - #[allow(non_camel_case_types)] - rename(String), // #[cache_diff(rename="...")] - #[allow(non_camel_case_types)] - display(syn::Path), // #[cache_diff(display="...")] - #[allow(non_camel_case_types)] - ignore(Ignored), // #[cache_diff(ignore)] -} - -/// List all valid attributes for a field, mostly for error messages -fn known_attributes() -> String { - KnownAttribute::iter() - .map(|k| format!("`{k}`")) - .collect::>() - .join(", ") -} - -impl syn::parse::Parse for ParsedAttribute { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let name: Ident = input.parse()?; - let name_str = name.to_string(); - match KnownAttribute::from_str(&name_str).map_err(|_| { - let extra = match name_str.as_ref() { - "custom" => "\nThe cache_diff attribute `custom` is available on the struct, not the field", - _ => "" - }; - - syn::Error::new( - name.span(), - format!( - "Unknown cache_diff attribute: `{name_str}`. Must be one of {valid_keys}{extra}", - valid_keys = known_attributes() - ), - ) - })? { - KnownAttribute::rename => { - input.parse::()?; - Ok(ParsedAttribute::rename(input.parse::()?.value())) - } - KnownAttribute::display => { - input.parse::()?; - Ok(ParsedAttribute::display(input.parse()?)) - } - KnownAttribute::ignore => { - if input.peek(syn::Token![=]) { - input.parse::()?; - let value = input.parse::()?.value(); - if &value == "custom" { - Ok(ParsedAttribute::ignore(Ignored::IgnoreCustom)) - } else { - Ok(ParsedAttribute::ignore(Ignored::IgnoreOther)) - } - } else { - Ok(ParsedAttribute::ignore(Ignored::IgnoreOther)) - } - } - } - } -} - -/// Represents whether a field is included in the derive diff comparison or not and why -#[derive(Debug, PartialEq)] -pub(crate) enum Ignored { - /// Ignored because field is delegated to `custom = ` on the container. - /// This information is needed so we can raise an error when the container does not implement this attribute - IgnoreCustom, - /// Ignored for some other reason - IgnoreOther, -} - -fn is_pathbuf(ty: &syn::Type) -> bool { - if let syn::Type::Path(type_path) = ty { - if let Some(segment) = type_path.path.segments.last() { - return segment.ident == "PathBuf" && segment.arguments == PathArguments::None; - } - } - false -} - -#[cfg(test)] -mod test { - use super::*; - use indoc::formatdoc; - use pretty_assertions::assert_eq; - use syn::Attribute; - - fn attribute_on_field(attribute: Attribute, field: Field) -> Field { - let mut input = field.clone(); - input.attrs = vec![attribute]; - input - } - - #[test] - fn test_parse_all_rename() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(rename="Ruby version")] - }, - syn::parse_quote! { - version: String - }, - ); - let expected = ParsedField::Active(ActiveField { - name: "Ruby version".to_string(), - display_fn: syn::parse_str("std::convert::identity").unwrap(), - field_identifier: input.ident.to_owned().unwrap(), - }); - assert_eq!(expected, ParsedField::from_field(&input).unwrap()); - } - - #[test] - fn test_parse_all_display() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(display = my_function)] - }, - syn::parse_quote! { - version: String - }, - ); - let expected = ParsedField::Active(ActiveField { - name: "version".to_string(), - display_fn: syn::parse_str("my_function").unwrap(), - field_identifier: input.ident.to_owned().unwrap(), - }); - assert_eq!(expected, ParsedField::from_field(&input).unwrap()); - } - - #[test] - fn test_ignore_with_value() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(ignore = "value")] - }, - syn::parse_quote! { - version: String - }, - ); - assert_eq!( - ParsedField::IgnoredOther, - ParsedField::from_field(&input).unwrap() - ); - } - - #[test] - fn test_parse_all_ignore_no_value() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(ignore)] - }, - syn::parse_quote! { - version: String - }, - ); - assert_eq!( - ParsedField::IgnoredOther, - ParsedField::from_field(&input).unwrap() - ); - } - - #[test] - fn test_parse_all_ignore_custom() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(ignore = "custom")] - }, - syn::parse_quote! { - version: String - }, - ); - assert_eq!( - ParsedField::IgnoredCustom, - ParsedField::from_field(&input).unwrap() - ); - } - - #[test] - fn test_parse_accidental_custom() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(custom = "IDK")] - }, - syn::parse_quote! { - version: String - }, - ); - - let result = ParsedField::from_field(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()).trim(), - formatdoc! {" - Unknown cache_diff attribute: `custom`. Must be one of `rename`, `display`, `ignore` - The cache_diff attribute `custom` is available on the struct, not the field - "} - .trim() - ); - } - - #[test] - fn test_parse_all_unknown() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(unknown = "IDK")] - }, - syn::parse_quote! { - version: String - }, - ); - let result = ParsedField::from_field(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()), - r#"Unknown cache_diff attribute: `unknown`. Must be one of `rename`, `display`, `ignore`"# - ); - } - - #[test] - fn test_ignored_other_attributes() { - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(ignore = "reasons", display = my_function)] - }, - syn::parse_quote! { - version: String - }, - ); - let result = ParsedField::from_field(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()), - r#"The cache_diff attribute `ignore` renders other attributes useless, remove additional attributes"# - ); - - let input = attribute_on_field( - syn::parse_quote! { - #[cache_diff(display = my_function, ignore = "reasons")] - }, - syn::parse_quote! { - version: String - }, - ); - let result = ParsedField::from_field(&input); - assert!(result.is_err(), "Expected an error, got {:?}", result); - assert_eq!( - format!("{}", result.err().unwrap()), - r#"The cache_diff attribute `ignore` renders other attributes useless, remove additional attributes"# - ); - } -} diff --git a/cache_diff_derive/src/lib.rs b/cache_diff_derive/src/lib.rs index d14ff56..f92fe5d 100644 --- a/cache_diff_derive/src/lib.rs +++ b/cache_diff_derive/src/lib.rs @@ -1,10 +1,13 @@ -use cache_diff_container::CacheDiffContainer; -use cache_diff_field::ActiveField; +use parse_container::ParseContainer; +use parse_field::ParseField; use proc_macro::TokenStream; -use syn::DeriveInput; -mod cache_diff_container; -mod cache_diff_field; +mod parse_container; +mod parse_field; +mod shared; + +pub(crate) const NAMESPACE: &str = "cache_diff"; +pub(crate) const MACRO_NAME: &str = "CacheDiff"; #[proc_macro_derive(CacheDiff, attributes(cache_diff))] pub fn cache_diff(item: TokenStream) -> TokenStream { @@ -14,11 +17,14 @@ pub fn cache_diff(item: TokenStream) -> TokenStream { } fn create_cache_diff(item: proc_macro2::TokenStream) -> syn::Result { - let ast: DeriveInput = syn::parse2(item).unwrap(); - let container = CacheDiffContainer::from_ast(&ast)?; - let struct_identifier = &container.identifier; + let ParseContainer { + ident, + generics, + custom, + fields, + } = ParseContainer::from_derive_input(&syn::parse2(item)?)?; - let custom_diff = if let Some(ref custom_fn) = container.custom { + let custom_diff = if let Some(ref custom_fn) = custom { quote::quote! { let custom_diff = #custom_fn(old, self); for diff in &custom_diff { @@ -30,27 +36,32 @@ fn create_cache_diff(item: proc_macro2::TokenStream) -> syn::Result ::std::vec::Vec { let mut differences = ::std::vec::Vec::new(); #custom_diff diff --git a/cache_diff_derive/src/parse_container.rs b/cache_diff_derive/src/parse_container.rs new file mode 100644 index 0000000..a8fbdad --- /dev/null +++ b/cache_diff_derive/src/parse_container.rs @@ -0,0 +1,198 @@ +use crate::parse_field::ParseField; +use crate::shared::{attribute_lookup, check_empty, known_attribute, WithSpan}; +use crate::{MACRO_NAME, NAMESPACE}; + +/// Container (i.e. struct Metadata { ... }) and its parsed attributes +/// i.e. `#[cache_diff( ... )]` +#[derive(Debug)] +pub(crate) struct ParseContainer { + /// The proc-macro identifier for a container i.e. `struct Metadata { }` would be a programatic + /// reference to `Metadata` that can be used along with `quote!` to produce code. + pub(crate) ident: syn::Ident, + /// Info about generics, lifetimes and where clauses i.e. `struct Metadata { name: T }` + pub(crate) generics: syn::Generics, + /// An optional path to a custom diff function + /// Set via attribute on the container (#[cache_diff(custom = )]) + pub(crate) custom: Option, + /// Fields (i.e. `name: String`) and their associated attributes i.e. `#[cache_diff(...)]` + pub(crate) fields: Vec, +} + +impl ParseContainer { + pub(crate) fn from_derive_input(input: &syn::DeriveInput) -> Result { + let ident = input.ident.clone(); + let generics = input.generics.clone(); + let mut lookup = attribute_lookup::(&input.attrs)?; + let custom = lookup + .remove(&KnownAttribute::custom) + .map(WithSpan::into_inner) + .map(|parsed| match parsed { + ParseAttribute::custom(path) => path, + }); + + let fields = match input.data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }), + .. + }) => named, + _ => { + return Err(syn::Error::new( + ident.span(), + format!("{MACRO_NAME} can only be used on named structs"), + )) + } + } + .into_iter() + .map(ParseField::from_field) + .collect::, syn::Error>>()?; + + check_empty(lookup)?; + + if let Some(field) = fields + .iter() + .find(|field| matches!(field.ignore.as_deref(), Some("custom"))) + { + if custom.is_none() { + let mut error = syn::Error::new( + proc_macro2::Span::call_site(), + format!("`Expected `{ident}` to implement the `custom` attribute `#[{NAMESPACE}(custom = )]`, but it does not"), + ); + error.combine(syn::Error::new( + field.ident.span(), + format!( + "Field `{}` is ignored and requires `{ident}` to implement `custom`", + field.ident + ), + )); + return Err(error); + } + } + + if fields.iter().any(|f| f.ignore.is_none()) { + Ok(ParseContainer { + ident, + generics, + fields, + custom, + }) + } else { + Err(syn::Error::new(ident.span(), format!("No fields to compare for {MACRO_NAME}, ensure struct has at least one named field that isn't `{NAMESPACE}(ignore)`"))) + } + } +} + +/// A single field attribute +#[derive(strum::EnumDiscriminants, Debug, PartialEq)] +#[strum_discriminants( + name(KnownAttribute), + derive(strum::EnumIter, strum::Display, strum::EnumString, Hash) +)] +enum ParseAttribute { + #[allow(non_camel_case_types)] + custom(syn::Path), // #[cache_diff(custom=)] +} + +impl syn::parse::Parse for KnownAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + known_attribute(&input.parse()?) + } +} + +impl syn::parse::Parse for ParseAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let key: KnownAttribute = input.parse()?; + input.parse::()?; + match key { + KnownAttribute::custom => Ok(ParseAttribute::custom(input.parse()?)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_known_attributes() { + let attribute: KnownAttribute = syn::parse_str("custom").unwrap(); + assert_eq!(KnownAttribute::custom, attribute); + } + + #[test] + fn test_parse_attribute() { + let attribute: ParseAttribute = syn::parse_str("custom = my_function").unwrap(); + assert!(matches!(attribute, ParseAttribute::custom(_))); + + let result: Result = syn::parse_str("unknown"); + assert!(result.is_err(), "Expected an error, got {:?}", result); + assert_eq!( + r"Unknown cache_diff attribute: `unknown`. Must be one of `custom`", + format!("{}", result.err().unwrap()), + ); + } + + #[test] + fn test_custom_parse_attribute() { + let input: syn::DeriveInput = syn::parse_quote! { + #[cache_diff(custom = my_function)] + struct Metadata { + name: String + } + }; + + assert!(matches!( + attribute_lookup::(&input.attrs) + .unwrap() + .remove(&KnownAttribute::custom) + .map(WithSpan::into_inner), + Some(ParseAttribute::custom(_)) + )); + } + + #[test] + fn test_parses() { + let container = ParseContainer::from_derive_input(&syn::parse_quote! { + struct Metadata { + version: String + } + }) + .unwrap(); + assert_eq!(1, container.fields.len()); + + let container = ParseContainer::from_derive_input(&syn::parse_quote! { + struct Metadata { + version: String, + checksum: String + } + }) + .unwrap(); + assert_eq!(2, container.fields.len()); + } + + #[test] + fn test_no_fields() { + let result = ParseContainer::from_derive_input(&syn::parse_quote! { + struct Metadata { } + }); + assert!(result.is_err(), "Expected an error, got {:?}", result); + assert_eq!( + format!("{}", result.err().unwrap()), + r#"No fields to compare for CacheDiff, ensure struct has at least one named field that isn't `cache_diff(ignore)`"# + ); + } + + #[test] + fn test_all_ignored() { + let result = ParseContainer::from_derive_input(&syn::parse_quote! { + struct Metadata { + #[cache_diff(ignore)] + version: String + } + }); + assert!(result.is_err(), "Expected an error, got {:?}", result); + assert_eq!( + format!("{}", result.err().unwrap()), + r#"No fields to compare for CacheDiff, ensure struct has at least one named field that isn't `cache_diff(ignore)`"# + ); + } +} diff --git a/cache_diff_derive/src/parse_field.rs b/cache_diff_derive/src/parse_field.rs new file mode 100644 index 0000000..ec0f623 --- /dev/null +++ b/cache_diff_derive/src/parse_field.rs @@ -0,0 +1,248 @@ +use crate::{ + shared::{attribute_lookup, check_empty, known_attribute, WithSpan}, + MACRO_NAME, NAMESPACE, +}; +use syn::spanned::Spanned; + +/// Field (i.e. `name: String`) of a container (struct) and its parsed attributes +/// i.e. `#[cache_diff(rename = "Ruby version")]` +#[derive(Debug)] +pub(crate) struct ParseField { + /// The proc-macro identifier for a field i.e. `name: String` would be a programatic + /// reference to `name` that can be used along with `quote!` to produce code. + pub(crate) ident: syn::Ident, + /// What the user will see when this field differs and invalidates the cache + /// i.e. `age: usize` will be `"age"`. + pub(crate) name: String, + /// Whether or not the field is included in the derived diff comparison + pub(crate) ignore: Option, + /// The function to use when rendering values on the field + /// i.e. `age: 42` will be `"42"` + pub(crate) display: syn::Path, +} + +fn is_pathbuf(ty: &syn::Type) -> bool { + if let syn::Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + return segment.ident == "PathBuf" && segment.arguments == syn::PathArguments::None; + } + } + false +} + +impl ParseField { + pub(crate) fn from_field(field: &syn::Field) -> Result { + let ident = field.ident.clone().ok_or_else(|| { + syn::Error::new( + field.span(), + format!("{MACRO_NAME} can only be used on structs with named fields"), + ) + })?; + + let mut lookup = attribute_lookup(&field.attrs)?; + let name = lookup + .remove(&KnownAttribute::rename) + .map(WithSpan::into_inner) + .map(|parsed| match parsed { + ParseAttribute::rename(inner) => inner, + _ => unreachable!(), + }) + .unwrap_or_else(|| ident.to_string().replace("_", " ")); + let display = lookup + .remove(&KnownAttribute::display) + .map(WithSpan::into_inner) + .map(|parsed| match parsed { + ParseAttribute::display(inner) => inner, + _ => unreachable!(), + }) + .unwrap_or_else(|| { + if is_pathbuf(&field.ty) { + syn::parse_str("std::path::Path::display") + .expect("PathBuf::display parses as a syn::Path") + } else { + syn::parse_str("std::convert::identity") + .expect("std::convert::identity parses as a syn::Path") + } + }); + let ignore = lookup + .remove(&KnownAttribute::ignore) + .map(WithSpan::into_inner) + .map(|parsed| match parsed { + ParseAttribute::ignore(inner) => inner, + _ => unreachable!(), + }); + check_empty(lookup)?; + + Ok(ParseField { + ident, + name, + ignore, + display, + }) + } +} + +/// A single attribute +#[derive(strum::EnumDiscriminants, Debug, PartialEq)] +#[strum_discriminants( + name(KnownAttribute), + derive(strum::EnumIter, strum::Display, strum::EnumString, Hash) +)] +enum ParseAttribute { + #[allow(non_camel_case_types)] + rename(String), // #[cache_diff(rename="...")] + #[allow(non_camel_case_types)] + display(syn::Path), // #[cache_diff(display=)] + #[allow(non_camel_case_types)] + ignore(String), // #[cache_diff(ignore)] +} + +impl syn::parse::Parse for KnownAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let identity = input.parse::()?; + known_attribute(&identity).map_err(|mut err| { + if identity == "custom" { + err.combine(syn::Error::new( + identity.span(), + format!( + "\nThe {NAMESPACE} attribute `custom` is available on the struct, not the field" + ), + )) + }; + err + }) + } +} + +impl syn::parse::Parse for ParseAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let key: KnownAttribute = input.parse()?; + + match key { + KnownAttribute::rename => { + input.parse::()?; + Ok(ParseAttribute::rename( + input.parse::()?.value(), + )) + } + KnownAttribute::display => { + input.parse::()?; + Ok(ParseAttribute::display(input.parse()?)) + } + KnownAttribute::ignore => { + if input.peek(syn::Token![=]) { + input.parse::()?; + Ok(ParseAttribute::ignore( + input.parse::()?.value(), + )) + } else { + Ok(ParseAttribute::ignore("default".to_string())) + } + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use syn::parse::Parse; + + #[test] + fn test_attribute_lookup_raises_on_duplicate() { + let attribute: syn::Attribute = syn::parse_quote! { + #[cache_diff(rename="Ruby version", rename = "oops")] + }; + + let result = attribute_lookup::(&[attribute]); + assert!(result.is_err(), "Expected an error, got {:?}", result); + assert_eq!( + "CacheDiff duplicate attribute: `rename`".to_string(), + format!("{}", result.err().unwrap()) + ); + } + + #[test] + fn test_known_attributes() { + let parsed: KnownAttribute = syn::parse_str("rename").unwrap(); + assert_eq!(KnownAttribute::rename, parsed); + + let parsed: KnownAttribute = syn::parse_str("ignore").unwrap(); + assert_eq!(KnownAttribute::ignore, parsed); + + let parsed: KnownAttribute = syn::parse_str("display").unwrap(); + assert_eq!(KnownAttribute::display, parsed); + + let result: Result = syn::parse_str("unknown"); + assert!(result.is_err(), "Expected an error, got {:?}", result); + assert_eq!( + format!("{}", result.err().unwrap()), + r#"Unknown cache_diff attribute: `unknown`. Must be one of `rename`, `display`, `ignore`"# + ); + } + + #[test] + fn test_parse_rename_attribute() { + let attribute: syn::Attribute = syn::parse_quote! { + #[cache_diff(rename="Ruby version")] + }; + + assert_eq!( + ParseAttribute::rename("Ruby version".to_string()), + attribute.parse_args_with(ParseAttribute::parse).unwrap() + ); + } + + #[test] + fn test_parse_rename_ignore_attribute() { + let field: syn::Field = syn::parse_quote! { + #[cache_diff(rename="Ruby version", ignore)] + name: String + }; + + let mut lookup = attribute_lookup::(&field.attrs).unwrap(); + assert_eq!( + lookup.remove(&KnownAttribute::rename).unwrap().into_inner(), + ParseAttribute::rename("Ruby version".to_string()) + ); + + assert_eq!( + lookup.remove(&KnownAttribute::ignore).unwrap().into_inner(), + ParseAttribute::ignore("default".to_string()) + ); + } + + #[test] + fn test_requires_named_struct() { + let field: syn::Field = syn::parse_quote! {()}; + + let result = ParseField::from_field(&field); + assert!(result.is_err(), "Expected an error, got {:?}", result); + assert_eq!( + format!("{}", result.err().unwrap()), + r#"CacheDiff can only be used on structs with named fields"# + ); + } + + #[test] + fn test_parse_field_rename_ignore_attribute() { + let field: syn::Field = syn::parse_quote! { + #[cache_diff(rename="Ruby version", ignore)] + name: String + }; + + let ParseField { + ident: _, + name, + ignore, + display, + } = ParseField::from_field(&field).unwrap(); + + assert_eq!("Ruby version".to_string(), name); + assert!(ignore.is_some()); + assert_eq!( + syn::parse_str::("std::convert::identity").unwrap(), + display + ); + } +} diff --git a/cache_diff_derive/src/shared.rs b/cache_diff_derive/src/shared.rs new file mode 100644 index 0000000..d5de6fc --- /dev/null +++ b/cache_diff_derive/src/shared.rs @@ -0,0 +1,128 @@ +use crate::{MACRO_NAME, NAMESPACE}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; + +/// Parses all attributes and returns a lookup with the parsed value and span information where it was found +/// +/// - Guarantees attributes are not duplicated +pub(crate) fn attribute_lookup( + attrs: &[syn::Attribute], +) -> Result>, syn::Error> +where + T: strum::IntoDiscriminant + syn::parse::Parse, + T::Discriminant: Eq + Display + std::hash::Hash + Copy, +{ + let mut seen = HashMap::new(); + let parsed_attributes = parse_attrs::>(attrs)?; + for attribute_with_span in parsed_attributes { + let WithSpan(ref parsed, span) = attribute_with_span; + let key = parsed.discriminant(); + if let Some(WithSpan(_, prior)) = seen.insert(key, attribute_with_span) { + let mut error = + syn::Error::new(span, format!("{MACRO_NAME} duplicate attribute: `{key}`")); + error.combine(syn::Error::new( + prior, + format!("previously `{key}` defined here"), + )); + return Err(error); + } + } + + Ok(seen) +} + +pub(crate) fn check_empty(lookup: HashMap>) -> syn::Result<()> +where + T: strum::IntoDiscriminant, + T::Discriminant: Display + std::hash::Hash, +{ + if lookup.is_empty() { + Ok(()) + } else { + let mut error = syn::Error::new( + proc_macro2::Span::call_site(), + "Internal error: The developer forgot to implement some logic", + ); + for (key, WithSpan(_, span)) in lookup.into_iter() { + error.combine(syn::Error::new( + span, + format!("Attribute `{key}` parsed but not used"), + )); + } + Err(error) + } +} + +/// Parses one bare word like "rename" for any iterable enum and that's it +/// +/// Won't parse an equal sign or anything else +pub(crate) fn known_attribute(identity: &syn::Ident) -> syn::Result +where + T: FromStr + strum::IntoEnumIterator + Display, +{ + let name_str = &identity.to_string(); + T::from_str(name_str).map_err(|_| { + syn::Error::new( + identity.span(), + format!( + "Unknown {NAMESPACE} attribute: `{identity}`. Must be one of {valid_keys}", + valid_keys = T::iter() + .map(|key| format!("`{key}`")) + .collect::>() + .join(", ") + ), + ) + }) +} + +/// Helper type for parsing a type and preserving the original span +/// +/// Used with [syn::punctuated::Punctuated] to capture the inner span of an attribute. +#[derive(Debug)] +pub(crate) struct WithSpan(pub(crate) T, pub(crate) proc_macro2::Span); + +impl WithSpan { + pub(crate) fn into_inner(self) -> T { + self.0 + } +} + +impl syn::parse::Parse for WithSpan { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let span = input.span(); + Ok(WithSpan(input.parse()?, span)) + } +} + +fn parse_attrs(attrs: &[syn::Attribute]) -> Result, syn::Error> +where + T: syn::parse::Parse, +{ + let mut attributes = Vec::new(); + for attr in attrs.iter().filter(|attr| attr.path().is_ident(NAMESPACE)) { + for attribute in attr + .parse_args_with(syn::punctuated::Punctuated::::parse_terminated)? + { + attributes.push(attribute) + } + } + + Ok(attributes) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_attrs_vec() { + let field: syn::Field = syn::parse_quote! { + #[cache_diff("Ruby version")] + name: String + }; + + assert_eq!( + vec![syn::parse_str::(r#""Ruby version""#).unwrap()], + parse_attrs::(&field.attrs).unwrap() + ); + } +} diff --git a/usage/Cargo.toml b/usage/Cargo.toml deleted file mode 100644 index e8bf868..0000000 --- a/usage/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "usage" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -cache_diff = { path = "../cache_diff" } - -[dev-dependencies] -trybuild = "1.0.101" -pretty_assertions.workspace = true -indoc.workspace = true -serde.workspace = true diff --git a/usage/src/main.rs b/usage/src/main.rs deleted file mode 100644 index 7604c49..0000000 --- a/usage/src/main.rs +++ /dev/null @@ -1,206 +0,0 @@ -use cache_diff::CacheDiff; - -#[derive(CacheDiff)] -struct Hello { - name: String, -} - -#[derive(CacheDiff)] -#[cache_diff(custom = diff_fn)] -struct CustomDiffFn { - name: String, -} - -fn diff_fn(old: &CustomDiffFn, now: &CustomDiffFn) -> Vec { - let mut diff = Vec::new(); - diff.push(format!( - "Totally custom old: {} now: {}", - old.name, now.name - )); - diff -} - -fn main() { - let _ = Hello { - name: "world".to_string(), - }; - println!("Hello, world!"); - let _ = CustomDiffFn { - name: "Hello".to_string(), - }; -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - use std::path::PathBuf; - - fn is_diff(_in: &T) {} - - #[test] - fn custom_diff_function() { - let diff = CustomDiffFn { - name: "Richard".to_string(), - } - .diff(&CustomDiffFn { - name: "Schneems".to_string(), - }); - - assert_eq!( - [ - "Totally custom old: Schneems now: Richard".to_string(), - "name (`Schneems` to `Richard`)".to_string() - ], - diff[..] - ); - } - - #[test] - fn ignore_a_field() { - #[derive(CacheDiff)] - struct Metadata { - ruby_version: String, - #[cache_diff(ignore)] - #[allow(dead_code)] - _modified_by: String, - } - - let metadata = Metadata { - ruby_version: "3.4.0".to_string(), - _modified_by: "richard".to_string(), - }; - - let diff = metadata.diff(&Metadata { - ruby_version: "3.3.0".to_string(), - _modified_by: "not rich".to_string(), - }); - assert_eq!(diff.len(), 1); - let contents = diff.join(" "); - assert!( - !contents.contains("modified"), - "Unexpected contents {contents}" - ); - } - - #[test] - fn auto_display_path_buff() { - #[derive(CacheDiff)] - struct Metadata { - path: PathBuf, - } - let metadata = Metadata { - path: PathBuf::from("/tmp"), - }; - let diff = metadata.diff(&Metadata { - path: PathBuf::from("/tmp2"), - }); - - assert_eq!(diff.len(), 1); - let contents = diff.join(" "); - assert!( - contents.contains("/tmp"), - "Unexpected contents '{contents}'" - ); - } - - #[test] - fn ignore_rename_display_field() { - fn my_display(value: &String) -> String { - format!("custom {value}") - } - #[derive(CacheDiff)] - struct Metadata { - #[cache_diff(rename="Ruby version", display=my_display)] - version: String, - } - let metadata = Metadata { - version: "3.4.0".to_string(), - }; - let diff = metadata.diff(&Metadata { - version: "3.3.0".to_string(), - }); - - assert_eq!(diff.len(), 1); - let contents = diff.join(" "); - assert!( - contents.contains("custom 3.4.0"), - "Expected `{contents}` to contain 'custom 3.4.0'" - ); - } - - #[test] - fn ignore_rename_field() { - #[derive(CacheDiff)] - struct Metadata { - #[cache_diff(rename = "Ruby version")] - version: String, - } - let metadata = Metadata { - version: "3.4.0".to_string(), - }; - let diff = metadata.diff(&Metadata { - version: "3.3.0".to_string(), - }); - - assert_eq!(diff.len(), 1); - let contents = diff.join(" "); - assert!( - contents.contains("Ruby version"), - "Expected `{contents}` to contain Ruby version" - ); - } - - // #[test] - // fn ignore_field() { - // #[derive(CacheDiff)] - // struct Metadata { - // ruby_version: String, - // #[cache_diff(ignore)] - // modified_by: String, - // } - // let metadata = Metadata { - // ruby_version: "3.4.0".to_string(), - // modified_by: "richard".to_string(), - // }; - // let diff = metadata.diff(&Metadata { - // ruby_version: "3.3.0".to_string(), - // modified_by: "not rich".to_string(), - // }); - - // assert_eq!(diff.len(), 1); - // } - - #[test] - fn test_replace_space() { - #[derive(CacheDiff)] - struct Metadata { - ruby_version: String, - } - let metadata = Metadata { - ruby_version: "3.4.0".to_string(), - }; - let diff = metadata.diff(&Metadata { - ruby_version: "3.3.0".to_string(), - }); - assert_eq!(diff.len(), 1); - assert!(diff.join(" ").contains("ruby version")); - } - - #[test] - fn test_cache_diff() { - #[derive(CacheDiff)] - struct Person { - _name: String, - } - let richard = Person { - _name: "richard".to_string(), - }; - is_diff(&richard); - let diff = richard.diff(&Person { - _name: "rich".to_string(), - }); - - assert_eq!(diff.len(), 1); - } -} diff --git a/usage/src/multiple_structs.rs b/usage/src/multiple_structs.rs deleted file mode 100644 index 7089bcf..0000000 --- a/usage/src/multiple_structs.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Ensure multiple derives can be in the same file -#[derive(CacheDiff)] -struct Dog { - woof: String, -} -#[allow(dead_code)] -#[derive(CacheDiff)] -struct Cat { - meow: String, -} -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_cat() { - let diff = Cat { - meow: "Meow".to_string(), - } - .diff(&Cat { - meow: "Woem".to_string(), - }); - assert!(diff.len() == 1); - } - #[test] - fn test_dog() { - let diff = Dog { - woof: "Woof".to_string(), - } - .diff(&Dog { - woof: "Foow".to_string(), - }); - assert!(diff.len() == 1); - } -} diff --git a/usage/tests/fails/missing_custom.stderr b/usage/tests/fails/missing_custom.stderr deleted file mode 100644 index 1ee98bf..0000000 --- a/usage/tests/fails/missing_custom.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: field `i_am_a_custom_field` on MissingCustom marked ignored as custom, but no `#[cache_diff(custom = )]` found on `MissingCustom` - --> tests/fails/missing_custom.rs:4:8 - | -4 | struct MissingCustom { - | ^^^^^^^^^^^^^