use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::parse::{self, Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{Error, Field, GenericParam, Generics, Ident, LitStr, Token, Type, TypeParam}; /// Error message when attempting to flatten multiple fields. const MULTIPLE_FLATTEN_ERROR: &str = "At most one instance of #[config(flatten)] is supported"; /// Use this crate's name as log target. const LOG_TARGET: &str = env!("CARGO_PKG_NAME"); pub fn derive_deserialize( ident: Ident, generics: Generics, fields: Punctuated, ) -> TokenStream { // Create all necessary tokens for the implementation. let GenericsStreams { unconstrained, constrained, phantoms } = generics_streams(generics.params); let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields); let visitor = format_ident!("{}Visitor", ident); // Generate deserialization impl. let tokens = quote! { #[derive(Default)] #[allow(non_snake_case)] struct #visitor < #unconstrained > { #phantoms } impl<'de, #constrained> serde::de::Visitor<'de> for #visitor < #unconstrained > { type Value = #ident < #unconstrained >; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a mapping") } fn visit_map(self, mut map: M) -> Result where M: serde::de::MapAccess<'de>, { let mut config = Self::Value::default(); // NOTE: This could be used to print unused keys. let mut unused = serde_yaml::Mapping::new(); while let Some((key, value)) = map.next_entry::()? { match key.as_str() { #match_assignments _ => { unused.insert(serde_yaml::Value::String(key), value); }, } } #flatten Ok(config) } } impl<'de, #constrained> serde::Deserialize<'de> for #ident < #unconstrained > { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_map(#visitor :: default()) } } }; tokens.into() } // Token streams created from the fields in the struct. #[derive(Default)] struct FieldStreams { match_assignments: TokenStream2, flatten: TokenStream2, } /// Create the deserializers for match arms and flattened fields. fn fields_deserializer(fields: &Punctuated) -> FieldStreams { let mut field_streams = FieldStreams::default(); // Create the deserialization stream for each field. for field in fields.iter() { if let Err(err) = field_deserializer(&mut field_streams, field) { field_streams.flatten = err.to_compile_error(); return field_streams; } } field_streams } /// Append a single field deserializer to the stream. fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result<(), Error> { let ident = field.ident.as_ref().expect("unreachable tuple struct"); let literal = ident.to_string(); let mut literals = vec![literal.clone()]; // Create default stream for deserializing fields. let mut match_assignment_stream = quote! { match serde::Deserialize::deserialize(value) { Ok(value) => config.#ident = value, Err(err) => { log::error!(target: #LOG_TARGET, "Config error: {}: {}", #literal, err); }, } }; // Iterate over all #[config(...)] attributes. for attr in field.attrs.iter().filter(|attr| crate::path_ends_with(&attr.path, "config")) { let parsed = match attr.parse_args::() { Ok(parsed) => parsed, Err(_) => continue, }; match parsed.ident.as_str() { // Skip deserialization for `#[config(skip)]` fields. "skip" => return Ok(()), "flatten" => { // NOTE: Currently only a single instance of flatten is supported per struct // for complexity reasons. if !field_streams.flatten.is_empty() { return Err(Error::new(attr.span(), MULTIPLE_FLATTEN_ERROR)); } // Create the tokens to deserialize the flattened struct from the unused fields. field_streams.flatten.extend(quote! { let unused = serde_yaml::Value::Mapping(unused); config.#ident = serde::Deserialize::deserialize(unused).unwrap_or_default(); }); }, "deprecated" => { // Construct deprecation message and append optional attribute override. let mut message = format!("Config warning: {} is deprecated", literal); if let Some(warning) = parsed.param { message = format!("{}; {}", message, warning.value()); } // Append stream to log deprecation warning. match_assignment_stream.extend(quote! { log::warn!(target: #LOG_TARGET, #message); }); }, // Add aliases to match pattern. "alias" => { if let Some(alias) = parsed.param { literals.push(alias.value()); } }, _ => (), } } // Create token stream for deserializing "none" string into `Option`. if let Type::Path(type_path) = &field.ty { if crate::path_ends_with(&type_path.path, "Option") { match_assignment_stream = quote! { if value.as_str().map_or(false, |s| s.eq_ignore_ascii_case("none")) { config.#ident = None; continue; } #match_assignment_stream }; } } // Create the token stream for deserialization and error handling. field_streams.match_assignments.extend(quote! { #(#literals)|* => { #match_assignment_stream }, }); Ok(()) } /// Field attribute. struct Attr { ident: String, param: Option, } impl Parse for Attr { fn parse(input: ParseStream<'_>) -> parse::Result { let ident = input.parse::()?.to_string(); let param = input.parse::().and_then(|_| input.parse()).ok(); Ok(Self { ident, param }) } } /// Storage for all necessary generics information. #[derive(Default)] struct GenericsStreams { unconstrained: TokenStream2, constrained: TokenStream2, phantoms: TokenStream2, } /// Create the necessary generics annotations. /// /// This will create three different token streams, which might look like this: /// - unconstrained: `T` /// - constrained: `T: Default + Deserialize<'de>` /// - phantoms: `T: PhantomData,` fn generics_streams(params: Punctuated) -> GenericsStreams { let mut generics = GenericsStreams::default(); for generic in params { // NOTE: Lifetimes and const params are not supported. if let GenericParam::Type(TypeParam { ident, .. }) = generic { generics.unconstrained.extend(quote!( #ident , )); generics.constrained.extend(quote! { #ident : Default + serde::Deserialize<'de> , }); generics.phantoms.extend(quote! { #ident : std::marker::PhantomData < #ident >, }); } } generics }