From 9826b2cda730116e148120dafe0fa89bd389626e Mon Sep 17 00:00:00 2001 From: Raphael Cohn Date: Fri, 1 Sep 2017 09:06:27 +0100 Subject: Added HJSON (Human-Readable JSON) as a config file format --- Cargo.toml | 4 ++- README.md | 4 ++- src/file/format/hjson.rs | 55 ++++++++++++++++++++++++++++++++ src/file/format/mod.rs | 13 ++++++++ src/lib.rs | 5 ++- tests/Settings-invalid.hjson | 4 +++ tests/Settings.hjson | 16 ++++++++++ tests/datetime.rs | 19 +++++++++++ tests/file_hjson.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 src/file/format/hjson.rs create mode 100644 tests/Settings-invalid.hjson create mode 100644 tests/Settings.hjson create mode 100644 tests/file_hjson.rs diff --git a/Cargo.toml b/Cargo.toml index 1317a27..c3789ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,10 @@ license = "MIT/Apache-2.0" travis-ci = { repository = "mehcode/config-rs" } [features] -default = ["toml", "json", "yaml"] +default = ["toml", "json", "yaml", "hjson"] json = ["serde_json"] yaml = ["yaml-rust"] +hjson = ["serde-hjson"] [dependencies] lazy_static = "0.2" @@ -26,6 +27,7 @@ nom = "^3.0.0" toml = { version = "^0.4.1", optional = true } serde_json = { version = "^1.0.2", optional = true } yaml-rust = { version = "^0.3.5", optional = true } +serde-hjson = { version = "^0.8.1", optional = true } [dev-dependencies] serde_derive = "^1.0.8" diff --git a/README.md b/README.md index 066e68e..9beaac6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - Set defaults - Set explicit values (to programmatically override) - - Read from [JSON], [TOML], and [YAML] files + - Read from [JSON], [TOML], [YAML] and [HJSON] files - Read from environment - Loosely typed — Configuration values may be read in any supported type, as long as there exists a reasonable conversion - Access nested fields using a formatted path — Uses a subset of JSONPath; currently supports the child ( `redis.port` ) and subscript operators ( `databases[0].name` ) @@ -18,6 +18,7 @@ [JSON]: https://github.com/serde-rs/json [TOML]: https://github.com/toml-lang/toml [YAML]: https://github.com/chyh1990/yaml-rust +[HJSON]: https://github.com/hjson/hjson-rust ## Usage @@ -27,6 +28,7 @@ config = "0.6" ``` - `json` - Adds support for reading JSON files + - `hjson` - Adds support for reading HJSON files - `yaml` - Adds support for reading YAML files - `toml` - Adds support for reading TOML files (included by default) diff --git a/src/file/format/hjson.rs b/src/file/format/hjson.rs new file mode 100644 index 0000000..3d4ad1b --- /dev/null +++ b/src/file/format/hjson.rs @@ -0,0 +1,55 @@ +use serde_hjson; +use source::Source; +use std::collections::HashMap; +use std::error::Error; +use value::{Value, ValueKind}; + +pub fn parse( + uri: Option<&String>, + text: &str, +) -> Result, Box> { + // Parse a JSON object value from the text + // TODO: Have a proper error fire if the root of a file is ever not a Table + let value = from_hjson_value(uri, &serde_hjson::from_str(text)?); + match value.kind { + ValueKind::Table(map) => Ok(map), + + _ => Ok(HashMap::new()), + } +} + +fn from_hjson_value(uri: Option<&String>, value: &serde_hjson::Value) -> Value { + match *value { + serde_hjson::Value::String(ref value) => Value::new(uri, ValueKind::String(value.clone())), + + serde_hjson::Value::I64(value) => Value::new(uri, ValueKind::Integer(value)), + + serde_hjson::Value::U64(value) => Value::new(uri, ValueKind::Integer(value as i64)), + + serde_hjson::Value::F64(value) => Value::new(uri, ValueKind::Float(value)), + + serde_hjson::Value::Bool(value) => Value::new(uri, ValueKind::Boolean(value)), + + serde_hjson::Value::Object(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.to_lowercase().clone(), from_hjson_value(uri, value)); + } + + Value::new(uri, ValueKind::Table(m)) + } + + serde_hjson::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_hjson_value(uri, value)); + } + + Value::new(uri, ValueKind::Array(l)) + } + + serde_hjson::Value::Null => Value::new(uri, ValueKind::Nil), + } +} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index a90dfda..5dfdfde 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -16,6 +16,9 @@ mod json; #[cfg(feature = "yaml")] mod yaml; +#[cfg(feature = "hjson")] +mod hjson; + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum FileFormat { /// TOML (parsed with toml) @@ -29,6 +32,10 @@ pub enum FileFormat { /// YAML (parsed with yaml_rust) #[cfg(feature = "yaml")] Yaml, + + /// HJSON (parsed with serde_hjson) + #[cfg(feature = "hjson")] + Hjson, } lazy_static! { @@ -46,6 +53,9 @@ lazy_static! { #[cfg(feature = "yaml")] formats.insert(FileFormat::Yaml, vec!["yaml", "yml"]); + #[cfg(feature = "hjson")] + formats.insert(FileFormat::Hjson, vec!["hjson"]); + formats }; } @@ -77,6 +87,9 @@ impl FileFormat { #[cfg(feature = "yaml")] FileFormat::Yaml => yaml::parse(uri, text), + + #[cfg(feature = "hjson")] + FileFormat::Hjson => hjson::parse(uri, text), } } } diff --git a/src/lib.rs b/src/lib.rs index bed60b4..b6eb0f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! - Environment variables //! - Another Config instance //! - Remote configuration: etcd, Consul -//! - Files: JSON, YAML, TOML +//! - Files: JSON, YAML, TOML, HJSON //! - Manual, programmatic override (via a `.set` method on the Config instance) //! //! Additionally, Config supports: @@ -38,6 +38,9 @@ extern crate serde_json; #[cfg(feature = "yaml")] extern crate yaml_rust; +#[cfg(feature = "hjson")] +extern crate serde_hjson; + mod error; mod value; mod de; diff --git a/tests/Settings-invalid.hjson b/tests/Settings-invalid.hjson new file mode 100644 index 0000000..7e31ec3 --- /dev/null +++ b/tests/Settings-invalid.hjson @@ -0,0 +1,4 @@ +{ + ok: true, + error +} diff --git a/tests/Settings.hjson b/tests/Settings.hjson new file mode 100644 index 0000000..3e04ccf --- /dev/null +++ b/tests/Settings.hjson @@ -0,0 +1,16 @@ +{ + debug: 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/datetime.rs b/tests/datetime.rs index 8a50c01..89a34a1 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -29,6 +29,15 @@ fn make() -> Config { FileFormat::Toml, )) .unwrap() + .merge(File::from_str( + r#" + { + "hjson_datetime": "2017-05-10T02:14:53Z" + } + "#, + FileFormat::Hjson, + )) + .unwrap() .clone() } @@ -50,6 +59,11 @@ fn test_datetime_string() { let date: String = s.get("yaml_datetime").unwrap(); assert_eq!(&date, "2017-06-12T10:58:30Z"); + + // HJSON + let date: String = s.get("hjson_datetime").unwrap(); + + assert_eq!(&date, "2017-05-10T02:14:53Z"); } #[test] @@ -70,4 +84,9 @@ fn test_datetime() { let date: DateTime = s.get("yaml_datetime").unwrap(); assert_eq!(date, Utc.ymd(2017, 6, 12).and_hms(10, 58, 30)); + + // HJSON + let date: DateTime = s.get("hjson_datetime").unwrap(); + + assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); } diff --git a/tests/file_hjson.rs b/tests/file_hjson.rs new file mode 100644 index 0000000..f36843a --- /dev/null +++ b/tests/file_hjson.rs @@ -0,0 +1,75 @@ +extern crate config; +extern crate serde; +extern crate float_cmp; + +#[macro_use] +extern crate serde_derive; + +use std::collections::HashMap; +use float_cmp::ApproxEqUlps; +use config::*; + +#[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 { + let mut c = Config::default(); + c.merge(File::new("tests/Settings", FileFormat::Hjson)) + .unwrap(); + + c +} + +#[test] +fn test_file() { + let c = make(); + + // Deserialize the entire file as single struct + let s: Settings = c.deserialize().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_str().unwrap(), + "John Smith".to_string() + ); +} + +#[test] +fn test_error_parse() { + let mut c = Config::default(); + let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Hjson)); + + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + "Found a punctuator where a key name was expected (check your syntax or use quotes if the key name includes {}[],: or whitespace) at line 1 column 1 in tests/Settings-invalid.hjson".to_string() + ); +} -- cgit v1.2.3