diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2021-05-15 15:02:37 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-15 15:02:37 +0200 |
commit | 3e5cae69c4a0057ed523df2e8d42db83f7e684ba (patch) | |
tree | 5d70657498b8be4a7ecf13e1304c85256d2b308b | |
parent | 266f504d9f23e192c03ef486f58a678847249b60 (diff) | |
parent | c461a6ff6b141b4300c4fc590c07d5896cfc4c5c (diff) |
Merge pull request #206 from matthiasbeyer/json5-support
Json5 support
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | src/error.rs | 12 | ||||
-rw-r--r-- | src/file/format/json5.rs | 66 | ||||
-rw-r--r-- | src/file/format/mod.rs | 14 | ||||
-rw-r--r-- | src/lib.rs | 6 | ||||
-rw-r--r-- | tests/Settings-invalid.json5 | 4 | ||||
-rw-r--r-- | tests/Settings.json5 | 18 | ||||
-rw-r--r-- | tests/errors.rs | 17 | ||||
-rw-r--r-- | tests/file_json5.rs | 83 |
10 files changed, 225 insertions, 4 deletions
@@ -15,11 +15,12 @@ edition = "2018" maintenance = { status = "actively-developed" } [features] -default = ["toml", "json", "yaml", "hjson", "ini", "ron"] +default = ["toml", "json", "yaml", "hjson", "ini", "ron", "json5"] json = ["serde_json"] yaml = ["yaml-rust"] hjson = ["serde-hjson"] ini = ["rust-ini"] +json5 = ["json5_rs"] [dependencies] lazy_static = "1.0" @@ -32,6 +33,7 @@ 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 } +json5_rs = { version = "0.3", optional = true, package = "json5" } [dev-dependencies] serde_derive = "1.0.8" @@ -1,4 +1,5 @@ # config-rs + ![Rust](https://img.shields.io/badge/rust-stable-brightgreen.svg) [![Build Status](https://travis-ci.org/mehcode/config-rs.svg?branch=master)](https://travis-ci.org/mehcode/config-rs) [![Crates.io](https://img.shields.io/crates/d/config.svg)](https://crates.io/crates/config) @@ -10,7 +11,7 @@ - Set defaults - Set explicit values (to programmatically override) - - Read from [JSON], [TOML], [YAML], [HJSON], [INI], [RON] files + - Read from [JSON], [TOML], [YAML], [HJSON], [INI], [RON], [JSON5] 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` ) @@ -21,6 +22,7 @@ [HJSON]: https://github.com/hjson/hjson-rust [INI]: https://github.com/zonyitoo/rust-ini [RON]: https://github.com/ron-rs/ron +[JSON5]: https://github.com/callum-oakley/json5-rs ## Usage @@ -35,6 +37,7 @@ config = "0.11" - `yaml` - Adds support for reading YAML files - `toml` - Adds support for reading TOML files - `ron` - Adds support for reading RON files + - `json5` - Adds support for reading JSON5 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/error.rs b/src/error.rs index 28142b5..9778649 100644 --- a/src/error.rs +++ b/src/error.rs @@ -94,6 +94,18 @@ impl ConfigError { } } + // Have a proper error fire if the root of a file is ever not a Table + // TODO: for now only json5 checked, need to finish others + #[doc(hidden)] + pub fn invalid_root(origin: Option<&String>, unexpected: Unexpected) -> Box<Self> { + Box::new(ConfigError::Type { + origin: origin.map(|s| s.to_owned()), + unexpected, + expected: "a map", + key: None, + }) + } + // FIXME: pub(crate) #[doc(hidden)] pub fn extend_with_key(self, key: &str) -> Self { diff --git a/src/file/format/json5.rs b/src/file/format/json5.rs new file mode 100644 index 0000000..33a3135 --- /dev/null +++ b/src/file/format/json5.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; +use std::error::Error; + +use crate::error::{ConfigError, Unexpected}; +use crate::value::{Value, ValueKind}; + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub enum Val { + Null, + Boolean(bool), + Integer(i64), + Float(f64), + String(String), + Array(Vec<Val>), + Object(HashMap<String, Val>), +} + +pub fn parse( + uri: Option<&String>, + text: &str, +) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { + match json5_rs::from_str::<Val>(&text)? { + Val::String(ref value) => Err(Unexpected::Str(value.clone())), + Val::Integer(value) => Err(Unexpected::Integer(value)), + Val::Float(value) => Err(Unexpected::Float(value)), + Val::Boolean(value) => Err(Unexpected::Bool(value)), + Val::Array(_) => Err(Unexpected::Seq), + Val::Null => Err(Unexpected::Unit), + Val::Object(o) => match from_json5_value(uri, Val::Object(o)).kind { + ValueKind::Table(map) => Ok(map), + _ => Ok(HashMap::new()), + }, + } + .map_err(|err| ConfigError::invalid_root(uri, err)) + .map_err(|err| Box::new(err) as Box<dyn Error + Send + Sync>) +} + +fn from_json5_value(uri: Option<&String>, value: Val) -> Value { + let vk = match value { + Val::Null => ValueKind::Nil, + Val::String(v) => ValueKind::String(v), + Val::Integer(v) => ValueKind::Integer(v), + Val::Float(v) => ValueKind::Float(v), + Val::Boolean(v) => ValueKind::Boolean(v), + Val::Object(table) => { + let m = table + .into_iter() + .map(|(k, v)| (k, from_json5_value(uri, v))) + .collect(); + + ValueKind::Table(m) + } + + Val::Array(array) => { + let l = array + .into_iter() + .map(|v| from_json5_value(uri, v)) + .collect(); + + ValueKind::Array(l) + } + }; + + Value::new(uri, vk) +} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index bbd62a2..53bacf6 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -25,6 +25,9 @@ mod ini; #[cfg(feature = "ron")] mod ron; +#[cfg(feature = "json5")] +mod json5; + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum FileFormat { /// TOML (parsed with toml) @@ -42,6 +45,7 @@ pub enum FileFormat { /// HJSON (parsed with serde_hjson) #[cfg(feature = "hjson")] Hjson, + /// INI (parsed with rust_ini) #[cfg(feature = "ini")] Ini, @@ -49,6 +53,10 @@ pub enum FileFormat { /// RON (parsed with ron) #[cfg(feature = "ron")] Ron, + + /// JSON5 (parsed with json5) + #[cfg(feature = "json5")] + Json5, } lazy_static! { @@ -75,6 +83,9 @@ lazy_static! { #[cfg(feature = "ron")] formats.insert(FileFormat::Ron, vec!["ron"]); + #[cfg(feature = "json5")] + formats.insert(FileFormat::Json5, vec!["json5"]); + formats }; } @@ -115,6 +126,9 @@ impl FileFormat { #[cfg(feature = "ron")] FileFormat::Ron => ron::parse(uri, text), + + #[cfg(feature = "json5")] + FileFormat::Json5 => json5::parse(uri, text), } } } @@ -6,7 +6,7 @@ //! - Environment variables //! - Another Config instance //! - Remote configuration: etcd, Consul -//! - Files: TOML, JSON, YAML, HJSON, INI, RON +//! - Files: TOML, JSON, YAML, HJSON, INI, RON, JSON5 //! - Manual, programmatic override (via a `.set` method on the Config instance) //! //! Additionally, Config supports: @@ -25,7 +25,6 @@ extern crate serde; #[cfg(test)] -#[macro_use] extern crate serde_derive; extern crate nom; @@ -51,6 +50,9 @@ extern crate ini; #[cfg(feature = "ron")] extern crate ron; +#[cfg(feature = "json5")] +extern crate json5_rs; + mod builder; mod config; mod de; diff --git a/tests/Settings-invalid.json5 b/tests/Settings-invalid.json5 new file mode 100644 index 0000000..7e97bc1 --- /dev/null +++ b/tests/Settings-invalid.json5 @@ -0,0 +1,4 @@ +{ + ok: true + error +} diff --git a/tests/Settings.json5 b/tests/Settings.json5 new file mode 100644 index 0000000..cfab1f5 --- /dev/null +++ b/tests/Settings.json5 @@ -0,0 +1,18 @@ +{ + // c + /* c */ + 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/errors.rs b/tests/errors.rs index 54bbe95..e11ad69 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -137,3 +137,20 @@ inner: panic!("Wrong error {:?}", e); } } + +#[test] +fn test_error_root_not_table() { + match Config::builder() + .add_source(File::from_str(r#"false"#, FileFormat::Json5)) + .build() + { + Ok(_) => panic!("Should not merge if root is not a table"), + Err(e) => match e { + ConfigError::FileParse { cause, .. } => assert_eq!( + "invalid type: boolean `false`, expected a map", + format!("{}", cause) + ), + _ => panic!("Wrong error: {:?}", e), + }, + } +} diff --git a/tests/file_json5.rs b/tests/file_json5.rs new file mode 100644 index 0000000..d22b726 --- /dev/null +++ b/tests/file_json5.rs @@ -0,0 +1,83 @@ +#![cfg(feature = "json5")] + +extern crate config; +extern crate float_cmp; +extern crate serde; + +#[macro_use] +extern crate serde_derive; + +use config::*; +use float_cmp::ApproxEqUlps; +use std::collections::HashMap; +use std::path::PathBuf; + +#[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::Json5)) + .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_error_parse() { + let res = Config::builder() + .add_source(File::new("tests/Settings-invalid", FileFormat::Json5)) + .build(); + + let path_with_extension: PathBuf = ["tests", "Settings-invalid.json5"].iter().collect(); + + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + format!( + " --> 2:7\n |\n2 | ok: true␊\n | ^---\n |\n = expected null in {}", + path_with_extension.display() + ) + ); +} |