Compare commits

...

11 Commits
v0.3.1 ... main

6 changed files with 396 additions and 12 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "macroconf"
version = "0.3.1"
version = "0.3.6"
edition = "2021"
description = "macro for creating configurations using miniconf"
license = "MIT OR Apache-2.0"

View File

@ -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`

View File

@ -5,12 +5,12 @@ 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, spanned::Spanned};
use syn::{braced, parse::Parse, parse_quote, spanned::Spanned, Token};
#[derive(Debug, FromField)]
#[darling(attributes(config))]
@ -26,6 +26,7 @@ pub struct ConfigField {
typ: Option<syn::Type>,
get: Option<syn::Path>,
set: Option<syn::Path>,
no_serde: Flag,
#[darling(skip)]
description: Option<syn::Expr>,
#[darling(skip)]
@ -34,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"))
@ -50,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,)*)]}
@ -59,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();
@ -152,6 +154,9 @@ 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!(::core::default::Default::default());
@ -166,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! {
@ -180,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
@ -200,7 +208,7 @@ impl ConfigField {
#conversion
}
}
}
})
}
pub(crate) fn helper_tree(&self) -> TokenStream {
@ -346,6 +354,13 @@ impl ConfigField {
.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>(
@ -370,8 +385,7 @@ impl ConfigField {
#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!(),
}
}
@ -387,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>
@ -402,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!(),
}
}
@ -420,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!(),
}
}
@ -494,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
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);
}
}

View File

@ -28,6 +28,17 @@ struct Config {
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];

View File

@ -31,6 +31,14 @@ struct Config {
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"])]