diff --git a/examples/simple.rs b/examples/simple.rs index dda569f..89e2d3e 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,16 @@ use std::net::IpAddr; use confique::{Config, Partial}; +#[derive(Debug, Config)] +struct Conf { + #[config(child)] + http: Http, + + #[config(child)] + cat: Cat, +} + + #[derive(Debug, Config)] struct Http { #[config(default = 8080)] @@ -8,11 +18,14 @@ struct Http { #[config(default = "127.0.0.1")] bind: IpAddr, +} +#[derive(Debug, Config)] +struct Cat { foo: Option, } fn main() { - println!("{:?}", Http::from_partial(::Partial::default_values())); + println!("{:#?}", Conf::from_partial(::Partial::default_values())); } diff --git a/macro/src/gen.rs b/macro/src/gen.rs index 7222c19..a8cdfd3 100644 --- a/macro/src/gen.rs +++ b/macro/src/gen.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; use syn::Ident; -use crate::ir; +use crate::ir::{self, FieldKind}; pub(crate) fn gen(input: ir::Input) -> TokenStream { @@ -22,14 +22,24 @@ fn gen_config_impl(input: &ir::Input) -> TokenStream { let field_names = input.fields.iter().map(|f| &f.name); let from_exprs = input.fields.iter().map(|f| { let field_name = &f.name; - match unwrap_option(&f.ty) { - Some(_) => quote! { partial.#field_name }, - None => { - let path = field_name.to_string(); - quote! { - partial.#field_name.ok_or(confique::Error::MissingValue(#path))? - } + let path = field_name.to_string(); + if !f.is_leaf() { + quote! { + confique::Config::from_partial(partial.#field_name).map_err(|e| { + match e { + confique::Error::MissingValue(path) => { + confique::Error::MissingValue(format!("{}.{}", #path, path)) + } + e => e, + } + })? } + } else if unwrap_option(&f.ty).is_none() { + quote! { + partial.#field_name.ok_or(confique::Error::MissingValue(#path.into()))? + } + } else { + quote! { partial.#field_name } } }); @@ -65,13 +75,25 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { // Prepare some tokens per field. let field_names = input.fields.iter().map(|f| &f.name).collect::>(); let field_types = input.fields.iter().map(|f| { - let inner = unwrap_option(&f.ty).unwrap_or(&f.ty); - quote! { Option<#inner> } + if f.is_leaf() { + let inner = unwrap_option(&f.ty).unwrap_or(&f.ty); + quote! { Option<#inner> } + } else { + let ty = &f.ty; + quote! { <#ty as confique::Config>::Partial } + } + }); + let empty_values = input.fields.iter().map(|f| { + if f.is_leaf() { + quote! { None } + } else { + quote! { confique::Partial::empty() } + } }); let defaults = input.fields.iter().map(|f| { - match &f.default { - None => quote! { None }, - Some(default) => { + match &f.kind { + FieldKind::Leaf { default: None } => quote! { None }, + FieldKind::Leaf { default: Some(default) } => { let msg = format!( "default config value for `{}::{}` cannot be deserialized", input.name, @@ -81,7 +103,22 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { quote! { Some(confique::internal::deserialize_default(#default).expect(#msg)) } - }, + } + FieldKind::Child => { + if unwrap_option(&f.ty).is_some() { + quote! { Some(confique::Partial::default_values()) } + } else { + quote! { confique::Partial::default_values() } + } + } + } + }); + let fallbacks= input.fields.iter().map(|f| { + let name = &f.name; + if f.is_leaf() { + quote! { self.#name.or(fallback.#name) } + } else { + quote! { self.#name.with_fallback(fallback.#name) } } }); @@ -97,7 +134,7 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { impl confique::Partial for #struct_name { fn empty() -> Self { Self { - #( #field_names: None, )* + #( #field_names: #empty_values, )* } } @@ -109,7 +146,7 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { fn with_fallback(self, fallback: Self) -> Self { Self { - #( #field_names: self.#field_names.or(fallback.#field_names), )* + #( #field_names: #fallbacks, )* } } } diff --git a/macro/src/ir.rs b/macro/src/ir.rs index a79f0da..f9f0469 100644 --- a/macro/src/ir.rs +++ b/macro/src/ir.rs @@ -17,7 +17,7 @@ pub(crate) struct Field { pub(crate) doc: Vec, pub(crate) name: syn::Ident, pub(crate) ty: syn::Type, - pub(crate) default: Option, + pub(crate) kind: FieldKind, // TODO: // - serde attributes @@ -25,6 +25,14 @@ pub(crate) struct Field { // - example } +#[derive(Debug)] +pub(crate) enum FieldKind { + Leaf { + default: Option, + }, + Child, +} + /// The kinds of expressions (just literals) we allow for default or example /// values. #[derive(Debug)] @@ -65,15 +73,34 @@ 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.child { + if attrs.default.is_some() { + return Err(Error::new( + field.ident.span(), + "cannot specify `child` and `default` attributes at the same time", + )); + } + + FieldKind::Child + } else { + FieldKind::Leaf { + default: attrs.default, + } + }; Ok(Self { doc, - default: attrs.default, name: field.ident.expect("bug: expected named field"), ty: field.ty, + kind, }) } + + pub(crate) fn is_leaf(&self) -> bool { + matches!(self.kind, FieldKind::Leaf { .. }) + } } impl Expr { @@ -139,20 +166,30 @@ fn extract_internal_attrs( } }); + let mut out = InternalAttrs::default(); for attr in internal_attrs { - match attr.parse_args::()? { - InternalAttr::Default(expr) => { - if out.default.is_some() { - let msg = format!( - "duplicate '{}' confique attribute", - attr.path.get_ident().unwrap() - ); - return Err(Error::new(attr.span(), msg)); - } + let parsed = attr.parse_args::()?; + 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::Child => { + duplicate_if!(out.child); + out.child = true; + } } } @@ -161,24 +198,38 @@ fn extract_internal_attrs( #[derive(Default)] struct InternalAttrs { + child: bool, default: Option, } enum InternalAttr { + Child, Default(Expr), } +impl InternalAttr { + fn keyword(&self) -> &'static str { + match self { + Self::Child => "child", + Self::Default(_) => "default", + } + } +} + impl Parse for InternalAttr { fn parse(input: ParseStream) -> Result { let ident: syn::Ident = input.parse()?; match &*ident.to_string() { + "child" => { + assert_empty(input)?; + Ok(Self::Child) + } "default" => { let _: Token![=] = input.parse()?; let expr = Expr::from_lit(input.parse()?)?; assert_empty(input)?; Ok(Self::Default(expr)) } - _ => Err(syn::Error::new(ident.span(), "unknown confique attribute")), } } diff --git a/src/lib.rs b/src/lib.rs index 4020536..eab0ce2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ pub enum Error { /// Returned by `Config::from_partial` when the partial does not contain /// values for all required configuration values. The string is a /// human-readable path to the value, e.g. `http.port`. - MissingValue(&'static str), + MissingValue(String), } impl std::error::Error for Error {}