diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 78cface..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,10 +1,5 @@ - - - - - diff --git a/README.md b/README.md index f56f472..00651cd 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,124 @@ let connection: Connection = builder.build().unwrap(); If you choose to derive builder then `::builder()` method will be added to target struct. +#### Validators + +Library supports running optional validators on values before building the resulting struct: + +```rust +use uclicious::*; +mod validators { + use uclicious::ObjectError; + pub fn is_positive(lookup_path: &str, value: &i64) -> Result<(), ObjectError> { + if *value > 0 { + Ok(()) + } else { + Err(ObjectError::other(format!("{} is not a positive number", lookup_path))) + } + } +} +#[derive(Debug,Uclicious)] +struct Validated { + #[ucl(default, validate="validators::is_positive")] + number: i64 +} +let mut builder = Validated::builder().unwrap(); + +let input = "number = -1"; +builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap(); +assert!(builder.build().is_err()) +``` +#### Type Mapping + +If your target structure has types that don't implement `FromObject` you can use `From` or `TryFrom` +via intermediate that does: + +```rust +use uclicious::*; +use std::convert::{From,TryFrom}; + +#[derive(Debug, Eq, PartialEq)] +enum Mode { + On, + Off, +} + +impl TryFrom for Mode { + type Error = ObjectError; + fn try_from(src: String) -> Result { + match src.to_lowercase().as_str() { + "on" => Ok(Mode::On), + "off" => Ok(Mode::Off), + _ => Err(ObjectError::other(format!("{} is not supported value", src))) + } + } +} + +#[derive(Debug, Eq, PartialEq)] +struct WrappedInt(i64); + +impl From for WrappedInt { + fn from(src: i64) -> WrappedInt { + WrappedInt(src) + } +} + +#[derive(Debug,Uclicious, Eq, PartialEq)] +struct Mapped { + #[ucl(from="i64")] + number: WrappedInt, + #[ucl(try_from="String")] + mode: Mode +} +let mut builder = Mapped::builder().unwrap(); + +let input = r#" + number = -1, + mode = "on" +"#; +builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap(); +let actual = builder.build().unwrap(); +let expected = Mapped { +number: WrappedInt(-1), +mode: Mode::On +}; +assert_eq!(expected, actual); +``` + +Additionally you can provide mapping to you type from ObjectRef: +```rust +use uclicious::*; + +#[derive(Debug, Eq, PartialEq)] +pub enum Mode { + On, + Off, +} + +pub fn map_bool(src: ObjectRef) -> Result { + let bool: bool = src.try_into()?; + if bool { + Ok(Mode::On) + } else { + Ok(Mode::Off) + } +} +#[derive(Debug,Uclicious, Eq, PartialEq)] +struct Mapped { + #[ucl(map="map_bool")] + mode: Mode +} +let mut builder = Mapped::builder().unwrap(); + +let input = r#" + mode = on +"#; +builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap(); +let actual = builder.build().unwrap(); +let expected = Mapped { + mode: Mode::On +}; +``` ### Supported attributes (`#[ucl(..)]`) #### Structure level @@ -152,24 +270,36 @@ If you choose to derive builder then `::builder()` method will be added to targe - Used to add files into the parser. - If file doesn't exist or failed to parse, then error will be returned in a constructor. - Has following nested attirbutes: - - (required) `path(string)` + - (required) `path = string` - File path. Can be absolute or relative to CWD. - - (optional) `priority(u32)` + - (optional) `priority = u32` - 0-15 priority for the source. Consult the libUCL documentation for more information. - - (optional) `strategy(uclicious::DuplicateStrategy)` + - (optional) `strategy = uclicious::DuplicateStrategy` - Strategy to use for duplicate keys. Consult the libUCL documentation for more information. #### Field level + All field level options are optional. - `default` - Use Default::default if key not found in object. - - `default(expression)` + - `default = expression` - Use this _expression_ as value if key not found. - Could be a value or a function call. - - `path(string)` + - `path = string` - By default field name is used as path. - If set that would be used as a key. - dot notation for key is supported. + - `validate = path::to_method` + - `Fn(key: &str, value: &T) -> Result<(), E>` + - Error needs to be convertable into `ObjectError` + - `from = Type` + - Try to convert `ObjectRef` to `Type` and then use `std::convert::From` to convert into target type + - `try_from = Type` + - Try to convert `ObjectRef` to `Type` and then use `std::convert::TryFrom` to convert into target type + - Error type needs to be convertable into ObjectError + - `map = path::to_method` + - `Fn(src: ObjectRef) -> Result` + - A way to map foreign objects that can't implement `From` or `TryFrom` or when error is not convertable into `ObjectError` ### Additional notes - If target type is an array, but key is a single value — an implicit list is created. diff --git a/src/lib.rs b/src/lib.rs index 2ee0d50..04efefc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,6 +203,41 @@ //! }; //! assert_eq!(expected, actual); //! ``` +//! +//! Additionally you can provide mapping to you type from ObjectRef: +//! ```rust +//! use uclicious::*; +//! +//! #[derive(Debug, Eq, PartialEq)] +//! pub enum Mode { +//! On, +//! Off, +//! } +//! +//! pub fn map_bool(src: ObjectRef) -> Result { +//! let bool: bool = src.try_into()?; +//! if bool { +//! Ok(Mode::On) +//! } else { +//! Ok(Mode::Off) +//! } +//! } +//! #[derive(Debug,Uclicious, Eq, PartialEq)] +//! struct Mapped { +//! #[ucl(map="map_bool")] +//! mode: Mode +//! } +//! let mut builder = Mapped::builder().unwrap(); +//! +//! let input = r#" +//! mode = on +//! "#; +//! builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap(); +//! let actual = builder.build().unwrap(); +//! let expected = Mapped { +//! mode: Mode::On +//! }; +//! ``` //! ### Supported attributes (`#[ucl(..)]`) //! //! #### Structure level @@ -255,6 +290,14 @@ //! - `validate = path::to_method` //! - `Fn(key: &str, value: &T) -> Result<(), E>` //! - Error needs to be convertable into `ObjectError` +//! - `from = Type` +//! - Try to convert `ObjectRef` to `Type` and then use `std::convert::From` to convert into target type +//! - `try_from = Type` +//! - Try to convert `ObjectRef` to `Type` and then use `std::convert::TryFrom` to convert into target type +//! - Error type needs to be convertable into ObjectError +//! - `map = path::to_method` +//! - `Fn(src: ObjectRef) -> Result` +//! - A way to map foreign objects that can't implement `From` or `TryFrom` or when error is not convertable into `ObjectError` //! //! ### Additional notes //! - If target type is an array, but key is a single value — an implicit list is created. @@ -307,7 +350,7 @@ pub use raw::{ DuplicateStrategy, Object, ObjectError, ObjectRef, Parser, ParserFlags, Priority, DEFAULT_DUPLICATE_STRATEGY, DEFAULT_PARSER_FLAG, }; -pub use traits::FromObject; +pub use traits::{FromObject, TryInto}; #[cfg(feature = "uclicious_derive")] #[allow(unused_imports)] diff --git a/uclicious_derive/src/initializer.rs b/uclicious_derive/src/initializer.rs index b305777..b97a623 100644 --- a/uclicious_derive/src/initializer.rs +++ b/uclicious_derive/src/initializer.rs @@ -19,6 +19,7 @@ pub struct Initializer<'a> { pub validation: Option, pub from: Option, pub try_from: Option, + pub map: Option, } impl<'a> ToTokens for Initializer<'a> { @@ -54,15 +55,17 @@ impl<'a> Initializer<'a> { } } fn match_some(&'a self) -> MatchSome { - match (&self.validation, &self.from, &self.try_from) { - (None, None, None) => MatchSome::Simple, - (Some(validation), None, None) => MatchSome::Validation(validation), - (None, Some(src_type), None) => MatchSome::From(src_type), - (None, None, Some(src_type)) => MatchSome::TryFrom(src_type), - (Some(validation), Some(from), None) => MatchSome::FromValidation(from, validation), - (Some(validation), None, Some(from)) => MatchSome::TryFromValidation(from, validation), + match (&self.validation, &self.from, &self.try_from, &self.map) { + (None, None, None, None) => MatchSome::Simple, + (Some(validation), None, None, None) => MatchSome::Validation(validation), + (None, Some(src_type), None, None) => MatchSome::From(src_type), + (None, None, Some(src_type), None) => MatchSome::TryFrom(src_type), + (Some(validation), Some(from), None, None) => MatchSome::FromValidation(from, validation), + (Some(validation), None, Some(from), None) => MatchSome::TryFromValidation(from, validation), + (None, None, None, Some(map_func)) => MatchSome::Map(map_func), + (Some(validation), None, None, Some(map_func)) => MatchSome::MapValidation(map_func, validation), _ => panic!( - "field {}: Can't have both from and try_from", + "field {}: map, from and try_from are mutually exclusive", self.field_ident ), } @@ -88,6 +91,8 @@ enum MatchSome<'a> { FromValidation(&'a Path, &'a Path), TryFrom(&'a Path), TryFromValidation(&'a Path, &'a Path), + Map(&'a Path), + MapValidation(&'a Path, &'a Path), } impl<'a> ToTokens for MatchNone<'a> { @@ -139,6 +144,13 @@ impl<'a> ToTokens for MatchSome<'a> { let v = #try_into_trait::try_into(v)?; #validation(&lookup_path, &v).map(|_| v)? ), + MatchSome::Map(map_func) => quote!( + #map_func(obj)? + ), + MatchSome::MapValidation(map_func, validation) => quote!( + let v = #map_func(obj)?; + #validation(&lookup_path, &v).map(|_| v)? + ) }; tokens.append_all(quote); } diff --git a/uclicious_derive/src/options.rs b/uclicious_derive/src/options.rs index 675f3c2..51ab6e9 100644 --- a/uclicious_derive/src/options.rs +++ b/uclicious_derive/src/options.rs @@ -195,6 +195,8 @@ pub struct Field { from: Option, #[darling(default)] try_from: Option, + #[darling(default)] + map: Option, } impl FlagVisibility for Field { fn public(&self) -> &Flag { @@ -454,6 +456,7 @@ impl<'a> FieldWithDefaults<'a> { validation: self.field.validate.clone(), from: self.field.from.clone(), try_from: self.field.try_from.clone(), + map: self.field.map.clone(), } } }