Skip to content

Commit 51286fd

Browse files
committed
feat: support jsonc format
1 parent 07f13ff commit 51286fd

16 files changed

+395
-2
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ json = ["serde_json"]
126126
yaml = ["yaml-rust2"]
127127
ini = ["rust-ini"]
128128
json5 = ["json5_rs", "serde/derive"]
129+
jsonc = ["jsonc-parser"]
129130
convert-case = ["convert_case"]
130-
preserve_order = ["indexmap", "toml?/preserve_order", "serde_json?/preserve_order", "ron?/indexmap"]
131+
preserve_order = ["indexmap", "toml?/preserve_order", "serde_json?/preserve_order", "jsonc-parser?/preserve_order", "ron?/indexmap"]
131132
async = ["async-trait"]
132133
toml = ["dep:toml"]
133134

@@ -141,6 +142,7 @@ yaml-rust2 = { version = "0.10", optional = true }
141142
rust-ini = { version = "0.21", optional = true }
142143
ron = { version = "0.8", optional = true }
143144
json5_rs = { version = "0.4", optional = true, package = "json5" }
145+
jsonc-parser = { version = "0.26.3", optional = true }
144146
indexmap = { version = "2.10.0", features = ["serde"], optional = true }
145147
convert_case = { version = "0.6", optional = true }
146148
pathdiff = "0.2"

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
- Set defaults
1313
- Set explicit values (to programmatically override)
14-
- Read from [JSON], [TOML], [YAML], [INI], [RON], [JSON5] files
14+
- Read from [JSON], [TOML], [YAML], [INI], [RON], [JSON5], [JSONC] files
1515
- Read from environment
1616
- Loosely typed — Configuration values may be read in any supported type, as long as there exists a reasonable conversion
1717
- Access nested fields using a formatted path — Uses a subset of JSONPath; currently supports the child ( `redis.port` ) and subscript operators ( `databases[0].name` )
@@ -22,6 +22,7 @@
2222
[INI]: https://github.com/zonyitoo/rust-ini
2323
[RON]: https://github.com/ron-rs/ron
2424
[JSON5]: https://github.com/callum-oakley/json5-rs
25+
[JSONC]: https://github.com/dprint/jsonc-parser
2526

2627
Please note that this library can not be used to write changed configuration
2728
values back to the configuration file(s)!
@@ -36,6 +37,7 @@ values back to the configuration file(s)!
3637
- `toml` - Adds support for reading TOML files
3738
- `ron` - Adds support for reading RON files
3839
- `json5` - Adds support for reading JSON5 files
40+
- `jsonc` - Adds support for reading JSONC files
3941

4042
### Support for custom formats
4143

src/file/format/jsonc.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use std::error::Error;
2+
3+
use crate::format;
4+
use crate::map::Map;
5+
use crate::value::{Value, ValueKind};
6+
use jsonc_parser::JsonValue;
7+
8+
pub(crate) fn parse(
9+
uri: Option<&String>,
10+
text: &str,
11+
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
12+
let value = match jsonc_parser::parse_to_value(text, &Default::default())? {
13+
Some(json_value) => from_jsonc_value(uri, json_value),
14+
None => Value::new(uri, ValueKind::Nil),
15+
};
16+
format::extract_root_table(uri, value)
17+
}
18+
19+
fn from_jsonc_value(uri: Option<&String>, value: JsonValue<'_>) -> Value {
20+
let vk = match value {
21+
JsonValue::Null => ValueKind::Nil,
22+
JsonValue::String(v) => ValueKind::String(v.to_string()),
23+
JsonValue::Number(number) => {
24+
if let Ok(v) = number.parse::<i64>() {
25+
ValueKind::I64(v)
26+
} else if let Ok(v) = number.parse::<f64>() {
27+
ValueKind::Float(v)
28+
} else {
29+
unreachable!();
30+
}
31+
}
32+
JsonValue::Boolean(v) => ValueKind::Boolean(v),
33+
JsonValue::Object(table) => {
34+
let m = table
35+
.into_iter()
36+
.map(|(k, v)| (k, from_jsonc_value(uri, v)))
37+
.collect();
38+
ValueKind::Table(m)
39+
}
40+
JsonValue::Array(array) => {
41+
let l = array
42+
.into_iter()
43+
.map(|v| from_jsonc_value(uri, v))
44+
.collect();
45+
ValueKind::Array(l)
46+
}
47+
};
48+
Value::new(uri, vk)
49+
}

