Skip to content

Commit 532392b

Browse files
clauderyo33
authored andcommitted
Add #[eure(parse_error = CustomError)] container attribute for ParseDocument derive
Allows specifying a custom error type for the generated ParseDocument impl. The custom error type is used instead of ParseError for `type Error`. The custom type must implement `From<ParseError>` for `?` to work. This enables: - Combining multiple error sources with enum error types - Adding custom validation errors from nested ParseDocument impls - Using domain-specific error types throughout the parsing chain
1 parent aca0df8 commit 532392b

File tree

8 files changed

+294
-2
lines changed

8 files changed

+294
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/eure-macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ syn = { version = "2.0", features = ["extra-traits", "full"] }
2222
[dev-dependencies]
2323
automod = { workspace = true }
2424
eure = { workspace = true }
25+
thiserror = { workspace = true }

crates/eure-macros/src/attrs/container.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ pub struct ContainerAttrs {
2424
/// By default (false), unknown extensions cause a parse error.
2525
/// When true, skips the `deny_unknown_extensions()` check.
2626
pub allow_unknown_extensions: bool,
27+
/// Custom error type for the ParseDocument impl.
28+
/// When specified, the generated `type Error` is set to this type instead of `ParseError`.
29+
/// The custom error type must implement `From<ParseError>` for `?` to work.
30+
pub parse_error: Option<Path>,
2731
}

crates/eure-macros/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct MacroConfig {
1313
pub allow_unknown_fields: bool,
1414
/// Allow unknown extensions instead of denying them.
1515
pub allow_unknown_extensions: bool,
16+
/// Custom error type for the ParseDocument impl.
17+
pub parse_error: Option<TokenStream>,
1618
}
1719

1820
impl MacroConfig {
@@ -22,13 +24,15 @@ impl MacroConfig {
2224
.crate_path
2325
.map(|path| path.into_token_stream())
2426
.unwrap_or_else(|| quote! { ::eure::document });
27+
let parse_error = attrs.parse_error.map(|path| path.into_token_stream());
2528
Self {
2629
document_crate,
2730
rename_all: attrs.rename_all,
2831
rename_all_fields: attrs.rename_all_fields,
2932
parse_ext: attrs.parse_ext,
3033
allow_unknown_fields: attrs.allow_unknown_fields,
3134
allow_unknown_extensions: attrs.allow_unknown_extensions,
35+
parse_error,
3236
}
3337
}
3438
}

