mirror of
https://github.com/OMGeeky/confique.git
synced 2026-01-01 17:20:24 +01:00
Disallow Option<_> type for fields with #[nested] or #[default]
Regarding nested fields: I cannot imagine a situation where that distinction is useful. Also, specifying an empty nested object looks stupid in TOML and YAML anyway. Regarding default fields: If there is a default value, then the field should not be declared as optional to begin with.
This commit is contained in:
144
macro/src/gen.rs
144
macro/src/gen.rs
@@ -23,18 +23,23 @@ fn gen_config_impl(input: &ir::Input) -> TokenStream {
|
||||
let from_exprs = input.fields.iter().map(|f| {
|
||||
let field_name = &f.name;
|
||||
let path = field_name.to_string();
|
||||
if !f.is_leaf() {
|
||||
quote! {
|
||||
confique::Config::from_partial(partial.#field_name).map_err(|e| {
|
||||
confique::internal::prepend_missing_value_error(e, #path)
|
||||
})?
|
||||
match f.kind {
|
||||
FieldKind::Nested { .. } => {
|
||||
quote! {
|
||||
confique::Config::from_partial(partial.#field_name).map_err(|e| {
|
||||
confique::internal::prepend_missing_value_error(e, #path)
|
||||
})?
|
||||
}
|
||||
}
|
||||
} else if !is_option(&f.ty) {
|
||||
quote! {
|
||||
partial.#field_name.ok_or(confique::internal::missing_value_error(#path.into()))?
|
||||
FieldKind::OptionalLeaf { .. } => {
|
||||
quote! { partial.#field_name }
|
||||
}
|
||||
FieldKind::RequiredLeaf { .. } => {
|
||||
quote! {
|
||||
partial.#field_name
|
||||
.ok_or(confique::internal::missing_value_error(#path.into()))?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! { partial.#field_name }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,23 +74,22 @@ fn partial_names(original_name: &Ident) -> (Ident, Ident) {
|
||||
fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||
let (mod_name, struct_name) = partial_names(&input.name);
|
||||
let visibility = &input.visibility;
|
||||
let inner_visibility = inner_visibility(&input.visibility);
|
||||
let inner_vis = inner_visibility(&input.visibility);
|
||||
|
||||
// Prepare some tokens per field.
|
||||
let field_names = input.fields.iter().map(|f| &f.name).collect::<Vec<_>>();
|
||||
let struct_fields = input.fields.iter().map(|f| {
|
||||
let name = &f.name;
|
||||
if f.is_leaf() {
|
||||
let inner = unwrap_option(&f.ty).unwrap_or(&f.ty);
|
||||
quote! { #inner_visibility #name: Option<#inner>, }
|
||||
} else {
|
||||
let ty = &f.ty;
|
||||
quote! {
|
||||
match &f.kind {
|
||||
FieldKind::RequiredLeaf { ty, .. } => quote! { #inner_vis #name: Option<#ty> },
|
||||
FieldKind::OptionalLeaf { inner_ty } => quote! { #inner_vis #name: Option<#inner_ty> },
|
||||
FieldKind::Nested { ty } => quote! {
|
||||
#[serde(default = "confique::Partial::empty")]
|
||||
#inner_visibility #name: <#ty as confique::Config>::Partial,
|
||||
}
|
||||
#inner_vis #name: <#ty as confique::Config>::Partial
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let empty_values = input.fields.iter().map(|f| {
|
||||
if f.is_leaf() {
|
||||
quote! { None }
|
||||
@@ -93,10 +97,13 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||
quote! { confique::Partial::empty() }
|
||||
}
|
||||
});
|
||||
|
||||
let defaults = input.fields.iter().map(|f| {
|
||||
match &f.kind {
|
||||
FieldKind::Leaf { default: None } => quote! { None },
|
||||
FieldKind::Leaf { default: Some(default) } => {
|
||||
FieldKind::RequiredLeaf { default: None, .. } | FieldKind::OptionalLeaf { .. } => {
|
||||
quote! { None }
|
||||
}
|
||||
FieldKind::RequiredLeaf { default: Some(default), .. } => {
|
||||
let msg = format!(
|
||||
"default config value for `{}::{}` cannot be deserialized",
|
||||
input.name,
|
||||
@@ -107,15 +114,10 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||
Some(confique::internal::deserialize_default(#default).expect(#msg))
|
||||
}
|
||||
}
|
||||
FieldKind::Nested => {
|
||||
if is_option(&f.ty) {
|
||||
quote! { Some(confique::Partial::default_values()) }
|
||||
} else {
|
||||
quote! { confique::Partial::default_values() }
|
||||
}
|
||||
}
|
||||
FieldKind::Nested { .. } => quote! { confique::Partial::default_values() },
|
||||
}
|
||||
});
|
||||
|
||||
let fallbacks= input.fields.iter().map(|f| {
|
||||
let name = &f.name;
|
||||
if f.is_leaf() {
|
||||
@@ -124,24 +126,22 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||
quote! { self.#name.with_fallback(fallback.#name) }
|
||||
}
|
||||
});
|
||||
|
||||
let is_empty_exprs = input.fields.iter().map(|f| {
|
||||
let name = &f.name;
|
||||
match f.kind {
|
||||
FieldKind::Leaf { .. } => quote! { self.#name.is_none() },
|
||||
FieldKind::Nested => quote! { self.#name.is_empty() },
|
||||
if f.is_leaf() {
|
||||
quote! { self.#name.is_none() }
|
||||
} else {
|
||||
quote! { self.#name.is_empty() }
|
||||
}
|
||||
});
|
||||
|
||||
let is_complete_expr = input.fields.iter().map(|f| {
|
||||
let name = &f.name;
|
||||
match f.kind {
|
||||
FieldKind::Leaf { .. } => {
|
||||
if is_option(&f.ty) {
|
||||
quote! { true }
|
||||
} else {
|
||||
quote! { self.#name.is_some() }
|
||||
}
|
||||
}
|
||||
FieldKind::Nested => quote! { self.#name.is_complete() },
|
||||
FieldKind::OptionalLeaf { .. } => quote! { true },
|
||||
FieldKind::RequiredLeaf { .. } => quote! { self.#name.is_some() },
|
||||
FieldKind::Nested { .. } => quote! { self.#name.is_complete() },
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,8 +150,8 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||
use super::*;
|
||||
|
||||
#[derive(confique::serde::Deserialize)]
|
||||
#inner_visibility struct #struct_name {
|
||||
#( #struct_fields )*
|
||||
#inner_vis struct #struct_name {
|
||||
#( #struct_fields, )*
|
||||
}
|
||||
|
||||
impl confique::Partial for #struct_name {
|
||||
@@ -194,26 +194,28 @@ fn gen_meta(input: &ir::Input) -> TokenStream {
|
||||
let name = f.name.to_string();
|
||||
let doc = &f.doc;
|
||||
let kind = match &f.kind {
|
||||
FieldKind::Nested => {
|
||||
let ty = &f.ty;
|
||||
FieldKind::Nested { ty }=> {
|
||||
quote! {
|
||||
confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META }
|
||||
}
|
||||
}
|
||||
FieldKind::Leaf { default } => {
|
||||
let default_value = gen_meta_default(default, &f.ty);
|
||||
FieldKind::OptionalLeaf { .. } => {
|
||||
quote! {
|
||||
confique::meta::FieldKind::Leaf { default: #default_value }
|
||||
confique::meta::FieldKind::OptionalLeaf
|
||||
}
|
||||
}
|
||||
FieldKind::RequiredLeaf { default, ty } => {
|
||||
let default_value = gen_meta_default(default, &ty);
|
||||
quote! {
|
||||
confique::meta::FieldKind::RequiredLeaf { default: #default_value }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let is_optional = is_option(&f.ty);
|
||||
quote! {
|
||||
confique::meta::Field {
|
||||
name: #name,
|
||||
doc: &[ #(#doc),* ],
|
||||
optional: #is_optional,
|
||||
kind: #kind,
|
||||
}
|
||||
}
|
||||
@@ -301,52 +303,6 @@ fn gen_meta_default(default: &Option<Expr>, ty: &syn::Type) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the given type is an `Option` and if so, return the inner type.
|
||||
///
|
||||
/// Note: this function clearly shows one of the major shortcomings of proc
|
||||
/// macros right now: we do not have access to the compiler's type tables and
|
||||
/// can only check if it "looks" like an `Option`. Of course, stuff can go
|
||||
/// wrong. But that's the best we can do and it's highly unlikely that someone
|
||||
/// shadows `Option`.
|
||||
fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
let ty = match ty {
|
||||
syn::Type::Path(path) => path,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if ty.qself.is_some() || ty.path.leading_colon.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let valid_paths = [
|
||||
&["Option"] as &[_],
|
||||
&["std", "option", "Option"],
|
||||
&["core", "option", "Option"],
|
||||
];
|
||||
if !valid_paths.iter().any(|vp| ty.path.segments.iter().map(|s| &s.ident).eq(*vp)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let args = match &ty.path.segments.last().unwrap().arguments {
|
||||
syn::PathArguments::AngleBracketed(args) => args,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if args.args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match &args.args[0] {
|
||||
syn::GenericArgument::Type(t) => Some(t),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given type is `Option<_>`.
|
||||
fn is_option(ty: &syn::Type) -> bool {
|
||||
unwrap_option(ty).is_some()
|
||||
}
|
||||
|
||||
impl ToTokens for ir::Expr {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned};
|
||||
|
||||
use crate::util::{is_option, unwrap_option};
|
||||
|
||||
|
||||
/// The parsed input to the `gen_config` macro.
|
||||
#[derive(Debug)]
|
||||
@@ -16,7 +18,6 @@ pub(crate) struct Input {
|
||||
pub(crate) struct Field {
|
||||
pub(crate) doc: Vec<String>,
|
||||
pub(crate) name: syn::Ident,
|
||||
pub(crate) ty: syn::Type,
|
||||
pub(crate) kind: FieldKind,
|
||||
|
||||
// TODO:
|
||||
@@ -27,10 +28,21 @@ pub(crate) struct Field {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum FieldKind {
|
||||
Leaf {
|
||||
/// A non-optional leaf. `ty` is not `Option<_>`.
|
||||
RequiredLeaf {
|
||||
default: Option<Expr>,
|
||||
ty: syn::Type,
|
||||
},
|
||||
|
||||
/// A leaf with type `Option<_>`.
|
||||
OptionalLeaf {
|
||||
inner_ty: syn::Type,
|
||||
},
|
||||
|
||||
/// A nested configuration. The type is never `Option<_>`.
|
||||
Nested {
|
||||
ty: syn::Type,
|
||||
},
|
||||
Nested,
|
||||
}
|
||||
|
||||
/// The kinds of expressions (just literals) we allow for default or example
|
||||
@@ -76,6 +88,12 @@ impl Field {
|
||||
|
||||
// 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(),
|
||||
@@ -83,23 +101,33 @@ impl Field {
|
||||
));
|
||||
}
|
||||
|
||||
FieldKind::Nested
|
||||
FieldKind::Nested { ty: field.ty }
|
||||
} else {
|
||||
FieldKind::Leaf {
|
||||
default: attrs.default,
|
||||
match unwrap_option(&field.ty) {
|
||||
None => FieldKind::RequiredLeaf { 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::OptionalLeaf { inner_ty: inner.clone() }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
doc,
|
||||
name: field.ident.expect("bug: expected named field"),
|
||||
ty: field.ty,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_leaf(&self) -> bool {
|
||||
matches!(self.kind, FieldKind::Leaf { .. })
|
||||
matches!(self.kind, FieldKind::RequiredLeaf { .. } | FieldKind::OptionalLeaf { .. })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use proc_macro::TokenStream as TokenStream1;
|
||||
|
||||
mod gen;
|
||||
mod ir;
|
||||
mod util;
|
||||
|
||||
|
||||
#[proc_macro_derive(Config, attributes(config))]
|
||||
|
||||
46
macro/src/util.rs
Normal file
46
macro/src/util.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
/// Checks if the given type is an `Option` and if so, return the inner type.
|
||||
///
|
||||
/// Note: this function clearly shows one of the major shortcomings of proc
|
||||
/// macros right now: we do not have access to the compiler's type tables and
|
||||
/// can only check if it "looks" like an `Option`. Of course, stuff can go
|
||||
/// wrong. But that's the best we can do and it's highly unlikely that someone
|
||||
/// shadows `Option`.
|
||||
pub(crate) fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
let ty = match ty {
|
||||
syn::Type::Path(path) => path,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if ty.qself.is_some() || ty.path.leading_colon.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let valid_paths = [
|
||||
&["Option"] as &[_],
|
||||
&["std", "option", "Option"],
|
||||
&["core", "option", "Option"],
|
||||
];
|
||||
if !valid_paths.iter().any(|vp| ty.path.segments.iter().map(|s| &s.ident).eq(*vp)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let args = match &ty.path.segments.last().unwrap().arguments {
|
||||
syn::PathArguments::AngleBracketed(args) => args,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if args.args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match &args.args[0] {
|
||||
syn::GenericArgument::Type(t) => Some(t),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given type is `Option<_>`.
|
||||
pub(crate) fn is_option(ty: &syn::Type) -> bool {
|
||||
unwrap_option(ty).is_some()
|
||||
}
|
||||
@@ -22,15 +22,15 @@ pub struct Meta {
|
||||
pub struct Field {
|
||||
pub name: &'static str,
|
||||
pub doc: &'static [&'static str],
|
||||
pub optional: bool,
|
||||
pub kind: FieldKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum FieldKind {
|
||||
Leaf {
|
||||
RequiredLeaf {
|
||||
default: Option<Expr>,
|
||||
},
|
||||
OptionalLeaf,
|
||||
Nested {
|
||||
meta: &'static Meta,
|
||||
},
|
||||
|
||||
25
src/toml.rs
25
src/toml.rs
@@ -141,24 +141,19 @@ fn format_impl(
|
||||
}
|
||||
|
||||
match &field.kind {
|
||||
FieldKind::Leaf { default } => {
|
||||
FieldKind::RequiredLeaf { default } => {
|
||||
// Emit comment about default value or the value being required
|
||||
if options.comments {
|
||||
match default {
|
||||
Some(v) => {
|
||||
if !field.doc.is_empty() {
|
||||
emit!("#");
|
||||
}
|
||||
emit!("# Default value: {}", PrintExpr(v));
|
||||
if let Some(v) = default {
|
||||
if !field.doc.is_empty() {
|
||||
emit!("#");
|
||||
}
|
||||
None => {
|
||||
if !field.optional {
|
||||
if !field.doc.is_empty() {
|
||||
emit!("#");
|
||||
}
|
||||
emit!("# Required! This value must be specified.");
|
||||
}
|
||||
emit!("# Default value: {}", PrintExpr(v));
|
||||
} else {
|
||||
if !field.doc.is_empty() {
|
||||
emit!("#");
|
||||
}
|
||||
emit!("# Required! This value must be specified.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +164,8 @@ fn format_impl(
|
||||
}
|
||||
}
|
||||
|
||||
FieldKind::OptionalLeaf => emit!("#{} =", field.name),
|
||||
|
||||
FieldKind::Nested { meta } => {
|
||||
let child_path = path.iter().copied().chain([field.name]).collect();
|
||||
format_impl(s, meta, child_path, options);
|
||||
|
||||
Reference in New Issue
Block a user