From ba883ca731ea5222b0cadbad53b144fd4908f5f1 Mon Sep 17 00:00:00 2001 From: Levente Morva Date: Tue, 20 Apr 2021 09:34:04 +0200 Subject: [PATCH] Add support for RON format --- Cargo.toml | 3 +- README.md | 4 +- src/file/format/mod.rs | 13 ++++++ src/file/format/ron.rs | 68 +++++++++++++++++++++++++++++++ src/lib.rs | 5 ++- tests/Settings-invalid.ron | 4 ++ tests/Settings.ron | 18 +++++++++ tests/datetime.rs | 20 +++++++++ tests/file_ron.rs | 83 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 src/file/format/ron.rs create mode 100644 tests/Settings-invalid.ron create mode 100644 tests/Settings.ron create mode 100644 tests/file_ron.rs diff --git a/Cargo.toml b/Cargo.toml index bb265bb9..040124f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ license = "MIT/Apache-2.0" maintenance = { status = "actively-developed" } [features] -default = ["toml", "json", "yaml", "hjson", "ini"] +default = ["toml", "json", "yaml", "hjson", "ini", "ron"] json = ["serde_json"] yaml = ["yaml-rust"] hjson = ["serde-hjson"] @@ -30,6 +30,7 @@ serde_json = { version = "1.0.2", optional = true } yaml-rust = { version = "0.4", optional = true } serde-hjson = { version = "0.9", default-features = false, optional = true } rust-ini = { version = "0.17", optional = true } +ron = { version = "0.6", optional = true } [dev-dependencies] serde_derive = "1.0.8" diff --git a/README.md b/README.md index 243b42cf..7f43c628 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - Set defaults - Set explicit values (to programmatically override) - - Read from [JSON], [TOML], [YAML], [HJSON], [INI] files + - Read from [JSON], [TOML], [YAML], [HJSON], [INI], [RON] 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` ) @@ -20,6 +20,7 @@ [YAML]: https://github.com/chyh1990/yaml-rust [HJSON]: https://github.com/hjson/hjson-rust [INI]: https://github.com/zonyitoo/rust-ini +[RON]: https://github.com/ron-rs/ron ## Usage @@ -33,6 +34,7 @@ config = "0.11" - `hjson` - Adds support for reading HJSON files - `yaml` - Adds support for reading YAML files - `toml` - Adds support for reading TOML files + - `ron` - Adds support for reading RON files See the [documentation](https://docs.rs/config) or [examples](https://github.com/mehcode/config-rs/tree/master/examples) for more usage information. diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index 0ceeaf4b..bbd62a27 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -22,6 +22,9 @@ mod hjson; #[cfg(feature = "ini")] mod ini; +#[cfg(feature = "ron")] +mod ron; + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum FileFormat { /// TOML (parsed with toml) @@ -42,6 +45,10 @@ pub enum FileFormat { /// INI (parsed with rust_ini) #[cfg(feature = "ini")] Ini, + + /// RON (parsed with ron) + #[cfg(feature = "ron")] + Ron, } lazy_static! { @@ -65,6 +72,9 @@ lazy_static! { #[cfg(feature = "ini")] formats.insert(FileFormat::Ini, vec!["ini"]); + #[cfg(feature = "ron")] + formats.insert(FileFormat::Ron, vec!["ron"]); + formats }; } @@ -102,6 +112,9 @@ impl FileFormat { #[cfg(feature = "ini")] FileFormat::Ini => ini::parse(uri, text), + + #[cfg(feature = "ron")] + FileFormat::Ron => ron::parse(uri, text), } } } diff --git a/src/file/format/ron.rs b/src/file/format/ron.rs new file mode 100644 index 00000000..d3a97cb7 --- /dev/null +++ b/src/file/format/ron.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; +use std::error::Error; + +use ron; + +use crate::value::{Value, ValueKind}; + +pub fn parse( + uri: Option<&String>, + text: &str, +) -> Result, Box> { + let value = from_ron_value(uri, ron::from_str(text)?)?; + match value.kind { + ValueKind::Table(map) => Ok(map), + + _ => Ok(HashMap::new()), + } +} + +fn from_ron_value( + uri: Option<&String>, + value: ron::Value, +) -> Result> { + let kind = match value { + ron::Value::Option(value) => match value { + Some(value) => from_ron_value(uri, *value)?.kind, + None => ValueKind::Nil, + }, + + ron::Value::Unit => ValueKind::Nil, + + ron::Value::Bool(value) => ValueKind::Boolean(value), + + ron::Value::Number(value) => match value { + ron::Number::Float(value) => ValueKind::Float(value.get()), + ron::Number::Integer(value) => ValueKind::Integer(value), + }, + + ron::Value::Char(value) => ValueKind::String(value.to_string()), + + ron::Value::String(value) => ValueKind::String(value), + + ron::Value::Seq(values) => { + let array = values + .into_iter() + .map(|value| from_ron_value(uri, value)) + .collect::, _>>()?; + + ValueKind::Array(array) + } + + ron::Value::Map(values) => { + let map = values + .iter() + .map(|(key, value)| -> Result<_, Box> { + let key = key.clone().into_rust::()?; + let value = from_ron_value(uri, value.clone())?; + + Ok((key, value)) + }) + .collect::, _>>()?; + + ValueKind::Table(map) + } + }; + + Ok(Value::new(uri, kind)) +} diff --git a/src/lib.rs b/src/lib.rs index 96031183..03a82c18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! - Environment variables //! - Another Config instance //! - Remote configuration: etcd, Consul -//! - Files: JSON, YAML, TOML, HJSON +//! - Files: TOML, JSON, YAML, HJSON, INI, RON //! - Manual, programmatic override (via a `.set` method on the Config instance) //! //! Additionally, Config supports: @@ -48,6 +48,9 @@ extern crate serde_hjson; #[cfg(feature = "ini")] extern crate ini; +#[cfg(feature = "ron")] +extern crate ron; + mod config; mod de; mod env; diff --git a/tests/Settings-invalid.ron b/tests/Settings-invalid.ron new file mode 100644 index 00000000..0f41c5cd --- /dev/null +++ b/tests/Settings-invalid.ron @@ -0,0 +1,4 @@ +( + ok: true, + error +) diff --git a/tests/Settings.ron b/tests/Settings.ron new file mode 100644 index 00000000..528fd615 --- /dev/null +++ b/tests/Settings.ron @@ -0,0 +1,18 @@ +( + debug: true, + production: false, + arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + place: ( + initials: ('T', 'P'), + name: "Torre di Pisa", + longitude: 43.7224985, + latitude: 10.3970522, + favorite: false, + reviews: 3866, + rating: Some(4.5), + telephone: None, + creator: { + "name": "John Smith" + } + ) +) diff --git a/tests/datetime.rs b/tests/datetime.rs index 6c1e6201..471da478 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -4,6 +4,7 @@ feature = "hjson", feature = "yaml", feature = "ini", + feature = "ron", ))] extern crate chrono; @@ -53,6 +54,15 @@ fn make() -> Config { FileFormat::Ini, )) .unwrap() + .merge(File::from_str( + r#" + ( + ron_datetime: "2021-04-19T11:33:02Z" + ) + "#, + FileFormat::Ron, + )) + .unwrap() .clone() } @@ -84,6 +94,11 @@ fn test_datetime_string() { let date: String = s.get("ini_datetime").unwrap(); assert_eq!(&date, "2017-05-10T02:14:53Z"); + + // RON + let date: String = s.get("ron_datetime").unwrap(); + + assert_eq!(&date, "2021-04-19T11:33:02Z"); } #[test] @@ -114,4 +129,9 @@ fn test_datetime() { let date: DateTime = s.get("ini_datetime").unwrap(); assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); + + // RON + let date: DateTime = s.get("ron_datetime").unwrap(); + + assert_eq!(date, Utc.ymd(2021, 4, 19).and_hms(11, 33, 2)); } diff --git a/tests/file_ron.rs b/tests/file_ron.rs new file mode 100644 index 00000000..1f1ede22 --- /dev/null +++ b/tests/file_ron.rs @@ -0,0 +1,83 @@ +#![cfg(feature = "ron")] + +extern crate config; +extern crate float_cmp; +extern crate serde; + +#[macro_use] +extern crate serde_derive; + +use std::collections::HashMap; +use std::path::PathBuf; + +use config::*; +use float_cmp::ApproxEqUlps; + +#[derive(Debug, Deserialize)] +struct Place { + initials: (char, char), + 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::Ron)) + .unwrap(); + + c +} + +#[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.initials, ('T', 'P')); + 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_error_parse() { + let mut c = Config::default(); + let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Ron)); + + let path_with_extension: PathBuf = ["tests", "Settings-invalid.ron"].iter().collect(); + + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + format!("4:1: Expected colon in {}", path_with_extension.display()) + ); +}