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}