1use std::fmt;
5use std::path::Path;
6
7use serde_json::Value;
8
9use crate::error::Error;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum Format {
18 Json,
20 Toml,
22 Yaml,
24}
25
26impl fmt::Display for Format {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Format::Json => f.write_str("JSON"),
30 Format::Toml => f.write_str("TOML"),
31 Format::Yaml => f.write_str("YAML"),
32 }
33 }
34}
35
36impl Format {
37 pub fn from_path(path: &Path) -> Result<Self, Error> {
44 let ext = path
46 .extension()
47 .and_then(|ext| ext.to_str())
48 .map(str::to_ascii_lowercase);
49 match ext.as_deref() {
50 Some("json") => Ok(Format::Json),
51 Some("toml") => Ok(Format::Toml),
52 Some("yaml" | "yml") => Ok(Format::Yaml),
53 _ => Err(Error::UnknownExtension {
54 path: path.to_path_buf(),
55 }),
56 }
57 }
58
59 pub(crate) fn parse(self, text: &str, origin: Option<&Path>) -> Result<Value, Error> {
63 match self {
64 Format::Json => parse_json(text, origin),
65 Format::Toml => parse_toml(text, origin),
66 Format::Yaml => parse_yaml(text, origin),
67 }
68 }
69}
70
71pub(crate) fn merge(base: &mut Value, overlay: Value) {
77 match (base, overlay) {
78 (Value::Object(base_map), Value::Object(overlay_map)) => {
79 for (key, value) in overlay_map {
80 match base_map.get_mut(&key) {
81 Some(slot) => merge(slot, value),
82 None => {
83 base_map.insert(key, value);
84 }
85 }
86 }
87 }
88 (slot, value) => *slot = value,
89 }
90}
91
92fn parse_error(origin: Option<&Path>, format: Format, message: String) -> Error {
94 Error::Parse {
95 origin: origin.map(Path::to_path_buf),
96 format,
97 message,
98 }
99}
100
101fn parse_json(text: &str, origin: Option<&Path>) -> Result<Value, Error> {
103 serde_json::from_str(text).map_err(|err| parse_error(origin, Format::Json, err.to_string()))
104}
105
106#[cfg(feature = "toml")]
108fn parse_toml(text: &str, origin: Option<&Path>) -> Result<Value, Error> {
109 let value: toml::Value = toml::from_str(text)
110 .map_err(|err: toml::de::Error| parse_error(origin, Format::Toml, err.to_string()))?;
111 toml_to_json(value).map_err(|reason| parse_error(origin, Format::Toml, reason))
112}
113
114#[cfg(not(feature = "toml"))]
116fn parse_toml(_text: &str, _origin: Option<&Path>) -> Result<Value, Error> {
117 Err(Error::FeatureDisabled {
118 format: Format::Toml,
119 feature: "toml",
120 })
121}
122
123#[cfg(feature = "yaml")]
125fn parse_yaml(text: &str, origin: Option<&Path>) -> Result<Value, Error> {
126 let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(text)
127 .map_err(|err| parse_error(origin, Format::Yaml, err.to_string()))?;
128 yaml_to_json(value).map_err(|reason| parse_error(origin, Format::Yaml, reason))
129}
130
131#[cfg(not(feature = "yaml"))]
133fn parse_yaml(_text: &str, _origin: Option<&Path>) -> Result<Value, Error> {
134 Err(Error::FeatureDisabled {
135 format: Format::Yaml,
136 feature: "yaml",
137 })
138}
139
140#[cfg(feature = "toml")]
146fn toml_to_json(value: toml::Value) -> Result<Value, String> {
147 Ok(match value {
148 toml::Value::String(s) => Value::String(s),
149 toml::Value::Integer(i) => Value::Number(i.into()),
150 toml::Value::Float(f) => Value::Number(
151 serde_json::Number::from_f64(f)
153 .ok_or_else(|| format!("non-finite float `{f}` is not supported"))?,
154 ),
155 toml::Value::Boolean(b) => Value::Bool(b),
156 toml::Value::Datetime(dt) => Value::String(dt.to_string()),
157 toml::Value::Array(items) => Value::Array(
158 items
159 .into_iter()
160 .map(toml_to_json)
161 .collect::<Result<_, _>>()?,
162 ),
163 toml::Value::Table(table) => {
164 let mut object = serde_json::Map::with_capacity(table.len());
165 for (key, value) in table {
166 object.insert(key, toml_to_json(value)?);
167 }
168 Value::Object(object)
169 }
170 })
171}
172
173#[cfg(feature = "yaml")]
175fn yaml_to_json(value: serde_yaml_ng::Value) -> Result<Value, String> {
176 use serde_yaml_ng::Value as Yaml;
177
178 Ok(match value {
179 Yaml::Null => Value::Null,
180 Yaml::Bool(b) => Value::Bool(b),
181 Yaml::Number(n) => {
182 if let Some(i) = n.as_i64() {
183 Value::Number(i.into())
184 } else if let Some(u) = n.as_u64() {
185 Value::Number(u.into())
186 } else if let Some(f) = n.as_f64() {
187 Value::Number(
189 serde_json::Number::from_f64(f)
190 .ok_or_else(|| format!("non-finite float `{f}` is not supported"))?,
191 )
192 } else {
193 return Err(format!("unrepresentable YAML number `{n}`"));
194 }
195 }
196 Yaml::String(s) => Value::String(s),
197 Yaml::Sequence(items) => Value::Array(
198 items
199 .into_iter()
200 .map(yaml_to_json)
201 .collect::<Result<_, _>>()?,
202 ),
203 Yaml::Mapping(mapping) => {
204 let mut object = serde_json::Map::with_capacity(mapping.len());
205 for (key, value) in mapping {
206 let key = match key {
209 Yaml::String(s) => s,
210 Yaml::Bool(b) => b.to_string(),
211 Yaml::Number(n) => n.to_string(),
212 other => return Err(format!("unsupported YAML mapping key: {other:?}")),
213 };
214 object.insert(key, yaml_to_json(value)?);
215 }
216 Value::Object(object)
217 }
218 Yaml::Tagged(tagged) => yaml_to_json(tagged.value)?,
220 })
221}
222
223#[cfg(test)]
224mod tests {
225 use serde_json::json;
226
227 use super::*;
228
229 #[test]
231 fn detect_format_from_extension() {
232 assert_eq!(
233 Format::from_path(Path::new("a/app.toml")).unwrap(),
234 Format::Toml
235 );
236 assert_eq!(
237 Format::from_path(Path::new("app.yaml")).unwrap(),
238 Format::Yaml
239 );
240 assert_eq!(
241 Format::from_path(Path::new("app.yml")).unwrap(),
242 Format::Yaml
243 );
244 assert_eq!(
245 Format::from_path(Path::new("app.JSON")).unwrap(),
246 Format::Json
247 );
248 }
249
250 #[test]
252 fn detect_format_unknown_extension() {
253 for path in ["app", "app.ini", "app.conf"] {
254 let err = Format::from_path(Path::new(path)).unwrap_err();
255 assert!(
256 matches!(err, Error::UnknownExtension { .. }),
257 "{path}: {err}"
258 );
259 }
260 }
261
262 #[test]
265 fn merge_semantics() {
266 let mut base = json!({
267 "server": { "host": "localhost", "port": 8080 },
268 "tags": ["a", "b"],
269 "debug": false,
270 });
271 let overlay = json!({
272 "server": { "port": 9090 },
273 "tags": ["c"],
274 "debug": null,
275 "extra": 1,
276 });
277 merge(&mut base, overlay);
278 assert_eq!(
279 base,
280 json!({
281 "server": { "host": "localhost", "port": 9090 },
282 "tags": ["c"],
283 "debug": null,
284 "extra": 1,
285 })
286 );
287 }
288
289 #[test]
291 fn merge_scalar_replaces_object() {
292 let mut base = json!({ "a": { "b": 1 } });
293 merge(&mut base, json!({ "a": 42 }));
294 assert_eq!(base, json!({ "a": 42 }));
295 }
296
297 #[test]
299 fn json_parse_error_has_origin() {
300 let err = Format::Json
301 .parse("{ bad", Some(Path::new("cfg.json")))
302 .unwrap_err();
303 assert!(matches!(
304 err,
305 Error::Parse {
306 format: Format::Json,
307 ..
308 }
309 ));
310 assert!(err.to_string().contains("cfg.json"), "{err}");
311 }
312
313 #[cfg(feature = "toml")]
315 #[test]
316 fn toml_datetime_becomes_string() {
317 let value = Format::Toml
318 .parse("ts = 2026-01-02T03:04:05Z", None)
319 .unwrap();
320 assert_eq!(value, json!({ "ts": "2026-01-02T03:04:05Z" }));
321 }
322
323 #[cfg(feature = "toml")]
326 #[test]
327 fn toml_non_finite_float_is_rejected() {
328 let err = Format::Toml.parse("f = nan", None).unwrap_err();
329 assert!(
330 matches!(
331 err,
332 Error::Parse {
333 format: Format::Toml,
334 ..
335 }
336 ),
337 "{err}"
338 );
339 }
340
341 #[cfg(feature = "yaml")]
343 #[test]
344 fn yaml_scalar_keys_are_stringified() {
345 let value = Format::Yaml
346 .parse("1: one\ntrue: yes_value\n", None)
347 .unwrap();
348 assert_eq!(value, json!({ "1": "one", "true": "yes_value" }));
349 }
350
351 #[cfg(feature = "yaml")]
353 #[test]
354 fn yaml_complex_key_is_rejected() {
355 let err = Format::Yaml.parse("? [a, b]\n: value\n", None).unwrap_err();
356 assert!(
357 matches!(
358 err,
359 Error::Parse {
360 format: Format::Yaml,
361 ..
362 }
363 ),
364 "{err}"
365 );
366 }
367
368 #[cfg(feature = "yaml")]
370 #[test]
371 fn yaml_tagged_value_is_unwrapped() {
372 let value = Format::Yaml.parse("v: !Custom 7\n", None).unwrap();
373 assert_eq!(value, json!({ "v": 7 }));
374 }
375}