diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index 737af48e..1743ff80 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -19,6 +19,7 @@ pub struct FieldAttr { pub validate: Option>, pub default: Option, pub default_with: Option, + pub flatten: Option, } impl FieldAttr { @@ -33,6 +34,7 @@ pub(crate) trait FieldExt { fn first_field_name(&self) -> Result>; fn stripped_ty(&self) -> syn::Type; fn name_buf_opt(&self) -> Result; + fn is_flattened(&self) -> Result; } #[derive(FromMeta)] @@ -201,6 +203,13 @@ impl FieldExt for Field<'_> { .map(|name| quote_spanned!(span => Some(#_form::NameBuf::from((__c.__parent, #name))))) .unwrap_or_else(|| quote_spanned!(span => None::<#_form::NameBuf>))) } + + fn is_flattened(&self) -> Result { + Ok(FieldAttr::from_attrs(FieldAttr::NAME, &self.attrs)? + .into_iter() + .filter_map(|attr| attr.flatten) + .last().unwrap_or(false)) + } } struct RecordMemberAccesses(Vec); diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index f27c906d..9463cf14 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -11,9 +11,10 @@ type WherePredicates = syn::punctuated::Punctuated(fields: Fields<'_>, map_f: F) -> Result - where F: Fn(&syn::Type, &syn::Expr) -> TokenStream + where F: Fn(&syn::Type, &syn::Expr, &syn::Expr) -> TokenStream { let mut matchers = vec![]; + let mut flattened_field = None; for field in fields.iter() { let (ident, ty) = (field.context_ident(), field.stripped_ty()); let field_context: syn::Expr = syn::parse2(quote_spanned!(ty.span() => { @@ -21,27 +22,45 @@ fn fields_map(fields: Fields<'_>, map_f: F) -> Result __c.#ident.get_or_insert_with(|| <#ty as #_form::FromForm<'r>>::init(__o)) })).expect("form context expression"); - let push = map_f(&ty, &field_context); + let push = |field: &syn::Expr| map_f(&ty, &field_context, field); if fields.are_unnamed() { // If we have unnamed fields, then we have exactly one by virtue of // the earlier validation. Push directly to it and return. + let push = push(&syn::parse2(quote!(__f.shift())).expect("field value expression")); return Ok(quote_spanned!(ident.span() => __c.__parent = __f.name.parent(); #push )); } - matchers.extend(field.field_names()?.into_iter().map(|f| match f { - Cased(name) => quote!(#name => { #push }), - Uncased(name) => quote!(__n if __n.as_uncased() == #name => { #push }), - })); + let is_flattened = field.is_flattened()?; + + if is_flattened { + let push = push(&syn::parse2(quote!(__f)).expect("field value expression")); + if let Some(flattened_field) = flattened_field { + drop(flattened_field); + todo!("Emit an error."); + } else { + flattened_field = Some(push); + } + } else { + let push = push(&syn::parse2(quote!(__f.shift())).expect("field value expression")); + matchers.extend(field.field_names()?.into_iter().map(|f| match f { + Cased(name) => quote!(#name => { #push }), + Uncased(name) => quote!(__n if __n.as_uncased() == #name => { #push }), + })); + } } + // TODO: check in options that they aren't strict or pass to flattened fields strict = false. + + let flattened_field = flattened_field.iter(); Ok(quote! { __c.__parent = __f.name.parent(); match __f.name.key_lossy().as_str() { #(#matchers,)* + #(_ => { #flattened_field },)* __k if __k == "_method" || !__c.__opts.strict => { /* ok */ }, _ => __c.__errors.push(__f.unexpected()), } @@ -181,8 +200,8 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { #output } }) - .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { - <#ty as #_form::FromForm<'r>>::push_value(#ctxt, __f.shift()); + .try_fields_map(|_, f| fields_map(f, |ty, ctxt, field| quote_spanned!(ty.span() => { + <#ty as #_form::FromForm<'r>>::push_value(#ctxt, #field); }))) ) .inner_mapper(MapperBuild::new() @@ -201,8 +220,8 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { }) // Without the `let _fut`, we get a wild lifetime error. It don't // make no sense, Rust async/await: it don't make no sense. - .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { - let _fut = <#ty as #_form::FromForm<'r>>::push_data(#ctxt, __f.shift()); + .try_fields_map(|_, f| fields_map(f, |ty, ctxt, field| quote_spanned!(ty.span() => { + let _fut = <#ty as #_form::FromForm<'r>>::push_data(#ctxt, #field); _fut.await; }))) )