Compare commits

...

23 Commits
v0.1.0 ... main

Author SHA1 Message Date
f3d102b1eb chore: Release macroconf version 0.3.6 2024-08-20 15:31:03 +02:00
abb2170186 actually set set enum variant when deserializing 2024-08-20 15:29:07 +02:00
3f9599e01b chore: Release macroconf version 0.3.5 2024-08-20 13:17:00 +02:00
82256007e8 add some tests for the enum macro 2024-08-20 13:15:42 +02:00
a5bf501ebd implement enum variant generation 2024-08-19 22:26:43 +02:00
9d3289c3d3 chore: Release macroconf version 0.3.4 2024-08-18 19:51:24 +02:00
c5d46270b4 make serde Serialize and Deserialize generation optional 2024-08-18 19:51:00 +02:00
700e98917d chore: Release macroconf version 0.3.3 2024-08-18 18:35:38 +02:00
903a3c7589 don't generate default with getter 2024-08-18 18:28:29 +02:00
0e59545f92 chore: Release macroconf version 0.3.2 2024-08-18 17:38:33 +02:00
d4fdc2d642 remove conflicting default impl 2024-08-18 17:37:53 +02:00
3d95c32d93 chore: Release macroconf version 0.3.1 2024-08-16 21:04:35 +02:00
ea6c3a34b8 use rstest 2024-08-16 20:59:05 +02:00
0e63dc3c22 add custom getter and setter functions 2024-08-16 20:20:54 +02:00
eeed85aaa9 chore: Release macroconf version 0.3.0 2024-08-16 18:45:15 +02:00
10d0750678 fix failing tests 2024-08-16 18:44:04 +02:00
aa1b8d9272 use darling to parse attributes
the code is much more readable now
2024-08-16 18:14:56 +02:00
96f081b9da chore: Release macroconf version 0.2.1 2024-08-03 13:18:03 +02:00
09043598ba make miniconf serialization and deserialization obey limits 2024-08-03 13:16:40 +02:00
9fb96bd2c3 implement TreeAny 2024-07-25 13:09:18 +02:00
14ce7ba845 minimum for miniconf v0.13 2024-07-25 12:54:33 +02:00
d142655a25 minor cleanup 2024-07-25 11:03:46 +02:00
35afc0f484 add test 2024-05-13 22:01:08 +02:00
6 changed files with 1118 additions and 585 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "macroconf"
version = "0.1.0"
version = "0.3.6"
edition = "2021"
description = "macro for creating configurations using miniconf"
license = "MIT OR Apache-2.0"
@ -14,7 +14,6 @@ authors = ["Max Känner <max.kaenner@gmail.com>"]
unsafe_code = "forbid"
[lints.clippy]
enum_glob_use = "deny"
pedantic = "deny"
nursery = "deny"
cargo = "warn"
@ -29,7 +28,10 @@ syn = { version = "2.0", features = ["full", "extra-traits"] }
proc-macro2 = "1.0"
quote = "1.0"
convert_case = "0.6.0"
darling = "0.20"
[dev-dependencies]
miniconf = "0.9"
miniconf = { version = "0.13", features = ["json-core"] }
serde = "1.0"
rstest = "0.22"
serde-json-core = "0.5.1"

View File

