mirror of
https://github.com/OMGeeky/confique.git
synced 2025-12-29 15:56:00 +01:00
Add handling of nested configuration types via #[config(child)]
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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, )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user