Add handling of nested configuration types via #[config(child)]

This commit is contained in:
Lukas Kalbertodt
2021-05-14 22:13:31 +02:00
parent 3fd3835fdf
commit 8abc07bcda
4 changed files with 131 additions and 30 deletions

View File

@@ -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<String>,
}
fn main() {
println!("{:?}", Http::from_partial(<Http as Config>::Partial::default_values()));
println!("{:#?}", Conf::from_partial(<Conf as Config>::Partial::default_values()));
}

View File

@@ -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::<Vec<_>>();
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, )*
}
}
}

View File

@@ -17,7 +17,7 @@ pub(crate) struct Field {
pub(crate) doc: Vec<String>,
pub(crate) name: syn::Ident,
pub(crate) ty: syn::Type,
pub(crate) default: Option<Expr>,
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<Expr>,
},
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<Self, Error> {
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>()? {
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::<InternalAttr>()?;
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<Expr>,
}
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<Self, syn::Error> {
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")),
}
}

View File

@@ -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 {}