227 lines
7.8 KiB
Rust
227 lines
7.8 KiB
Rust
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<T>(
|
|
ident: Ident,
|
|
generics: Generics,
|
|
fields: Punctuated<Field, T>,
|
|
) -> 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
|
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::<String, serde_yaml::Value>()? {
|
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
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<T>(fields: &Punctuated<Field, T>) -> 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::<Attr>() {
|
|
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<T>`.
|
|
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<LitStr>,
|
|
}
|
|
|
|
impl Parse for Attr {
|
|
fn parse(input: ParseStream) -> parse::Result<Self> {
|
|
let ident = input.parse::<Ident>()?.to_string();
|
|
let param = input.parse::<Token![=]>().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<T>,`
|
|
fn generics_streams<T>(params: Punctuated<GenericParam, T>) -> 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
|
|
}
|