Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
f3d102b1eb | |||
abb2170186 | |||
3f9599e01b | |||
82256007e8 | |||
a5bf501ebd | |||
9d3289c3d3 | |||
c5d46270b4 | |||
700e98917d | |||
903a3c7589 | |||
0e59545f92 | |||
d4fdc2d642 | |||
3d95c32d93 | |||
ea6c3a34b8 | |||
0e63dc3c22 |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "macroconf"
|
||||
version = "0.3.0"
|
||||
version = "0.3.6"
|
||||
edition = "2021"
|
||||
description = "macro for creating configurations using miniconf"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -33,3 +33,5 @@ darling = "0.20"
|
||||
[dev-dependencies]
|
||||
miniconf = { version = "0.13", features = ["json-core"] }
|
||||
serde = "1.0"
|
||||
rstest = "0.22"
|
||||
serde-json-core = "0.5.1"
|
||||
|
29
src/lib.rs
29
src/lib.rs
@ -2,12 +2,41 @@
|
||||
//! extra information about the field.
|
||||
|
||||
use darling::{util::PathList, FromMeta};
|
||||
use parser::Enum;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, parse_quote, DataStruct, DeriveInput};
|
||||
|
||||
mod parser;
|
||||
|
||||
/// Implements `miniconf::Tree` for the given enum.
|
||||
///
|
||||
/// This implementation of `miniconf::Tree` adds a value and a variants path.
|
||||
/// The value path serializes the enum.
|
||||
/// The variants path serializes all possible variants as an array.
|
||||
/// Optionally default and description paths can be generated.
|
||||
/// The description is generated from the docstring describing the enum.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use macroconf::ConfigEnum;
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
///
|
||||
/// /// Description
|
||||
/// #[derive(Default, ConfigEnum, Serialize, Deserialize)]
|
||||
/// enum Test {
|
||||
/// #[default]
|
||||
/// Variant1,
|
||||
/// Variant2,
|
||||
/// Variant3,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(ConfigEnum, attributes(default))]
|
||||
pub fn config_enum(item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as Enum);
|
||||
input.generate_tree().into()
|
||||
}
|
||||
|
||||
/// Creates structs for the values to extend them with extra metadata.
|
||||
///
|
||||
/// supported metadata is `min`, `max` and `default`. Doc comments are parsed as `description`
|
||||
|
354
src/parser.rs
354
src/parser.rs
@ -1,14 +1,16 @@
|
||||
#![allow(clippy::option_if_let_else)]
|
||||
|
||||
use std::convert::identity;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use darling::{
|
||||
ast::{self, Data},
|
||||
util::{Override, PathList},
|
||||
util::{Flag, Override, PathList},
|
||||
Error, FromDeriveInput, FromField, FromMeta,
|
||||
};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::parse_quote;
|
||||
use syn::{braced, parse::Parse, parse_quote, spanned::Spanned, Token};
|
||||
|
||||
#[derive(Debug, FromField)]
|
||||
#[darling(attributes(config))]
|
||||
@ -21,6 +23,10 @@ pub struct ConfigField {
|
||||
min: Option<Override<syn::Expr>>,
|
||||
max: Option<Override<syn::Expr>>,
|
||||
default: Option<Override<syn::Expr>>,
|
||||
typ: Option<syn::Type>,
|
||||
get: Option<syn::Path>,
|
||||
set: Option<syn::Path>,
|
||||
no_serde: Flag,
|
||||
#[darling(skip)]
|
||||
description: Option<syn::Expr>,
|
||||
#[darling(skip)]
|
||||
@ -29,11 +35,12 @@ pub struct ConfigField {
|
||||
|
||||
impl ConfigField {
|
||||
pub(crate) fn needs_newtype(&self) -> bool {
|
||||
self.helper_keys().len() > 1
|
||||
self.helper_keys().len() > 1 || self.typ.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn helper(&self, attrs: &[syn::Attribute]) -> (Option<TokenStream>, syn::Type) {
|
||||
if self.needs_newtype() {
|
||||
let default = self.helper_default();
|
||||
let derives = attrs
|
||||
.iter()
|
||||
.find(|attr| attr.path().is_ident("derive"))
|
||||
@ -45,6 +52,7 @@ impl ConfigField {
|
||||
derive.contains("Tree")
|
||||
|| derive.contains("Serialize")
|
||||
|| derive.contains("Deserialize")
|
||||
|| (default.is_some() && derive.contains("Default"))
|
||||
}) == Some(false)
|
||||
});
|
||||
quote! {#[derive(#(#derives,)*)]}
|
||||
@ -54,7 +62,6 @@ impl ConfigField {
|
||||
let ty = &self.ty;
|
||||
|
||||
let new = self.helper_new();
|
||||
let default = self.helper_default();
|
||||
let serde = self.helper_serde();
|
||||
let tree = self.helper_tree();
|
||||
|
||||
@ -88,8 +95,9 @@ impl ConfigField {
|
||||
}
|
||||
|
||||
pub(crate) fn has_custom_limits(&self) -> bool {
|
||||
!((self.min.is_none() || self.min == Some(Override::Inherit))
|
||||
&& (self.max.is_none() || self.max == Some(Override::Inherit)))
|
||||
self.typ.is_none()
|
||||
&& !((self.min.is_none() || self.min == Some(Override::Inherit))
|
||||
&& (self.max.is_none() || self.max == Some(Override::Inherit)))
|
||||
}
|
||||
|
||||
pub(crate) fn helper_new(&self) -> TokenStream {
|
||||
@ -146,9 +154,12 @@ impl ConfigField {
|
||||
}
|
||||
|
||||
pub(crate) fn helper_default(&self) -> Option<TokenStream> {
|
||||
if self.typ.is_some() {
|
||||
return None;
|
||||
}
|
||||
self.default.as_ref().map(|default| {
|
||||
let ident = self.helper_ident();
|
||||
let default_default = parse_quote!(default::Default());
|
||||
let default_default = parse_quote!(::core::default::Default::default());
|
||||
let default = default.as_ref().unwrap_or(&default_default);
|
||||
quote! {
|
||||
impl ::core::default::Default for #ident {
|
||||
@ -160,7 +171,10 @@ impl ConfigField {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn helper_serde(&self) -> TokenStream {
|
||||
pub(crate) fn helper_serde(&self) -> Option<TokenStream> {
|
||||
if self.no_serde.is_present() {
|
||||
return None;
|
||||
}
|
||||
let ident = self.helper_ident();
|
||||
let conversion = if self.has_custom_limits() {
|
||||
quote! {
|
||||
@ -174,7 +188,7 @@ impl ConfigField {
|
||||
}
|
||||
};
|
||||
let ty = &self.ty;
|
||||
quote! {
|
||||
Some(quote! {
|
||||
impl ::serde::Serialize for #ident {
|
||||
fn serialize<S>(&self, serializer: S) -> ::core::result::Result::<S::Ok, S::Error>
|
||||
where
|
||||
@ -194,7 +208,7 @@ impl ConfigField {
|
||||
#conversion
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn helper_tree(&self) -> TokenStream {
|
||||
@ -209,7 +223,7 @@ impl ConfigField {
|
||||
macro_rules! field_to_key {
|
||||
($field: ident, $default: expr) => {
|
||||
self.$field.as_ref().map(|value| {
|
||||
let ty = &self.ty;
|
||||
let ty = self.typ.as_ref().unwrap_or(&self.ty);
|
||||
(stringify!($field),
|
||||
value.clone().explicit().map(|val| {
|
||||
parse_quote!({
|
||||
@ -288,6 +302,7 @@ impl ConfigField {
|
||||
.helper_keys()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(1)
|
||||
.map(|(i, (_, expr))| {
|
||||
quote! {
|
||||
#i => ::serde::Serialize::serialize(&#expr, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
|
||||
@ -295,6 +310,10 @@ impl ConfigField {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let ident = self.helper_ident();
|
||||
let get = self
|
||||
.get
|
||||
.as_ref()
|
||||
.map_or_else(|| quote! {self}, |getter| quote! {#getter(&self.0)});
|
||||
quote! {
|
||||
impl ::miniconf::TreeSerialize<1> for #ident {
|
||||
fn serialize_by_key<K, S>(
|
||||
@ -307,13 +326,14 @@ impl ConfigField {
|
||||
S: ::serde::Serializer,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else {
|
||||
return ::serde::Serialize::serialize(&self, ser).map_err(|err| ::miniconf::Error::Inner(1, err)).map(|_| 1);
|
||||
return ::serde::Serialize::serialize(&#get, ser).map_err(|err| ::miniconf::Error::Inner(0, err)).map(|_| 0);
|
||||
};
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(0))?;
|
||||
if !keys.finalize() {
|
||||
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(1).into());
|
||||
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(0).into());
|
||||
}
|
||||
match index {
|
||||
0 => ::serde::Serialize::serialize(&#get, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
|
||||
#(#matches)*
|
||||
_ => unreachable!(),
|
||||
}?;
|
||||
@ -326,6 +346,21 @@ impl ConfigField {
|
||||
pub(crate) fn helper_tree_deserialize(&self) -> TokenStream {
|
||||
let ident = self.helper_ident();
|
||||
let num_keys = self.helper_keys().len();
|
||||
let set = self.set.as_ref().map_or_else(
|
||||
|| quote!((|val| {*self = val; core::result::Result::<_, ::miniconf::Traversal>::Ok(())})),
|
||||
|set| quote!((|val| #set(&mut self.0, val).map_err(|e| ::miniconf::Traversal::Invalid(0, e)))),
|
||||
);
|
||||
let typ = self
|
||||
.typ
|
||||
.as_ref()
|
||||
.map_or_else(|| quote!(Self), |typ| quote!(#typ));
|
||||
let match_range = if num_keys > 1 {
|
||||
Some(
|
||||
quote!(1..=#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(0, "Cannot write limits").into()),),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
quote! {
|
||||
impl<'de> ::miniconf::TreeDeserialize<'de, 1> for #ident {
|
||||
fn deserialize_by_key<K, D>(
|
||||
@ -338,7 +373,7 @@ impl ConfigField {
|
||||
D: ::serde::Deserializer<'de>,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else {
|
||||
*self = <Self as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?;
|
||||
#set(<#typ as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?)?;
|
||||
return ::core::result::Result::Ok(0);
|
||||
};
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
@ -347,11 +382,10 @@ impl ConfigField {
|
||||
}
|
||||
match index {
|
||||
0 => {
|
||||
*self = <Self as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?;
|
||||
#set(<#typ as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?)?;
|
||||
Ok(0)
|
||||
}
|
||||
,
|
||||
1..=#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(0, "Cannot write limits").into()),
|
||||
#match_range
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@ -367,6 +401,13 @@ impl ConfigField {
|
||||
} else {
|
||||
quote!(::core::result::Result::Ok(&mut *self))
|
||||
};
|
||||
let match_range = if num_keys > 1 {
|
||||
Some(
|
||||
quote!(1..#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(1, "cannot return reference to local variable")),),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
quote! {
|
||||
impl ::miniconf::TreeAny<1> for #ident {
|
||||
fn ref_any_by_key<K>(&self, mut keys: K) -> ::core::result::Result<&dyn ::core::any::Any, ::miniconf::Traversal>
|
||||
@ -382,7 +423,7 @@ impl ConfigField {
|
||||
}
|
||||
match index {
|
||||
0 => ::core::result::Result::Ok(&*self),
|
||||
1..#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(1, "cannot return reference to local variable")),
|
||||
#match_range
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@ -400,7 +441,7 @@ impl ConfigField {
|
||||
}
|
||||
match index {
|
||||
0 => #ref_mut,
|
||||
1..#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(1, "cannot return reference to local variable")),
|
||||
#match_range
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@ -436,6 +477,26 @@ impl Config {
|
||||
parse_quote!(#description)
|
||||
});
|
||||
field.parent_ident = Some(ident.clone());
|
||||
let get_set_typ = [
|
||||
field.typ.is_some(),
|
||||
field.get.is_some(),
|
||||
field.set.is_some(),
|
||||
];
|
||||
if get_set_typ.iter().copied().any(identity) && !get_set_typ.into_iter().all(identity) {
|
||||
return Err(
|
||||
Error::custom("need all or none of 'typ', 'get', 'set'").with_span(
|
||||
&field.typ.as_ref().map_or_else(
|
||||
|| {
|
||||
field.get.as_ref().map_or_else(
|
||||
|| field.set.as_ref().unwrap().span(),
|
||||
Spanned::span,
|
||||
)
|
||||
},
|
||||
Spanned::span,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
@ -454,3 +515,256 @@ impl Config {
|
||||
&mut fields.fields
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Enum {
|
||||
doc: Option<String>,
|
||||
ident: syn::Ident,
|
||||
variants: Vec<syn::Ident>,
|
||||
default: Option<syn::Ident>,
|
||||
}
|
||||
|
||||
impl Enum {
|
||||
pub fn generate_tree(&self) -> TokenStream {
|
||||
let mut tokens = self.generate_tree_key();
|
||||
tokens.extend(self.generate_tree_serialize());
|
||||
tokens.extend(self.generate_tree_deserialize());
|
||||
tokens.extend(self.generate_tree_any());
|
||||
tokens
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<(&'static str, syn::Expr)> {
|
||||
let variants = &self.variants;
|
||||
let mut keys = vec![
|
||||
("value", parse_quote!(self)),
|
||||
("variants", parse_quote!([#(Self::#variants,)*])),
|
||||
];
|
||||
if let Some(ref doc) = self.doc {
|
||||
keys.push(("description", parse_quote!(#doc)));
|
||||
}
|
||||
if let Some(ref default) = self.default {
|
||||
keys.push(("default", parse_quote!(Self::#default)));
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn generate_tree_key(&self) -> TokenStream {
|
||||
let ident = &self.ident;
|
||||
let keys = self.keys();
|
||||
let num_keys = keys.len();
|
||||
let max_length = keys.iter().map(|(path, _)| path.len()).max();
|
||||
let keys = keys.iter().map(|(path, _)| path);
|
||||
quote! {
|
||||
impl ::miniconf::KeyLookup for #ident {
|
||||
const LEN: usize = #num_keys;
|
||||
const NAMES: &'static [&'static str] = &[#(#keys,)*];
|
||||
|
||||
fn name_to_index(value: &str) -> Option<usize> {
|
||||
Self::NAMES.iter().position(|name| *name == value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::miniconf::TreeKey<1> for #ident {
|
||||
fn metadata() -> ::miniconf::Metadata {
|
||||
let mut metadata = ::miniconf::Metadata::default();
|
||||
metadata.max_depth = 1;
|
||||
metadata.count = #num_keys;
|
||||
metadata.max_length = #max_length;
|
||||
metadata
|
||||
}
|
||||
|
||||
fn traverse_by_key<K, F, E>(mut keys: K, mut func: F) -> ::core::result::Result<usize, ::miniconf::Error<E>>
|
||||
where
|
||||
K: ::miniconf::Keys,
|
||||
// Writing this to return an iterator instead of using a callback
|
||||
// would have worse performance (O(n^2) instead of O(n) for matching)
|
||||
F: FnMut(usize, Option<&'static str>, usize) -> ::core::result::Result<(), E>,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else { return ::core::result::Result::Ok(0) };
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
let name = <Self as ::miniconf::KeyLookup>::NAMES
|
||||
.get(index)
|
||||
.ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
func(index, Some(name), #num_keys).map_err(|err| ::miniconf::Error::Inner(1, err))?;
|
||||
::core::result::Result::Ok(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_tree_serialize(&self) -> TokenStream {
|
||||
let ident = &self.ident;
|
||||
let matches = self
|
||||
.keys()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (_, expr))| {
|
||||
quote! {
|
||||
#i => ::serde::Serialize::serialize(&#expr, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
quote! {
|
||||
impl ::miniconf::TreeSerialize<1> for #ident {
|
||||
fn serialize_by_key<K, S>(
|
||||
&self,
|
||||
mut keys: K,
|
||||
ser: S,
|
||||
) -> ::core::result::Result<usize, ::miniconf::Error<S::Error>>
|
||||
where
|
||||
K: ::miniconf::Keys,
|
||||
S: ::serde::Serializer,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else {
|
||||
return ::serde::Serialize::serialize(self, ser).map_err(|err| ::miniconf::Error::Inner(0, err)).map(|_| 0);
|
||||
};
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(0))?;
|
||||
if !keys.finalize() {
|
||||
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(0).into());
|
||||
}
|
||||
match index {
|
||||
#(#matches)*
|
||||
_ => unreachable!(),
|
||||
}?;
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_tree_deserialize(&self) -> TokenStream {
|
||||
let ident = &self.ident;
|
||||
let num_keys = self.keys().len();
|
||||
quote! {
|
||||
impl<'de> ::miniconf::TreeDeserialize<'de, 1> for #ident {
|
||||
fn deserialize_by_key<K, D>(
|
||||
&mut self,
|
||||
mut keys: K,
|
||||
de: D,
|
||||
) -> ::core::result::Result<usize, ::miniconf::Error<D::Error>>
|
||||
where
|
||||
K: ::miniconf::Keys,
|
||||
D: ::serde::Deserializer<'de>,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else {
|
||||
*self = <Self as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?;
|
||||
return ::core::result::Result::Ok(0);
|
||||
};
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
if !keys.finalize() {
|
||||
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(1).into());
|
||||
}
|
||||
match index {
|
||||
0 => {
|
||||
*self = <Self as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?;
|
||||
Ok(0)
|
||||
}
|
||||
1..=#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(0, "Cannot write limits").into()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_tree_any(&self) -> TokenStream {
|
||||
let ident = &self.ident;
|
||||
let num_keys = self.keys().len();
|
||||
quote! {
|
||||
impl ::miniconf::TreeAny<1> for #ident {
|
||||
fn ref_any_by_key<K>(&self, mut keys: K) -> ::core::result::Result<&dyn ::core::any::Any, ::miniconf::Traversal>
|
||||
where
|
||||
K: ::miniconf::Keys,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else {
|
||||
return ::core::result::Result::Ok(self);
|
||||
};
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
if !keys.finalize() {
|
||||
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(1));
|
||||
}
|
||||
match index {
|
||||
0 => ::core::result::Result::Ok(self),
|
||||
1..#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(1, "cannot return reference to local variable")),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn mut_any_by_key<K>(&mut self, mut keys: K) -> ::core::result::Result<&mut dyn ::core::any::Any, ::miniconf::Traversal>
|
||||
where
|
||||
K: ::miniconf::Keys,
|
||||
{
|
||||
let ::core::result::Result::Ok(key) = keys.next::<Self>() else {
|
||||
return Ok(self);
|
||||
};
|
||||
let index = ::miniconf::Key::find::<Self>(&key).ok_or(::miniconf::Traversal::NotFound(1))?;
|
||||
if !keys.finalize() {
|
||||
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(1));
|
||||
}
|
||||
match index {
|
||||
0 => Ok(self),
|
||||
1..#num_keys => ::core::result::Result::Err(::miniconf::Traversal::Access(1, "cannot return reference to local variable")),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Enum {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let attrs = input.call(syn::Attribute::parse_outer)?;
|
||||
let doc = attrs
|
||||
.iter()
|
||||
.find(|attr| attr.path().is_ident("doc"))
|
||||
.map(|attr| {
|
||||
let meta = attr.meta.require_name_value()?;
|
||||
if let syn::Expr::Lit(syn::ExprLit {
|
||||
attrs: _,
|
||||
lit: syn::Lit::Str(ref lit),
|
||||
}) = meta.value
|
||||
{
|
||||
Ok(lit.value().trim().to_owned())
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(
|
||||
&meta.value,
|
||||
"Expected string literal for doc comment",
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let _vis = input.parse::<syn::Visibility>()?;
|
||||
let _enum_token = input.parse::<Token![enum]>()?;
|
||||
|
||||
let ident = input.parse::<syn::Ident>()?;
|
||||
|
||||
let content;
|
||||
let _brace = braced!(content in input);
|
||||
let variants = content.parse_terminated(syn::Variant::parse, Token![,])?;
|
||||
if let Some(variant) = variants.iter().find(|variant| !variant.fields.is_empty()) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&variant.fields,
|
||||
"only unit variants are supported for now",
|
||||
));
|
||||
}
|
||||
let default = variants
|
||||
.iter()
|
||||
.find(|variant| {
|
||||
variant
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("default"))
|
||||
})
|
||||
.map(|variant| variant.ident.clone());
|
||||
let variants = variants.into_iter().map(|variant| variant.ident).collect();
|
||||
|
||||
Ok(Self {
|
||||
doc,
|
||||
ident,
|
||||
variants,
|
||||
default,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
62
tests/enum.rs
Normal file
62
tests/enum.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use macroconf::ConfigEnum;
|
||||
use miniconf::{Error::Traversal, JsonCoreSlash, JsonPath, Traversal::Access, TreeKey};
|
||||
use rstest::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::from_utf8;
|
||||
|
||||
/// Description
|
||||
#[derive(Debug, Default, PartialEq, Eq, ConfigEnum, Serialize, Deserialize)]
|
||||
enum Test {
|
||||
#[default]
|
||||
Variant1,
|
||||
Variant2,
|
||||
Variant3,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nodes() {
|
||||
let nodes = Test::nodes::<JsonPath<String>>()
|
||||
.map(|p| p.unwrap().0.into_inner())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
nodes,
|
||||
vec![".value", ".variants", ".description", ".default"]
|
||||
);
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
#[once]
|
||||
fn test() -> Test {
|
||||
Test::Variant2
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("/", "\"Variant2\"")]
|
||||
#[case("/value", "\"Variant2\"")]
|
||||
#[case("/description", "\"Description\"")]
|
||||
#[case("/default", "\"Variant1\"")]
|
||||
#[case("/variants", "[\"Variant1\",\"Variant2\",\"Variant3\"]")]
|
||||
fn serialize(test: &Test, #[case] path: &str, #[case] expected: &str) {
|
||||
let mut buffer = [0u8; 64];
|
||||
let len = test.get_json(path, &mut buffer).unwrap();
|
||||
assert_eq!(from_utf8(&buffer[..len]), Ok(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("/", Ok(10))]
|
||||
#[case("/value", Ok(10))]
|
||||
#[case("/variants", Err(Traversal(Access(0, "Cannot write limits"))))]
|
||||
#[case("/default", Err(Traversal(Access(0, "Cannot write limits"))))]
|
||||
#[case("/description", Err(Traversal(Access(0, "Cannot write limits"))))]
|
||||
fn deserialize(
|
||||
#[case] path: &str,
|
||||
#[case] expected: Result<usize, miniconf::Error<serde_json_core::de::Error>>,
|
||||
) {
|
||||
let mut config = Test::Variant2;
|
||||
|
||||
let res = config.set_json(path, b"\"Variant3\"");
|
||||
assert_eq!(res, expected);
|
||||
if res.is_ok() {
|
||||
assert_eq!(config, Test::Variant3);
|
||||
}
|
||||
}
|
65
tests/get_set.rs
Normal file
65
tests/get_set.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::{cell::Cell, str::from_utf8};
|
||||
|
||||
use macroconf::config;
|
||||
use miniconf::JsonCoreSlash;
|
||||
|
||||
fn set_cell<const MIN: i32, const MAX: i32>(
|
||||
cell: &Cell<i32>,
|
||||
val: i32,
|
||||
) -> Result<(), &'static str> {
|
||||
if (MIN..=MAX).contains(&val) {
|
||||
cell.set(val);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("value out of bounds")
|
||||
}
|
||||
}
|
||||
|
||||
#[config]
|
||||
struct Config {
|
||||
#[config(
|
||||
min = "-128",
|
||||
max = "128",
|
||||
default,
|
||||
typ = "i32",
|
||||
get = "Cell::get",
|
||||
set = "set_cell::<-128,128>"
|
||||
)]
|
||||
field: Cell<i32>,
|
||||
}
|
||||
|
||||
#[config]
|
||||
struct WithSetterAndDefault {
|
||||
#[config(
|
||||
default = "42",
|
||||
typ = "i32",
|
||||
get = "Cell::get",
|
||||
set = "set_cell::<{i32::MIN}, {i32::MAX}>"
|
||||
)]
|
||||
field: Cell<i32>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get() {
|
||||
let mut buffer = [0u8; 32];
|
||||
let config = Config {
|
||||
field: __ConfigField::new(Cell::new(42)),
|
||||
};
|
||||
let len = config.get_json("/field", &mut buffer).unwrap();
|
||||
assert_eq!(from_utf8(&buffer[..len]), Ok("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set() {
|
||||
let mut config = Config {
|
||||
field: __ConfigField::new(Cell::new(42)),
|
||||
};
|
||||
config.set_json("/field", b"-32").unwrap();
|
||||
assert_eq!(config.field.get(), -32);
|
||||
config
|
||||
.set_json("/field", b"256")
|
||||
.expect_err("result not checked");
|
||||
config
|
||||
.set_json("/field", b"-256")
|
||||
.expect_err("result not checked");
|
||||
}
|
155
tests/simple.rs
155
tests/simple.rs
@ -7,6 +7,7 @@ use miniconf::{
|
||||
Traversal::{Access, TooLong},
|
||||
Tree, TreeKey,
|
||||
};
|
||||
use rstest::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[config]
|
||||
@ -30,27 +31,47 @@ struct Config {
|
||||
sub_config: SubConfig,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys() {
|
||||
for (id, field) in ["skipped", "min", "max", "default", "description"]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(
|
||||
SubConfig::traverse_by_key([field].into_keys(), |index, name, _len| {
|
||||
assert_eq!((id, Some(field)), (index, name));
|
||||
Ok::<_, ()>(())
|
||||
}),
|
||||
Ok(1)
|
||||
);
|
||||
}
|
||||
/// Config with default derive and default field
|
||||
#[config]
|
||||
#[derive(Default)]
|
||||
struct _DefaultConfig {
|
||||
#[config(default)]
|
||||
field: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_keys() {
|
||||
#[rstest]
|
||||
#[case(0, ["skipped"])]
|
||||
#[case(1, ["min"])]
|
||||
#[case(2, ["max"])]
|
||||
#[case(3, ["default"])]
|
||||
#[case(4, ["description"])]
|
||||
fn key<const N: usize>(#[case] id: usize, #[case] field: [&str; N]) {
|
||||
assert_eq!(
|
||||
SubConfig::traverse_by_key(["skipped", "value"].into_keys(), |_, _, _| Ok::<_, ()>(())),
|
||||
Err(Traversal(TooLong(1)))
|
||||
SubConfig::traverse_by_key(field.into_keys(), |index, name, _len| {
|
||||
assert_eq!((id, Some(field[field.len() - 1])), (index, name));
|
||||
Ok::<_, ()>(())
|
||||
}),
|
||||
Ok(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(["skipped", "value"], Err(Traversal(TooLong(1))))]
|
||||
#[case(["min", "value"], Ok(2))]
|
||||
#[case(["max", "value"], Ok(2))]
|
||||
#[case(["default", "value"], Ok(2))]
|
||||
#[case(["description", "value"], Ok(2))]
|
||||
#[case(["min", "min"], Ok(2))]
|
||||
#[case(["max", "max"], Ok(2))]
|
||||
#[case(["default", "default"], Ok(2))]
|
||||
#[case(["description", "description"], Ok(2))]
|
||||
fn sub_keys<const N: usize>(
|
||||
#[case] fields: [&str; N],
|
||||
#[case] expected: Result<usize, miniconf::Error<()>>,
|
||||
) {
|
||||
assert_eq!(
|
||||
SubConfig::traverse_by_key(fields.into_keys(), |_, _, _| Ok::<_, ()>(())),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
SubConfig::traverse_by_key(["skipped"].into_keys(), |_, _, _| Ok::<_, ()>(())),
|
||||
@ -68,39 +89,59 @@ fn sub_keys() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let mut buffer = [0u8; 32];
|
||||
let config = SubConfig {
|
||||
#[fixture]
|
||||
#[once]
|
||||
fn sub_config() -> SubConfig {
|
||||
SubConfig {
|
||||
skipped: 1,
|
||||
min: __SubConfigMin::new(2),
|
||||
max: __SubConfigMax::new(3),
|
||||
default: __SubConfigDefault::new(4),
|
||||
description: __SubConfigDescription::new(5),
|
||||
};
|
||||
|
||||
for (input, output) in [
|
||||
("/skipped", "1"),
|
||||
("/min", "2"),
|
||||
("/min/value", "2"),
|
||||
("/min/min", "-2147483648"),
|
||||
("/max", "3"),
|
||||
("/max/value", "3"),
|
||||
("/max/max", "2147483647"),
|
||||
("/default", "4"),
|
||||
("/default/value", "4"),
|
||||
("/default/default", "0"),
|
||||
("/description", "5"),
|
||||
("/description/value", "5"),
|
||||
("/description/description", "\"This is a description\""),
|
||||
] {
|
||||
let len = config.get_json(input, &mut buffer).unwrap();
|
||||
assert_eq!(from_utf8(&buffer[..len]), Ok(output));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
#[rstest]
|
||||
#[case("/skipped", "1")]
|
||||
#[case("/min", "2")]
|
||||
#[case("/min/value", "2")]
|
||||
#[case("/min/min", "-2147483648")]
|
||||
#[case("/max", "3")]
|
||||
#[case("/max/value", "3")]
|
||||
#[case("/max/max", "2147483647")]
|
||||
#[case("/default", "4")]
|
||||
#[case("/default/value", "4")]
|
||||
#[case("/default/default", "0")]
|
||||
#[case("/description", "5")]
|
||||
#[case("/description/value", "5")]
|
||||
#[case("/description/description", "\"This is a description\"")]
|
||||
fn serialize(sub_config: &SubConfig, #[case] path: &str, #[case] expected: &str) {
|
||||
let mut buffer = [0u8; 32];
|
||||
let len = sub_config.get_json(path, &mut buffer).unwrap();
|
||||
assert_eq!(from_utf8(&buffer[..len]), Ok(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("/skipped", Ok(2))]
|
||||
#[case("/min", Ok(2))]
|
||||
#[case("/min/value", Ok(2))]
|
||||
#[case("/max", Ok(2))]
|
||||
#[case("/max/value", Ok(2))]
|
||||
#[case("/default", Ok(2))]
|
||||
#[case("/default/value", Ok(2))]
|
||||
#[case("/description", Ok(2))]
|
||||
#[case("/description/value", Ok(2))]
|
||||
#[case("/min/min", Err(Traversal(Access(1, "Cannot write limits"))))]
|
||||
#[case("/max/max", Err(Traversal(Access(1, "Cannot write limits"))))]
|
||||
#[case("/default/default", Err(Traversal(Access(1, "Cannot write limits"))))]
|
||||
#[case(
|
||||
"/description/description",
|
||||
Err(Traversal(Access(1, "Cannot write limits")))
|
||||
)]
|
||||
fn deserialize(
|
||||
#[case] path: &str,
|
||||
#[case] expected: Result<usize, miniconf::Error<serde_json_core::de::Error>>,
|
||||
) {
|
||||
let mut config = SubConfig {
|
||||
skipped: 0,
|
||||
min: __SubConfigMin::new(0),
|
||||
@ -109,34 +150,12 @@ fn deserialize() {
|
||||
description: __SubConfigDescription::new(0),
|
||||
};
|
||||
|
||||
for input in [
|
||||
"/skipped",
|
||||
"/min",
|
||||
"/min/value",
|
||||
"/max",
|
||||
"/max/value",
|
||||
"/default",
|
||||
"/default/value",
|
||||
"/description",
|
||||
"/description/value",
|
||||
] {
|
||||
let res = config.set_json(input, b"10");
|
||||
assert_eq!(res, Ok(2));
|
||||
}
|
||||
|
||||
for input in [
|
||||
"/min/min",
|
||||
"/max/max",
|
||||
"/default/default",
|
||||
"/description/description",
|
||||
] {
|
||||
let res = config.set_json(input, b"10");
|
||||
assert_eq!(res, Err(Traversal(Access(1, "Cannot write limits"))));
|
||||
}
|
||||
let res = config.set_json(path, b"10");
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subconfig() {
|
||||
fn config_paths() {
|
||||
let control = vec![
|
||||
"/sub_config/skipped".to_owned(),
|
||||
"/sub_config/min/value".to_owned(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user