Skip to content

Commit 0309732

Browse files
authored
Merge pull request #75 from Hihaheho/claude/buildschema-trait-derive-ZwTgi
Add BuildSchema trait and derive macro for generating EureSchema from Rust types
2 parents 407b198 + 2683fd6 commit 0309732

File tree

16 files changed

+1415
-16
lines changed

16 files changed

+1415
-16
lines changed

Cargo.lock

Lines changed: 2 additions & 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ syn = { version = "2.0", features = ["extra-traits", "full"] }
2222
[dev-dependencies]
2323
automod = { workspace = true }
2424
eure = { workspace = true }
25+
eure-document = { workspace = true }
26+
eure-schema = { workspace = true }
2527
thiserror = { workspace = true }

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ pub struct ContainerAttrs {
2828
/// When specified, the generated `type Error` is set to this type instead of `ParseError`.
2929
/// The custom error type must implement `From<ParseError>` for `?` to work.
3030
pub parse_error: Option<Path>,
31+
/// Type name for BuildSchema registration in `$types` namespace.
32+
/// When specified, the type is registered as `$types.<type_name>`.
33+
/// Example: `#[eure(type_name = "user")]` registers as `$types.user`.
34+
pub type_name: Option<String>,
3135
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//! BuildSchema derive macro implementation
2+
3+
mod build_record;
4+
mod build_union;
5+
6+
use proc_macro2::TokenStream;
7+
use quote::quote;
8+
use syn::Data;
9+
10+
use crate::context::MacroContext;
11+
12+
pub fn derive(context: MacroContext) -> TokenStream {
13+
match &context.input.data {
14+
Data::Struct(data) => build_record::generate_record_schema(&context, data),
15+
Data::Union(_) => quote! { compile_error!("Union is not supported for BuildSchema") },
16+
Data::Enum(data) => build_union::generate_union_schema(&context, data),
17+
}
18+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//! BuildSchema derive implementation for structs (records)
2+
3+
use darling::FromField;
4+
use proc_macro2::TokenStream;
5+
use quote::{format_ident, quote};
6+
use syn::{DataStruct, Fields};
7+
8+
use crate::attrs::FieldAttrs;
9+
use crate::context::MacroContext;
10+
11+
pub fn generate_record_schema(context: &MacroContext, input: &DataStruct) -> TokenStream {
12+
match &input.fields {
13+
Fields::Named(fields) => generate_named_struct(context, &fields.named),
14+
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
15+
generate_newtype_struct(context, &fields.unnamed[0].ty)
16+
}
17+
Fields::Unnamed(fields) => generate_tuple_struct(context, &fields.unnamed),
18+
Fields::Unit => generate_unit_struct(context),
19+
}
20+
}
21+
22+
fn generate_named_struct(
23+
context: &MacroContext,
24+
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
25+
) -> TokenStream {
26+
let schema_crate = context.schema_crate();
27+
28+
let field_schemas: Vec<_> = fields
29+
.iter()
30+
.enumerate()
31+
.map(|(idx, f)| {
32+
let field_name = f.ident.as_ref().expect("named fields must have names");
33+
let field_ty = &f.ty;
34+
let attrs = FieldAttrs::from_field(f).expect("failed to parse field attributes");
35+
36+
// Skip flatten fields for now - they need special handling
37+
if attrs.flatten || attrs.flatten_ext {
38+
panic!("flatten is not yet supported in BuildSchema derive");
39+
}
40+
41+
let field_name_str = attrs
42+
.rename
43+
.clone()
44+
.unwrap_or_else(|| context.apply_rename(&field_name.to_string()));
45+
46+
let schema_var = format_ident!("field_{}_schema", idx);
47+
48+
// Check if the field is Option<T> to mark as optional
49+
let is_optional = is_option_type(field_ty);
50+
51+
(field_name_str, schema_var, field_ty.clone(), is_optional)
52+
})
53+
.collect();
54+
55+
// Generate schema building for each field
56+
let field_builds: Vec<_> = field_schemas
57+
.iter()
58+
.map(|(_, schema_var, field_ty, _)| {
59+
quote! {
60+
let #schema_var = ctx.build::<#field_ty>();
61+
}
62+
})
63+
.collect();
64+
65+
// Generate the properties HashMap entries
66+
let properties_entries: Vec<_> = field_schemas
67+
.iter()
68+
.map(|(name, schema_var, _, is_optional)| {
69+
quote! {
70+
(
71+
#name.to_string(),
72+
#schema_crate::RecordFieldSchema {
73+
schema: #schema_var,
74+
optional: #is_optional,
75+
binding_style: None,
76+
}
77+
)
78+
}
79+
})
80+
.collect();
81+
82+
// Determine unknown fields policy
83+
let unknown_fields_policy = if context.config.allow_unknown_fields {
84+
quote! { #schema_crate::UnknownFieldsPolicy::Allow }
85+
} else {
86+
quote! { #schema_crate::UnknownFieldsPolicy::Deny }
87+
};
88+
89+
let content = quote! {
90+
#(#field_builds)*
91+
92+
#schema_crate::SchemaNodeContent::Record(#schema_crate::RecordSchema {
93+
properties: [
94+
#(#properties_entries),*
95+
].into_iter().collect(),
96+
unknown_fields: #unknown_fields_policy,
97+
})
98+
};
99+
100+
context.impl_build_schema(content)
101+
}
102+
103+
fn generate_unit_struct(context: &MacroContext) -> TokenStream {
104+
let schema_crate = context.schema_crate();
105+
let content = quote! {
106+
#schema_crate::SchemaNodeContent::Null
107+
};
108+
context.impl_build_schema(content)
109+
}
110+
111+
fn generate_tuple_struct(
112+
context: &MacroContext,
113+
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
114+
) -> TokenStream {
115+
let schema_crate = context.schema_crate();
116+
117+
let field_builds: Vec<_> = fields
118+
.iter()
119+
.enumerate()
120+
.map(|(idx, f)| {
121+
let field_ty = &f.ty;
122+
let schema_var = format_ident!("field_{}_schema", idx);
123+
quote! {
124+
let #schema_var = ctx.build::<#field_ty>();
125+
}
126+
})
127+
.collect();
128+
129+
let schema_vars: Vec<_> = (0..fields.len())
130+
.map(|idx| format_ident!("field_{}_schema", idx))
131+
.collect();
132+
133+
let content = quote! {
134+
#(#field_builds)*
135+
136+
#schema_crate::SchemaNodeContent::Tuple(#schema_crate::TupleSchema {
137+
elements: vec![#(#schema_vars),*],
138+
binding_style: None,
139+
})
140+
};
141+
142+
context.impl_build_schema(content)
143+
}
144+
145+
fn generate_newtype_struct(context: &MacroContext, field_ty: &syn::Type) -> TokenStream {
146+
// Newtype just delegates to the inner type
147+
let content = quote! {
148+
<#field_ty as BuildSchema>::build_schema(ctx)
149+
};
150+
context.impl_build_schema(content)
151+
}
152+
153+
/// Check if a type is Option<T>
154+
fn is_option_type(ty: &syn::Type) -> bool {
155+
if let syn::Type::Path(type_path) = ty
156+
&& let Some(segment) = type_path.path.segments.last()
157+
{
158+
return segment.ident == "Option";
159+
}
160+
false
161+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//! BuildSchema derive implementation for enums (unions)
2+
3+
use darling::FromField;
4+
use darling::FromVariant;
5+
use proc_macro2::TokenStream;
6+
use quote::{format_ident, quote};
7+
use syn::DataEnum;
8+
9+
use crate::attrs::{FieldAttrs, VariantAttrs};
10+
use crate::context::MacroContext;
11+
12+
pub fn generate_union_schema(context: &MacroContext, input: &DataEnum) -> TokenStream {
13+
let schema_crate = context.schema_crate();
14+
15+
let variant_schemas: Vec<_> = input
16+
.variants
17+
.iter()
18+
.enumerate()
19+
.map(|(idx, variant)| {
20+
let variant_attrs =
21+
VariantAttrs::from_variant(variant).expect("failed to parse variant attributes");
22+
23+
let variant_name = variant_attrs
24+
.rename
25+
.clone()
26+
.unwrap_or_else(|| context.apply_rename(&variant.ident.to_string()));
27+
28+
let schema_var = format_ident!("variant_{}_schema", idx);
29+
30+
let schema_build = match &variant.fields {
31+
syn::Fields::Unit => {
32+
// Unit variant -> null schema
33+
quote! {
34+
let #schema_var = ctx.create_node(#schema_crate::SchemaNodeContent::Null);
35+
}
36+
}
37+
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
38+
// Newtype variant -> delegate to inner type
39+
let field_ty = &fields.unnamed[0].ty;
40+
quote! {
41+
let #schema_var = ctx.build::<#field_ty>();
42+
}
43+
}
44+
syn::Fields::Unnamed(fields) => {
45+
// Tuple variant -> tuple schema
46+
let field_builds: Vec<_> = fields
47+
.unnamed
48+
.iter()
49+
.enumerate()
50+
.map(|(fidx, f)| {
51+
let field_ty = &f.ty;
52+
let field_var = format_ident!("variant_{}_field_{}", idx, fidx);
53+
quote! {
54+
let #field_var = ctx.build::<#field_ty>();
55+
}
56+
})
57+
.collect();
58+
59+
let field_vars: Vec<_> = (0..fields.unnamed.len())
60+
.map(|fidx| format_ident!("variant_{}_field_{}", idx, fidx))
61+
.collect();
62+
63+
quote! {
64+
#(#field_builds)*
65+
let #schema_var = ctx.create_node(#schema_crate::SchemaNodeContent::Tuple(
66+
#schema_crate::TupleSchema {
67+
elements: vec![#(#field_vars),*],
68+
binding_style: None,
69+
}
70+
));
71+
}
72+
}
73+
syn::Fields::Named(fields) => {
74+
// Struct variant -> record schema
75+
let field_builds: Vec<_> = fields
76+
.named
77+
.iter()
78+
.enumerate()
79+
.map(|(fidx, f)| {
80+
let field_ty = &f.ty;
81+
let field_var = format_ident!("variant_{}_field_{}", idx, fidx);
82+
quote! {
83+
let #field_var = ctx.build::<#field_ty>();
84+
}
85+
})
86+
.collect();
87+
88+
let property_entries: Vec<_> = fields
89+
.named
90+
.iter()
91+
.enumerate()
92+
.map(|(fidx, f)| {
93+
let field_name = f.ident.as_ref().unwrap();
94+
let field_attrs = FieldAttrs::from_field(f)
95+
.expect("failed to parse field attributes");
96+
let field_name_str = field_attrs.rename.clone().unwrap_or_else(|| {
97+
context.apply_field_rename(&field_name.to_string())
98+
});
99+
let field_var = format_ident!("variant_{}_field_{}", idx, fidx);
100+
let is_optional = is_option_type(&f.ty);
101+
102+
quote! {
103+
(
104+
#field_name_str.to_string(),
105+
#schema_crate::RecordFieldSchema {
106+
schema: #field_var,
107+
optional: #is_optional,
108+
binding_style: None,
109+
}
110+
)
111+
}
112+
})
113+
.collect();
114+
115+
quote! {
116+
#(#field_builds)*
117+
let #schema_var = ctx.create_node(#schema_crate::SchemaNodeContent::Record(
118+
#schema_crate::RecordSchema {
119+
properties: [#(#property_entries),*].into_iter().collect(),
120+
unknown_fields: #schema_crate::UnknownFieldsPolicy::Deny,
121+
}
122+
));
123+
}
124+
}
125+
};
126+
127+
(variant_name, schema_var, schema_build)
128+
})
129+
.collect();
130+
131+
// Collect all schema builds
132+
let all_builds: Vec<_> = variant_schemas
133+
.iter()
134+
.map(|(_, _, build)| build.clone())
135+
.collect();
136+
137+
// Create the variants BTreeMap entries
138+
let variant_entries: Vec<_> = variant_schemas
139+
.iter()
140+
.map(|(name, schema_var, _)| {
141+
quote! {
142+
(#name.to_string(), #schema_var)
143+
}
144+
})
145+
.collect();
146+
147+
let content = quote! {
148+
use std::collections::{BTreeMap, HashSet};
149+
150+
#(#all_builds)*
151+
152+
#schema_crate::SchemaNodeContent::Union(#schema_crate::UnionSchema {
153+
variants: BTreeMap::from([#(#variant_entries),*]),
154+
unambiguous: HashSet::new(),
155+
repr: ::eure_document::data_model::VariantRepr::default(),
156+
deny_untagged: HashSet::new(),
157+
})
158+
};
159+
160+
context.impl_build_schema(content)
161+
}
162+
163+
/// Check if a type is Option<T>
164+
fn is_option_type(ty: &syn::Type) -> bool {
165+
if let syn::Type::Path(type_path) = ty
166+
&& let Some(segment) = type_path.path.segments.last()
167+
{
168+
return segment.ident == "Option";
169+
}
170+
false
171+
}

crates/eure-macros/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub struct MacroConfig {
1515
pub allow_unknown_extensions: bool,
1616
/// Custom error type for the ParseDocument impl.
1717
pub parse_error: Option<TokenStream>,
18+
/// Type name for BuildSchema registration.
19+
pub type_name: Option<String>,
1820
}
1921

2022
impl MacroConfig {
@@ -33,6 +35,7 @@ impl MacroConfig {
3335
allow_unknown_fields: attrs.allow_unknown_fields,
3436
allow_unknown_extensions: attrs.allow_unknown_extensions,
3537
parse_error,
38+
type_name: attrs.type_name,
3639
}
3740
}
3841
}

0 commit comments

Comments
 (0)