Skip to main content

hocon/
value_factory.rs

1// Copyright 2026 1o1 Co. Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! Value-factory helpers: [`empty`] and (with `serde` feature) [`from_map`].
10//!
11//! `empty` is always available.  `from_map` requires the `serde` feature
12//! (it accepts `serde_json::Map` as input).
13
14use crate::config::Config;
15use indexmap::IndexMap;
16
17#[cfg(feature = "serde")]
18use crate::error::ConfigError;
19#[cfg(feature = "serde")]
20use crate::value::{HoconValue, ScalarType, ScalarValue};
21
22/// Return an empty `Config` with no keys.
23///
24/// Equivalent to constructing an empty HOCON document. The resulting `Config`
25/// is always resolved (`is_resolved()` returns `true`).
26///
27/// `origin_description` is the user-visible source name for error messages.
28/// Pass `None` to omit.
29pub fn empty(origin_description: Option<&str>) -> Config {
30    Config::new_with_meta(IndexMap::new(), origin_description.map(|s| s.to_owned()))
31}
32
33/// Construct a resolved `Config` from a `serde_json::Map<String, Value>`.
34///
35/// Keys are treated as plain keys (NOT path expressions — the key `"a.b"` creates
36/// a top-level entry literally named `"a.b"`, not a nested `a.b`). Values are
37/// coerced to the internal HOCON representation per the E12 value-factory
38/// type-coercion table.
39///
40/// `from_map` never produces substitution placeholders; the returned `Config` is
41/// always resolved (`is_resolved()` returns `true`).
42///
43/// `origin_description` is the user-visible source name for error messages.
44/// Pass `None` to omit.
45///
46/// # Errors
47///
48/// Returns a `ConfigError` if a value cannot be coerced (e.g. a `serde_json::Number`
49/// that is not representable as either `i64` or finite `f64`).
50///
51/// This function requires the `serde` feature flag.
52#[cfg(feature = "serde")]
53pub fn from_map(
54    values: serde_json::Map<String, serde_json::Value>,
55    origin_description: Option<&str>,
56) -> Result<Config, ConfigError> {
57    let root = coerce_map(values)?;
58    Ok(Config::new_with_meta(
59        root,
60        origin_description.map(|s| s.to_owned()),
61    ))
62}
63
64#[cfg(feature = "serde")]
65fn coerce_map(
66    map: serde_json::Map<String, serde_json::Value>,
67) -> Result<IndexMap<String, HoconValue>, ConfigError> {
68    // Sorted key iteration for stable cross-impl JSON output.
69    let mut keys: Vec<String> = map.keys().cloned().collect();
70    keys.sort();
71    let mut result = IndexMap::new();
72    for k in keys {
73        let v = map.get(&k).unwrap().clone();
74        let hv = coerce_value(v).map_err(|msg| ConfigError {
75            path: k.clone(),
76            message: msg,
77        })?;
78        result.insert(k, hv);
79    }
80    Ok(result)
81}
82
83#[cfg(feature = "serde")]
84fn coerce_value(v: serde_json::Value) -> Result<HoconValue, String> {
85    use serde_json::Value;
86    match v {
87        Value::Null => Ok(HoconValue::Scalar(ScalarValue {
88            raw: "null".to_owned(),
89            value_type: ScalarType::Null,
90        })),
91        Value::Bool(b) => Ok(HoconValue::Scalar(ScalarValue {
92            raw: if b { "true" } else { "false" }.to_owned(),
93            value_type: ScalarType::Boolean,
94        })),
95        Value::String(s) => Ok(HoconValue::Scalar(ScalarValue {
96            raw: s,
97            value_type: ScalarType::String,
98        })),
99        Value::Number(n) => {
100            // T4 fix: use serde_json::Number's canonical JSON token form as the raw
101            // representation. This preserves exact integer values for numbers that
102            // fit in u64 but not i64 (e.g. u64::MAX), and avoids the precision loss
103            // and formatting change that `format!("{}", f)` introduced.
104            //
105            // Guard: serde_json::Number does not allow NaN/Inf natively, but we
106            // still check as_f64 finiteness for floats to be safe.
107            if n.is_i64() || n.is_u64() {
108                // Integer: canonical JSON token is exact; use it directly.
109                Ok(HoconValue::Scalar(ScalarValue {
110                    raw: n.to_string(),
111                    value_type: ScalarType::Number,
112                }))
113            } else if let Some(f) = n.as_f64() {
114                if !f.is_finite() {
115                    return Err(format!(
116                        "number {} is not finite (NaN/Inf not representable in HOCON)",
117                        n
118                    ));
119                }
120                // Float: canonical JSON token form from serde_json preserves the
121                // original source representation better than format!("{}", f).
122                Ok(HoconValue::Scalar(ScalarValue {
123                    raw: n.to_string(),
124                    value_type: ScalarType::Number,
125                }))
126            } else {
127                Err(format!(
128                    "number {} cannot be represented as i64, u64, or f64",
129                    n
130                ))
131            }
132        }
133        Value::Array(arr) => {
134            let mut items = Vec::with_capacity(arr.len());
135            for (i, elem) in arr.into_iter().enumerate() {
136                let hv = coerce_value(elem).map_err(|msg| format!("element[{}]: {}", i, msg))?;
137                items.push(hv);
138            }
139            Ok(HoconValue::Array(items))
140        }
141        Value::Object(obj) => {
142            let inner = coerce_map(obj).map_err(|e| e.message)?;
143            Ok(HoconValue::Object(inner))
144        }
145    }
146}