summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorup9cloud <8325632+up9cloud@users.noreply.github.com>2020-07-06 13:07:05 -0700
committerMatthias Beyer <mail@beyermatthias.de>2021-05-15 14:35:58 +0200
commit70c503af8b3cb3d73bbbea673bb49460df318e5e (patch)
tree3550f1015af6186e5f6c1cade83345ee48609e22
parent266f504d9f23e192c03ef486f58a678847249b60 (diff)
Support format json5
-rw-r--r--Cargo.toml5
-rw-r--r--README.md5
-rw-r--r--src/error.rs12
-rw-r--r--src/file/format/json5.rs78
-rw-r--r--src/file/format/mod.rs14
-rw-r--r--src/lib.rs8
-rw-r--r--tests/Settings-invalid.json54
-rw-r--r--tests/Settings.json518
-rw-r--r--tests/errors.rs17
-rw-r--r--tests/file_json5.rs83
10 files changed, 239 insertions, 5 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ca28c06..929f7ff 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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", "serde_derive"]
[dependencies]
lazy_static = "1.0"
@@ -32,6 +33,8 @@ 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" }
+serde_derive = { version = "1.0.8", optional = true }
[dev-dependencies]
serde_derive = "1.0.8"
diff --git a/README.md b/README.md
index 7f43c62..07fcadf 100644
--- a/README.md
+++ b/README.md
@@ -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..320e574
--- /dev/null
+++ b/src/file/format/json5.rs
@@ -0,0 +1,78 @@
+use serde_derive::Deserialize;
+
+use std::collections::HashMap;
+use std::error::Error;
+
+use crate::error::{ConfigError, Unexpected};
+use crate::value::{Value, ValueKind};
+
+#[derive(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>> {
+ let root = json5_rs::from_str::<Val>(&text)?;
+ if let Some(err) = match root {
+ Val::String(ref value) => Some(Unexpected::Str(value.clone())),
+ Val::Integer(value) => Some(Unexpected::Integer(value)),
+ Val::Float(value) => Some(Unexpected::Float(value)),
+ Val::Boolean(value) => Some(Unexpected::Bool(value)),
+ Val::Object(_) => None,
+ Val::Array(_) => Some(Unexpected::Seq),
+ Val::Null => Some(Unexpected::Unit),
+ } {
+ return Err(ConfigError::invalid_root(uri, err));
+ }
+
+ let value = from_json5_value(uri, root);
+ match value.kind {
+ ValueKind::Table(map) => Ok(map),
+
+ _ => Ok(HashMap::new()),
+ }
+}
+
+fn from_json5_value(uri: Option<&String>, value: Val) -> Value {
+ match value {
+ Val::String(v) => Value::new(uri, ValueKind::String(v)),
+
+ Val::Integer(v) => Value::new(uri, ValueKind::Integer(v)),
+
+ Val::Float(v) => Value::new(uri, ValueKind::Float(v)),
+
+ Val::Boolean(v) => Value::new(uri, ValueKind::Boolean(v)),
+
+ Val::Object(table) => {
+ let mut m = HashMap::new();
+
+ for (key, value) in table {
+ m.insert(key, from_json5_value(uri, value));
+ }
+
+ Value::new(uri, ValueKind::Table(m))
+ }
+
+ Val::Array(array) => {
+ let mut l = Vec::new();
+
+ for value in array {
+ l.push(from_json5_value(uri, value));
+ }
+
+ Value::new(uri, ValueKind::Array(l))
+ }
+
+ Val::Null => Value::new(uri, ValueKind::Nil),
+ }
+}
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),
}
}
}
diff --git a/src/lib.rs b/src/lib.rs
index ae02c36..986f36a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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:
@@ -24,8 +24,7 @@
#[macro_use]
extern crate serde;
-#[cfg(test)]
-#[macro_use]
+#[cfg(any(test, feature = "json5"))]
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()
+ )
+ );
+}