Skip to content

Commit 2492b24

Browse files
authored
Add nesting feature (#98)
* Add nesting feature * update readme and bounce version
1 parent f4915a5 commit 2492b24

File tree

8 files changed

+170
-2
lines changed

8 files changed

+170
-2
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ jobs:
2929
nix develop -c cargo clippy --features=option -- -Dwarnings
3030
nix develop -c cargo clippy --features=none_as_default -- -Dwarnings
3131
nix develop -c cargo clippy --features=keep_none -- -Dwarnings
32+
nix develop -c cargo clippy --features=nesting -- -Dwarnings

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
nix develop -c cargo run --no-default-features --example rename-patch-struct
2727
nix develop -c cargo run --no-default-features --example patch-attr
2828
nix develop -c cargo run --no-default-features --example time
29+
nix develop -c cargo run --no-default-features --features=nesting --example nesting
2930
nix develop -c cargo test --no-default-features
3031
3132
- name: Test with std features

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77

88
[workspace.package]
99
authors = ["Antonio Yang <yanganto@gmail.com>"]
10-
version = "0.9.4"
10+
version = "0.10.0"
1111
edition = "2021"
1212
categories = ["development-tools"]
1313
keywords = ["struct", "patch", "macro", "derive", "overlay"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ The [examples][examples] demo following scenarios.
126126
- show option field behavior
127127
- show operators about patches
128128
- show example with serde crates, ex: `humantime_serde` for duration
129+
- show a patch nesting other patch
129130
- show filler with all possible types
130131

131132
## Features
@@ -141,6 +142,7 @@ This crate also includes the following optional features:
141142
- default: `T` needs to implement `From<P>`. When patching on None, it will based on `from<P>` to cast T, and this let you patch structs containing fields with optional values.
142143
- `none_as_default`: `T` needs to implement `Default`. When patching on None, it will patch on a default instance, and this also let you patch structs containing fields with optional values.
143144
- `keep_none`: When patching on None, it is still None.
145+
- `nesting`(optional): allow a inner field with `Patch` derive and `#[patch(nesting)]` attribute
144146

145147
[crates-badge]: https://img.shields.io/crates/v/struct-patch.svg
146148
[crate-url]: https://crates.io/crates/struct-patch

struct-patch-derive/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ syn = { version = "2.0", features = ["parsing"] }
2222
status = []
2323
op = []
2424
merge = []
25+
nesting = []
2526

2627
[dev-dependencies]
2728
pretty_assertions_sorted = "1.2.3"

struct-patch-derive/src/patch.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ATTRIBUTE: &str = "attribute";
1313
const SKIP: &str = "skip";
1414
const ADDABLE: &str = "addable";
1515
const ADD: &str = "add";
16+
const NESTING: &str = "nesting";
1617

1718
pub(crate) struct Patch {
1819
visibility: syn::Visibility,
@@ -38,6 +39,8 @@ struct Field {
3839
retyped: bool,
3940
#[cfg(feature = "op")]
4041
addable: Addable,
42+
#[cfg(feature = "nesting")]
43+
nesting: bool,
4144
}
4245

4346
impl Patch {
@@ -56,20 +59,60 @@ impl Patch {
5659
.iter()
5760
.map(|f| f.to_token_stream())
5861
.collect::<Result<Vec<_>>>()?;
62+
63+
#[cfg(not(feature = "nesting"))]
5964
let field_names = fields.iter().map(|f| f.ident.as_ref()).collect::<Vec<_>>();
65+
#[cfg(feature = "nesting")]
66+
let field_names = fields
67+
.iter()
68+
.filter(|f| !f.nesting)
69+
.map(|f| f.ident.as_ref()).collect::<Vec<_>>();
6070

71+
#[cfg(not(feature = "nesting"))]
6172
let renamed_field_names = fields
6273
.iter()
6374
.filter(|f| f.retyped)
6475
.map(|f| f.ident.as_ref())
6576
.collect::<Vec<_>>();
77+
#[cfg(feature = "nesting")]
78+
let renamed_field_names = fields
79+
.iter()
80+
.filter(|f| f.retyped && !f.nesting)
81+
.map(|f| f.ident.as_ref())
82+
.collect::<Vec<_>>();
6683

84+
#[cfg(not(feature = "nesting"))]
6785
let original_field_names = fields
6886
.iter()
6987
.filter(|f| !f.retyped)
7088
.map(|f| f.ident.as_ref())
7189
.collect::<Vec<_>>();
7290

91+
#[cfg(feature = "nesting")]
92+
let original_field_names = fields
93+
.iter()
94+
.filter(|f| !f.retyped && !f.nesting)
95+
.map(|f| f.ident.as_ref())
96+
.collect::<Vec<_>>();
97+
98+
#[cfg(not(feature = "nesting"))]
99+
let nesting_field_names: Vec<String> = Vec::new();
100+
#[cfg(not(feature = "nesting"))]
101+
let nesting_field_types: Vec<Type> = Vec::new();
102+
103+
#[cfg(feature = "nesting")]
104+
let nesting_field_names = fields
105+
.iter()
106+
.filter(|f| f.nesting)
107+
.map(|f| f.ident.as_ref())
108+
.collect::<Vec<_>>();
109+
#[cfg(feature = "nesting")]
110+
let nesting_field_types = fields
111+
.iter()
112+
.filter(|f| f.nesting)
113+
.map(|f| f.ty.clone())
114+
.collect::<Vec<_>>();
115+
73116
let mapped_attributes = attributes
74117
.iter()
75118
.map(|a| {
@@ -119,6 +162,9 @@ impl Patch {
119162
#(
120163
#original_field_names: other.#original_field_names.or(self.#original_field_names),
121164
)*
165+
#(
166+
#nesting_field_names: other.#nesting_field_names.apply(self.#nesting_field_names),
167+
)*
122168
}
123169
}
124170
}
@@ -252,6 +298,9 @@ impl Patch {
252298
self.#original_field_names = v;
253299
}
254300
)*
301+
#(
302+
self.#nesting_field_names.apply(patch.#nesting_field_names);
303+
)*
255304
}
256305

