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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
29 changes: 12 additions & 17 deletions derive/src/catalyst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?
};

Expand Down Expand Up @@ -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()?
});
Expand All @@ -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"))
Expand All @@ -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));
Expand Down Expand Up @@ -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! {
Expand Down Expand Up @@ -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(())
});
Expand Down
12 changes: 9 additions & 3 deletions derive/src/filler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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!(
Expand All @@ -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()?;

Expand Down
12 changes: 3 additions & 9 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result<Option<syn:
} else {
Err(Error::new(
expr.span(),
format!(
"expected serde {} attribute to be lit: `{} = \"...\"`",
attr_name, attr_name
),
format!("expected serde {attr_name} attribute to be lit: `{attr_name} = \"...\"`"),
))
}
}
Expand All @@ -96,17 +93,14 @@ fn get_lit_str(attr_name: String, meta: &ParseNestedMeta) -> syn::Result<Option<
if !suffix.is_empty() {
return Err(Error::new(
lit.span(),
format!("unexpected suffix `{}` on string literal", suffix),
format!("unexpected suffix `{suffix}` on string literal"),
));
}
Ok(Some(lit.clone()))
} else {
Err(Error::new(
expr.span(),
format!(
"expected serde {} attribute to be a string: `{} = \"...\"`",
attr_name, attr_name
),
format!("expected serde {attr_name} attribute to be a string: `{attr_name} = \"...\"`",),
))
}
}
48 changes: 41 additions & 7 deletions derive/src/patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use syn::{parenthesized, DeriveInput, Lit, LitStr, Result, Type};
#[cfg(feature = "op")]
use crate::Addable;

#[cfg(feature = "copy_doc")]
const DOC: &str = "doc";
const PATCH: &str = "patch";
const NAME: &str = "name";
const ATTRIBUTE: &str = "attribute";
Expand Down Expand Up @@ -845,7 +847,11 @@ impl Patch {
self.#nesting_field_names.apply(patch.#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(patch.#nesting_field_names);
Expand Down Expand Up @@ -1328,6 +1334,13 @@ impl Patch {
let mut default_log_fn: Option<syn::Path> = 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;
}
Expand Down Expand Up @@ -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!(
Expand All @@ -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()?
}),
Expand Down Expand Up @@ -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! {
Expand Down Expand Up @@ -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! {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)]
Expand All @@ -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![
Expand All @@ -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")]
Expand Down Expand Up @@ -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::<syn::File>(result.to_token_stream().unwrap()).unwrap();
}
}
6 changes: 3 additions & 3 deletions docs/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
Expand Down Expand Up @@ -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}");
}
Expand Down Expand Up @@ -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}");
});
Expand Down
2 changes: 1 addition & 1 deletion examples/filler-examples/examples/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
Expand Down
5 changes: 5 additions & 0 deletions examples/patch-examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"]
Loading
Loading