Skip to content

Commit 83320ab

Browse files
ryo33claude
andcommitted
Add #[eure(flatten)] field attribute for ParseDocument derive
Allows embedding fields from a nested struct into the parent during parsing. Uses the existing FlattenContext runtime infrastructure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 45c0433 commit 83320ab

File tree

7 files changed

+278
-21
lines changed

7 files changed

+278
-21
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use darling::FromField;
2+
3+
#[derive(Debug, Default, FromField)]
4+
#[darling(default, attributes(eure))]
5+
pub struct FieldAttrs {
6+
pub flatten: bool,
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod container;
2+
mod field;
23
mod rename_all;
34

45
pub use container::ContainerAttrs;
6+
pub use field::FieldAttrs;
57
pub use rename_all::RenameAll;

crates/eure-macros/src/parse_document/parse_record.rs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#[cfg(test)]
22
mod tests;
33

4+
use darling::FromField;
45
use proc_macro2::TokenStream;
56
use quote::{format_ident, quote};
67
use syn::{DataStruct, Fields};
78

9+
use crate::attrs::FieldAttrs;
810
use crate::context::MacroContext;
911

1012
pub fn generate_record_parser(context: &MacroContext, input: &DataStruct) -> TokenStream {
@@ -37,19 +39,26 @@ fn generate_named_struct_from_record(
3739
ident: &syn::Ident,
3840
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
3941
) -> TokenStream {
40-
let field_names: Vec<_> = fields
42+
let field_assignments: Vec<_> = fields
4143
.iter()
42-
.map(|f| f.ident.as_ref().expect("named fields must have names"))
43-
.collect();
44-
let field_name_strs: Vec<_> = field_names
45-
.iter()
46-
.map(|n| context.apply_rename(&n.to_string()))
44+
.map(|f| {
45+
let field_name = f.ident.as_ref().expect("named fields must have names");
46+
let field_ty = &f.ty;
47+
let attrs = FieldAttrs::from_field(f).expect("failed to parse field attributes");
48+
49+
if attrs.flatten {
50+
quote! { #field_name: #field_ty::parse(&rec.flatten())? }
51+
} else {
52+
let field_name_str = context.apply_rename(&field_name.to_string());
53+
quote! { #field_name: rec.parse_field(#field_name_str)? }
54+
}
55+
})
4756
.collect();
4857

4958
context.impl_parse_document(quote! {
5059
let mut rec = ctx.parse_record()?;
5160
let value = #ident {
52-
#(#field_names: rec.parse_field(#field_name_strs)?),*
61+
#(#field_assignments),*
5362
};
5463
rec.deny_unknown_fields()?;
5564
Ok(value)
@@ -61,19 +70,26 @@ fn generate_named_struct_from_ext(
6170
ident: &syn::Ident,
6271
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
6372
) -> TokenStream {
64-
let field_names: Vec<_> = fields
73+
let field_assignments: Vec<_> = fields
6574
.iter()
66-
.map(|f| f.ident.as_ref().expect("named fields must have names"))
67-
.collect();
68-
let field_name_strs: Vec<_> = field_names
69-
.iter()
70-
.map(|n| context.apply_rename(&n.to_string()))
75+
.map(|f| {
76+
let field_name = f.ident.as_ref().expect("named fields must have names");
77+
let field_ty = &f.ty;
78+
let attrs = FieldAttrs::from_field(f).expect("failed to parse field attributes");
79+
80+
if attrs.flatten {
81+
quote! { #field_name: #field_ty::parse(&ext.flatten())? }
82+
} else {
83+
let field_name_str = context.apply_rename(&field_name.to_string());
84+
quote! { #field_name: ext.parse_ext(#field_name_str)? }
85+
}
86+
})
7187
.collect();
7288

7389
context.impl_parse_document(quote! {
7490
let mut ext = ctx.parse_extension();
7591
let value = #ident {
76-
#(#field_names: ext.parse_ext(#field_name_strs)?),*
92+
#(#field_assignments),*
7793
};
7894
ext.allow_unknown_extensions();
7995
Ok(value)

crates/eure-macros/src/parse_document/parse_record/tests.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,97 @@ fn test_parse_ext_with_rename_all() {
247247
.to_string()
248248
);
249249
}
250+
251+
#[test]
252+
fn test_flatten_field() {
253+
let input = generate(parse_quote! {
254+
struct Person {
255+
name: String,
256+
#[eure(flatten)]
257+
address: Address,
258+
}
259+
});
260+
assert_eq!(
261+
input.to_string(),
262+
quote! {
263+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for Person<> {
264+
type Error = ::eure::document::parse::ParseError;
265+
266+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
267+
let mut rec = ctx.parse_record()?;
268+
let value = Person {
269+
name: rec.parse_field("name")?,
270+
address: Address::parse(&rec.flatten())?
271+
};
272+
rec.deny_unknown_fields()?;
273+
Ok(value)
274+
}
275+
}
276+
}
277+
.to_string()
278+
);
279+
}
280+
281+
#[test]
282+
fn test_multiple_flatten_fields() {
283+
let input = generate(parse_quote! {
284+
struct Combined {
285+
id: i32,
286+
#[eure(flatten)]
287+
personal: PersonalInfo,
288+
#[eure(flatten)]
289+
contact: ContactInfo,
290+
}
291+
});
292+
assert_eq!(
293+
input.to_string(),
294+
quote! {
295+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for Combined<> {
296+
type Error = ::eure::document::parse::ParseError;
297+
298+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
299+
let mut rec = ctx.parse_record()?;
300+
let value = Combined {
301+
id: rec.parse_field("id")?,
302+
personal: PersonalInfo::parse(&rec.flatten())?,
303+
contact: ContactInfo::parse(&rec.flatten())?
304+
};
305+
rec.deny_unknown_fields()?;
306+
Ok(value)
307+
}
308+
}
309+
}
310+
.to_string()
311+
);
312+
}
313+
314+
#[test]
315+
fn test_flatten_with_rename_all() {
316+
let input = generate(parse_quote! {
317+
#[eure(rename_all = "camelCase")]
318+
struct Person {
319+
full_name: String,
320+
#[eure(flatten)]
321+
address_info: AddressInfo,
322+
}
323+
});
324+
assert_eq!(
325+
input.to_string(),
326+
quote! {
327+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for Person<> {
328+
type Error = ::eure::document::parse::ParseError;
329+
330+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
331+
let mut rec = ctx.parse_record()?;
332+
let value = Person {
333+
full_name: rec.parse_field("fullName")?,
334+
address_info: AddressInfo::parse(&rec.flatten())?
335+
};
336+
rec.deny_unknown_fields()?;
337+
Ok(value)
338+
}
339+
}
340+
}
341+
.to_string()
342+
);
343+
}

crates/eure-macros/src/parse_document/parse_union.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#[cfg(test)]
22
mod tests;
33

4+
use darling::FromField;
45
use proc_macro2::TokenStream;
56
use quote::{format_ident, quote};
67
use syn::{DataEnum, Fields, Variant};
78

9+
use crate::attrs::FieldAttrs;
810
use crate::{config::MacroConfig, context::MacroContext};
911

1012
pub fn generate_union_parser(context: &MacroContext, input: &DataEnum) -> TokenStream {
@@ -101,20 +103,27 @@ fn generate_struct_variant(
101103
variant_ident: &syn::Ident,
102104
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
103105
) -> TokenStream {
104-
let field_names: Vec<_> = fields
106+
let field_assignments: Vec<_> = fields
105107
.iter()
106-
.map(|f| f.ident.as_ref().expect("struct fields must have names"))
107-
.collect();
108-
let field_name_strs: Vec<_> = field_names
109-
.iter()
110-
.map(|n| context.apply_field_rename(&n.to_string()))
108+
.map(|f| {
109+
let field_name = f.ident.as_ref().expect("struct fields must have names");
110+
let field_ty = &f.ty;
111+
let attrs = FieldAttrs::from_field(f).expect("failed to parse field attributes");
112+
113+
if attrs.flatten {
114+
quote! { #field_name: #field_ty::parse(&rec.flatten())? }
115+
} else {
116+
let field_name_str = context.apply_field_rename(&field_name.to_string());
117+
quote! { #field_name: rec.parse_field(#field_name_str)? }
118+
}
119+
})
111120
.collect();
112121

113122
quote! {
114123
.variant(#variant_name, |ctx: &#document_crate::parse::ParseContext<'_>| {
115124
let mut rec = ctx.parse_record()?;
116125
let value = #enum_ident::#variant_ident {
117-
#(#field_names: rec.parse_field(#field_name_strs)?),*
126+
#(#field_assignments),*
118127
};
119128
rec.deny_unknown_fields()?;
120129
Ok(value)

crates/eure-macros/src/parse_document/parse_union/tests.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,39 @@ fn test_struct_variant_with_both_rename_all_and_rename_all_fields() {
316316
.to_string()
317317
);
318318
}
319+
320+
#[test]
321+
fn test_struct_variant_with_flatten() {
322+
let input = generate(parse_quote! {
323+
enum Entity {
324+
Person {
325+
name: String,
326+
#[eure(flatten)]
327+
details: PersonDetails,
328+
},
329+
}
330+
});
331+
assert_eq!(
332+
input.to_string(),
333+
quote! {
334+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for Entity<> {
335+
type Error = ::eure::document::parse::ParseError;
336+
337+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
338+
ctx.parse_union(::eure::document::data_model::VariantRepr::default())?
339+
.variant("Person", |ctx: &::eure::document::parse::ParseContext<'_>| {
340+
let mut rec = ctx.parse_record()?;
341+
let value = Entity::Person {
342+
name: rec.parse_field("name")?,
343+
details: PersonDetails::parse(&rec.flatten())?
344+
};
345+
rec.deny_unknown_fields()?;
346+
Ok(value)
347+
})
348+
.parse()
349+
}
350+
}
351+
}
352+
.to_string()
353+
);
354+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use eure::ParseDocument;
2+
3+
#[derive(Debug, PartialEq, ParseDocument)]
4+
#[eure(crate = ::eure::document)]
5+
struct Address {
6+
city: String,
7+
country: String,
8+
}
9+
10+
#[derive(Debug, PartialEq, ParseDocument)]
11+
#[eure(crate = ::eure::document)]
12+
struct Person {
13+
name: String,
14+
#[eure(flatten)]
15+
address: Address,
16+
}
17+
18+
#[derive(Debug, PartialEq, ParseDocument)]
19+
#[eure(crate = ::eure::document)]
20+
struct ContactInfo {
21+
email: String,
22+
phone: String,
23+
}
24+
25+
#[derive(Debug, PartialEq, ParseDocument)]
26+
#[eure(crate = ::eure::document)]
27+
struct FullProfile {
28+
id: i32,
29+
#[eure(flatten)]
30+
address: Address,
31+
#[eure(flatten)]
32+
contact: ContactInfo,
33+
}
34+
35+
#[test]
36+
fn test_flatten_basic() {
37+
use eure::eure;
38+
let doc = eure!({ name = "Alice", city = "Tokyo", country = "Japan" });
39+
assert_eq!(
40+
doc.parse::<Person>(doc.get_root_id()).unwrap(),
41+
Person {
42+
name: "Alice".to_string(),
43+
address: Address {
44+
city: "Tokyo".to_string(),
45+
country: "Japan".to_string(),
46+
}
47+
}
48+
);
49+
}
50+
51+
#[test]
52+
fn test_flatten_multiple() {
53+
use eure::eure;
54+
let doc = eure!({
55+
id = 42,
56+
city = "New York",
57+
country = "USA",
58+
email = "test@example.com",
59+
phone = "123-456-7890"
60+
});
61+
assert_eq!(
62+
doc.parse::<FullProfile>(doc.get_root_id()).unwrap(),
63+
FullProfile {
64+
id: 42,
65+
address: Address {
66+
city: "New York".to_string(),
67+
country: "USA".to_string(),
68+
},
69+
contact: ContactInfo {
70+
email: "test@example.com".to_string(),
71+
phone: "123-456-7890".to_string(),
72+
}
73+
}
74+
);
75+
}
76+
77+
#[test]
78+
fn test_flatten_unknown_field_error() {
79+
use eure::eure;
80+
// Unknown field should be detected by root parser
81+
let doc = eure!({ name = "Alice", city = "Tokyo", country = "Japan", extra = "field" });
82+
let result = doc.parse::<Person>(doc.get_root_id());
83+
assert!(result.is_err());
84+
}
85+
86+
#[test]
87+
fn test_flatten_missing_field_error() {
88+
use eure::eure;
89+
// Missing required field in flattened type
90+
let doc = eure!({ name = "Alice", city = "Tokyo" }); // missing country
91+
let result = doc.parse::<Person>(doc.get_root_id());
92+
assert!(result.is_err());
93+
}

0 commit comments

Comments
 (0)