257306
fn into_patch(self) -> #name #generics {
@@ -262,6 +311,9 @@ impl Patch {
262311
#(
263312
#original_field_names: Some(self.#original_field_names),
264313
)*
314+
#(
315+
#nesting_field_names: self.#nesting_field_names.into_patch(),
316+
)*
265317
}
266318
}
267319

@@ -283,6 +335,9 @@ impl Patch {
283335
None
284336
},
285337
)*
338+
#(
339+
#nesting_field_names: self.#nesting_field_names.into_patch_by_diff(previous_struct.#nesting_field_names),
340+
)*
286341
}
287342
}
288343

@@ -291,6 +346,9 @@ impl Patch {
291346
#(
292347
#field_names: None,
293348
)*
349+
#(
350+
#nesting_field_names: #nesting_field_types::new_empty_patch(),
351+
)*
294352
}
295353
}
296354
}
@@ -403,6 +461,8 @@ impl Field {
403461
ident,
404462
ty,
405463
attributes,
464+
#[cfg(feature = "nesting")]
465+
nesting,
406466
..
407467
} = self;
408468

@@ -415,14 +475,48 @@ impl Field {
415475
})
416476
.collect::<Vec<_>>();
417477
match ident {
478+
#[cfg(not(feature = "nesting"))]
418479
Some(ident) => Ok(quote! {
419480
#(#attributes)*
420481
pub #ident: Option<#ty>,
421482
}),
483+
#[cfg(feature = "nesting")]
484+
Some(ident) => {
485+
if *nesting {
486+
// TODO handle rename
487+
let patch_type = syn::Ident::new(&format!("{}Patch", &ty.to_token_stream()), Span::call_site());
488+
Ok(quote! {
489+
#(#attributes)*
490+
pub #ident: #patch_type,
491+
})
492+
} else {
493+
Ok(quote! {
494+
#(#attributes)*
495+
pub #ident: Option<#ty>,
496+
})
497+
}
498+
},
499+
#[cfg(not(feature = "nesting"))]
422500
None => Ok(quote! {
423501
#(#attributes)*
424502
pub Option<#ty>,
425503
}),
504+
#[cfg(feature = "nesting")]
505+
None => {
506+
if *nesting {
507+
// TODO handle rename
508+
let patch_type = syn::Ident::new(&format!("{}Patch", &ty.to_token_stream()), Span::call_site());
509+
Ok(quote! {
510+
#(#attributes)*
511+
pub #patch_type,
512+
})
513+
} else {
514+
Ok(quote! {
515+
#(#attributes)*
516+
pub Option<#ty>,
517+
})
518+
}
519+
},
426520
}
427521
}
428522

