From 4c40a35959f8afee2f481406ac82a321606f503e Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 17 Oct 2022 11:55:29 +0200 Subject: [PATCH] Move code around in `macro` crate --- macro/src/gen/meta.rs | 193 ++++++++++++++++++++ macro/src/{gen.rs => gen/mod.rs} | 215 +++------------------- macro/src/ir.rs | 296 ------------------------------ macro/src/lib.rs | 1 + macro/src/parse.rs | 299 +++++++++++++++++++++++++++++++ 5 files changed, 515 insertions(+), 489 deletions(-) create mode 100644 macro/src/gen/meta.rs rename macro/src/{gen.rs => gen/mod.rs} (60%) create mode 100644 macro/src/parse.rs diff --git a/macro/src/gen/meta.rs b/macro/src/gen/meta.rs new file mode 100644 index 0000000..83d9e7d --- /dev/null +++ b/macro/src/gen/meta.rs @@ -0,0 +1,193 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::Ident; + +use crate::ir::{self, Expr, FieldKind, LeafKind}; + + + +/// Generates the whole `const META: ... = ...;` item. +pub(super) fn gen(input: &ir::Input) -> TokenStream { + fn env_tokens(env: &Option) -> TokenStream { + match env { + Some(key) => quote! { Some(#key) }, + None => quote! { None }, + } + } + + let name_str = input.name.to_string(); + let doc = &input.doc; + let meta_fields = input.fields.iter().map(|f| { + let name = f.name.to_string(); + let doc = &f.doc; + let kind = match &f.kind { + FieldKind::Nested { ty }=> { + quote! { + confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META } + } + } + FieldKind::Leaf { env, kind: LeafKind::Optional { .. }, ..} => { + let env = env_tokens(env); + quote! { + confique::meta::FieldKind::Leaf { + env: #env, + kind: confique::meta::LeafKind::Optional, + } + } + } + FieldKind::Leaf { env, kind: LeafKind::Required { default, ty, .. }, ..} => { + let env = env_tokens(env); + let default_value = match default { + Some(default) => { + let meta = default_value_to_meta_expr(default, Some(&ty)); + quote! { Some(#meta) } + }, + None => quote! { None }, + }; + quote! { + confique::meta::FieldKind::Leaf { + env: #env, + kind: confique::meta::LeafKind::Required { + default: #default_value, + }, + } + } + } + }; + + quote! { + confique::meta::Field { + name: #name, + doc: &[ #(#doc),* ], + kind: #kind, + } + } + }); + + quote! { + const META: confique::meta::Meta = confique::meta::Meta { + name: #name_str, + doc: &[ #(#doc),* ], + fields: &[ #( #meta_fields ),* ], + }; + } +} + +/// Generates the meta expression of type `meta::Expr` to be used for the +/// `default` field. `ty` is the type of the field that is used to better infer +/// the exact type of the default value. +fn default_value_to_meta_expr(default: &Expr, ty: Option<&syn::Type>) -> TokenStream { + match default { + Expr::Bool(v) => quote! { confique::meta::Expr::Bool(#v) }, + Expr::Str(s) => quote! { confique::meta::Expr::Str(#s) }, + Expr::Int(i) => { + let variant = infer_type(i.suffix(), ty, "I32", int_type_to_variant); + quote! { confique::meta::Expr::Integer(confique::meta::Integer::#variant(#i)) } + } + Expr::Float(f) => { + let variant = infer_type(f.suffix(), ty, "F64", float_type_to_variant); + quote! { confique::meta::Expr::Float(confique::meta::Float::#variant(#f)) } + } + Expr::Array(items) => { + let item_type = ty.and_then(get_item_ty); + let items = items.iter().map(|item| default_value_to_meta_expr(item, item_type)); + quote! { confique::meta::Expr::Array(&[#( #items ),*]) } + } + } +} + +/// Maps an integer type to the `meta::Expr` variant (e.g. `u32` -> `U32`). +fn int_type_to_variant(suffix: &str) -> Option<&'static str> { + match suffix { + "u8" => Some("U8"), + "u16" => Some("U16"), + "u32" => Some("U32"), + "u64" => Some("U64"), + "u128" => Some("U128"), + "usize" => Some("Usize"), + "i8" => Some("I8"), + "i16" => Some("I16"), + "i32" => Some("I32"), + "i64" => Some("I64"), + "i128" => Some("I128"), + "isize" => Some("Isize"), + _ => None, + } +} + +/// Maps a float type to the `meta::Expr` variant (e.g. `f32` -> `F32`). +fn float_type_to_variant(suffix: &str) -> Option<&'static str> { + match suffix { + "f32" => Some("F32"), + "f64" => Some("F64"), + _ => None, + } +} + +/// Tries to infer the type of an int or float default value. +/// +/// To figure out the type of int or float literals, we first look at the type +/// suffix of the literal. If it is specified, we use that. Otherwise we check +/// if the field type is a known float/integer type. If so, we use that. +/// Otherwise we use a default. +fn infer_type( + suffix: &str, + field_ty: Option<&syn::Type>, + default: &str, + map: fn(&str) -> Option<&'static str>, +) -> Ident { + let variant = map(suffix) + .or_else(|| { + if let Some(syn::Type::Path(syn::TypePath { qself: None, path })) = field_ty { + path.get_ident().and_then(|i| map(&i.to_string())) + } else { + None + } + }) + .unwrap_or(default); + + Ident::new(variant, Span::call_site()) +} + + +/// Tries to extract the type of the item of a field with an array default +/// value. Examples: `&[u32]` -> `u32`, `Vec` -> `String`. +fn get_item_ty(ty: &syn::Type) -> Option<&syn::Type> { + match ty { + // The easy types. + syn::Type::Slice(slice) => Some(&slice.elem), + syn::Type::Array(array) => Some(&*array.elem), + + // This is the least clear case. We certainly want to cover `Vec` but + // ideally some more cases. On the other hand, we just can't really + // know, so some incorrect guesses are definitely expected here. Most + // are likely filtered out by applying `gen_meta_default` to it, but + // some will result in a wrong default value type. But people can + // always just add a prefix to the literal in those cases. + // + // We simply check if the last element in the path has exactly one + // generic type argument, in which case we use that. + syn::Type::Path(p) => { + let args = match &p.path.segments.last().expect("empty type path").arguments { + syn::PathArguments::AngleBracketed(args) => &args.args, + _ => return None, + }; + + if args.len() != 1 { + return None; + } + + match &args[0] { + syn::GenericArgument::Type(t) => Some(t), + _ => None, + } + }, + + // Just recurse on inner type. + syn::Type::Reference(r) => get_item_ty(&r.elem), + syn::Type::Group(g) => get_item_ty(&g.elem), + syn::Type::Paren(p) => get_item_ty(&p.elem), + + _ => None, + } +} diff --git a/macro/src/gen.rs b/macro/src/gen/mod.rs similarity index 60% rename from macro/src/gen.rs rename to macro/src/gen/mod.rs index 409abec..cbcfa63 100644 --- a/macro/src/gen.rs +++ b/macro/src/gen/mod.rs @@ -2,9 +2,12 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned}; use syn::{Ident, spanned::Spanned}; -use crate::ir::{self, Expr, FieldKind, LeafKind}; +use crate::ir::{self, FieldKind, LeafKind}; + +mod meta; +/// The main function to generate the output token stream from the parse IR. pub(crate) fn gen(input: ir::Input) -> TokenStream { let partial_mod = gen_partial_mod(&input); let config_impl = gen_config_impl(&input); @@ -15,6 +18,7 @@ pub(crate) fn gen(input: ir::Input) -> TokenStream { } } +/// Generates the `impl Config for ... { ... }`. fn gen_config_impl(input: &ir::Input) -> TokenStream { let name = &input.name; let (partial_mod_name, partial_struct_name) = partial_names(&input.name); @@ -44,7 +48,7 @@ fn gen_config_impl(input: &ir::Input) -> TokenStream { }); - let meta_item = gen_meta(input); + let meta_item = meta::gen(input); quote! { #[automatically_derived] impl confique::Config for #name { @@ -61,17 +65,8 @@ fn gen_config_impl(input: &ir::Input) -> TokenStream { } } - -/// Returns the names of the module and struct for the partial type: -/// `(mod_name, struct_name)`. -fn partial_names(original_name: &Ident) -> (Ident, Ident) { - use heck::SnakeCase; - ( - format_ident!("confique_partial_{}", original_name.to_string().to_snake_case()), - format_ident!("Partial{original_name}"), - ) -} - +/// Generates the whole `mod ... { ... }` that defines the partial type and +/// related items. fn gen_partial_mod(input: &ir::Input) -> TokenStream { let (mod_name, struct_name) = partial_names(&input.name); let visibility = &input.visibility; @@ -135,7 +130,7 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { input.name, f.name, ); - let expr = default_deserialize_expr(&default); + let expr = default_value_to_deserializable_expr(&default); match deserialize_with { None => quote! { @@ -277,198 +272,32 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { } } - -/// Generates the whole `const META` item. -fn gen_meta(input: &ir::Input) -> TokenStream { - fn env_tokens(env: &Option) -> TokenStream { - match env { - Some(key) => quote! { Some(#key) }, - None => quote! { None }, - } - } - - let name_str = input.name.to_string(); - let doc = &input.doc; - let meta_fields = input.fields.iter().map(|f| { - let name = f.name.to_string(); - let doc = &f.doc; - let kind = match &f.kind { - FieldKind::Nested { ty }=> { - quote! { - confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META } - } - } - FieldKind::Leaf { env, kind: LeafKind::Optional { .. }, ..} => { - let env = env_tokens(env); - quote! { - confique::meta::FieldKind::Leaf { - env: #env, - kind: confique::meta::LeafKind::Optional, - } - } - } - FieldKind::Leaf { env, kind: LeafKind::Required { default, ty, .. }, ..} => { - let env = env_tokens(env); - let default_value = match default { - Some(default) => { - let meta = gen_meta_default(default, Some(&ty)); - quote! { Some(#meta) } - }, - None => quote! { None }, - }; - quote! { - confique::meta::FieldKind::Leaf { - env: #env, - kind: confique::meta::LeafKind::Required { - default: #default_value, - }, - } - } - } - }; - - quote! { - confique::meta::Field { - name: #name, - doc: &[ #(#doc),* ], - kind: #kind, - } - } - }); - - quote! { - const META: confique::meta::Meta = confique::meta::Meta { - name: #name_str, - doc: &[ #(#doc),* ], - fields: &[ #( #meta_fields ),* ], - }; - } +/// Returns the names of the module and struct for the partial type: +/// `(mod_name, struct_name)`. +fn partial_names(original_name: &Ident) -> (Ident, Ident) { + use heck::SnakeCase; + ( + format_ident!("confique_partial_{}", original_name.to_string().to_snake_case()), + format_ident!("Partial{original_name}"), + ) } -/// Generates the meta expression of type `meta::Expr` to be used for the -/// `default` field. -fn gen_meta_default(default: &Expr, ty: Option<&syn::Type>) -> TokenStream { - fn int_type_to_variant(suffix: &str) -> Option<&'static str> { - match suffix { - "u8" => Some("U8"), - "u16" => Some("U16"), - "u32" => Some("U32"), - "u64" => Some("U64"), - "u128" => Some("U128"), - "usize" => Some("Usize"), - "i8" => Some("I8"), - "i16" => Some("I16"), - "i32" => Some("I32"), - "i64" => Some("I64"), - "i128" => Some("I128"), - "isize" => Some("Isize"), - _ => None, - } - } - - fn float_type_to_variant(suffix: &str) -> Option<&'static str> { - match suffix { - "f32" => Some("F32"), - "f64" => Some("F64"), - _ => None, - } - } - - // To figure out the type of int or float literals, we first look at the - // type suffix of the literal. If it is specified, we use that. Otherwise - // we check if the field type is a known float/integer type. If so, we use - // that. Otherwise we use a default. - fn infer_type( - suffix: &str, - field_ty: Option<&syn::Type>, - default: &str, - map: fn(&str) -> Option<&'static str>, - ) -> Ident { - let variant = map(suffix) - .or_else(|| { - if let Some(syn::Type::Path(syn::TypePath { qself: None, path })) = field_ty { - path.get_ident().and_then(|i| map(&i.to_string())) - } else { - None - } - }) - .unwrap_or(default); - - Ident::new(variant, Span::call_site()) - } - - /// Tries to extract the type of the item of a field with an array default value. - fn get_item_ty(ty: &syn::Type) -> Option<&syn::Type> { - match ty { - syn::Type::Slice(slice) => Some(&slice.elem), - syn::Type::Array(array) => Some(&*array.elem), - syn::Type::Path(p) => { - // This is the least clear case. We certainly want to cover - // `Vec` but ideally some more cases. On the other hand, we - // just can't really know, so some incorrect guesses are - // definitely expected here. Most are likely filtered out by - // applying `gen_meta_default` to it, but some will result in a - // wrong default value type. But people can always just add a - // prefix to the literal in those cases. - // - // We simply check if the last element in the path has exactly - // one generic type argument, in which case we use that. - let args = match &p.path.segments.last().expect("empty type path").arguments { - syn::PathArguments::AngleBracketed(args) => &args.args, - _ => return None, - }; - - if args.len() != 1 { - return None; - } - - match &args[0] { - syn::GenericArgument::Type(t) => Some(t), - _ => None, - } - }, - syn::Type::Reference(r) => get_item_ty(&r.elem), - syn::Type::Group(g) => get_item_ty(&g.elem), - syn::Type::Paren(p) => get_item_ty(&p.elem), - _ => None, - } - } - - - let v = match default { - Expr::Bool(v) => quote! { confique::meta::Expr::Bool(#v) }, - Expr::Str(s) => quote! { confique::meta::Expr::Str(#s) }, - Expr::Int(i) => { - let variant = infer_type(i.suffix(), ty, "I32", int_type_to_variant); - quote! { confique::meta::Expr::Integer(confique::meta::Integer::#variant(#i)) } - } - Expr::Float(f) => { - let variant = infer_type(f.suffix(), ty, "F64", float_type_to_variant); - quote! { confique::meta::Expr::Float(confique::meta::Float::#variant(#f)) } - } - Expr::Array(items) => { - let item_type = ty.and_then(get_item_ty); - let items = items.iter().map(|item| gen_meta_default(item, item_type)); - quote! { confique::meta::Expr::Array(&[#( #items ),*]) } - } - }; - - quote! { #v } -} - -fn default_deserialize_expr(expr: &ir::Expr) -> TokenStream { +/// Generates a Rust expression from the default value that implemenets +/// `serde::de::IntoDeserializer`. +fn default_value_to_deserializable_expr(expr: &ir::Expr) -> TokenStream { match expr { ir::Expr::Str(lit) => quote! { #lit }, ir::Expr::Int(lit) => quote! { #lit }, ir::Expr::Float(lit) => quote! { #lit }, ir::Expr::Bool(lit) => quote! { #lit }, ir::Expr::Array(arr) => { - let items = arr.iter().map(default_deserialize_expr); + let items = arr.iter().map(default_value_to_deserializable_expr); quote! { confique::internal::ArrayIntoDeserializer([ #(#items),* ]) } }, } } +/// Returns tokens defining the visibility of the items in the inner module. fn inner_visibility(outer: &syn::Visibility, span: Span) -> TokenStream { match outer { // These visibilities can be used as they are. No adjustment needed. diff --git a/macro/src/ir.rs b/macro/src/ir.rs index bcc5ee2..577e80b 100644 --- a/macro/src/ir.rs +++ b/macro/src/ir.rs @@ -1,9 +1,5 @@ //! Definition of the intermediate representation. -use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned, punctuated::Punctuated}; - -use crate::util::{is_option, unwrap_option}; - /// The parsed input to the `gen_config` macro. pub(crate) struct Input { @@ -72,295 +68,3 @@ pub(crate) enum Expr { Bool(syn::LitBool), Array(Vec), } - -impl Input { - pub(crate) fn from_ast(mut input: syn::DeriveInput) -> Result { - let fields = match input.data { - syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(f), .. }) => f, - _ => return Err(Error::new( - input.span(), - "`confique::Config` can only be derive for structs with named fields", - )), - }; - - let doc = extract_doc(&mut input.attrs); - let fields = fields.named.into_iter() - .map(Field::from_ast) - .collect::, _>>()?; - - - Ok(Self { - doc, - visibility: input.vis, - name: input.ident, - fields, - }) - } -} - -impl Field { - fn from_ast(mut field: syn::Field) -> Result { - let doc = extract_doc(&mut field.attrs); - let attrs = extract_internal_attrs(&mut field.attrs)?; - - // TODO: check no other attributes are here - let kind = if attrs.nested { - if is_option(&field.ty) { - return Err(Error::new( - field.ident.span(), - "nested configurations cannot be optional (type `Option<_>`)", - )); - } - if attrs.default.is_some() { - return Err(Error::new( - field.ident.span(), - "cannot specify `nested` and `default` attributes at the same time", - )); - } - if attrs.env.is_some() { - return Err(Error::new( - field.ident.span(), - "cannot specify `nested` and `env` attributes at the same time", - )); - } - if attrs.deserialize_with.is_some() { - return Err(Error::new( - field.ident.span(), - "cannot specify `nested` and `deserialize_with` attributes at the same time", - )); - } - - FieldKind::Nested { ty: field.ty } - } else { - match unwrap_option(&field.ty) { - None => FieldKind::Leaf { - env: attrs.env, - deserialize_with: attrs.deserialize_with, - kind: LeafKind::Required { - default: attrs.default, - ty: field.ty, - }, - }, - Some(inner) => { - if attrs.default.is_some() { - return Err(Error::new( - field.ident.span(), - "optional fields (type `Option<_>`) cannot have default \ - values (`#[config(default = ...)]`)", - )); - } - - FieldKind::Leaf { - env: attrs.env, - deserialize_with: attrs.deserialize_with, - kind: LeafKind::Optional { - inner_ty: inner.clone(), - }, - } - } - } - }; - - Ok(Self { - doc, - name: field.ident.expect("bug: expected named field"), - kind, - }) - } - - pub(crate) fn is_leaf(&self) -> bool { - matches!(self.kind, FieldKind::Leaf { .. }) - } -} - -impl Parse for Expr { - fn parse(input: ParseStream) -> Result { - let msg = "invalid default value. Allowed are only: certain literals \ - (string, integer, float, bool), and arrays"; - - if input.peek(syn::token::Bracket) { - let content; - syn::bracketed!(content in input); - - let items = >::parse_terminated(&content)?; - Ok(Self::Array(items.into_iter().collect())) - } else { - let lit: syn::Lit = input.parse() - .map_err(|_| Error::new(input.span(), msg))?; - match lit { - syn::Lit::Str(l) => Ok(Self::Str(l)), - syn::Lit::Int(l) => Ok(Self::Int(l)), - syn::Lit::Float(l) => Ok(Self::Float(l)), - syn::Lit::Bool(l) => Ok(Self::Bool(l)), - _ => Err(Error::new(lit.span(), msg)), - } - } - } -} - -/// Extracts all doc string attributes from the list and return them as list of -/// strings (in order). -fn extract_doc(attrs: &mut Vec) -> Vec { - extract_attrs(attrs, |attr| { - match attr.parse_meta().ok()? { - syn::Meta::NameValue(syn::MetaNameValue { - lit: syn::Lit::Str(s), - path, - .. - }) if path.is_ident("doc") => Some(s.value()), - _ => None, - } - }) -} - -fn extract_attrs(attrs: &mut Vec, mut pred: P) -> Vec -where - P: FnMut(&syn::Attribute) -> Option, -{ - // TODO: use `Vec::drain_filter` once stabilized. The current impl is O(n²). - let mut i = 0; - let mut out = Vec::new(); - while i < attrs.len() { - match pred(&attrs[i]) { - Some(v) => { - out.push(v); - attrs.remove(i); - } - None => i += 1, - } - } - - out -} - -fn extract_internal_attrs( - attrs: &mut Vec, -) -> Result { - let internal_attrs = extract_attrs(attrs, |attr| { - if attr.path.is_ident("config") { - // TODO: clone not necessary once we use drain_filter - Some(attr.clone()) - } else { - None - } - }); - - - let mut out = InternalAttrs::default(); - for attr in internal_attrs { - type AttrList = Punctuated; - let parsed_list = attr.parse_args_with(AttrList::parse_terminated)?; - - for parsed in parsed_list { - let keyword = parsed.keyword(); - - macro_rules! duplicate_if { - ($cond:expr) => { - if $cond { - let msg = format!("duplicate '{keyword}' confique attribute"); - return Err(Error::new(attr.tokens.span(), msg)); - } - }; - } - - match parsed { - InternalAttr::Default(expr) => { - duplicate_if!(out.default.is_some()); - out.default = Some(expr); - } - InternalAttr::Nested => { - duplicate_if!(out.nested); - out.nested = true; - } - InternalAttr::Env(key) => { - duplicate_if!(out.env.is_some()); - out.env = Some(key); - } - InternalAttr::DeserializeWith(path) => { - duplicate_if!(out.deserialize_with.is_some()); - out.deserialize_with = Some(path); - } - } - } - } - - Ok(out) -} - -#[derive(Default)] -struct InternalAttrs { - nested: bool, - default: Option, - env: Option, - deserialize_with: Option, -} - -enum InternalAttr { - Nested, - Default(Expr), - Env(String), - DeserializeWith(syn::Path), -} - -impl InternalAttr { - fn keyword(&self) -> &'static str { - match self { - Self::Nested => "nested", - Self::Default(_) => "default", - Self::Env(_) => "env", - Self::DeserializeWith(_) => "deserialize_with", - } - } -} - -impl Parse for InternalAttr { - fn parse(input: ParseStream) -> Result { - let ident: syn::Ident = input.parse()?; - match &*ident.to_string() { - "nested" => { - assert_empty_or_comma(input)?; - Ok(Self::Nested) - } - - "default" => { - let _: Token![=] = input.parse()?; - let expr: Expr = input.parse()?; - assert_empty_or_comma(input)?; - Ok(Self::Default(expr)) - } - - "env" => { - let _: Token![=] = input.parse()?; - let key: syn::LitStr = input.parse()?; - assert_empty_or_comma(input)?; - let value = key.value(); - if value.contains('=') || value.contains('\0') { - Err(syn::Error::new( - key.span(), - "environment variable key must not contain '=' or null bytes", - )) - } else { - Ok(Self::Env(value)) - } - } - - "deserialize_with" => { - let _: Token![=] = input.parse()?; - let path: syn::Path = input.parse()?; - assert_empty_or_comma(input)?; - - Ok(Self::DeserializeWith(path)) - } - - _ => Err(syn::Error::new(ident.span(), "unknown confique attribute")), - } - } -} - -fn assert_empty_or_comma(input: ParseStream) -> Result<(), Error> { - if input.is_empty() || input.peek(Token![,]) { - Ok(()) - } else { - Err(Error::new(input.span(), "unexpected tokens, expected no more tokens in this context")) - } -} diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 51eaba7..77dab45 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -3,6 +3,7 @@ use proc_macro::TokenStream as TokenStream1; mod gen; mod ir; +mod parse; mod util; diff --git a/macro/src/parse.rs b/macro/src/parse.rs new file mode 100644 index 0000000..60a45e5 --- /dev/null +++ b/macro/src/parse.rs @@ -0,0 +1,299 @@ +use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned, punctuated::Punctuated}; + +use crate::{ + ir::{Input, Field, FieldKind, LeafKind, Expr}, + util::{unwrap_option, is_option}, +}; + + +impl Input { + pub(crate) fn from_ast(mut input: syn::DeriveInput) -> Result { + let fields = match input.data { + syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(f), .. }) => f, + _ => return Err(Error::new( + input.span(), + "`confique::Config` can only be derive for structs with named fields", + )), + }; + + let doc = extract_doc(&mut input.attrs); + let fields = fields.named.into_iter() + .map(Field::from_ast) + .collect::, _>>()?; + + + Ok(Self { + doc, + visibility: input.vis, + name: input.ident, + fields, + }) + } +} + +impl Field { + fn from_ast(mut field: syn::Field) -> Result { + let doc = extract_doc(&mut field.attrs); + let attrs = extract_internal_attrs(&mut field.attrs)?; + + // TODO: check no other attributes are here + let kind = if attrs.nested { + if is_option(&field.ty) { + return Err(Error::new( + field.ident.span(), + "nested configurations cannot be optional (type `Option<_>`)", + )); + } + if attrs.default.is_some() { + return Err(Error::new( + field.ident.span(), + "cannot specify `nested` and `default` attributes at the same time", + )); + } + if attrs.env.is_some() { + return Err(Error::new( + field.ident.span(), + "cannot specify `nested` and `env` attributes at the same time", + )); + } + if attrs.deserialize_with.is_some() { + return Err(Error::new( + field.ident.span(), + "cannot specify `nested` and `deserialize_with` attributes at the same time", + )); + } + + FieldKind::Nested { ty: field.ty } + } else { + match unwrap_option(&field.ty) { + None => FieldKind::Leaf { + env: attrs.env, + deserialize_with: attrs.deserialize_with, + kind: LeafKind::Required { + default: attrs.default, + ty: field.ty, + }, + }, + Some(inner) => { + if attrs.default.is_some() { + return Err(Error::new( + field.ident.span(), + "optional fields (type `Option<_>`) cannot have default \ + values (`#[config(default = ...)]`)", + )); + } + + FieldKind::Leaf { + env: attrs.env, + deserialize_with: attrs.deserialize_with, + kind: LeafKind::Optional { + inner_ty: inner.clone(), + }, + } + } + } + }; + + Ok(Self { + doc, + name: field.ident.expect("bug: expected named field"), + kind, + }) + } + + pub(crate) fn is_leaf(&self) -> bool { + matches!(self.kind, FieldKind::Leaf { .. }) + } +} + +impl Parse for Expr { + fn parse(input: ParseStream) -> Result { + let msg = "invalid default value. Allowed are only: certain literals \ + (string, integer, float, bool), and arrays"; + + if input.peek(syn::token::Bracket) { + let content; + syn::bracketed!(content in input); + + let items = >::parse_terminated(&content)?; + Ok(Self::Array(items.into_iter().collect())) + } else { + let lit: syn::Lit = input.parse() + .map_err(|_| Error::new(input.span(), msg))?; + match lit { + syn::Lit::Str(l) => Ok(Self::Str(l)), + syn::Lit::Int(l) => Ok(Self::Int(l)), + syn::Lit::Float(l) => Ok(Self::Float(l)), + syn::Lit::Bool(l) => Ok(Self::Bool(l)), + _ => Err(Error::new(lit.span(), msg)), + } + } + } +} + +/// Extracts all doc string attributes from the list and return them as list of +/// strings (in order). +fn extract_doc(attrs: &mut Vec) -> Vec { + extract_attrs(attrs, |attr| { + match attr.parse_meta().ok()? { + syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Str(s), + path, + .. + }) if path.is_ident("doc") => Some(s.value()), + _ => None, + } + }) +} + +fn extract_attrs(attrs: &mut Vec, mut pred: P) -> Vec +where + P: FnMut(&syn::Attribute) -> Option, +{ + // TODO: use `Vec::drain_filter` once stabilized. The current impl is O(n²). + let mut i = 0; + let mut out = Vec::new(); + while i < attrs.len() { + match pred(&attrs[i]) { + Some(v) => { + out.push(v); + attrs.remove(i); + } + None => i += 1, + } + } + + out +} + +fn extract_internal_attrs( + attrs: &mut Vec, +) -> Result { + let internal_attrs = extract_attrs(attrs, |attr| { + if attr.path.is_ident("config") { + // TODO: clone not necessary once we use drain_filter + Some(attr.clone()) + } else { + None + } + }); + + + let mut out = InternalAttrs::default(); + for attr in internal_attrs { + type AttrList = Punctuated; + let parsed_list = attr.parse_args_with(AttrList::parse_terminated)?; + + for parsed in parsed_list { + let keyword = parsed.keyword(); + + macro_rules! duplicate_if { + ($cond:expr) => { + if $cond { + let msg = format!("duplicate '{keyword}' confique attribute"); + return Err(Error::new(attr.tokens.span(), msg)); + } + }; + } + + match parsed { + InternalAttr::Default(expr) => { + duplicate_if!(out.default.is_some()); + out.default = Some(expr); + } + InternalAttr::Nested => { + duplicate_if!(out.nested); + out.nested = true; + } + InternalAttr::Env(key) => { + duplicate_if!(out.env.is_some()); + out.env = Some(key); + } + InternalAttr::DeserializeWith(path) => { + duplicate_if!(out.deserialize_with.is_some()); + out.deserialize_with = Some(path); + } + } + } + } + + Ok(out) +} + +#[derive(Default)] +struct InternalAttrs { + nested: bool, + default: Option, + env: Option, + deserialize_with: Option, +} + +enum InternalAttr { + Nested, + Default(Expr), + Env(String), + DeserializeWith(syn::Path), +} + +impl InternalAttr { + fn keyword(&self) -> &'static str { + match self { + Self::Nested => "nested", + Self::Default(_) => "default", + Self::Env(_) => "env", + Self::DeserializeWith(_) => "deserialize_with", + } + } +} + +impl Parse for InternalAttr { + fn parse(input: ParseStream) -> Result { + let ident: syn::Ident = input.parse()?; + match &*ident.to_string() { + "nested" => { + assert_empty_or_comma(input)?; + Ok(Self::Nested) + } + + "default" => { + let _: Token![=] = input.parse()?; + let expr: Expr = input.parse()?; + assert_empty_or_comma(input)?; + Ok(Self::Default(expr)) + } + + "env" => { + let _: Token![=] = input.parse()?; + let key: syn::LitStr = input.parse()?; + assert_empty_or_comma(input)?; + let value = key.value(); + if value.contains('=') || value.contains('\0') { + Err(syn::Error::new( + key.span(), + "environment variable key must not contain '=' or null bytes", + )) + } else { + Ok(Self::Env(value)) + } + } + + "deserialize_with" => { + let _: Token![=] = input.parse()?; + let path: syn::Path = input.parse()?; + assert_empty_or_comma(input)?; + + Ok(Self::DeserializeWith(path)) + } + + _ => Err(syn::Error::new(ident.span(), "unknown confique attribute")), + } + } +} + +fn assert_empty_or_comma(input: ParseStream) -> Result<(), Error> { + if input.is_empty() || input.peek(Token![,]) { + Ok(()) + } else { + Err(Error::new(input.span(), "unexpected tokens, expected no more tokens in this context")) + } +}