crates/eure-macros/src/context.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ impl MacroContext {
9292

9393
#[allow(non_snake_case)]
9494
pub fn ParseError(&self) -> TokenStream {
95-
let document_crate = &self.config.document_crate;
96-
quote!(#document_crate::parse::ParseError)
95+
if let Some(ref custom_error) = self.config.parse_error {
96+
custom_error.clone()
97+
} else {
98+
let document_crate = &self.config.document_crate;
99+
quote!(#document_crate::parse::ParseError)
100+
}
97101
}
98102

99103
#[allow(non_snake_case)]

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,92 @@ fn test_flatten_ext_field() {
381381
.to_string()
382382
);
383383
}
384+
385+
#[test]
386+
fn test_custom_parse_error() {
387+
let input = generate(parse_quote! {
388+
#[eure(parse_error = MyCustomError)]
389+
struct User {
390+
name: String,
391+
age: i32,
392+
}
393+
});
394+
assert_eq!(
395+
input.to_string(),
396+
quote! {
397+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for User<> {
398+
type Error = MyCustomError;
399+
400+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
401+
let rec = ctx.parse_record()?;
402+
let value = User {
403+
name: rec.parse_field("name")?,
404+
age: rec.parse_field("age")?
405+
};
406+
rec.deny_unknown_fields()?;
407+
ctx.deny_unknown_extensions()?;
408+
Ok(value)
409+
}
410+
}
411+
}
412+
.to_string()
413+
);
414+
}
415+
416+
#[test]
417+
fn test_custom_parse_error_with_path() {
418+
let input = generate(parse_quote! {
419+
#[eure(parse_error = crate::errors::MyCustomError)]
420+
struct User {
421+
name: String,
422+
}
423+
});
424+
assert_eq!(
425+
input.to_string(),
426+
quote! {
427+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for User<> {
428+
type Error = crate::errors::MyCustomError;
429+
430+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
431+
let rec = ctx.parse_record()?;
432+
let value = User {
433+
name: rec.parse_field("name")?
434+
};
435+
rec.deny_unknown_fields()?;
436+
ctx.deny_unknown_extensions()?;
437+
Ok(value)
438+
}
439+
}
440+
}
441+
.to_string()
442+
);
443+
}
444+
445+
#[test]
446+
fn test_custom_parse_error_with_custom_crate() {
447+
let input = generate(parse_quote! {
448+
#[eure(crate = ::eure_document, parse_error = MyError)]
449+
struct User {
450+
name: String,
451+
}
452+
});
453+
assert_eq!(
454+
input.to_string(),
455+
quote! {
456+
impl<'doc,> ::eure_document::parse::ParseDocument<'doc> for User<> {
457+
type Error = MyError;
458+
459+
fn parse(ctx: &::eure_document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
460+
let rec = ctx.parse_record()?;
461+
let value = User {
462+
name: rec.parse_field("name")?
463+
};
464+
rec.deny_unknown_fields()?;
465+
ctx.deny_unknown_extensions()?;
466+
Ok(value)
467+
}
468+
}
469+
}
470+
.to_string()
471+
);
472+
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,30 @@ fn test_struct_variant_with_flatten() {
352352
.to_string()
353353
);
354354
}
355+
356+
#[test]
357+
fn test_enum_custom_parse_error() {
358+
let input = generate(parse_quote! {
359+
#[eure(parse_error = MyCustomError)]
360+
enum TestEnum {
361+
Unit,
362+
Tuple(i32, bool),
363+
}
364+
});
365+
assert_eq!(
366+
input.to_string(),
367+
quote! {
368+
impl<'doc,> ::eure::document::parse::ParseDocument<'doc> for TestEnum<> {
369+
type Error = MyCustomError;
370+
371+
fn parse(ctx: &::eure::document::parse::ParseContext<'doc>) -> Result<Self, Self::Error> {
372+
ctx.parse_union(::eure::document::data_model::VariantRepr::default())?
373+
.variant("Unit", ::eure::document::parse::DocumentParserExt::map(::eure::document::parse::VariantLiteralParser("Unit"), |_| TestEnum::Unit))
374+
.parse_variant::<(i32, bool,)>("Tuple", |(field_0, field_1,)| Ok(TestEnum::Tuple(field_0, field_1)))
375+
.parse()
376+
}
377+
}
378+
}
379+
.to_string()
380+
);
381+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use eure::document::parse::{ParseContext, ParseError, ParseErrorKind};
2+
use eure::ParseDocument;
3+
4+
/// A custom inner error type that can be returned from manual ParseDocument implementations.
5+
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
6+
pub enum InnerError {
7+
#[error("parse error: {0}")]
8+
Parse(#[from] ParseError),
9+
#[error("validation error: {message}")]
10+
Validation { message: String },
11+
}
12+
13+
/// A custom error type that combines ParseError and InnerError.
14+
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
15+
pub enum CustomError {
16+
#[error("parse error: {0}")]
17+
Parse(#[from] ParseError),
18+
#[error("inner error: {0}")]
19+
Inner(#[from] InnerError),
20+
}
21+
22+
/// An inner struct that uses manual ParseDocument implementation
23+
/// that returns InnerError.
24+
#[derive(Debug, PartialEq)]
25+
pub struct InnerStruct {
26+
pub value: i32,
27+
}
28+
29+
impl<'doc> eure::document::parse::ParseDocument<'doc> for InnerStruct {
30+
type Error = InnerError;
31+
32+
fn parse(ctx: &ParseContext<'doc>) -> Result<Self, Self::Error> {
33+
let rec = ctx.parse_record()?;
34+
let value: i32 = rec.parse_field("value")?;
35+
36+
// Demonstrate returning a custom InnerError based on validation
37+
if value < 0 {
38+
return Err(InnerError::Validation {
39+
message: "value must be non-negative".to_string(),
40+
});
41+
}
42+
43+
rec.deny_unknown_fields()?;
44+
Ok(InnerStruct { value })
45+
}
46+
}
47+
48+
/// An outer struct that uses the derive macro with custom error type.
49+
/// The derive macro will use CustomError as the error type instead of ParseError.
50+
/// Since CustomError implements From<InnerError>, the `?` operator will convert
51+
/// InnerError to CustomError automatically.
52+
#[derive(Debug, PartialEq, ParseDocument)]
53+
#[eure(crate = ::eure::document, parse_error = CustomError)]
54+
pub struct OuterStruct {
55+
pub name: String,
56+
pub inner: InnerStruct,
57+
}
58+
59+
#[test]
60+
fn test_custom_error_success() {
61+
use eure::eure;
62+
let doc = eure!({
63+
name = "test"
64+
inner { value = 42 }
65+
});
66+
let result = doc.parse::<OuterStruct>(doc.get_root_id());
67+
assert_eq!(
68+
result.unwrap(),
69+
OuterStruct {
70+
name: "test".to_string(),
71+
inner: InnerStruct { value: 42 }
72+
}
73+
);
74+
}
75+
76+
#[test]
77+
fn test_custom_error_from_parse_error() {
78+
use eure::eure;
79+
// Missing required field "name"
80+
let doc = eure!({
81+
inner { value = 42 }
82+
});
83+
let result = doc.parse::<OuterStruct>(doc.get_root_id());
84+
assert!(result.is_err());
85+
let err = result.unwrap_err();
86+
// Error should be converted from ParseError
87+
match err {
88+
CustomError::Parse(parse_err) => {
89+
assert!(matches!(parse_err.kind, ParseErrorKind::MissingField(_)));
90+
}
91+
_ => panic!("expected CustomError::Parse, got {:?}", err),
92+
}
93+
}
94+
95+
#[test]
96+
fn test_custom_error_from_inner_error() {
97+
use eure::eure;
98+
// InnerStruct validation fails (negative value)
99+
let doc = eure!({
100+
name = "test"
101+
inner { value = -1 }
102+
});
103+
let result = doc.parse::<OuterStruct>(doc.get_root_id());
104+
assert!(result.is_err());
105+
let err = result.unwrap_err();
106+
// Error should be converted from InnerError via From<InnerError>
107+
match err {
108+
CustomError::Inner(InnerError::Validation { message }) => {
109+
assert_eq!(message, "value must be non-negative");
110+
}
111+
_ => panic!("expected CustomError::Inner(InnerError::Validation), got {:?}", err),
112+
}
113+
}
114+
115+
/// A nested struct that contains another derived struct
116+
/// to test that nested types also work with custom errors.
117+
#[derive(Debug, PartialEq, ParseDocument)]
118+
#[eure(crate = ::eure::document, parse_error = CustomError)]
119+
pub struct NestedStruct {
120+
pub outer: OuterStruct,
121+
}
122+
123+
#[test]
124+
fn test_nested_custom_error() {
125+
use eure::eure;
126+
let doc = eure!({
127+
outer {
128+
name = "test"
129+
inner { value = 10 }
130+
}
131+
});
132+
let result = doc.parse::<NestedStruct>(doc.get_root_id());
133+
assert_eq!(
134+
result.unwrap(),
135+
NestedStruct {
136+
outer: OuterStruct {
137+
name: "test".to_string(),
138+
inner: InnerStruct { value: 10 }
139+
}
140+
}
141+
);
142+
}
143+
144+
#[test]
145+
fn test_nested_inner_error_bubbles_up() {
146+
use eure::eure;
147+
let doc = eure!({
148+
outer {
149+
name = "test"
150+
inner { value = -5 }
151+
}
152+
});
153+
let result = doc.parse::<NestedStruct>(doc.get_root_id());
154+
assert!(result.is_err());
155+
let err = result.unwrap_err();
156+
match err {
157+
CustomError::Inner(InnerError::Validation { message }) => {
158+
assert_eq!(message, "value must be non-negative");
159+
}
160+
_ => panic!("expected CustomError::Inner(InnerError::Validation), got {:?}", err),
161+
}
162+
}

0 commit comments

Comments
 (0)