Skip to main content

knf/
ir.rs

1//! The owned value tree every layer is merged as.
2//!
3//! Deliberately a superset of JSON and TOML rather than either one: [`Null`] is
4//! JSON-only, [`Datetime`] is TOML-only, and both survive the merge untouched so
5//! that the format crates are only involved at the parse and emit boundaries.
6//!
7//! [`Null`]: Value::Null
8//! [`Datetime`]: Value::Datetime
9
10/// The object type. `indexmap` rather than `BTreeMap` so input key order
11/// survives, and rather than `Vec<(String, Value)>` so merge's per-key lookup is
12/// not quadratic.
13pub type Map = indexmap::IndexMap<String, Value>;
14
15/// A parsed document, or any node within one.
16#[derive(Debug, Clone, PartialEq)]
17pub enum Value {
18    Null,
19    Bool(bool),
20    Number(Number),
21    String(String),
22    /// An RFC 3339-ish TOML datetime, kept as its source spelling.
23    ///
24    /// Every datetime originates in the TOML parser. It round-trips exactly through
25    /// `Display`/`FromStr` for all four TOML forms (offset datetime, local
26    /// datetime, local date, local time), so a string is enough to carry it
27    /// across a merge without the IR naming `toml`. JSON has no
28    /// datetime, so it renders as a string on the way out.
29    Datetime(String),
30    Array(Vec<Value>),
31    Object(Map),
32}
33
34/// A number, kept in the widest lossless representation of its source.
35///
36/// Three variants rather than a single `f64`: JSON integers above [`i64::MAX`]
37/// (snowflake IDs, hashes) are real and must round-trip exactly, and `f64`
38/// silently rounds them.
39///
40/// `U64` is reserved for values that do not fit an `i64`; construct through
41/// [`Number::from_u64`] to keep that canonical. Without it, derived
42/// [`PartialEq`] would make `I64(1) != U64(1)` and equality would depend on
43/// which parser produced the value.
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum Number {
46    I64(i64),
47    U64(u64),
48    F64(f64),
49}
50
51impl Number {
52    /// Demotes to [`I64`](Number::I64) when the value fits, so that every
53    /// representable integer has exactly one representation.
54    pub fn from_u64(u: u64) -> Self {
55        match i64::try_from(u) {
56            Ok(i) => Self::I64(i),
57            Err(_) => Self::U64(u),
58        }
59    }
60}
61
62impl From<i64> for Number {
63    fn from(i: i64) -> Self {
64        Self::I64(i)
65    }
66}
67
68impl From<u64> for Number {
69    fn from(u: u64) -> Self {
70        Self::from_u64(u)
71    }
72}
73
74impl From<f64> for Number {
75    fn from(f: f64) -> Self {
76        Self::F64(f)
77    }
78}
79
80impl Value {
81    /// The kind of a value, for conflict reporting and parse errors.
82    ///
83    /// All numbers are one kind: an int layer overriding a float (or the
84    /// reverse) is a routine thing to write and carries no risk of shadowing a
85    /// subtree, which is what strict mode exists to catch. Datetimes are their
86    /// own kind — they are not strings until JSON conversion.
87    pub fn kind(&self) -> &'static str {
88        match self {
89            Self::Object(_) => "object",
90            Self::Array(_) => "array",
91            Self::String(_) => "string",
92            Self::Datetime(_) => "datetime",
93            Self::Number(_) => "number",
94            Self::Bool(_) => "bool",
95            Self::Null => "null",
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn u64_that_fits_normalises_to_i64() {
106        assert_eq!(Number::from_u64(1), Number::I64(1));
107        assert_eq!(Number::from_u64(i64::MAX as u64), Number::I64(i64::MAX));
108        assert_eq!(Number::from_u64(u64::MAX), Number::U64(u64::MAX));
109    }
110
111    #[test]
112    fn int_and_float_share_a_kind_but_datetime_does_not() {
113        assert_eq!(Value::Number(Number::I64(1)).kind(), "number");
114        assert_eq!(Value::Number(Number::F64(1.5)).kind(), "number");
115        assert_eq!(Value::String("x".into()).kind(), "string");
116        assert_eq!(
117            Value::Datetime("1979-05-27T07:32:00Z".into()).kind(),
118            "datetime"
119        );
120    }
121}