Skip to content

Commit 3e5cae6

Browse files
Merge pull request #206 from matthiasbeyer/json5-support
Json5 support
2 parents 266f504 + c461a6f commit 3e5cae6

File tree

10 files changed

+225
-4
lines changed

10 files changed

+225
-4
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ edition = "2018"
1515
maintenance = { status = "actively-developed" }
1616

1717
[features]
18-
default = ["toml", "json", "yaml", "hjson", "ini", "ron"]
18+
default = ["toml", "json", "yaml", "hjson", "ini", "ron", "json5"]
1919
json = ["serde_json"]
2020
yaml = ["yaml-rust"]
2121
hjson = ["serde-hjson"]
2222
ini = ["rust-ini"]
23+
json5 = ["json5_rs"]
2324

2425
[dependencies]
2526
lazy_static = "1.0"
@@ -32,6 +33,7 @@ yaml-rust = { version = "0.4", optional = true }
3233
serde-hjson = { version = "0.9", default-features = false, optional = true }
3334
rust-ini = { version = "0.17", optional = true }
3435
ron = { version = "0.6", optional = true }
36+
json5_rs = { version = "0.3", optional = true, package = "json5" }
3537

3638
[dev-dependencies]
3739
serde_derive = "1.0.8"

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# config-rs
2+
23
![Rust](https://img.shields.io/badge/rust-stable-brightgreen.svg)
34
[![Build Status](https://travis-ci.org/mehcode/config-rs.svg?branch=master)](https://travis-ci.org/mehcode/config-rs)
45
[![Crates.io](https://img.shields.io/crates/d/config.svg)](https://crates.io/crates/config)
@@ -10,7 +11,7 @@
1011

1112
- Set defaults
1213
- Set explicit values (to programmatically override)
13-
- Read from [JSON], [TOML], [YAML], [HJSON], [INI], [RON] files
14+
- Read from [JSON], [TOML], [YAML], [HJSON], [INI], [RON], [JSON5] files
1415
- Read from environment
1516
- Loosely typed — Configuration values may be read in any supported type, as long as there exists a reasonable conversion
1617
- 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 @@
2122
[HJSON]: https://github.com/hjson/hjson-rust
2223
[INI]: https://github.com/zonyitoo/rust-ini
2324
[RON]: https://github.com/ron-rs/ron
25+
[JSON5]: https://github.com/callum-oakley/json5-rs
2426

2527
## Usage
2628

@@ -35,6 +37,7 @@ config = "0.11"
3537
- `yaml` - Adds support for reading YAML files
3638
- `toml` - Adds support for reading TOML files
3739
- `ron` - Adds support for reading RON files
40+
- `json5` - Adds support for reading JSON5 files
3841

3942
See the [documentation](https://docs.rs/config) or [examples](https://github.com/mehcode/config-rs/tree/master/examples) for
4043
more usage information.

src/error.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ impl ConfigError {
9494
}
9595
}
9696

97+
// Have a proper error fire if the root of a file is ever not a Table
98+
// TODO: for now only json5 checked, need to finish others
99+
#[doc(hidden)]
100+
pub fn invalid_root(origin: Option<&String>, unexpected: Unexpected) -> Box<Self> {
101+
Box::new(ConfigError::Type {
102+
origin: origin.map(|s| s.to_owned()),
103+
unexpected,
104+
expected: "a map",
105+
key: None,
106+
})
107+
}
108+
97109
// FIXME: pub(crate)
98110
#[doc(hidden)]
99111
pub fn extend_with_key(self, key: &str) -> Self {

src/file/format/json5.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::collections::HashMap;
2+
use std::error::Error;
3+
4+
use crate::error::{ConfigError, Unexpected};
5+
use crate::value::{Value, ValueKind};
6+
7+
#[derive(serde::Deserialize, Debug)]
8+
#[serde(untagged)]
9+
pub enum Val {
10+
Null,
11+
Boolean(bool),
12+
Integer(i64),
13+
Float(f64),
14+
String(String),
15+
Array(Vec<Val>),
16+
Object(HashMap<String, Val>),
17+
}
18+
19+
pub fn parse(
20+
uri: Option<&String>,
21+
text: &str,
22+
) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> {
23+
match json5_rs::from_str::<Val>(&text)? {
24+
Val::String(ref value) => Err(Unexpected::Str(value.clone())),
25+
Val::Integer(value) => Err(Unexpected::Integer(value)),
26+
Val::Float(value) => Err(Unexpected::Float(value)),
27+
Val::Boolean(value) => Err(Unexpected::Bool(value)),
28+
Val::Array(_) => Err(Unexpected::Seq),
29+
Val::Null => Err(Unexpected::Unit),
30+
Val::Object(o) => match from_json5_value(uri, Val::Object(o)).kind {
31+
ValueKind::Table(map) => Ok(map),
32+
_ => Ok(HashMap::new()),
33+
},
34+
}
35+
.map_err(|err| ConfigError::invalid_root(uri, err))
36+
.map_err(|err| Box::new(err) as Box<dyn Error + Send + Sync>)
37+
}
38+
39+
fn from_json5_value(uri: Option<&String>, value: Val) -> Value {
40+
let vk = match value {
41+
Val::Null => ValueKind::Nil,
42+
Val::String(v) => ValueKind::String(v),
43+
Val::Integer(v) => ValueKind::Integer(v),
44+
Val::Float(v) => ValueKind::Float(v),
45+
Val::Boolean(v) => ValueKind::Boolean(v),
46+
Val::Object(table) => {
47+
let m = table
48+
.into_iter()
49+
.map(|(k, v)| (k, from_json5_value(uri, v)))
50+
.collect();
51+
52+
ValueKind::Table(m)
53+
}
54+
55+
Val::Array(array) => {
56+
let l = array
57+
.into_iter()
58+
.map(|v| from_json5_value(uri, v))
59+
.collect();
60+
61+
ValueKind::Array(l)
62+
}
63+
};
64+
65+
Value::new(uri, vk)
66+
}

src/file/format/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ mod ini;
2525
#[cfg(feature = "ron")]
2626
mod ron;
2727

28+
#[cfg(feature = "json5")]
29+
mod json5;
30+
2831
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
2932
pub enum FileFormat {
3033
/// TOML (parsed with toml)
@@ -42,13 +45,18 @@ pub enum FileFormat {
4245
/// HJSON (parsed with serde_hjson)
4346
#[cfg(feature = "hjson")]
4447
Hjson,
48+
4549
/// INI (parsed with rust_ini)
4650
#[cfg(feature = "ini")]
4751
Ini,
4852

4953
/// RON (parsed with ron)
5054
#[cfg(feature = "ron")]
5155
Ron,
56+
57+
/// JSON5 (parsed with json5)
58+
#[cfg(feature = "json5")]
59+
Json5,
5260
}
5361

5462
lazy_static! {
@@ -75,6 +83,9 @@ lazy_static! {
7583
#[cfg(feature = "ron")]
7684
formats.insert(FileFormat::Ron, vec!["ron"]);
7785

86+
#[cfg(feature = "json5")]
87+
formats.insert(FileFormat::Json5, vec!["json5"]);
88+
7889
formats
7990
};
8091
}
@@ -115,6 +126,9 @@ impl FileFormat {
115126

116127
#[cfg(feature = "ron")]
117128
FileFormat::Ron => ron::parse(uri, text),
129+
130+
#[cfg(feature = "json5")]
131+
FileFormat::Json5 => json5::parse(uri, text),
118132
}
119133
}
120134
}

src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! - Environment variables
77
//! - Another Config instance
88
//! - Remote configuration: etcd, Consul
9-
//! - Files: TOML, JSON, YAML, HJSON, INI, RON
9+
//! - Files: TOML, JSON, YAML, HJSON, INI, RON, JSON5
1010
//! - Manual, programmatic override (via a `.set` method on the Config instance)
1111
//!
1212
//! Additionally, Config supports:
@@ -25,7 +25,6 @@
2525
extern crate serde;
2626

2727
#[cfg(test)]
28-
#[macro_use]
2928
extern crate serde_derive;
3029

3130
extern crate nom;
@@ -51,6 +50,9 @@ extern crate ini;
5150
#[cfg(feature = "ron")]
5251
extern crate ron;
5352

53+
#[cfg(feature = "json5")]
54+
extern crate json5_rs;
55+
5456
mod builder;
5557
mod config;
5658
mod de;

tests/Settings-invalid.json5

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
ok: true
3+
error
4+
}

tests/Settings.json5

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
// c
3+
/* c */
4+
debug: true,
5+
production: false,
6+
arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,],
7+
place: {
8+
name: 'Torre di Pisa',
9+
longitude: 43.7224985,
10+
latitude: 10.3970522,
11+
favorite: false,
12+
reviews: 3866,
13+
rating: 4.5,
14+
creator: {
15+
name: "John Smith",
16+
}
17+
}
18+
}

tests/errors.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,20 @@ inner:
137137
panic!("Wrong error {:?}", e);
138138
}
139139
}
140+
141+
#[test]
142+
fn test_error_root_not_table() {
143+
match Config::builder()
144+
.add_source(File::from_str(r#"false"#, FileFormat::Json5))
145+
.build()
146+
{
147+
Ok(_) => panic!("Should not merge if root is not a table"),
148+
Err(e) => match e {
149+
ConfigError::FileParse { cause, .. } => assert_eq!(
150+
"invalid type: boolean `false`, expected a map",
151+
format!("{}", cause)
152+
),
153+
_ => panic!("Wrong error: {:?}", e),
154+
},
155+
}
156+
}

tests/file_json5.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#![cfg(feature = "json5")]
2+
3+
extern crate config;
4+
extern crate float_cmp;
5+
extern crate serde;
6+
7+
#[macro_use]
8+
extern crate serde_derive;
9+
10+
use config::*;
11+
use float_cmp::ApproxEqUlps;
12+
use std::collections::HashMap;
13+
use std::path::PathBuf;
14+
15+
#[derive(Debug, Deserialize)]
16+
struct Place {
17+
name: String,
18+
longitude: f64,
19+
latitude: f64,
20+
favorite: bool,
21+
telephone: Option<String>,
22+
reviews: u64,
23+
creator: HashMap<String, Value>,
24+
rating: Option<f32>,
25+
}
26+
27+
#[derive(Debug, Deserialize)]
28+
struct Settings {
29+
debug: f64,
30+
production: Option<String>,
31+
place: Place,
32+
#[serde(rename = "arr")]
33+
elements: Vec<String>,
34+
}
35+
36+
fn make() -> Config {
37+
Config::builder()
38+
.add_source(File::new("tests/Settings", FileFormat::Json5))
39+
.build()
40+
.unwrap()
41+
}
42+
43+
#[test]
44+
fn test_file() {
45+
let c = make();
46+
47+
// Deserialize the entire file as single struct
48+
let s: Settings = c.try_into().unwrap();
49+
50+
assert!(s.debug.approx_eq_ulps(&1.0, 2));
51+
assert_eq!(s.production, Some("false".to_string()));
52+
assert_eq!(s.place.name, "Torre di Pisa");
53+
assert!(s.place.longitude.approx_eq_ulps(&43.7224985, 2));
54+
assert!(s.place.latitude.approx_eq_ulps(&10.3970522, 2));
55+
assert_eq!(s.place.favorite, false);
56+
assert_eq!(s.place.reviews, 3866);
57+
assert_eq!(s.place.rating, Some(4.5));
58+
assert_eq!(s.place.telephone, None);
59+
assert_eq!(s.elements.len(), 10);
60+
assert_eq!(s.elements[3], "4".to_string());
61+
assert_eq!(
62+
s.place.creator["name"].clone().into_string().unwrap(),
63+
"John Smith".to_string()
64+
);
65+
}
66+
67+
#[test]
68+
fn test_error_parse() {
69+
let res = Config::builder()
70+
.add_source(File::new("tests/Settings-invalid", FileFormat::Json5))
71+
.build();
72+
73+
let path_with_extension: PathBuf = ["tests", "Settings-invalid.json5"].iter().collect();
74+
75+
assert!(res.is_err());
76+
assert_eq!(
77+
res.unwrap_err().to_string(),
78+
format!(
79+
" --> 2:7\n |\n2 | ok: true␊\n | ^---\n |\n = expected null in {}",
80+
path_with_extension.display()
81+
)
82+
);
83+
}

0 commit comments

Comments
 (0)