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")) } }