src/file/format/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ mod ron;
2323
#[cfg(feature = "json5")]
2424
mod json5;
2525

26+
#[cfg(feature = "jsonc")]
27+
mod jsonc;
28+
2629
/// File formats provided by the library.
2730
///
2831
/// Although it is possible to define custom formats using [`Format`] trait it is recommended to use `FileFormat` if possible.
@@ -52,6 +55,10 @@ pub enum FileFormat {
5255
/// JSON5 (parsed with json5)
5356
#[cfg(feature = "json5")]
5457
Json5,
58+
59+
/// JSONC (parsed with jsonc)
60+
#[cfg(feature = "jsonc")]
61+
Jsonc,
5562
}
5663

5764
pub(crate) fn all_extensions() -> &'static HashMap<FileFormat, Vec<&'static str>> {
@@ -79,6 +86,12 @@ pub(crate) fn all_extensions() -> &'static HashMap<FileFormat, Vec<&'static str>
7986
#[cfg(feature = "json5")]
8087
formats.insert(FileFormat::Json5, vec!["json5"]);
8188

89+
#[cfg(all(feature = "jsonc", feature = "json"))]
90+
formats.insert(FileFormat::Jsonc, vec!["jsonc"]);
91+
92+
#[cfg(all(feature = "jsonc", not(feature = "json")))]
93+
formats.insert(FileFormat::Jsonc, vec!["jsonc", "json"]);
94+
8295
formats
8396
})
8497
}
@@ -115,13 +128,17 @@ impl FileFormat {
115128
#[cfg(feature = "json5")]
116129
FileFormat::Json5 => json5::parse(uri, text),
117130

131+
#[cfg(feature = "jsonc")]
132+
FileFormat::Jsonc => jsonc::parse(uri, text),
133+
118134
#[cfg(all(
119135
not(feature = "toml"),
120136
not(feature = "json"),
121137
not(feature = "yaml"),
122138
not(feature = "ini"),
123139
not(feature = "ron"),
124140
not(feature = "json5"),
141+
not(feature = "jsonc"),
125142
))]
126143
_ => unreachable!("No features are enabled, this library won't work without features"),
127144
}

tests/testsuite/file_ini.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,16 @@ fn ini() {
252252
let date: DateTime<Utc> = s.get("ini_datetime").unwrap();
253253
assert_eq!(date, Utc.with_ymd_and_hms(2017, 5, 10, 2, 14, 53).unwrap());
254254
}
255+
256+
#[test]
257+
fn test_nothing() {
258+
let res = Config::builder()
259+
.add_source(File::from_str("", FileFormat::Ini))
260+
.build();
261+
assert!(res.is_ok());
262+
let c = res.unwrap();
263+
assert_data_eq!(
264+
format!("{:?}", c),
265+
str!("Config { defaults: {}, overrides: {}, sources: [], cache: Value { origin: None, kind: Table({}) } }")
266+
);
267+
}

tests/testsuite/file_json.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,15 @@ fn json() {
316316
let date: DateTime<Utc> = s.get("json_datetime").unwrap();
317317
assert_eq!(date, Utc.with_ymd_and_hms(2017, 5, 10, 2, 14, 53).unwrap());
318318
}
319+
320+
#[test]
321+
fn test_nothing() {
322+
let res = Config::builder()
323+
.add_source(File::from_str("", FileFormat::Json))
324+
.build();
325+
assert!(res.is_err());
326+
assert_data_eq!(
327+
res.unwrap_err().to_string(),
328+
format!("EOF while parsing a value at line 1 column 0")
329+
);
330+
}

tests/testsuite/file_json5.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,15 @@ fn json() {
325325
let date: DateTime<Utc> = s.get("json_datetime").unwrap();
326326
assert_eq!(date, Utc.with_ymd_and_hms(2017, 5, 10, 2, 14, 53).unwrap());
327327
}
328+
329+
#[test]
330+
fn test_nothing() {
331+
let res = Config::builder()
332+
.add_source(File::from_str("", FileFormat::Json5))
333+
.build();
334+
assert!(res.is_err());
335+
assert_data_eq!(
336+
res.unwrap_err().to_string(),
337+
format!(" --> 1:1\n |\n1 | \n | ^---\n |\n = expected array, boolean, null, number, object, or string")
338+
);
339+
}

tests/testsuite/file_jsonc.enum.jsonc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
// foo
3+
"bar": "bar is a lowercase param",
4+
}
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+
}

0 commit comments

Comments
 (0)