diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7103cee..9ac3241 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,3 +33,4 @@ jobs: nix develop .#ci -c cargo clippy --features=none_as_default -- -Dwarnings nix develop .#ci -c cargo clippy --features=keep_none -- -Dwarnings nix develop .#ci -c cargo clippy --features=nesting -- -Dwarnings + nix develop .#ci -c cargo clippy --features=copy_doc -- -Dwarnings diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b2fce2..063eaeb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,7 @@ jobs: nix develop .#ci -c run-test std nix develop .#ci -c run-test merge nix develop .#ci -c run-test default + nix develop .#ci -c run-test keep_log - name: Test patch examples run: | diff --git a/README.md b/README.md index 11862d1..71326fd 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,8 @@ This crate includes the following optional features: - `substrate` *(optional)*: enables the `Substrate` derive macro for exposing a struct's field layout so downstream crates can access it via `expose()` or source parsing. - `catalyst` *(optional)*: enables the `Catalyst` and `Complex` derive macros for extending a struct with fields from another crate. Implies `substrate`. - `unsafe` *(optional)*: avoid memory moves. Only meaningful with the `catalyst` feature. +- `copy_doc` *(optional)*: copy the documentation of the original struct and its fields to the patched one. + This is useful when a trait, derived on the patch structure, uses the doc attributes, typically for auto-documentation of some sort. [crates-badge]: https://img.shields.io/crates/v/struct-patch.svg [crate-url]: https://crates.io/crates/struct-patch diff --git a/derive/Cargo.toml b/derive/Cargo.toml index c1d1a7f..9cbadf3 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -32,6 +32,8 @@ substrate = [ "syn-serde", "syn/full" ] catalyst = [ "substrate", "dep:cargo_metadata" ] unsafe = [] box = [] +copy_doc = [] [dev-dependencies] pretty_assertions_sorted = "1.2.3" +syn = { version = "3", features = ["full"] } \ No newline at end of file diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 8826f4a..77539d5 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -56,7 +56,7 @@ impl Catalyst { } = self; let substrate_name: Ident = { - let lit = LitStr::new(&bind, Span::call_site()); + let lit = LitStr::new(bind, Span::call_site()); lit.parse()? }; @@ -355,7 +355,7 @@ impl Catalyst { )); } let complex_struct_name = name.unwrap_or({ - let ts = TokenStream::from_str(&format!("{}Complex", &ident,)).unwrap(); + let ts = TokenStream::from_str(&format!("{ident}Complex")).unwrap(); let lit = LitStr::new(&ts.to_string(), Span::call_site()); lit.parse()? }); @@ -381,8 +381,7 @@ fn find_fields_from_src(src: &str, struct_name: &str) -> syn::Fields { .split_once(':') .expect("src attribute must be 'crate_name:/path/to/file.rs'"); - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") - .expect("CARGO_MANIFEST_DIR is not set"); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set"); let meta = cargo_metadata::MetadataCommand::new() .manifest_path(std::path::Path::new(&manifest_dir).join("Cargo.toml")) @@ -400,8 +399,7 @@ fn find_fields_from_src(src: &str, struct_name: &str) -> syn::Fields { .parent() .expect("manifest_path has no parent directory"); - let src_path = std::path::Path::new(pkg_dir.as_str()) - .join(rel_path.trim_start_matches('/')); + let src_path = std::path::Path::new(pkg_dir.as_str()).join(rel_path.trim_start_matches('/')); let content = std::fs::read_to_string(&src_path) .unwrap_or_else(|e| panic!("cannot read `{}`: {}", src_path.display(), e)); @@ -447,13 +445,13 @@ impl Field { } if keep_field_attribute { - if attrs.len() > 0 { + if !attrs.is_empty() { // from substrate: filter out excluded attribute namespaces let filtered_attrs: Vec<&Attribute> = attrs .iter() .filter(|a| { let path = a.path().to_token_stream().to_string(); - !exclude_field_attributes.iter().any(|e| path == *e) + !exclude_field_attributes.contains(&path) }) .collect(); Ok(quote! { @@ -529,15 +527,12 @@ impl Field { } let _ = attr.parse_nested_meta(|meta| { let path = meta.path.to_string(); - match path.as_str() { - ATTRIBUTE => { - // #[complex(attribute(serde(default))] - let content; - parenthesized!(content in meta.input); - let attribute: TokenStream = content.parse()?; - attributes.push(attribute); - } - _ => {} + if path.as_str() == ATTRIBUTE { + // #[complex(attribute(serde(default))] + let content; + parenthesized!(content in meta.input); + let attribute: TokenStream = content.parse()?; + attributes.push(attribute); } Ok(()) }); diff --git a/derive/src/filler.rs b/derive/src/filler.rs index dda4b75..7d77752 100644 --- a/derive/src/filler.rs +++ b/derive/src/filler.rs @@ -321,7 +321,11 @@ impl Filler { self.#nesting_field_names.apply(filler.#nesting_field_names); )* }; - #[cfg(all(feature = "nesting", not(feature = "simple-nesting"), not(feature = "log")))] + #[cfg(all( + feature = "nesting", + not(feature = "simple-nesting"), + not(feature = "log") + ))] let nesting_apply_section: TokenStream = quote! { #( self.#nesting_field_names.apply(filler.#nesting_field_names); @@ -524,7 +528,9 @@ impl Filler { } #[cfg(not(feature = "log"))] DEFAULT_LOG => { - return Err(meta.error("`default_log` attribute requires the `log` feature")); + return Err( + meta.error("`default_log` attribute requires the `log` feature") + ); } _ => { return Err(meta.error(format_args!( @@ -542,7 +548,7 @@ impl Filler { fields.push(f); } } - let ts = TokenStream::from_str(&format!("{}Filler", &ident,)).unwrap(); + let ts = TokenStream::from_str(&format!("{ident}Filler")).unwrap(); let lit = LitStr::new(&ts.to_string(), Span::call_site()); let filler_struct_name = lit.parse()?; diff --git a/derive/src/lib.rs b/derive/src/lib.rs index f66f4ae..9ce37d5 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -73,10 +73,7 @@ fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result syn::Result = None; for attr in attrs { + #[cfg(feature = "copy_doc")] + if attr.path().to_string().as_str() == DOC { + if let syn::Meta::NameValue(name_value) = &attr.meta { + attributes.push(name_value.to_token_stream()); + } + } + if attr.path().to_string().as_str() != PATCH { continue; } @@ -1368,7 +1381,9 @@ impl Patch { } #[cfg(not(feature = "log"))] DEFAULT_LOG => { - return Err(meta.error("`default_log` attribute requires the `log` feature")); + return Err( + meta.error("`default_log` attribute requires the `log` feature") + ); } _ => { return Err(meta.error(format_args!( @@ -1390,7 +1405,7 @@ impl Patch { Ok(Patch { visibility: vis, patch_struct_name: name.unwrap_or({ - let ts = TokenStream::from_str(&format!("{}Patch", &ident,)).unwrap(); + let ts = TokenStream::from_str(&format!("{ident}Patch")).unwrap(); let lit = LitStr::new(&ts.to_string(), Span::call_site()); lit.parse()? }), @@ -1445,7 +1460,7 @@ impl Field { if *nesting { // TODO handle rename let patch_type = syn::Ident::new( - &format!("{}Patch", &ty.to_token_stream()), + &format!("{}Patch", ty.to_token_stream()), Span::call_site(), ); Ok(quote! { @@ -1483,7 +1498,7 @@ impl Field { if *nesting { // TODO handle rename let patch_type = syn::Ident::new( - &format!("{}Patch", &ty.to_token_stream()), + &format!("{}Patch", ty.to_token_stream()), Span::call_site(), ); Ok(quote! { @@ -1522,6 +1537,13 @@ impl Field { let mut nesting = false; for attr in attrs { + #[cfg(feature = "copy_doc")] + if attr.path().to_string().as_str() == DOC { + if let syn::Meta::NameValue(name_value) = &attr.meta { + attributes.push(name_value.to_token_stream()); + } + } + if attr.path().to_string().as_str() != PATCH { continue; } @@ -1702,9 +1724,11 @@ mod tests { fn parse_patch() { // Test case 1: Valid patch with attributes and fields let input = quote! { + /// Item struct doc #[derive(Patch)] #[patch(name = "MyPatch", attribute(derive(Debug, PartialEq, Clone, Serialize, Deserialize)))] pub struct Item { + /// Field 1 doc #[patch(name = "SubItemPatch")] pub field1: SubItem, #[patch(skip)] @@ -1718,7 +1742,11 @@ mod tests { struct_name: syn::Ident::new("Item", Span::call_site()), patch_struct_name: syn::Ident::new("MyPatch", Span::call_site()), generics: syn::Generics::default(), - attributes: vec![quote! { derive(Debug, PartialEq, Clone, Serialize, Deserialize) }], + attributes: vec![ + #[cfg(feature = "copy_doc")] + quote! { doc = r" Item struct doc" }, + quote! { derive(Debug, PartialEq, Clone, Serialize, Deserialize) }, + ], #[cfg(feature = "log")] default_log_fn: None, fields: vec![ @@ -1729,7 +1757,10 @@ mod tests { .parse() .unwrap(), ), - attributes: vec![], + attributes: vec![ + #[cfg(feature = "copy_doc")] + quote! { doc = r" Field 1 doc" }, + ], #[cfg(feature = "op")] addable: Addable::Disable, #[cfg(feature = "nesting")] @@ -1758,5 +1789,8 @@ mod tests { format!("{:?}", result.to_token_stream()), format!("{:?}", expected.to_token_stream()) ); + + // Check that the generated token stream is syntactically correct. + syn::parse2::(result.to_token_stream().unwrap()).unwrap(); } } diff --git a/docs/logs.md b/docs/logs.md index 7702237..2a8d819 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -86,7 +86,7 @@ fn my_log(prefixes: &[&str], field: &str) { let path = if prefixes.is_empty() { field.to_string() } else { - format!("{}.{}", prefixes.join("."), field) + format!("{}.{field}", prefixes.join(".")) }; println!("patched: {path}"); } @@ -125,7 +125,7 @@ fn log_field(prefix: &str, field: &str) { let path = if prefix.is_empty() { field.to_string() } else { - format!("{}.{}", prefix, field) + format!("{prefix}.{field}") }; println!("patched: {path}"); } @@ -163,7 +163,7 @@ item.apply_with_log(patch, |prefix, field| { let path = if prefix.is_empty() { field.to_string() } else { - format!("{}.{}", prefix, field) + format!("{prefix}.{field}") }; println!("patched: {path}"); }); diff --git a/examples/filler-examples/examples/log.rs b/examples/filler-examples/examples/log.rs index e83b572..6aa8015 100644 --- a/examples/filler-examples/examples/log.rs +++ b/examples/filler-examples/examples/log.rs @@ -13,7 +13,7 @@ fn log_filler_field(prefixes: &[&str], field: &str) { let path = if prefixes.is_empty() { field.to_string() } else { - format!("{}.{}", prefixes.join("."), field) + format!("{}.{field}", prefixes.join(".")) }; println!("[default_log] filler field: {path}"); } diff --git a/examples/patch-examples/Cargo.toml b/examples/patch-examples/Cargo.toml index 0dfcda9..e8a55bb 100644 --- a/examples/patch-examples/Cargo.toml +++ b/examples/patch-examples/Cargo.toml @@ -13,6 +13,7 @@ serde_with = "3.9.0" toml = "1.1.2" humantime-serde = "1.1.1" clap = { version = "4.4.7", features = ["derive"] } +schemars = "1.2.3" [features] default = ["status", "op"] @@ -30,3 +31,7 @@ keep_none = ["option", "struct-patch/keep_none"] [[example]] name = "box" required-features = ["box"] + +[[example]] +name = "jsonschema" +required-features = ["struct-patch/copy_doc"] \ No newline at end of file diff --git a/examples/patch-examples/examples/jsonschema.rs b/examples/patch-examples/examples/jsonschema.rs new file mode 100644 index 0000000..4f4c0c7 --- /dev/null +++ b/examples/patch-examples/examples/jsonschema.rs @@ -0,0 +1,48 @@ +use schemars::{schema_for, JsonSchema}; +use struct_patch::Patch; + +/// The documentation for my structure. +#[derive(JsonSchema, Patch)] +#[patch(attribute(derive(JsonSchema)))] +struct MyStruct { + /// The documentation for my field. + my_field: bool, +} + +fn main() { + let schema = schema_for!(MyStruct); + assert_eq!(schema.get("title").unwrap(), "MyStruct"); + assert_eq!( + schema.get("description").unwrap(), + "The documentation for my structure." + ); + assert_eq!( + schema + .get("properties") + .unwrap() + .get("my_field") + .unwrap() + .get("description") + .unwrap(), + "The documentation for my field." + ); + dbg!(schema); + + let schema = schema_for!(MyStructPatch); + assert_eq!(schema.get("title").unwrap(), "MyStructPatch"); + assert_eq!( + schema.get("description").unwrap(), + "The documentation for my structure." + ); + assert_eq!( + schema + .get("properties") + .unwrap() + .get("my_field") + .unwrap() + .get("description") + .unwrap(), + "The documentation for my field." + ); + dbg!(schema); +} diff --git a/examples/patch-examples/examples/log.rs b/examples/patch-examples/examples/log.rs index 2a9f34d..1b0d92a 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -10,7 +10,7 @@ fn log_patch_field(prefixes: &[&str], field: &str) { let path = if prefixes.is_empty() { field.to_string() } else { - format!("{}.{}", prefixes.join("."), field) + format!("{}.{field}", prefixes.join(".")) }; println!("[default_log] patch field: {path}"); } @@ -56,10 +56,7 @@ fn main() { // [default_log] patch field: host // [default_log] patch field: port - println!( - "host={}, port={}", - config.host, config.port - ); + println!("host={}, port={}", config.host, config.port); println!("\n--- Patch: apply_with_log() with custom format ---"); config.apply_with_log( @@ -74,10 +71,7 @@ fn main() { // Prints: // [custom_log] patch field port was updated - println!( - "host={}, port={}", - config.host, config.port - ); + println!("host={}, port={}", config.host, config.port); } #[cfg(feature = "nesting")] @@ -117,7 +111,7 @@ fn main() { let path = if prefixes.is_empty() { field.to_string() } else { - format!("{}.{}", prefixes.join("."), field) + format!("{}.{field}", prefixes.join(".")) }; println!("[custom_log] patch field: '{path}'"); }, @@ -177,7 +171,7 @@ fn main() { let path = if prefixes.is_empty() { field.to_string() } else { - format!("{}.{}", prefixes.join("."), field) + format!("{}.{field}", prefixes.join(".")) }; println!("[custom_log] patch field: '{path}'"); }, @@ -188,8 +182,6 @@ fn main() { println!( "config.port={}, config.logging.level={}, config.logging.format={}", - server4.config.port, - server4.config.logging.level, - server4.config.logging.format + server4.config.port, server4.config.logging.level, server4.config.logging.format ); } diff --git a/examples/patch-examples/examples/op.rs b/examples/patch-examples/examples/op.rs index 1433d18..df9c8c4 100644 --- a/examples/patch-examples/examples/op.rs +++ b/examples/patch-examples/examples/op.rs @@ -1,6 +1,6 @@ #[cfg(feature = "op")] fn str_concat(a: String, b: String) -> String { - format!("{}, {}", a, b) + format!("{a}, {b}") } #[cfg(feature = "op")] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8bd65eb..70a224a 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -55,4 +55,6 @@ catalyst = [ unsafe = [ "struct-patch-derive/unsafe" ] - +copy_doc = [ + "struct-patch-derive/copy_doc" +] diff --git a/lib/src/box.rs b/lib/src/box.rs index ee5353f..6bb6093 100644 --- a/lib/src/box.rs +++ b/lib/src/box.rs @@ -2,4 +2,3 @@ extern crate alloc; pub use alloc::boxed::Box; - diff --git a/lib/src/lib.rs b/lib/src/lib.rs index ea2ee22..740e61d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -407,6 +407,7 @@ mod tests { fn test_add_patches() { #[derive(Patch)] #[patch(attribute(derive(Debug, PartialEq)))] + #[allow(dead_code, reason = "used for the test of derive(Patch)")] struct Item { field: u32, other: String, @@ -435,6 +436,7 @@ mod tests { #[should_panic] fn test_add_conflict_patches_panic() { #[derive(Patch, Debug, PartialEq)] + #[allow(dead_code, reason = "used for the test of derive(Patch)")] struct Item { field: u32, } diff --git a/lib/src/traits.rs b/lib/src/traits.rs index b553d6d..ef66cc8 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -86,7 +86,7 @@ /// let path = if prefixes.is_empty() { /// field.to_string() /// } else { -/// format!("{}.{}", prefixes.join("."), field) +/// format!("{}.{field}", prefixes.join(".")) /// }; /// println!("patched: {path}"); /// } @@ -95,7 +95,7 @@ /// let path = if prefix.is_empty() { /// field.to_string() /// } else { -/// format!("{}.{}", prefix, field) +/// format!("{prefix}.{field}") /// }; /// println!("patched: {path}"); /// } @@ -208,7 +208,7 @@ pub trait Patch

