diff --git a/Cargo.toml b/Cargo.toml index 8b08f5be..1c10fe35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,12 @@ edition = "2018" maintenance = { status = "actively-developed" } [features] -default = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] +default = ["toml", "json", "yaml", "ini", "ron", "json5", "dhall", "convert-case", "async"] json = ["serde_json"] yaml = ["yaml-rust"] ini = ["rust-ini"] json5 = ["json5_rs", "serde/derive"] +dhall = ["serde_dhall"] convert-case = ["convert_case"] preserve_order = ["indexmap", "toml?/preserve_order", "serde_json?/preserve_order", "ron?/indexmap"] async = ["async-trait"] @@ -36,6 +37,7 @@ yaml-rust = { version = "0.4", optional = true } rust-ini = { version = "0.19", optional = true } ron = { version = "0.8", optional = true } json5_rs = { version = "0.4", optional = true, package = "json5" } +serde_dhall = { version = "0.10", optional = true } indexmap = { version = "2.0.0", features = ["serde"], optional = true } convert_case = { version = "0.6", optional = true } pathdiff = "0.2" diff --git a/src/file/format/dhall.rs b/src/file/format/dhall.rs new file mode 100644 index 00000000..03bee38c --- /dev/null +++ b/src/file/format/dhall.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; +use std::error::Error; + +use crate::{ + error::Unexpected, + value::{Value, ValueKind}, + ConfigError, +}; + +pub fn parse( + uri: Option<&String>, + text: &str, +) -> Result, Box> { + let value = from_dhall_value(uri, serde_dhall::from_str(text).parse()?); + match value.kind { + ValueKind::Table(map) => Ok(map), + ValueKind::Nil => Err(Unexpected::Unit), + ValueKind::Boolean(value) => Err(Unexpected::Bool(value)), + ValueKind::Integer(value) => Err(Unexpected::Integer(value)), + ValueKind::Float(value) => Err(Unexpected::Float(value)), + ValueKind::String(value) => Err(Unexpected::Str(value)), + ValueKind::Array(value) => Err(Unexpected::Seq), + } + .map_err(|err| ConfigError::invalid_root(uri, err)) + .map_err(|err| Box::new(err) as Box) +} + +fn from_dhall_value(uri: Option<&String>, value: serde_dhall::SimpleValue) -> Value { + match value { + serde_dhall::SimpleValue::Num(num) => match num { + serde_dhall::NumKind::Bool(b) => Value::new(uri, ValueKind::Boolean(b)), + serde_dhall::NumKind::Natural(n) => Value::new(uri, ValueKind::Integer(n as i64)), + serde_dhall::NumKind::Integer(i) => Value::new(uri, ValueKind::Integer(i)), + serde_dhall::NumKind::Double(d) => Value::new(uri, ValueKind::Float(f64::from(d))), + }, + serde_dhall::SimpleValue::Text(string) => Value::new(uri, ValueKind::String(string)), + serde_dhall::SimpleValue::List(list) => Value::new( + uri, + ValueKind::Array(list.into_iter().map(|v| from_dhall_value(uri, v)).collect()), + ), + serde_dhall::SimpleValue::Record(rec) => Value::new( + uri, + ValueKind::Table( + rec.into_iter() + .map(|(k, v)| (k, from_dhall_value(uri, v))) + .collect(), + ), + ), + serde_dhall::SimpleValue::Optional(Some(value)) + | serde_dhall::SimpleValue::Union(_, Some(value)) => from_dhall_value(uri, *value), + serde_dhall::SimpleValue::Optional(None) | serde_dhall::SimpleValue::Union(_, None) => { + Value::new(uri, ValueKind::Nil) + } + } +} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index 025e98a9..bdb46c02 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -27,6 +27,9 @@ mod ron; #[cfg(feature = "json5")] mod json5; +#[cfg(feature = "dhall")] +mod dhall; + /// File formats provided by the library. /// /// Although it is possible to define custom formats using [`Format`] trait it is recommended to use FileFormat if possible. @@ -55,6 +58,10 @@ pub enum FileFormat { /// JSON5 (parsed with json5) #[cfg(feature = "json5")] Json5, + + /// Dhall (parsed with serde_dhall) + #[cfg(feature = "dhall")] + Dhall, } lazy_static! { @@ -81,6 +88,9 @@ lazy_static! { #[cfg(feature = "json5")] formats.insert(FileFormat::Json5, vec!["json5"]); + #[cfg(feature = "dhall")] + formats.insert(FileFormat::Dhall, vec!["dhall"]); + formats }; } @@ -117,6 +127,9 @@ impl FileFormat { #[cfg(feature = "json5")] FileFormat::Json5 => json5::parse(uri, text), + #[cfg(feature = "dhall")] + FileFormat::Dhall => dhall::parse(uri, text), + #[cfg(all( not(feature = "toml"), not(feature = "json"), @@ -124,6 +137,7 @@ impl FileFormat { not(feature = "ini"), not(feature = "ron"), not(feature = "json5"), + not(feature = "dhall"), ))] _ => unreachable!("No features are enabled, this library won't work without features"), } diff --git a/tests/Settings.dhall b/tests/Settings.dhall new file mode 100644 index 00000000..9b88caf2 --- /dev/null +++ b/tests/Settings.dhall @@ -0,0 +1,15 @@ +{ + debug = True +, debug_json = True +, production = False +, arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +, place = { + name = "Torre di Pisa" +, longitude = 43.7224985 +, latitude = 10.3970522 +, favorite = False +, reviews = 3866 +, rating = 4.5 +, creator.name = "John Smith" + } +} diff --git a/tests/file_dhall.rs b/tests/file_dhall.rs new file mode 100644 index 00000000..f45c854a --- /dev/null +++ b/tests/file_dhall.rs @@ -0,0 +1,86 @@ +#![cfg(feature = "dhall")] + +extern crate config; +extern crate float_cmp; +extern crate serde; + +#[macro_use] +extern crate serde_derive; + +use std::collections::HashMap; + +use config::*; +use float_cmp::ApproxEqUlps; + +#[derive(Debug, Deserialize)] +struct Place { + name: String, + longitude: f64, + latitude: f64, + favorite: bool, + telephone: Option, + reviews: u64, + creator: HashMap, + rating: Option, +} + +#[derive(Debug, Deserialize)] +struct Settings { + debug: f64, + production: Option, + place: Place, + #[serde(rename = "arr")] + elements: Vec, +} + +fn make() -> Config { + Config::builder() + .add_source(File::new("tests/Settings", FileFormat::Dhall)) + .build() + .unwrap() +} + +#[test] +fn test_file() { + let c = make(); + + // Deserialize the entire file as single struct + let s: Settings = c.try_into().unwrap(); + + assert!(s.debug.approx_eq_ulps(&1.0, 2)); + assert_eq!(s.production, Some("false".to_string())); + assert_eq!(s.place.name, "Torre di Pisa"); + assert!(s.place.longitude.approx_eq_ulps(&43.7224985, 2)); + assert!(s.place.latitude.approx_eq_ulps(&10.3970522, 2)); + assert_eq!(s.place.favorite, false); + assert_eq!(s.place.reviews, 3866); + assert_eq!(s.place.rating, Some(4.5)); + assert_eq!(s.place.telephone, None); + assert_eq!(s.elements.len(), 10); + assert_eq!(s.elements[3], "4".to_string()); + assert_eq!( + s.place.creator["name"].clone().into_string().unwrap(), + "John Smith".to_string() + ); +} + +#[test] +fn test_dhall_vec() { + let c = Config::builder() + .add_source(File::from_str( + r#" + { + WASTE = ["example_dir1", "example_dir2"] + } + "#, + FileFormat::Dhall, + )) + .build() + .unwrap(); + + let v = c.get_array("WASTE").unwrap(); + let mut vi = v.into_iter(); + assert_eq!(vi.next().unwrap().into_string().unwrap(), "example_dir1"); + assert_eq!(vi.next().unwrap().into_string().unwrap(), "example_dir2"); + assert!(vi.next().is_none()); +}