Skip to content

Commit

Permalink
feat(derive): Map conversion helper
Browse files Browse the repository at this point in the history
  • Loading branch information
andoriyu committed Apr 4, 2020
1 parent e308cd4 commit 51056a6
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 19 deletions.
5 changes: 0 additions & 5 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

140 changes: 135 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> for Mode {
type Error = ObjectError;
fn try_from(src: String) -> Result<Mode, ObjectError> {
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<i64> 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<Mode, ObjectError> {
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
Expand Down Expand Up @@ -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<T, E>`
- 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.
Expand Down
45 changes: 44 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mode, ObjectError> {
//! 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
Expand Down Expand Up @@ -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<T, E>`
//! - 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.
Expand Down Expand Up @@ -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)]
Expand Down
28 changes: 20 additions & 8 deletions uclicious_derive/src/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Initializer<'a> {
pub validation: Option<Path>,
pub from: Option<Path>,
pub try_from: Option<Path>,
pub map: Option<Path>,
}

impl<'a> ToTokens for Initializer<'a> {
Expand Down Expand Up @@ -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
),
}
Expand All @@ -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> {
Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions uclicious_derive/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ pub struct Field {
from: Option<Path>,
#[darling(default)]
try_from: Option<Path>,
#[darling(default)]
map: Option<Path>,
}
impl FlagVisibility for Field {
fn public(&self) -> &Flag {
Expand Down Expand Up @@ -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(),
}
}
}

0 comments on commit 51056a6

Please sign in to comment.