Skip to content

Commit

Permalink
feat: add serde_dhall support
Browse files Browse the repository at this point in the history
  • Loading branch information
sphw authored and polarathene committed Oct 4, 2023
1 parent d7c1656 commit 1f1286e
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 1 deletion.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions src/file/format/dhall.rs
Original file line number Diff line number Diff line change
@@ -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<HashMap<String, Value>, Box<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>)
}

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)
}
}
}
14 changes: 14 additions & 0 deletions src/file/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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! {
Expand All @@ -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
};
}
Expand Down Expand Up @@ -117,13 +127,17 @@ 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"),
not(feature = "yaml"),
not(feature = "ini"),
not(feature = "ron"),
not(feature = "json5"),
not(feature = "dhall"),
))]
_ => unreachable!("No features are enabled, this library won't work without features"),
}
Expand Down
15 changes: 15 additions & 0 deletions tests/Settings.dhall
Original file line number Diff line number Diff line change
@@ -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"
}
}
86 changes: 86 additions & 0 deletions tests/file_dhall.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
reviews: u64,
creator: HashMap<String, Value>,
rating: Option<f32>,
}

#[derive(Debug, Deserialize)]
struct Settings {
debug: f64,
production: Option<String>,
place: Place,
#[serde(rename = "arr")]
elements: Vec<String>,
}

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());
}

0 comments on commit 1f1286e

Please sign in to comment.