Skip to main content

agent_first_data/document/
value.rs

1//! Custom Value type — zero external format dependencies.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
6
7use super::error::DocumentError;
8
9/// Custom Value IR independent of any format crate.
10/// Supports all formats: JSON, TOML, YAML, dotenv, and INI.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Value {
13    Null,
14    Bool(bool),
15    Integer(i64),
16    Unsigned(u64),
17    Float(f64),
18    /// A numeric literal that would lose digits if forced through
19    /// [`Value::Integer`]/[`Value::Unsigned`]/[`Value::Float`]: an integer
20    /// outside `u64::MAX`, or any literal written with a fractional part or
21    /// exponent (`3.14`, `1e-10`, `-0.0`, …). Holds the exact source text
22    /// verbatim — never parsed and reserialized through `f64` — so a 30-digit
23    /// integer or a high-precision decimal round-trips digit for digit.
24    ///
25    /// Produced by the JSON backend's reader (`format::json::load`), which is
26    /// the format whose number grammar is genuinely arbitrary-precision;
27    /// TOML integers are grammar-limited to `i64` and TOML/YAML floats are
28    /// canonically `f64` at the format level, so those backends keep using
29    /// `Integer`/`Unsigned`/`Float` for values in range. `set --value-type
30    /// number` also produces this variant, preserving the CLI argument's
31    /// literal spelling the same way.
32    Number(String),
33    String(String),
34    Array(Vec<Value>),
35    /// Object map. Keys are stored sorted (BTreeMap), not in insertion order.
36    Object(BTreeMap<String, Value>),
37}
38
39impl Value {
40    /// Stable value-kind label for diagnostics and machine-readable metadata.
41    ///
42    /// Signed and unsigned fixed-width integers both report `integer`; exact
43    /// numeric literals stored in [`Value::Number`] report `number`.
44    #[must_use]
45    pub const fn kind_name(&self) -> &'static str {
46        match self {
47            Self::Null => "null",
48            Self::Bool(_) => "boolean",
49            Self::Integer(_) | Self::Unsigned(_) => "integer",
50            Self::Float(_) => "float",
51            Self::Number(_) => "number",
52            Self::String(_) => "string",
53            Self::Array(_) => "array",
54            Self::Object(_) => "object",
55        }
56    }
57
58    pub fn is_null(&self) -> bool {
59        matches!(self, Value::Null)
60    }
61
62    pub fn is_bool(&self) -> bool {
63        matches!(self, Value::Bool(_))
64    }
65
66    pub fn as_bool(&self) -> Option<bool> {
67        match self {
68            Value::Bool(b) => Some(*b),
69            _ => None,
70        }
71    }
72
73    /// True for `Integer`/`Unsigned`, and for a [`Value::Number`] literal
74    /// with no fractional part or exponent — a big integer that only missed
75    /// `Integer`/`Unsigned` because it overflows `i64`/`u64`, not because it
76    /// is actually fractional.
77    pub fn is_integer(&self) -> bool {
78        match self {
79            Value::Integer(_) | Value::Unsigned(_) => true,
80            Value::Number(text) => is_integer_literal(text),
81            _ => false,
82        }
83    }
84
85    /// Exact `i64` value, when this holds one. `None` for a [`Value::Number`]
86    /// literal even when it is integral — by construction that variant only
87    /// exists because the literal does *not* fit `i64`/`u64`; read the exact
88    /// digits via [`Value::as_number_literal`] instead.
89    pub fn as_integer(&self) -> Option<i64> {
90        match self {
91            Value::Integer(i) => Some(*i),
92            _ => None,
93        }
94    }
95
96    pub fn as_unsigned(&self) -> Option<u64> {
97        match self {
98            Value::Unsigned(value) => Some(*value),
99            _ => None,
100        }
101    }
102
103    /// True for `Float`, and for a [`Value::Number`] literal with a
104    /// fractional part or exponent.
105    pub fn is_float(&self) -> bool {
106        match self {
107            Value::Float(_) => true,
108            Value::Number(text) => !is_integer_literal(text),
109            _ => false,
110        }
111    }
112
113    /// Best-effort numeric magnitude as `f64` — lossy for a
114    /// [`Value::Number`] literal outside `f64`'s exact range, but never
115    /// fails for a well-formed literal. Callers that need the exact digits
116    /// (not just the magnitude) must read [`Value::as_number_literal`]
117    /// instead; this accessor exists for magnitude-only consumers (numeric
118    /// comparisons, lint checks) that cannot use arbitrary precision anyway.
119    pub fn as_float(&self) -> Option<f64> {
120        match self {
121            Value::Float(f) => Some(*f),
122            Value::Number(text) => text.parse::<f64>().ok(),
123            _ => None,
124        }
125    }
126
127    /// True for a [`Value::Number`] literal (a number outside
128    /// `Integer`/`Unsigned`/`Float`'s exact range, preserved verbatim).
129    pub fn is_number_literal(&self) -> bool {
130        matches!(self, Value::Number(_))
131    }
132
133    /// The exact source text of a [`Value::Number`] literal, digit for
134    /// digit. `None` for every other variant, including `Integer`/
135    /// `Unsigned`/`Float` — those already round-trip exactly through their
136    /// own `Display`, so this accessor is specifically for the literal-only
137    /// case where that would corrupt the value.
138    pub fn as_number_literal(&self) -> Option<&str> {
139        match self {
140            Value::Number(text) => Some(text),
141            _ => None,
142        }
143    }
144
145    pub fn is_string(&self) -> bool {
146        matches!(self, Value::String(_))
147    }
148
149    pub fn as_str(&self) -> Option<&str> {
150        match self {
151            Value::String(s) => Some(s),
152            _ => None,
153        }
154    }
155
156    pub fn is_array(&self) -> bool {
157        matches!(self, Value::Array(_))
158    }
159
160    pub fn as_array(&self) -> Option<&Vec<Value>> {
161        match self {
162            Value::Array(a) => Some(a),
163            _ => None,
164        }
165    }
166
167    pub fn as_array_mut(&mut self) -> Option<&mut Vec<Value>> {
168        match self {
169            Value::Array(a) => Some(a),
170            _ => None,
171        }
172    }
173
174    pub fn is_object(&self) -> bool {
175        matches!(self, Value::Object(_))
176    }
177
178    pub fn as_object(&self) -> Option<&BTreeMap<String, Value>> {
179        match self {
180            Value::Object(o) => Some(o),
181            _ => None,
182        }
183    }
184
185    pub fn as_object_mut(&mut self) -> Option<&mut BTreeMap<String, Value>> {
186        match self {
187            Value::Object(o) => Some(o),
188            _ => None,
189        }
190    }
191
192    pub fn get(&self, key: &str) -> Option<&Value> {
193        self.as_object().and_then(|o| o.get(key))
194    }
195
196    pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
197        self.as_object_mut().and_then(|o| o.get_mut(key))
198    }
199}
200
201/// True when a numeric literal's text has no fractional part or exponent —
202/// a plain (optionally negative) run of decimal digits.
203fn is_integer_literal(text: &str) -> bool {
204    !text.contains(['.', 'e', 'E'])
205}
206
207impl std::fmt::Display for Value {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        match self {
210            Value::Null => write!(f, "null"),
211            Value::Bool(b) => write!(f, "{}", b),
212            Value::Integer(i) => write!(f, "{}", i),
213            Value::Unsigned(i) => write!(f, "{}", i),
214            Value::Float(fl) => write!(f, "{}", fl),
215            Value::Number(text) => write!(f, "{text}"),
216            Value::String(s) => write!(f, "\"{}\"", s.escape_default()),
217            Value::Array(_) => write!(f, "[...]"),
218            Value::Object(_) => write!(f, "{{...}}"),
219        }
220    }
221}
222
223impl Serialize for Value {
224    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
225        match self {
226            Value::Null => serializer.serialize_none(),
227            Value::Bool(value) => serializer.serialize_bool(*value),
228            Value::Integer(value) => serializer.serialize_i64(*value),
229            Value::Unsigned(value) => serializer.serialize_u64(*value),
230            Value::Float(value) if value.is_finite() => serializer.serialize_f64(*value),
231            Value::Float(_) => Err(serde::ser::Error::custom(
232                "non-finite float is not representable as structured data",
233            )),
234            Value::Number(text) => {
235                let number = serde_json::from_str::<serde_json::Value>(text)
236                    .ok()
237                    .filter(serde_json::Value::is_number)
238                    .ok_or_else(|| {
239                        serde::ser::Error::custom(format!(
240                            "invalid preserved number literal `{text}`"
241                        ))
242                    })?;
243                number.serialize(serializer)
244            }
245            Value::String(value) => serializer.serialize_str(value),
246            Value::Array(values) => values.serialize(serializer),
247            Value::Object(values) => values.serialize(serializer),
248        }
249    }
250}
251
252impl<'de> Deserialize<'de> for Value {
253    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
254        struct ValueVisitor;
255
256        impl<'de> de::Visitor<'de> for ValueVisitor {
257            type Value = Value;
258
259            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260                formatter.write_str("a document value")
261            }
262
263            fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
264                Ok(Value::Null)
265            }
266
267            fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
268                Ok(Value::Null)
269            }
270
271            fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
272                Ok(Value::Bool(value))
273            }
274
275            fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
276                Ok(Value::Integer(value))
277            }
278
279            fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
280                Ok(Value::Unsigned(value))
281            }
282
283            fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
284                Ok(Value::Float(value))
285            }
286
287            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
288                Ok(Value::String(value.to_string()))
289            }
290
291            fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
292                Ok(Value::String(value))
293            }
294
295            fn visit_seq<A: de::SeqAccess<'de>>(
296                self,
297                mut access: A,
298            ) -> Result<Self::Value, A::Error> {
299                let mut values = Vec::new();
300                while let Some(value) = access.next_element()? {
301                    values.push(value);
302                }
303                Ok(Value::Array(values))
304            }
305
306            fn visit_map<A: de::MapAccess<'de>>(
307                self,
308                mut access: A,
309            ) -> Result<Self::Value, A::Error> {
310                let mut values = BTreeMap::new();
311                while let Some((key, value)) = access.next_entry()? {
312                    values.insert(key, value);
313                }
314                Ok(Value::Object(values))
315            }
316        }
317
318        deserializer.deserialize_any(ValueVisitor)
319    }
320}
321
322mod json_convert {
323    use super::*;
324
325    impl From<serde_json::Value> for Value {
326        fn from(v: serde_json::Value) -> Self {
327            match v {
328                serde_json::Value::Null => Value::Null,
329                serde_json::Value::Bool(b) => Value::Bool(b),
330                serde_json::Value::Number(n) => {
331                    // With the crate's `arbitrary_precision` feature enabled,
332                    // `n.as_str()` is the exact source literal — `as_i64`/
333                    // `as_u64` only succeed for a literal with no fractional
334                    // part or exponent that also fits the target width, so a
335                    // plain in-range integer still takes the exact
336                    // `Integer`/`Unsigned` path (unchanged from before);
337                    // everything else (every float literal, and integers
338                    // beyond `u64::MAX`) is preserved digit for digit as
339                    // `Value::Number` instead of being forced through `f64`.
340                    if let Some(i) = n.as_i64() {
341                        Value::Integer(i)
342                    } else if let Some(u) = n.as_u64() {
343                        Value::Unsigned(u)
344                    } else {
345                        Value::Number(n.as_str().to_string())
346                    }
347                }
348                serde_json::Value::String(s) => Value::String(s),
349                serde_json::Value::Array(a) => {
350                    Value::Array(a.into_iter().map(Value::from).collect())
351                }
352                serde_json::Value::Object(o) => {
353                    let map = o.into_iter().map(|(k, v)| (k, Value::from(v))).collect();
354                    Value::Object(map)
355                }
356            }
357        }
358    }
359
360    impl TryFrom<&Value> for serde_json::Value {
361        type Error = DocumentError;
362
363        fn try_from(value: &Value) -> Result<Self, Self::Error> {
364            let unsupported = |detail: String| DocumentError::UnsupportedOperation {
365                format: "JSON".to_string(),
366                operation: "serialize".to_string(),
367                detail,
368            };
369            match value {
370                Value::Null => Ok(Self::Null),
371                Value::Bool(value) => Ok(Self::Bool(*value)),
372                Value::Integer(value) => Ok(Self::Number((*value).into())),
373                Value::Unsigned(value) => Ok(Self::Number((*value).into())),
374                Value::Float(value) => serde_json::Number::from_f64(*value)
375                    .map(Self::Number)
376                    .ok_or_else(|| {
377                        unsupported("non-finite float is not representable in JSON".to_string())
378                    }),
379                Value::Number(text) => serde_json::from_str::<Self>(text)
380                    .ok()
381                    .filter(Self::is_number)
382                    .ok_or_else(|| unsupported(format!("invalid number literal `{text}`"))),
383                Value::String(value) => Ok(Self::String(value.clone())),
384                Value::Array(values) => values
385                    .iter()
386                    .map(Self::try_from)
387                    .collect::<Result<Vec<_>, _>>()
388                    .map(Self::Array),
389                Value::Object(values) => {
390                    let mut object = serde_json::Map::new();
391                    for (key, value) in values {
392                        object.insert(key.clone(), Self::try_from(value)?);
393                    }
394                    Ok(Self::Object(object))
395                }
396            }
397        }
398    }
399
400    impl TryFrom<Value> for serde_json::Value {
401        type Error = DocumentError;
402
403        fn try_from(value: Value) -> Result<Self, Self::Error> {
404            Self::try_from(&value)
405        }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::Value;
412    use std::collections::BTreeMap;
413
414    #[test]
415    fn value_kind_names_are_stable() {
416        let cases = [
417            (Value::Null, "null"),
418            (Value::Bool(true), "boolean"),
419            (Value::Integer(-1), "integer"),
420            (Value::Unsigned(1), "integer"),
421            (Value::Float(1.5), "float"),
422            (Value::Number("1e1000".to_string()), "number"),
423            (Value::String("value".to_string()), "string"),
424            (Value::Array(Vec::new()), "array"),
425            (Value::Object(BTreeMap::new()), "object"),
426        ];
427
428        for (value, expected) in cases {
429            assert_eq!(value.kind_name(), expected);
430        }
431    }
432
433    #[test]
434    fn json_conversion_rejects_non_finite_float() {
435        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
436            let error = serde_json::Value::try_from(Value::Float(value))
437                .expect_err("non-finite float must fail");
438            assert_eq!(error.code(), "document_unsupported_operation");
439            assert!(error.to_string().contains("non-finite"));
440        }
441    }
442
443    #[test]
444    fn serde_rejects_non_finite_float_instead_of_emitting_null() {
445        let error =
446            serde_json::to_string(&Value::Float(f64::NAN)).expect_err("non-finite float must fail");
447        assert!(error.to_string().contains("non-finite"));
448    }
449}