{ /// let path = if prefix.is_empty() { /// field.to_string() /// } else { - /// format!("{}.{}", prefix, field) + /// format!("{prefix}.{field}") /// }; /// patched_fields.push(path); /// }); @@ -217,7 +217,7 @@ pub trait Patch

{ /// let path = if prefixes.is_empty() { /// field.to_string() /// } else { - /// format!("{}.{}", prefixes.join("."), field) + /// format!("{}.{field}", prefixes.join(".")) /// }; /// patched_fields.push(path); /// }); @@ -238,7 +238,11 @@ pub trait Patch

{ self.apply(patch); } - #[cfg(all(feature = "log", not(feature = "nesting"), not(feature = "simple-nesting")))] + #[cfg(all( + feature = "log", + not(feature = "nesting"), + not(feature = "simple-nesting") + ))] fn apply_with_log(&mut self, patch: P, _log: F) { self.apply(patch); } @@ -282,7 +286,7 @@ pub trait Filler { /// let path = if prefixes.is_empty() { /// field.to_string() /// } else { - /// format!("{}.{}", prefixes.join("."), field) + /// format!("{}.{field}", prefixes.join(".")) /// }; /// filled_fields.push(path); /// }); @@ -304,7 +308,11 @@ pub trait Filler { self.apply(filler); } - #[cfg(all(feature = "log", not(feature = "nesting"), not(feature = "simple-nesting")))] + #[cfg(all( + feature = "log", + not(feature = "nesting"), + not(feature = "simple-nesting") + ))] fn apply_with_log(&mut self, filler: F, _log: L) { self.apply(filler); }