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}