@@ -438,6 +532,8 @@ impl Field {
438532

439533
#[cfg(feature = "op")]
440534
let mut addable = Addable::Disable;
535+
#[cfg(feature = "nesting")]
536+
let mut nesting = false;
441537

442538
for attr in attrs {
443539
if attr.path().to_string().as_str() != PATCH {
@@ -491,6 +587,16 @@ impl Field {
491587
ADD => {
492588
return Err(syn::Error::new(ident.span(), "`add` needs `op` feature"));
493589
}
590+
#[cfg(feature = "nesting")]
591+
NESTING => {
592+
// #[patch(nesting)]
593+
nesting = true;
594+
}
595+
#[cfg(not(feature = "nesting"))]
596+
NESTING => {
597+
return Err(
598+
meta.error("#[patch(nesting)] only work with `nesting` feature"));
599+
}
494600
_ => {
495601
return Err(meta.error(format_args!(
496602
"unknown patch field attribute `{}`",
@@ -512,6 +618,8 @@ impl Field {
512618
attributes,
513619
#[cfg(feature = "op")]
514620
addable,
621+
#[cfg(feature = "nesting")]
622+
nesting,
515623
}))
516624
}
517625
}

struct-patch/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license.workspace = true
1111
readme.workspace = true
1212

1313
[dependencies]
14-
struct-patch-derive = { version = "=0.9.4", path = "../struct-patch-derive" }
14+
struct-patch-derive = { version = "=0.10.0", path = "../struct-patch-derive" }
1515

1616
[dev-dependencies]
1717
serde_json = "1.0"
@@ -35,5 +35,6 @@ merge = [
3535
std = ["box", "option"]
3636
box = []
3737
option = []
38+
nesting = []
3839
none_as_default = ["option"]
3940
keep_none = ["option"]

struct-patch/examples/nesting.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use serde::Deserialize;
2+
use struct_patch::Patch;
3+
4+
#[cfg(feature = "nesting")]
5+
#[derive(Clone, Debug, Default, Patch, PartialEq)]
6+
#[patch(attribute(derive(Debug, Deserialize, PartialEq)))]
7+
struct Item {
8+
field_complete: bool,
9+
field_int: usize,
10+
field_string: String,
11+
#[patch(nesting)]
12+
inner: Nesting,
13+
}
14+
15+
#[derive(Clone, Debug, Default, Patch, PartialEq)]
16+
#[patch(attribute(derive(Debug, Deserialize, PartialEq)))]
17+
struct Nesting {
18+
inner_int: usize,
19+
inner_string: String,
20+
}
21+
22+
#[cfg(not(feature = "nesting"))]
23+
fn main() {}
24+
25+
#[cfg(feature = "nesting")]
26+
fn main() {
27+
let item_a = Item::default();
28+
let item_b = Item {
29+
field_int: 7,
30+
inner: Nesting {
31+
inner_int: 100,
32+
..Default::default()
33+
},
34+
..Default::default()
35+
};
36+
37+
let patch = item_b.clone().into_patch_by_diff(item_a);
38+
assert_eq!(
39+
format!("{patch:?}"),
40+
"ItemPatch { field_complete: None, field_int: Some(7), field_string: None, inner: NestingPatch { inner_int: Some(100), inner_string: None } }"
41+
);
42+
43+
let data = r#"{
44+
"field_int": 7,
45+
"inner": {
46+
"inner_int": 100
47+
}
48+
}"#;
49+
assert_eq!(patch, serde_json::from_str(data).unwrap());
50+
51+
let mut item = Item::default();
52+
item.apply(patch);
53+
assert_eq!(item, item_b);
54+
}

0 commit comments

Comments
 (0)