figment_json5/
lib.rs

1//! A [`Format`] provider for JSON5.
2//!
3//! This crate provides a [Figment](https://docs.rs/figment/latest/figment/) provider for JSON5 files.
4//! JSON5 is a superset of JSON that allows comments, trailing commas, and more.
5//!
6//! This provider uses the [`json5`] crate for parsing.
7//!
8//! # Example
9//!
10//! ```rust
11//! use figment::Figment;
12//! use figment::providers::Format;
13//! use figment_json5::Json5;
14//! use serde::Deserialize;
15//!
16//! #[derive(Deserialize)]
17//! struct Config {
18//!     name: String,
19//!     description: String,
20//!
21//!     #[serde(rename = "leadingDecimalPoint")]
22//!     leading_decimal_point: f64,
23//!
24//!     #[serde(rename = "andTrailing")]
25//!     and_trailing: f64,
26//!
27//!     #[serde(rename = "positiveSign")]
28//!     positive_sign: i32,
29//!
30//!     age: u32
31//! }
32//!
33//! let config = r#"{
34//!     // Allow comments
35//!     "name": "powerumc",
36//!     "age": 0x28, // Allow hexadecimal numbers
37//!     "description": "This is a \
38//! test config", // Allow multiline strings
39//!     "leadingDecimalPoint": .8675309,
40//!     "andTrailing": 8675309.,
41//!     "positiveSign": +1,
42//!     "address": "Seoul", // Allow trailing commas
43//! }"#;
44//!
45//! let config: Config = Figment::new()
46//!    .merge(Json5::string(config))
47//!    .extract()
48//!    .unwrap();
49//!
50//! assert_eq!(config.name, "powerumc");
51//! assert_eq!(config.description, "This is a test config");
52//! assert_eq!(config.leading_decimal_point, 0.8675309);
53//! assert_eq!(config.and_trailing, 8675309.0);
54//! assert_eq!(config.positive_sign, 1);
55//! assert_eq!(config.age, 40);
56//! ```
57
58#[allow(unused_imports)]
59use figment::providers::{Data, Format};
60use figment::Provider;
61use serde::de::DeserializeOwned;
62
63#[allow(unused_macros)]
64macro_rules! impl_format {
65    ($name:ident $NAME:literal/$string:literal: $func:expr, $E:ty, $doc:expr) => (
66        #[doc = $doc]
67        pub struct $name;
68
69        impl Format for $name {
70            type Error = $E;
71
72            const NAME: &'static str = $NAME;
73
74            fn from_str<'de, T: DeserializeOwned>(s: &'de str) -> Result<T, $E> {
75                $func(s)
76            }
77        }
78    );
79
80    ($name:ident $NAME:literal/$string:literal: $func:expr, $E:ty) => (
81        impl_format!($name $NAME/$string: $func, $E, concat!(
82            "A ", $NAME, " [`Format`] [`Data`] provider.",
83            "\n\n",
84            "Static constructor methods on `", stringify!($name), "` return a
85            [`Data`] value with a generic marker of [`", stringify!($name), "`].
86            Thus, further use occurs via methods on [`Data`].",
87            "\n```\n",
88            "\nuse figment::providers::Format;",
89            "\nuse figment_json5::{", stringify!($name), "};",
90            "\n\n// Source directly from a source string...",
91            "\nlet provider = ", stringify!($name), r#"::string("source-string");"#,
92            "\n\n// Or read from a file on disk.",
93            "\nlet provider = ", stringify!($name), r#"::file("path-to-file");"#,
94            "\n\n// Or configured as nested (via Data::nested()):",
95            "\nlet provider = ", stringify!($name), r#"::file("path-to-file").nested();"#,
96            "\n```",
97            "\n\nSee also [`", stringify!($func), "`] for parsing details."
98        ));
99    )
100}
101
102impl_format!(Json5 "JSON5"/"json5": json5::from_str, json5::Error);
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::Json5;
108    use figment::Figment;
109    use serde::Deserialize;
110
111    #[test]
112    fn test_json5() {
113        #[derive(Deserialize)]
114        struct Config {
115            name: String,
116            description: String,
117
118            #[serde(rename = "leadingDecimalPoint")]
119            leading_decimal_point: f64,
120
121            #[serde(rename = "andTrailing")]
122            and_trailing: f64,
123
124            #[serde(rename = "positiveSign")]
125            positive_sign: i32,
126
127            age: u32,
128        }
129
130        let config = r#"
131            {
132                // Allow comments
133                "name": "powerumc",
134                "age": 0x28, // Allow hexadecimal numbers
135                "description": "This is a \
136test config", // Allow multiline strings
137                "leadingDecimalPoint": .8675309,
138                "andTrailing": 8675309.,
139                "positiveSign": +1,
140                "address": "Seoul", // Allow trailing commas
141            }
142        "#;
143
144        let config: Config = Figment::new()
145            .merge(Json5::string(config))
146            .extract()
147            .unwrap();
148
149        assert_eq!(config.name, "powerumc");
150        assert_eq!(config.description, "This is a test config");
151        assert_eq!(config.leading_decimal_point, 0.8675309);
152        assert_eq!(config.and_trailing, 8675309.0);
153        assert_eq!(config.positive_sign, 1);
154        assert_eq!(config.age, 40);
155    }
156}