Skip to content

Commit 70c503a

Browse files
up9cloudmatthiasbeyer
authored andcommitted
Support format json5
1 parent 266f504 commit 70c503a

File tree

10 files changed

+239
-5
lines changed

10 files changed

+239
-5
lines changed

Cargo.toml

Lines changed: 4 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", "serde_derive"]
2324

2425
[dependencies]
2526
lazy_static = "1.0"
@@ -32,6 +33,8 @@ 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" }
37+
serde_derive = { version = "1.0.8", optional = true }
3538

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

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: 5 additions & 3 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:
@@ -24,8 +24,7 @@
2424
#[macro_use]
2525
extern crate serde;
2626

27-
#[cfg(test)]
28-
#[macro_use]
27+
#[cfg(any(test, feature = "json5"))]
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)