From 70c503af8b3cb3d73bbbea673bb49460df318e5e Mon Sep 17 00:00:00 2001 From: up9cloud <8325632+up9cloud@users.noreply.github.com> Date: Mon, 6 Jul 2020 13:07:05 -0700 Subject: Support format json5 --- Cargo.toml | 5 ++- README.md | 5 ++- src/error.rs | 12 +++++++ src/file/format/json5.rs | 78 +++++++++++++++++++++++++++++++++++++++++ src/file/format/mod.rs | 14 ++++++++ src/lib.rs | 8 +++-- tests/Settings-invalid.json5 | 4 +++ tests/Settings.json5 | 18 ++++++++++ tests/errors.rs | 17 +++++++++ tests/file_json5.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 src/file/format/json5.rs create mode 100644 tests/Settings-invalid.json5 create mode 100644 tests/Settings.json5 create mode 100644 tests/file_json5.rs 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 { + 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), + Object(HashMap), +} + +pub fn parse( + uri: Option<&String>, + text: &str, +) -> Result, Box> { + let root = json5_rs::from_str::(&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, + 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 { + 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() + ) + ); +} -- cgit v1.2.3