@ -1,16 +1,41 @@
//! This crate creates `miniconf::Tree` implementations fields in a struct. These carry some extra
//! extra information about the field.
use std::iter::once;
use convert_case::{Case, Casing};
use darling::{util::PathList, FromMeta};
use parser::Enum;
use proc_macro::TokenStream;
use proc_macro2::{Span as Span2, TokenStream as TokenStream2, TokenTree as TokenTree2};
use quote::{format_ident, quote, ToTokens};
use syn::{
parse_macro_input, parse_quote, Attribute, DataStruct, DeriveInput, Expr, ExprLit, Field,
Ident, Lit, Type, Visibility,
};
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.
///
@ -23,516 +48,71 @@ use syn::{
/// #[config]
/// struct Config {
/// /// This will be parsed as description
/// #[min] // This will use i32::MIN for the minimum
/// #[max = 50] // The value 50 is used for the maximum
/// #[default = 42] // A `Default` implementation will be generated returning 42
/// // this value will have a minimum of i32::MIN,
/// // a maximum of 50 and a default of 42.
/// #[config(min, max = "50", default = "42")]
/// field1: i32,
/// }
/// ```
#[proc_macro_attribute]
pub fn config(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(item as DeriveInput);
let mut new_types = vec![];
let config = match parser::Config::parse(&input) {
Ok(c) => c,
Err(e) => return e.write_errors().into(),
};
match input.data {
syn::Data::Struct(DataStruct {
let newtypes = config
.fields()
.iter()
.map(|field| field.helper(&config.attrs[..]));
let newtype_types = newtypes.clone().map(|(gen, ty)| (ty, gen.is_none()));
let newtypes = newtypes.map(|(gen, _)| gen);
let syn::Data::Struct(DataStruct {
struct_token: _,
ref mut fields,
semi_token: _,
}) => {
for field in fields.iter_mut() {
if let Some(new_type) = generate_helper_struct(field, &input.ident, &input.vis) {
new_types.push(new_type);
}
}
}
syn::Data::Enum(_) => {
return quote! {compile_error!("Enums are not supported")}.into();
}
syn::Data::Union(_) => {
return quote! {compile_error!("Unions are not supported")}.into();
}) = input.data
else {
unreachable!()
};
// change types of fields to newtypes and remove the config attributes
for (field, (ty, skip)) in fields.iter_mut().zip(newtype_types) {
if skip {
continue;
}
field.ty = ty;
field.attrs.retain(|attr| !attr.path().is_ident("config"));
field.attrs.push(parse_quote!(#[tree(depth=1)]));
}
if let Some(attr) = input
if let Some(derive) = input
.attrs
.iter_mut()
.find(|attr| attr.path().is_ident("derive"))
{
if let Ok(meta) = attr.meta.require_list() {
let derives_tree = meta
.tokens
.clone()
.into_iter()
.filter_map(|token| match token {
TokenTree2::Ident(ident) if ident == Ident::new("Tree", ident.span()) => {
Some(ident)
}
_ => None,
})
.count()
== 1;
if !derives_tree {
match PathList::from_meta(&derive.meta) {
Ok(derives) => {
if !derives.iter().any(|path| {
path.segments
.last()
.map(|s| s.ident.to_string().contains("Tree"))
== Some(true)
}) {
input.attrs.push(parse_quote!(#[derive(::miniconf::Tree)]));
}
}
Err(e) => return e.write_errors().into(),
}
} else {
input.attrs.push(parse_quote!(#[derive(::miniconf::Tree)]));
}
quote! {
#input
#(#new_types)*
#(#newtypes)*
}
.into()
}
fn generate_helper_struct(
field: &mut Field,
input_ident: &Ident,
input_visibility: &Visibility,
) -> Option<TokenStream2> {
let ty = field.ty.clone();
let new_type_ident = format_ident!(
"__{}{}",
input_ident,
field
.ident
.as_ref()
.map_or("Value".to_owned(), |v| v.to_string().to_case(Case::Pascal))
);
let mut new_type_impls = TokenStream2::new();
let mut new_type_miniconf_names = vec![];
let mut new_type_miniconf_consts = vec![];
let mut extra_new_checks = TokenStream2::new();
for attr in &field.attrs {
if let Some((new_type_impl, new_check, const_ident, key)) =
parse_min(&new_type_ident, attr, &ty)
{
new_type_impls.extend(new_type_impl);
new_type_miniconf_consts.push(const_ident);
new_type_miniconf_names.push(key);
extra_new_checks.extend(new_check);
}
if let Some((new_type_impl, new_check, const_ident, key)) =
parse_max(&new_type_ident, attr, &ty)
{
new_type_impls.extend(new_type_impl);
new_type_miniconf_consts.push(const_ident);
new_type_miniconf_names.push(key);
extra_new_checks.extend(new_check);
}
if let Some((new_type_impl, const_ident, key)) = parse_default(&new_type_ident, attr, &ty) {
new_type_impls.extend(new_type_impl);
new_type_miniconf_consts.push(const_ident);
new_type_miniconf_names.push(key);
}
if let Some((new_type_impl, const_ident, key)) = parse_description(&new_type_ident, attr) {
new_type_impls.extend(new_type_impl);
new_type_miniconf_consts.push(const_ident);
new_type_miniconf_names.push(key);
}
}
if new_type_miniconf_names.is_empty() {
return None;
}
field.attrs.retain(|attr| {
!["min", "max", "default"]
.iter()
.any(|key| attr.path().is_ident(key))
});
field.attrs.push(parse_quote!(#[tree(depth(1))]));
let vis = if matches!(field.vis, Visibility::Public(_))
|| matches!(field.vis, Visibility::Inherited)
{
input_visibility
} else {
&field.vis
};
field.ty = parse_quote!(#new_type_ident);
let miniconf_fields = new_type_miniconf_names.len() + 1;
let new = generate_new(&new_type_ident, &ty, &extra_new_checks);
let serde = generate_serde(&new_type_ident, &ty, !extra_new_checks.is_empty());
let tree_key = generate_tree_key(&new_type_ident, new_type_miniconf_names.iter());
let tree_serialize = generate_tree_serialize(&new_type_ident, &new_type_miniconf_consts[..]);
let tree_deserialize = generate_tree_deserialize(&new_type_ident, miniconf_fields);
Some(quote! {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
#vis struct #new_type_ident(#ty);
#new_type_impls
#new
#serde
#tree_key
#tree_serialize
#tree_deserialize
})
}
fn parse_min(
ident: &Ident,
attr: &Attribute,
ty: &Type,
) -> Option<(TokenStream2, Option<TokenStream2>, Ident, &'static str)> {
const KEY: &str = "min";
if !attr.path().is_ident(KEY) {
return None;
}
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
let (value, new_check) = attr.meta.require_path_only().map_or_else(
|_| {
let value = match &attr.meta.require_name_value() {
Ok(meta) => &meta.value,
Err(e) => return (e.to_owned().into_compile_error(), None),
};
(
value.to_token_stream(),
Some(quote! {
if (value < Self::#const_ident.0) {
return None;
}
}),
)
},
|_| (quote!(#ty::#const_ident), None),
);
let impl_quote = quote! {
impl #ident {
const #const_ident: Self = Self(#value);
}
};
Some((impl_quote, new_check, const_ident, KEY))
}
fn parse_max(
ident: &Ident,
attr: &Attribute,
ty: &Type,
) -> Option<(TokenStream2, Option<TokenStream2>, Ident, &'static str)> {
const KEY: &str = "max";
if !attr.path().is_ident(KEY) {
return None;
}
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
let (value, new_check) = attr.meta.require_path_only().map_or_else(
|_| {
let value = match &attr.meta.require_name_value() {
Ok(meta) => &meta.value,
Err(e) => return (e.to_owned().into_compile_error(), None),
};
(
value.to_token_stream(),
Some(quote! {
if (value > Self::#const_ident.0) {
return None;
}
}),
)
},
|_| (quote!(#ty::#const_ident), None),
);
let impl_quote = quote! {
impl #ident {
const #const_ident: Self = Self(#value);
}
};
Some((impl_quote, new_check, const_ident, KEY))
}
fn parse_default(
ident: &Ident,
attr: &Attribute,
ty: &Type,
) -> Option<(TokenStream2, Ident, &'static str)> {
const KEY: &str = "default";
if !attr.path().is_ident(KEY) {
return None;
}
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
let value = attr.meta.require_path_only().map_or_else(
|_| match &attr.meta.require_name_value() {
Ok(meta) => meta.value.to_token_stream(),
Err(e) => e.to_owned().into_compile_error(),
},
|_| quote!(#ty::#const_ident),
);
let impl_quote = quote! {
impl #ident {
const #const_ident: Self = Self(#value);
}
impl ::core::default::Default for #ident {
fn default() -> Self {
Self::#const_ident
}
}
};
Some((impl_quote, const_ident, KEY))
}
fn parse_description(
ident: &Ident,
attr: &Attribute,
) -> Option<(TokenStream2, Ident, &'static str)> {
const KEY: &str = "description";
if !attr.path().is_ident("doc") {
return None;
}
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
let value = match attr.meta.require_name_value() {
Ok(meta) => &meta.value,
Err(e) => {
return Some((e.into_compile_error(), const_ident, KEY));
}
};
let value = match value {
Expr::Lit(ExprLit {
attrs: _,
lit: Lit::Str(ref string),
}) => string.value(),
_ => {
return Some((
quote!(compile_error!("doc comment must be a string")),
const_ident,
KEY,
))
}
};
let trimmed_value = value.trim();
let impl_quote = quote! {
impl #ident {
const #const_ident: &'static str = #trimmed_value;
}
};
Some((impl_quote, const_ident, KEY))
}
fn generate_new(ident: &Ident, ty: &Type, extra_checks: &TokenStream2) -> TokenStream2 {
if extra_checks.is_empty() {
quote! {
impl #ident {
pub const fn new(value: #ty) -> Self {
Self(value)
}
}
impl ::core::ops::Deref for #ident {
type Target = #ty;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ::core::ops::DerefMut for #ident {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
}
} else {
let const_new = if [
parse_quote!(u8),
parse_quote!(u16),
parse_quote!(u32),
parse_quote!(u64),
parse_quote!(u128),
parse_quote!(i8),
parse_quote!(i16),
parse_quote!(i32),
parse_quote!(i64),
parse_quote!(i128),
]
.contains(ty)
{
Some(quote!(const))
} else {
None
};
quote! {
impl #ident {
pub #const_new fn new(value: #ty) -> ::core::option::Option<Self> {
#extra_checks
::core::option::Option::Some(Self(value))
}
#[allow(dead_code)]
const unsafe fn new_unchecked(value: #ty) -> Self {
Self(value)
}
}
impl ::core::ops::Deref for #ident {
type Target = #ty;
fn deref(&self) -> &Self::Target {
&self.0
}
}
}
}
}
fn generate_serde(ident: &Ident, ty: &Type, checked_new: bool) -> TokenStream2 {
let conversion = if checked_new {
quote! {
Self::new(value).ok_or_else(|| {
<D::Error as ::serde::de::Error>::custom("checking value bounds")
})
}
} else {
quote! {
Ok(Self::new(value))
}
};
quote! {
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::core::result::Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> ::serde::Deserialize<'de> for #ident {
fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
type T = #ty;
let value = T::deserialize(deserializer)?;
#conversion
}
}
}
}
fn generate_tree_key<'a>(
ident: &Ident,
keys: impl Iterator<Item = &'a &'a str> + ExactSizeIterator + Clone,
) -> TokenStream2 {
let keys = once(&"value").chain(keys);
let num_keys = keys
.size_hint()
.1
.expect("safe because both iterators (once and original keys) are exact");
let max_length = keys.clone().map(|v| v.len()).max();
quote! {
impl #ident {
const __MINICONF_NAMES: [&'static str; #num_keys] = [#(#keys,)*];
}
impl ::miniconf::TreeKey<1> for #ident {
fn name_to_index(name: &str) -> ::core::option::Option<usize> {
Self::__MINICONF_NAMES.iter().position(|&n| n == name)
}
fn traverse_by_key<K, F, E>(mut keys: K, mut func: F) -> ::core::result::Result<usize, ::miniconf::Error<E>>
where
K: ::core::iter::Iterator,
K::Item: ::miniconf::Key,
// 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, &str) -> ::core::result::Result<(), E>,
{
let ::core::option::Option::Some(key) = keys.next() else { return ::core::result::Result::Ok(0) };
let index = ::miniconf::Key::find::<1, Self>(&key).ok_or(::miniconf::Error::NotFound(1))?;
let name = Self::__MINICONF_NAMES
.get(index)
.ok_or(::miniconf::Error::NotFound(1))?;
func(index, name)?;
::miniconf::Increment::increment(::core::result::Result::Ok(0))
}
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 generate_tree_serialize(ident: &Ident, consts: &[Ident]) -> TokenStream2 {
let matches = consts.iter().enumerate().map(|(i, ident)| {
let index = i + 1;
quote! {
#index => ::miniconf::Serialize::serialize(&Self::#ident, ser)?,
}
});
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: ::core::iter::Iterator,
K::Item: ::miniconf::Key,
S: ::serde::Serializer,
{
let ::core::option::Option::Some(key) = keys.next() else {
return ::miniconf::Increment::increment({
::miniconf::Serialize::serialize(&self.0, ser)?;
::core::result::Result::Ok(0)
});
};
let index = ::miniconf::Key::find::<1, Self>(&key).ok_or(miniconf::Error::NotFound(1))?;
if keys.next().is_some() {
return ::core::result::Result::Err(::miniconf::Error::TooLong(1));
}
::miniconf::Increment::increment({
match index {
0 => ::miniconf::Serialize::serialize(&self.0, ser)?,
#(#matches)*
_ => unreachable!(),
};
::core::result::Result::Ok(0)
})
}
}
}
}
fn generate_tree_deserialize(ident: &Ident, num_keys: usize) -> TokenStream2 {
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: ::core::iter::Iterator,
K::Item: ::miniconf::Key,
D: ::serde::Deserializer<'de>,
{
let ::core::option::Option::Some(key) = keys.next() else {
self.0 = ::miniconf::Deserialize::deserialize(de)?;
return ::core::result::Result::Ok(0);
};
let index = ::miniconf::Key::find::<1, Self>(&key).ok_or(::miniconf::Error::NotFound(1))?;
if keys.next().is_some() {
return ::core::result::Result::Err(miniconf::Error::TooLong(1));
}
match index {
0 => ::miniconf::Increment::increment({
self.0 = ::miniconf::Deserialize::deserialize(de)?;
Ok(0)
}),
1..=#num_keys => ::core::result::Result::Err(::miniconf::Error::Absent(0)),
_ => unreachable!(),
}
}
}
}
}

770
src/parser.rs Normal file
View File

@ -0,0 +1,770 @@
#![allow(clippy::option_if_let_else)]
use std::convert::identity;
use convert_case::{Case, Casing};
use darling::{
ast::{self, Data},
util::{Flag, Override, PathList},
Error, FromDeriveInput, FromField, FromMeta,
};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{braced, parse::Parse, parse_quote, spanned::Spanned, Token};
#[derive(Debug, FromField)]
#[darling(attributes(config))]
#[darling(forward_attrs(doc))]
pub struct ConfigField {
ident: Option<syn::Ident>,
vis: syn::Visibility,
ty: syn::Type,
attrs: Vec<syn::Attribute>,
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)]
parent_ident: Option<syn::Ident>,
}
impl ConfigField {
pub(crate) fn needs_newtype(&self) -> bool {
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"))
.map(|attr| {
let mut derives = (*PathList::from_meta(&attr.meta).unwrap()).clone();
derives.retain(|derive| {
derive.segments.last().map(|s| {
let derive = s.ident.to_string();
derive.contains("Tree")
|| derive.contains("Serialize")
|| derive.contains("Deserialize")
|| (default.is_some() && derive.contains("Default"))
}) == Some(false)
});
quote! {#[derive(#(#derives,)*)]}
});
let new_type_ident = self.helper_ident();
let vis = &self.vis;
let ty = &self.ty;
let new = self.helper_new();
let serde = self.helper_serde();
let tree = self.helper_tree();
(
Some(quote! {
#derives
#vis struct #new_type_ident(#ty);
#new
#default
#serde
#tree
}),
parse_quote!(#new_type_ident),
)
} else {
(None, self.ty.clone())
}
}
pub(crate) fn helper_ident(&self) -> syn::Ident {
format_ident!(
"__{}{}",
self.parent_ident
.as_ref()
.unwrap_or(&syn::Ident::new("Struct", Span::mixed_site())),
self.ident
.as_ref()
.map_or("Field".to_owned(), |v| v.to_string().to_case(Case::Pascal))
)
}
pub(crate) fn has_custom_limits(&self) -> bool {
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 {
let ident = self.helper_ident();
let ty = &self.ty;
if self.has_custom_limits() {
let min = self.min.as_ref().and_then(|v| v.as_ref().explicit());
let max = self.max.as_ref().and_then(|v| v.as_ref().explicit());
quote! {
impl #ident {
pub fn new(value: #ty) -> Option<Self> {
if (#min..=#max).contains(&value) {
Some(Self(value))
} else {
None
}
}
#[allow(dead_code)]
pub const unsafe fn new_unchecked(value: #ty) -> Self {
Self(value)
}
}
impl ::core::ops::Deref for #ident {
type Target = #ty;
fn deref(&self) -> &Self::Target {
&self.0
}
}
}
} else {
quote! {
impl #ident {
pub const fn new(value: #ty) -> Self {
Self(value)
}
}
impl ::core::ops::Deref for #ident {
type Target = #ty;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ::core::ops::DerefMut for #ident {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
}
}
}
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!(::core::default::Default::default());
let default = default.as_ref().unwrap_or(&default_default);
quote! {
impl ::core::default::Default for #ident {
fn default() -> Self {
Self(#default)
}
}
}
})
}
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! {
Self::new(value).ok_or_else(|| {
<D::Error as ::serde::de::Error>::custom("checking value bounds")
})
}
} else {
quote! {
Ok(Self::new(value))
}
};
let ty = &self.ty;
Some(quote! {
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::core::result::Result::<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> ::serde::Deserialize<'de> for #ident {
fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
type T = #ty;
let value = T::deserialize(deserializer)?;
#conversion
}
}
})
}
pub(crate) fn helper_tree(&self) -> TokenStream {
let mut tokens = self.helper_tree_key();
tokens.extend(self.helper_tree_serialize());
tokens.extend(self.helper_tree_deserialize());
tokens.extend(self.helper_tree_any());
tokens
}
pub(crate) fn helper_keys(&self) -> Vec<(&'static str, syn::Expr)> {
macro_rules! field_to_key {
($field: ident, $default: expr) => {
self.$field.as_ref().map(|value| {
let ty = self.typ.as_ref().unwrap_or(&self.ty);
(stringify!($field),
value.clone().explicit().map(|val| {
parse_quote!({
fn type_ascribe(val: #ty) -> #ty {val}
type_ascribe(#val)
})
}).unwrap_or_else(|| {
parse_quote!(#ty::$default)
}))
})
};
($field: ident) => {
self.$field.as_ref().map(|value| {
(stringify!($field), value.clone())
})
}
}
[
Some(("value", parse_quote!(self))),
field_to_key!(default, default()),
field_to_key!(min, MIN),
field_to_key!(max, MAX),
field_to_key!(description),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
}
pub(crate) fn helper_tree_key(&self) -> TokenStream {
let ident = self.helper_ident();
let keys = self.helper_keys();
let num_keys = keys.len();
let keys = keys.iter().map(|(name, _)| name);
let max_length = keys.clone().map(|v| v.len()).max();
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)
}
}
}
}
pub(crate) fn helper_tree_serialize(&self) -> TokenStream {
let matches = self
.helper_keys()
.iter()
.enumerate()
.skip(1)
.map(|(i, (_, expr))| {
quote! {
#i => ::serde::Serialize::serialize(&#expr, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
}
})
.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>(
&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(&#get, 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 {
0 => ::serde::Serialize::serialize(&#get, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
#(#matches)*
_ => unreachable!(),
}?;
Ok(0)
}
}
}
}
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>(
&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 {
#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))?;
if !keys.finalize() {
return ::core::result::Result::Err(::miniconf::Traversal::TooLong(1).into());
}
match index {
0 => {
#set(<#typ as ::serde::Deserialize>::deserialize(de).map_err(|err| ::miniconf::Error::Inner(0, err))?)?;
Ok(0)
}
#match_range
_ => unreachable!(),
}
}
}
}
}
pub(crate) fn helper_tree_any(&self) -> TokenStream {
let ident = self.helper_ident();
let num_keys = self.helper_keys().len();
let ref_mut = if self.has_custom_limits() {
quote! {::core::result::Result::Err(::miniconf::Traversal::Access(0, "field has custom limits"))}
} 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>
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),
#match_range
_ => 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 #ref_mut;
};
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 => #ref_mut,
#match_range
_ => unreachable!(),
}
}
}
}
}
}
#[derive(Debug, FromDeriveInput)]
#[darling(forward_attrs(derive))]
#[darling(supports(struct_any))]
pub struct Config {
ident: syn::Ident,
// generics: syn::Generics,
// vis: syn::Visibility,
data: ast::Data<(), ConfigField>,
pub attrs: Vec<syn::Attribute>,
}
impl Config {
pub(crate) fn parse(input: &syn::DeriveInput) -> Result<Self, Error> {
let mut config = Self::from_derive_input(input)?;
let ident = config.ident.clone();
let fields = config.fields_mut();
for field in fields.iter_mut() {
field.description = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("doc"))
.map(|attr| {
let description = syn::LitStr::from_meta(&attr.meta).unwrap().value();
let description = description.trim();
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)
}
pub(crate) fn fields(&self) -> &Vec<ConfigField> {
let Data::Struct(fields) = &self.data else {
unreachable!()
};
&fields.fields
}
pub(crate) fn fields_mut(&mut self) -> &mut Vec<ConfigField> {
let Data::Struct(fields) = &mut self.data else {
unreachable!()
};
&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
View 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
View 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");
}

View File

@ -1,120 +1,174 @@
use std::str::from_utf8;
use macroconf::config;
use miniconf::{Error::Absent, JsonCoreSlash, Tree, TreeKey};
use miniconf::{
Error::Traversal,
IntoKeys, JsonCoreSlash, Path,
Traversal::{Access, TooLong},
Tree, TreeKey,
};
use rstest::*;
use serde::{Deserialize, Serialize};
#[config]
#[derive(Debug, Clone, Copy, Tree)]
struct Config {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tree)]
struct SubConfig {
skipped: i32,
#[min]
#[config(min)]
min: i32,
#[max]
#[config(max)]
max: i32,
#[default = 0]
#[config(default = "0")]
default: i32,
/// This is a description
description: i32,
}
#[test]
fn keys() {
for (id, field) in ["skipped", "min", "max", "default", "description"]
.into_iter()
.enumerate()
{
#[config]
#[derive(Debug, Clone, Copy, Tree)]
struct Config {
#[tree(depth = 2)]
sub_config: SubConfig,
}
/// Config with default derive and default field
#[config]
#[derive(Default)]
struct _DefaultConfig {
#[config(default)]
field: i32,
}
#[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!(
Config::traverse_by_key(std::iter::once(field), |index, name| {
assert_eq!((id, field), (index, name));
SubConfig::traverse_by_key(field.into_keys(), |index, name, _len| {
assert_eq!((id, Some(field[field.len() - 1])), (index, name));
Ok::<_, ()>(())
}),
Ok(1)
);
}
}
#[test]
fn sub_keys() {
#[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!(
Config::traverse_by_key(["skipped", "value"].into_iter(), |_, _| Ok::<_, ()>(())),
SubConfig::traverse_by_key(fields.into_keys(), |_, _, _| Ok::<_, ()>(())),
expected
);
assert_eq!(
SubConfig::traverse_by_key(["skipped"].into_keys(), |_, _, _| Ok::<_, ()>(())),
Ok(1)
);
for field in ["min", "max", "default", "description"] {
assert_eq!(
Config::traverse_by_key([field, "value"].into_iter(), |_, _| Ok::<_, ()>(())),
SubConfig::traverse_by_key([field, "value"].into_keys(), |_, _, _| Ok::<_, ()>(())),
Ok(2)
);
assert_eq!(
Config::traverse_by_key([field, field].into_iter(), |_, _| Ok::<_, ()>(())),
SubConfig::traverse_by_key([field, field].into_keys(), |_, _, _| Ok::<_, ()>(())),
Ok(2)
);
}
}
#[test]
fn serialize() {
let mut buffer = [0u8; 32];
let config = Config {
#[fixture]
#[once]
fn sub_config() -> SubConfig {
SubConfig {
skipped: 1,
min: __ConfigMin::new(2),
max: __ConfigMax::new(3),
default: __ConfigDefault::new(4),
description: __ConfigDescription::new(5),
min: __SubConfigMin::new(2),
max: __SubConfigMax::new(3),
default: __SubConfigDefault::new(4),
description: __SubConfigDescription::new(5),
}
}
#[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),
max: __SubConfigMax::new(0),
default: __SubConfigDefault::new(0),
description: __SubConfigDescription::new(0),
};
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 res = config.get_json(input, &mut buffer);
assert_eq!(res, Ok(output.len()));
assert_eq!(from_utf8(&buffer[..output.len()]), Ok(output));
}
let res = config.set_json(path, b"10");
assert_eq!(res, expected);
}
#[test]
fn deserialize() {
let mut config = Config {
skipped: 0,
min: __ConfigMin::new(0),
max: __ConfigMax::new(0),
default: __ConfigDefault::new(0),
description: __ConfigDescription::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(Absent(1)));
}
fn config_paths() {
let control = vec![
"/sub_config/skipped".to_owned(),
"/sub_config/min/value".to_owned(),
"/sub_config/min/min".to_owned(),
"/sub_config/max/value".to_owned(),
"/sub_config/max/max".to_owned(),
"/sub_config/default/value".to_owned(),
"/sub_config/default/default".to_owned(),
"/sub_config/description/value".to_owned(),
"/sub_config/description/description".to_owned(),
];
let paths: Vec<String> = Config::nodes::<Path<String, '/'>>()
.filter_map(|path| path.ok().map(|(n, _)| n.0))
.collect();
assert_eq!(paths, control);
}