use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ Data, DeriveInput, Expr, Fields, LitStr, Result, Type, parse_macro_input, spanned::Spanned, }; #[proc_macro_derive(Decodal, attributes(decodal))] pub fn derive_decodal(input: TokenStream) -> TokenStream { match expand_decodal(parse_macro_input!(input as DeriveInput)) { Ok(tokens) => tokens.into(), Err(error) => error.to_compile_error().into(), } } fn expand_decodal(input: DeriveInput) -> Result { let name = input.ident; let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields.named, _ => { return Err(syn::Error::new( data.struct_token.span(), "Decodal derive supports structs with named fields only", )); } }, _ => { return Err(syn::Error::new( name.span(), "Decodal derive supports structs only", )); } }; let mut schema_fields = Vec::new(); let mut decode_fields = Vec::new(); let mut known_field_roots = Vec::new(); let mut rest_field = None; for field in fields { let ident = field.ident.expect("named field"); let ty = field.ty; let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?; if attrs.rest { if attrs.rename_explicit || attrs.default.is_some() || !attrs.constraints.is_empty() { return Err(syn::Error::new( ident.span(), "`rest` cannot be combined with rename, default, or field constraints", )); } if rest_field.is_some() { return Err(syn::Error::new( ident.span(), "Decodal derive supports only one `rest` field", )); } rest_field = Some((ident, ty)); continue; } let path = attrs.rename.clone(); known_field_roots.push( path.split('.') .next() .expect("field paths are non-empty") .to_owned(), ); let schema_value = schema_expr(&ty, &attrs)?; let decode_value = decode_expr(&ty, &path, &attrs); schema_fields.push(quote! { (#path, { #schema_value }) }); decode_fields.push(quote! { #ident: #decode_value }); } if let Some((ident, ty)) = &rest_field { decode_fields.push(quote! { #ident: <#ty as ::decodal::DecodalRest>::decodal_decode_rest( data, &[#(#known_field_roots),*], )? }); } let schema_entries = if schema_fields.is_empty() { quote! { ::core::iter::empty::<(&'static str, ::decodal::Value)>() } } else { quote! { [#(#schema_fields),*] } }; let build_schema = if let Some((_, ty)) = &rest_field { quote! { ::decodal::Value::object_from_paths_with_rest( #schema_entries, <#ty as ::decodal::DecodalRest>::decodal_rest_schema(), ) } } else { quote! { ::decodal::Value::object_from_paths(#schema_entries) } }; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); Ok(quote! { impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause { fn decodal_schema() -> ::decodal::Value { #build_schema } } impl #impl_generics ::decodal::DecodalDecode for #name #ty_generics #where_clause { fn decodal_decode(data: &::decodal::Data) -> ::decodal::DecodeResult { Ok(Self { #(#decode_fields),* }) } } }) } #[derive(Default)] struct FieldAttrs { rename: String, rename_explicit: bool, rest: bool, default: Option, constraints: Vec, } impl FieldAttrs { fn from_attrs(attrs: &[syn::Attribute], fallback_name: &str) -> Result { let mut output = Self { rename: fallback_name.to_string(), ..Self::default() }; for attr in attrs { if !attr.path().is_ident("decodal") { continue; } attr.parse_nested_meta(|meta| { if meta.path.is_ident("rename") { let value: LitStr = meta.value()?.parse()?; output.rename = value.value(); output.rename_explicit = true; return Ok(()); } if meta.path.is_ident("rest") { if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) { return Err(meta.error("`rest` does not accept a value")); } output.rest = true; return Ok(()); } if meta.path.is_ident("default") { if meta.input.peek(syn::Token![=]) { let expr: Expr = meta.value()?.parse()?; output.default = Some(DefaultAttr::Expr(expr)); } else { output.default = Some(DefaultAttr::Default); } return Ok(()); } if let Some(kind) = ConstraintKind::from_path(&meta.path) { let expr: Expr = meta.value()?.parse()?; output.constraints.push(ConstraintAttr { kind, expr }); return Ok(()); } Err(meta.error("unsupported decodal attribute")) })?; } Ok(output) } } enum DefaultAttr { Default, Expr(Expr), } struct ConstraintAttr { kind: ConstraintKind, expr: Expr, } #[derive(Clone, Copy)] enum ConstraintKind { Gt, Gte, Lt, Lte, } impl ConstraintKind { fn from_path(path: &syn::Path) -> Option { if path.is_ident("gt") { Some(Self::Gt) } else if path.is_ident("gte") { Some(Self::Gte) } else if path.is_ident("lt") { Some(Self::Lt) } else if path.is_ident("lte") { Some(Self::Lte) } else { None } } fn method(self) -> syn::Ident { match self { Self::Gt => format_ident!("gt"), Self::Gte => format_ident!("gte"), Self::Lt => format_ident!("lt"), Self::Lte => format_ident!("lte"), } } } fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result { let mut tokens = quote! { let mut schema = <#ty as ::decodal::DecodalSchema>::decodal_schema(); }; for constraint in &attrs.constraints { let method = constraint.kind.method(); let expr = &constraint.expr; tokens.extend(quote! { schema = schema.#method(#expr); }); } if let Some(default) = &attrs.default { let default_expr = match default { DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() }, DefaultAttr::Expr(expr) => quote! { (#expr) }, }; tokens.extend(quote! { schema = schema .default(::decodal::IntoValue::into_value(#default_expr)) .expect("Decodal derive generated a valid default"); }); } tokens.extend(quote! { schema }); Ok(tokens) } fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 { let missing = if let Some(default) = &attrs.default { match default { DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() }, DefaultAttr::Expr(expr) => quote! { ::core::convert::Into::into(#expr) }, } } else { quote! { return Err(::decodal::DecodeError::new(#path, "missing field")); } }; quote! { if let Some(value) = ::decodal::data_at_path(data, #path) { <#ty as ::decodal::DecodalDecode>::decodal_decode(value) .map_err(|error| ::decodal::prefix_decode_error(#path, error))? } else { #missing } } } #[cfg(test)] mod tests { use super::*; #[test] fn rejects_multiple_rest_fields() { let input: DeriveInput = syn::parse_quote! { struct Invalid { #[decodal(rest)] first: std::collections::BTreeMap, #[decodal(rest)] second: std::collections::BTreeMap, } }; assert!( expand_decodal(input) .unwrap_err() .to_string() .contains("only one `rest` field") ); } #[test] fn rejects_rest_field_modifiers() { let input: DeriveInput = syn::parse_quote! { struct Invalid { #[decodal(rest, default)] extra: std::collections::BTreeMap, } }; assert!( expand_decodal(input) .unwrap_err() .to_string() .contains("cannot be combined") ); } }