add custom getter and setter functions

This commit is contained in:
Max Känner 2024-08-16 20:20:54 +02:00
parent eeed85aaa9
commit 0e63dc3c22
2 changed files with 104 additions and 10 deletions

View File

@ -1,5 +1,7 @@
#![allow(clippy::option_if_let_else)] #![allow(clippy::option_if_let_else)]
use std::convert::identity;
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use darling::{ use darling::{
ast::{self, Data}, ast::{self, Data},
@ -8,7 +10,7 @@ use darling::{
}; };
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use syn::parse_quote; use syn::{parse_quote, spanned::Spanned};
#[derive(Debug, FromField)] #[derive(Debug, FromField)]
#[darling(attributes(config))] #[darling(attributes(config))]
@ -21,6 +23,9 @@ pub struct ConfigField {
min: Option<Override<syn::Expr>>, min: Option<Override<syn::Expr>>,
max: Option<Override<syn::Expr>>, max: Option<Override<syn::Expr>>,
default: Option<Override<syn::Expr>>, default: Option<Override<syn::Expr>>,
typ: Option<syn::Type>,
get: Option<syn::Path>,
set: Option<syn::Path>,
#[darling(skip)] #[darling(skip)]
description: Option<syn::Expr>, description: Option<syn::Expr>,
#[darling(skip)] #[darling(skip)]
@ -88,8 +93,9 @@ impl ConfigField {
} }
pub(crate) fn has_custom_limits(&self) -> bool { pub(crate) fn has_custom_limits(&self) -> bool {
!((self.min.is_none() || self.min == Some(Override::Inherit)) self.typ.is_none()
&& (self.max.is_none() || self.max == Some(Override::Inherit))) && !((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 { pub(crate) fn helper_new(&self) -> TokenStream {
@ -148,7 +154,7 @@ impl ConfigField {
pub(crate) fn helper_default(&self) -> Option<TokenStream> { pub(crate) fn helper_default(&self) -> Option<TokenStream> {
self.default.as_ref().map(|default| { self.default.as_ref().map(|default| {
let ident = self.helper_ident(); 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); let default = default.as_ref().unwrap_or(&default_default);
quote! { quote! {
impl ::core::default::Default for #ident { impl ::core::default::Default for #ident {
@ -209,7 +215,7 @@ impl ConfigField {
macro_rules! field_to_key { macro_rules! field_to_key {
($field: ident, $default: expr) => { ($field: ident, $default: expr) => {
self.$field.as_ref().map(|value| { self.$field.as_ref().map(|value| {
let ty = &self.ty; let ty = self.typ.as_ref().unwrap_or(&self.ty);
(stringify!($field), (stringify!($field),
value.clone().explicit().map(|val| { value.clone().explicit().map(|val| {
parse_quote!({ parse_quote!({
@ -288,6 +294,7 @@ impl ConfigField {
.helper_keys() .helper_keys()
.iter() .iter()
.enumerate() .enumerate()
.skip(1)
.map(|(i, (_, expr))| { .map(|(i, (_, expr))| {
quote! { quote! {
#i => ::serde::Serialize::serialize(&#expr, ser).map_err(|err| ::miniconf::Error::Inner(0, err)), #i => ::serde::Serialize::serialize(&#expr, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
@ -295,6 +302,10 @@ impl ConfigField {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let ident = self.helper_ident(); let ident = self.helper_ident();
let get = self
.get
.as_ref()
.map_or_else(|| quote! {self}, |getter| quote! {#getter(&self.0)});
quote! { quote! {
impl ::miniconf::TreeSerialize<1> for #ident { impl ::miniconf::TreeSerialize<1> for #ident {
fn serialize_by_key<K, S>( fn serialize_by_key<K, S>(
@ -307,13 +318,14 @@ impl ConfigField {
S: ::serde::Serializer, S: ::serde::Serializer,
{ {
let ::core::result::Result::Ok(key) = keys.next::<Self>() else { 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() { 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 { match index {
0 => ::serde::Serialize::serialize(&#get, ser).map_err(|err| ::miniconf::Error::Inner(0, err)),
#(#matches)* #(#matches)*
_ => unreachable!(), _ => unreachable!(),
}?; }?;
@ -326,6 +338,14 @@ impl ConfigField {
pub(crate) fn helper_tree_deserialize(&self) -> TokenStream { pub(crate) fn helper_tree_deserialize(&self) -> TokenStream {
let ident = self.helper_ident(); let ident = self.helper_ident();
let num_keys = self.helper_keys().len(); 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));
quote! { quote! {
impl<'de> ::miniconf::TreeDeserialize<'de, 1> for #ident { impl<'de> ::miniconf::TreeDeserialize<'de, 1> for #ident {
fn deserialize_by_key<K, D>( fn deserialize_by_key<K, D>(
@ -338,7 +358,7 @@ impl ConfigField {
D: ::serde::Deserializer<'de>, D: ::serde::Deserializer<'de>,
{ {
let ::core::result::Result::Ok(key) = keys.next::<Self>() else { 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); return ::core::result::Result::Ok(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(1))?;
@ -347,7 +367,7 @@ impl ConfigField {
} }
match index { match index {
0 => { 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) Ok(0)
} }
, ,
@ -436,6 +456,26 @@ impl Config {
parse_quote!(#description) parse_quote!(#description)
}); });
field.parent_ident = Some(ident.clone()); 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) Ok(config)
} }

54
tests/get_set.rs Normal file
View File

@ -0,0 +1,54 @@
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>,
}
#[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");
}