diff --git a/src/parser.rs b/src/parser.rs index 470d16a..3d6ecc2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,5 +1,7 @@ #![allow(clippy::option_if_let_else)] +use std::convert::identity; + use convert_case::{Case, Casing}; use darling::{ ast::{self, Data}, @@ -8,7 +10,7 @@ use darling::{ }; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; -use syn::parse_quote; +use syn::{parse_quote, spanned::Spanned}; #[derive(Debug, FromField)] #[darling(attributes(config))] @@ -21,6 +23,9 @@ pub struct ConfigField { min: Option>, max: Option>, default: Option>, + typ: Option, + get: Option, + set: Option, #[darling(skip)] description: Option, #[darling(skip)] @@ -88,8 +93,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 { @@ -148,7 +154,7 @@ impl ConfigField { pub(crate) fn helper_default(&self) -> Option { 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 { @@ -209,7 +215,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 +294,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 +302,10 @@ impl ConfigField { }) .collect::>(); 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( @@ -307,13 +318,14 @@ impl ConfigField { S: ::serde::Serializer, { let ::core::result::Result::Ok(key) = keys.next::() 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::(&key).ok_or(::miniconf::Traversal::NotFound(1))?; + let index = ::miniconf::Key::find::(&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 +338,14 @@ 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)); quote! { impl<'de> ::miniconf::TreeDeserialize<'de, 1> for #ident { fn deserialize_by_key( @@ -338,7 +358,7 @@ impl ConfigField { D: ::serde::Deserializer<'de>, { let ::core::result::Result::Ok(key) = keys.next::() else { - *self = ::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::(&key).ok_or(::miniconf::Traversal::NotFound(1))?; @@ -347,7 +367,7 @@ impl ConfigField { } match index { 0 => { - *self = ::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) } , @@ -436,6 +456,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) } diff --git a/tests/get_set.rs b/tests/get_set.rs new file mode 100644 index 0000000..255b6df --- /dev/null +++ b/tests/get_set.rs @@ -0,0 +1,54 @@ +use std::{cell::Cell, str::from_utf8}; + +use macroconf::config; +use miniconf::JsonCoreSlash; + +fn set_cell( + cell: &Cell, + 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, +} + +#[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"); +}