mirror of
https://github.com/OMGeeky/confique.git
synced 2026-02-23 15:38:30 +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 from_exprs = input.fields.iter().map(|f| {
|
||||||
let field_name = &f.name;
|
let field_name = &f.name;
|
||||||
let path = field_name.to_string();
|
let path = field_name.to_string();
|
||||||
if !f.is_leaf() {
|
match f.kind {
|
||||||
quote! {
|
FieldKind::Nested { .. } => {
|
||||||
confique::Config::from_partial(partial.#field_name).map_err(|e| {
|
quote! {
|
||||||
confique::internal::prepend_missing_value_error(e, #path)
|
confique::Config::from_partial(partial.#field_name).map_err(|e| {
|
||||||
})?
|
confique::internal::prepend_missing_value_error(e, #path)
|
||||||
|
})?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if !is_option(&f.ty) {
|
FieldKind::OptionalLeaf { .. } => {
|
||||||
quote! {
|
quote! { partial.#field_name }
|
||||||
partial.#field_name.ok_or(confique::internal::missing_value_error(#path.into()))?
|
}
|
||||||
|
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 {
|
fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||||
let (mod_name, struct_name) = partial_names(&input.name);
|
let (mod_name, struct_name) = partial_names(&input.name);
|
||||||
let visibility = &input.visibility;
|
let visibility = &input.visibility;
|
||||||
let inner_visibility = inner_visibility(&input.visibility);
|
let inner_vis = inner_visibility(&input.visibility);
|
||||||
|
|
||||||
// Prepare some tokens per field.
|
// Prepare some tokens per field.
|
||||||
let field_names = input.fields.iter().map(|f| &f.name).collect::<Vec<_>>();
|
let field_names = input.fields.iter().map(|f| &f.name).collect::<Vec<_>>();
|
||||||
let struct_fields = input.fields.iter().map(|f| {
|
let struct_fields = input.fields.iter().map(|f| {
|
||||||
let name = &f.name;
|
let name = &f.name;
|
||||||
if f.is_leaf() {
|
match &f.kind {
|
||||||
let inner = unwrap_option(&f.ty).unwrap_or(&f.ty);
|
FieldKind::RequiredLeaf { ty, .. } => quote! { #inner_vis #name: Option<#ty> },
|
||||||
quote! { #inner_visibility #name: Option<#inner>, }
|
FieldKind::OptionalLeaf { inner_ty } => quote! { #inner_vis #name: Option<#inner_ty> },
|
||||||
} else {
|
FieldKind::Nested { ty } => quote! {
|
||||||
let ty = &f.ty;
|
|
||||||
quote! {
|
|
||||||
#[serde(default = "confique::Partial::empty")]
|
#[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| {
|
let empty_values = input.fields.iter().map(|f| {
|
||||||
if f.is_leaf() {
|
if f.is_leaf() {
|
||||||
quote! { None }
|
quote! { None }
|
||||||
@@ -93,10 +97,13 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
|||||||
quote! { confique::Partial::empty() }
|
quote! { confique::Partial::empty() }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let defaults = input.fields.iter().map(|f| {
|
let defaults = input.fields.iter().map(|f| {
|
||||||
match &f.kind {
|
match &f.kind {
|
||||||
FieldKind::Leaf { default: None } => quote! { None },
|
FieldKind::RequiredLeaf { default: None, .. } | FieldKind::OptionalLeaf { .. } => {
|
||||||
FieldKind::Leaf { default: Some(default) } => {
|
quote! { None }
|
||||||
|
}
|
||||||
|
FieldKind::RequiredLeaf { default: Some(default), .. } => {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"default config value for `{}::{}` cannot be deserialized",
|
"default config value for `{}::{}` cannot be deserialized",
|
||||||
input.name,
|
input.name,
|
||||||
@@ -107,15 +114,10 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
|||||||
Some(confique::internal::deserialize_default(#default).expect(#msg))
|
Some(confique::internal::deserialize_default(#default).expect(#msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FieldKind::Nested => {
|
FieldKind::Nested { .. } => quote! { confique::Partial::default_values() },
|
||||||
if is_option(&f.ty) {
|
|
||||||
quote! { Some(confique::Partial::default_values()) }
|
|
||||||
} else {
|
|
||||||
quote! { confique::Partial::default_values() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let fallbacks= input.fields.iter().map(|f| {
|
let fallbacks= input.fields.iter().map(|f| {
|
||||||
let name = &f.name;
|
let name = &f.name;
|
||||||
if f.is_leaf() {
|
if f.is_leaf() {
|
||||||
@@ -124,24 +126,22 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
|||||||
quote! { self.#name.with_fallback(fallback.#name) }
|
quote! { self.#name.with_fallback(fallback.#name) }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_empty_exprs = input.fields.iter().map(|f| {
|
let is_empty_exprs = input.fields.iter().map(|f| {
|
||||||
let name = &f.name;
|
let name = &f.name;
|
||||||
match f.kind {
|
if f.is_leaf() {
|
||||||
FieldKind::Leaf { .. } => quote! { self.#name.is_none() },
|
quote! { self.#name.is_none() }
|
||||||
FieldKind::Nested => quote! { self.#name.is_empty() },
|
} else {
|
||||||
|
quote! { self.#name.is_empty() }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_complete_expr = input.fields.iter().map(|f| {
|
let is_complete_expr = input.fields.iter().map(|f| {
|
||||||
let name = &f.name;
|
let name = &f.name;
|
||||||
match f.kind {
|
match f.kind {
|
||||||
FieldKind::Leaf { .. } => {
|
FieldKind::OptionalLeaf { .. } => quote! { true },
|
||||||
if is_option(&f.ty) {
|
FieldKind::RequiredLeaf { .. } => quote! { self.#name.is_some() },
|
||||||
quote! { true }
|
FieldKind::Nested { .. } => quote! { self.#name.is_complete() },
|
||||||
} else {
|
|
||||||
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::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(confique::serde::Deserialize)]
|
#[derive(confique::serde::Deserialize)]
|
||||||
#inner_visibility struct #struct_name {
|
#inner_vis struct #struct_name {
|
||||||
#( #struct_fields )*
|
#( #struct_fields, )*
|
||||||
}
|
}
|
||||||
|
|
||||||
impl confique::Partial for #struct_name {
|
impl confique::Partial for #struct_name {
|
||||||
@@ -194,26 +194,28 @@ fn gen_meta(input: &ir::Input) -> TokenStream {
|
|||||||
let name = f.name.to_string();
|
let name = f.name.to_string();
|
||||||
let doc = &f.doc;
|
let doc = &f.doc;
|
||||||
let kind = match &f.kind {
|
let kind = match &f.kind {
|
||||||
FieldKind::Nested => {
|
FieldKind::Nested { ty }=> {
|
||||||
let ty = &f.ty;
|
|
||||||
quote! {
|
quote! {
|
||||||
confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META }
|
confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FieldKind::Leaf { default } => {
|
FieldKind::OptionalLeaf { .. } => {
|
||||||
let default_value = gen_meta_default(default, &f.ty);
|
|
||||||
quote! {
|
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! {
|
quote! {
|
||||||
confique::meta::Field {
|
confique::meta::Field {
|
||||||
name: #name,
|
name: #name,
|
||||||
doc: &[ #(#doc),* ],
|
doc: &[ #(#doc),* ],
|
||||||
optional: #is_optional,
|
|
||||||
kind: #kind,
|
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 {
|
impl ToTokens for ir::Expr {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned};
|
use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned};
|
||||||
|
|
||||||
|
use crate::util::{is_option, unwrap_option};
|
||||||
|
|
||||||
|
|
||||||
/// The parsed input to the `gen_config` macro.
|
/// The parsed input to the `gen_config` macro.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -16,7 +18,6 @@ pub(crate) struct Input {
|
|||||||
pub(crate) struct Field {
|
pub(crate) struct Field {
|
||||||
pub(crate) doc: Vec<String>,
|
pub(crate) doc: Vec<String>,
|
||||||
pub(crate) name: syn::Ident,
|
pub(crate) name: syn::Ident,
|
||||||
pub(crate) ty: syn::Type,
|
|
||||||
pub(crate) kind: FieldKind,
|
pub(crate) kind: FieldKind,
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
@@ -27,10 +28,21 @@ pub(crate) struct Field {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum FieldKind {
|
pub(crate) enum FieldKind {
|
||||||
Leaf {
|
/// A non-optional leaf. `ty` is not `Option<_>`.
|
||||||
|
RequiredLeaf {
|
||||||
default: Option<Expr>,
|
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
|
/// 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
|
// TODO: check no other attributes are here
|
||||||
let kind = if attrs.nested {
|
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() {
|
if attrs.default.is_some() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
field.ident.span(),
|
field.ident.span(),
|
||||||
@@ -83,23 +101,33 @@ impl Field {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
FieldKind::Nested
|
FieldKind::Nested { ty: field.ty }
|
||||||
} else {
|
} else {
|
||||||
FieldKind::Leaf {
|
match unwrap_option(&field.ty) {
|
||||||
default: attrs.default,
|
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 {
|
Ok(Self {
|
||||||
doc,
|
doc,
|
||||||
name: field.ident.expect("bug: expected named field"),
|
name: field.ident.expect("bug: expected named field"),
|
||||||
ty: field.ty,
|
|
||||||
kind,
|
kind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_leaf(&self) -> bool {
|
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 gen;
|
||||||
mod ir;
|
mod ir;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
|
||||||
#[proc_macro_derive(Config, attributes(config))]
|
#[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 struct Field {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub doc: &'static [&'static str],
|
pub doc: &'static [&'static str],
|
||||||
pub optional: bool,
|
|
||||||
pub kind: FieldKind,
|
pub kind: FieldKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum FieldKind {
|
pub enum FieldKind {
|
||||||
Leaf {
|
RequiredLeaf {
|
||||||
default: Option<Expr>,
|
default: Option<Expr>,
|
||||||
},
|
},
|
||||||
|
OptionalLeaf,
|
||||||
Nested {
|
Nested {
|
||||||
meta: &'static Meta,
|
meta: &'static Meta,
|
||||||
},
|
},
|
||||||
|
|||||||
25
src/toml.rs
25
src/toml.rs
@@ -141,24 +141,19 @@ fn format_impl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &field.kind {
|
match &field.kind {
|
||||||
FieldKind::Leaf { default } => {
|
FieldKind::RequiredLeaf { default } => {
|
||||||
// Emit comment about default value or the value being required
|
// Emit comment about default value or the value being required
|
||||||
if options.comments {
|
if options.comments {
|
||||||
match default {
|
if let Some(v) = default {
|
||||||
Some(v) => {
|
if !field.doc.is_empty() {
|
||||||
if !field.doc.is_empty() {
|
emit!("#");
|
||||||
emit!("#");
|
|
||||||
}
|
|
||||||
emit!("# Default value: {}", PrintExpr(v));
|
|
||||||
}
|
}
|
||||||
None => {
|
emit!("# Default value: {}", PrintExpr(v));
|
||||||
if !field.optional {
|
} else {
|
||||||
if !field.doc.is_empty() {
|
if !field.doc.is_empty() {
|
||||||
emit!("#");
|
emit!("#");
|
||||||
}
|
|
||||||
emit!("# Required! This value must be specified.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
emit!("# Required! This value must be specified.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +164,8 @@ fn format_impl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FieldKind::OptionalLeaf => emit!("#{} =", field.name),
|
||||||
|
|
||||||
FieldKind::Nested { meta } => {
|
FieldKind::Nested { meta } => {
|
||||||
let child_path = path.iter().copied().chain([field.name]).collect();
|
let child_path = path.iter().copied().chain([field.name]).collect();
|
||||||
format_impl(s, meta, child_path, options);
|
format_impl(s, meta, child_path, options);
|
||||||
|
|||||||
Reference in New Issue
Block a user