diff --git a/macro/Cargo.toml b/macro/Cargo.toml index 51dd2b2..020c08e 100644 --- a/macro/Cargo.toml +++ b/macro/Cargo.toml @@ -12,8 +12,7 @@ license = "MIT/Apache-2.0" proc-macro = true [dependencies] -darling = "0.13.0" -heck = "0.3.2" -proc-macro2 = "1.0" -quote = "1.0" syn = "1.0" +quote = "1.0" +proc-macro2 = "1.0" +heck = "0.3.2" diff --git a/macro/src/ir.rs b/macro/src/ir.rs index a0c5d64..e6ee088 100644 --- a/macro/src/ir.rs +++ b/macro/src/ir.rs @@ -1,5 +1,9 @@ //! Definition of the intermediate representation. +use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned}; + +use crate::util::{is_option, unwrap_option}; + /// The parsed input to the `gen_config` macro. pub(crate) struct Input { @@ -53,7 +57,6 @@ impl LeafKind { /// The kinds of expressions (just literals) we allow for default or example /// values. -#[derive(Debug)] pub(crate) enum Expr { Str(syn::LitStr), Int(syn::LitInt), @@ -61,3 +64,263 @@ pub(crate) enum Expr { Bool(syn::LitBool), // TODO: arrays? } + +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", + )); + } + + FieldKind::Nested { ty: field.ty } + } else { + match unwrap_option(&field.ty) { + None => FieldKind::Leaf { + env: attrs.env, + 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, + 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 Expr { + fn from_lit(lit: syn::Lit) -> Result { + 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)), + + _ => { + let msg = "only string, integer, float and bool literals are allowed here"; + 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 = syn::punctuated::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 '{}' confique attribute", keyword); + 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); + } + } + } + } + + Ok(out) +} + +#[derive(Default)] +struct InternalAttrs { + nested: bool, + default: Option, + env: Option +} + +enum InternalAttr { + Nested, + Default(Expr), + Env(String), +} + +impl InternalAttr { + fn keyword(&self) -> &'static str { + match self { + Self::Nested => "nested", + Self::Default(_) => "default", + Self::Env(_) => "env", + } + } +} + +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::from_lit(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)) + } + } + + _ => 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 bc1c79f..51eaba7 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -3,19 +3,14 @@ use proc_macro::TokenStream as TokenStream1; mod gen; mod ir; -mod parse; mod util; #[proc_macro_derive(Config, attributes(config))] pub fn config(input: TokenStream1) -> TokenStream1 { - let input = match syn::parse2::(input.into()) { - Err(e) => return e.to_compile_error().into(), - Ok(i) => i, - }; - - ir::Input::from_ast(input) + syn::parse2::(input.into()) + .and_then(ir::Input::from_ast) .map(gen::gen) - .unwrap_or_else(|e| e.write_errors()) + .unwrap_or_else(|e| e.to_compile_error()) .into() } diff --git a/macro/src/parse.rs b/macro/src/parse.rs deleted file mode 100644 index b85906e..0000000 --- a/macro/src/parse.rs +++ /dev/null @@ -1,187 +0,0 @@ -use darling::Error; -use syn::spanned::Spanned; - -use crate::{ir, util::{is_option, unwrap_option}}; - - -macro_rules! bail { - ($span:expr, $msg:expr $(,)?) => { - return Err(Error::custom($msg).with_span(&$span)) - }; -} - -impl ir::Input { - pub(crate) fn from_ast(mut input: syn::DeriveInput) -> Result { - let struct_fields = match input.data { - syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(f), .. }) => f, - _ => bail!( - input.span(), - "`confique::Config` can only be derive for structs with named fields", - ), - }; - - let doc = extract_doc(&mut input.attrs); - - let mut errors = Vec::new(); - let mut fields = Vec::new(); - for field in struct_fields.named { - match ir::Field::from_ast(field) { - Ok(f) => fields.push(f), - Err(e) => errors.push(e), - } - } - - if !errors.is_empty() { - return Err(Error::multiple(errors)); - } - - Ok(Self { - doc, - visibility: input.vis, - name: input.ident, - fields, - }) - } -} - -impl ir::Field { - fn from_ast(field: syn::Field) -> Result { - use darling::FromField; - let mut field = Field::from_field(&field)?; - let doc = extract_doc(&mut field.attrs); - - let kind = if field.nested { - // Nested field. - - if is_option(&field.ty) { - bail!( - field.ident.span(), - "nested configurations cannot be optional (type `Option<_>`)", - ); - } - if field.default.is_some() { - bail!( - field.ident.span(), - "cannot specify `nested` and `default` attributes at the same time", - ); - } - if field.env.is_some() { - bail!( - field.ident.span(), - "cannot specify `nested` and `env` attributes at the same time", - ); - } - - ir::FieldKind::Nested { ty: field.ty } - } else { - // Leaf field. - - match unwrap_option(&field.ty) { - None => ir::FieldKind::Leaf { - env: field.env, - kind: ir::LeafKind::Required { - default: field.default, - ty: field.ty, - }, - }, - Some(inner) => { - if field.default.is_some() { - bail!( - field.ident.span(), - "optional fields (type `Option<_>`) cannot have default \ - values (`#[config(default = ...)]`)", - ); - } - - ir::FieldKind::Leaf { - env: field.env, - kind: ir::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, ir::FieldKind::Leaf { .. }) - } -} - -impl darling::FromMeta for ir::Expr { - fn from_value(lit: &syn::Lit) -> Result { - match lit { - syn::Lit::Str(l) => Ok(Self::Str(l.clone())), - syn::Lit::Int(l) => Ok(Self::Int(l.clone())), - syn::Lit::Float(l) => Ok(Self::Float(l.clone())), - syn::Lit::Bool(l) => Ok(Self::Bool(l.clone())), - - _ => { - // let msg = "only string, integer, float and bool literals are allowed here"; - // Err(Error::new(lit.span(), msg)) - Err(darling::Error::unexpected_lit_type(lit)) - } - } - } -} - - - -#[derive(Debug, darling::FromField)] -#[darling(attributes(config), forward_attrs(doc))] -struct Field { - ident: Option, - ty: syn::Type, - attrs: Vec, - - #[darling(default)] - nested: bool, - - #[darling(default)] - env: Option, - - #[darling(default)] - default: Option, -} - -/// 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 -}