Skip to main content

edikt_core/
value.rs

1//! The value calculus domain.
2//!
3//! `Value` is the in-memory model the expression evaluator and format conversion
4//! operate over. It is *not* how format-preserving edits round-trip (those stay
5//! in the CST); it is the domain for computed values (`.count + 1`), query
6//! output, and cross-format conversion.
7//!
8//! Numbers keep an `Int`/`Float` distinction so integer output stays clean.
9//! Objects are insertion-ordered (`Vec` of pairs) because key order is
10//! user-visible.
11
12use std::cmp::Ordering;
13
14#[derive(Debug, Clone)]
15pub enum Value {
16    Null,
17    Bool(bool),
18    Int(i64),
19    Float(f64),
20    Str(String),
21    Array(Vec<Value>),
22    Object(Vec<(String, Value)>),
23}
24
25// Equality follows the total order, so `Int(1) == Float(1.0)`. This lets `Expr`
26// (which embeds `Value` in literals) derive `PartialEq` for tests.
27impl PartialEq for Value {
28    fn eq(&self, other: &Value) -> bool {
29        self.value_eq(other)
30    }
31}
32
33impl Value {
34    /// jq-style truthiness: only `false` and `null` are falsy.
35    pub fn is_truthy(&self) -> bool {
36        !matches!(self, Value::Null | Value::Bool(false))
37    }
38
39    /// The type name, as reported by the `type` builtin.
40    pub fn type_name(&self) -> &'static str {
41        match self {
42            Value::Null => "null",
43            Value::Bool(_) => "boolean",
44            Value::Int(_) | Value::Float(_) => "number",
45            Value::Str(_) => "string",
46            Value::Array(_) => "array",
47            Value::Object(_) => "object",
48        }
49    }
50
51    /// Numeric coercion to f64 for arithmetic and comparison.
52    pub fn as_f64(&self) -> Option<f64> {
53        match self {
54            Value::Int(i) => Some(*i as f64),
55            Value::Float(f) => Some(*f),
56            _ => None,
57        }
58    }
59
60    /// Total order across types (jq's ordering:
61    /// null < bool < number < string < array < object).
62    pub fn order(&self, other: &Value) -> Ordering {
63        fn rank(v: &Value) -> u8 {
64            match v {
65                Value::Null => 0,
66                Value::Bool(_) => 1,
67                Value::Int(_) | Value::Float(_) => 2,
68                Value::Str(_) => 3,
69                Value::Array(_) => 4,
70                Value::Object(_) => 5,
71            }
72        }
73        match (self, other) {
74            (Value::Null, Value::Null) => Ordering::Equal,
75            (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
76            (Value::Str(a), Value::Str(b)) => a.cmp(b),
77            (Value::Array(a), Value::Array(b)) => {
78                for (x, y) in a.iter().zip(b.iter()) {
79                    let c = x.order(y);
80                    if c != Ordering::Equal {
81                        return c;
82                    }
83                }
84                a.len().cmp(&b.len())
85            }
86            (Value::Object(a), Value::Object(b)) => {
87                // Compare by sorted keys, then by the values at those keys.
88                let mut ka: Vec<&String> = a.iter().map(|(k, _)| k).collect();
89                let mut kb: Vec<&String> = b.iter().map(|(k, _)| k).collect();
90                ka.sort();
91                kb.sort();
92                match ka.cmp(&kb) {
93                    Ordering::Equal => {}
94                    ne => return ne,
95                }
96                for k in ka {
97                    let va = a.iter().find(|(kk, _)| kk == k).map(|(_, v)| v);
98                    let vb = b.iter().find(|(kk, _)| kk == k).map(|(_, v)| v);
99                    if let (Some(va), Some(vb)) = (va, vb) {
100                        let c = va.order(vb);
101                        if c != Ordering::Equal {
102                            return c;
103                        }
104                    }
105                }
106                Ordering::Equal
107            }
108            _ => {
109                // Numbers compare numerically; otherwise fall back to type rank.
110                if let (Some(a), Some(b)) = (self.as_f64(), other.as_f64()) {
111                    a.partial_cmp(&b).unwrap_or(Ordering::Equal)
112                } else {
113                    rank(self).cmp(&rank(other))
114                }
115            }
116        }
117    }
118
119    /// Structural equality consistent with [`Value::order`].
120    pub fn value_eq(&self, other: &Value) -> bool {
121        self.order(other) == Ordering::Equal
122    }
123
124    /// Render as a raw scalar for query output (no surrounding quotes on
125    /// strings). Composite values are rendered as JSON.
126    pub fn to_raw_string(&self) -> String {
127        match self {
128            Value::Str(s) => s.clone(),
129            Value::Null => "null".to_string(),
130            Value::Bool(b) => b.to_string(),
131            Value::Int(i) => i.to_string(),
132            Value::Float(f) => format_f64(*f),
133            Value::Array(_) | Value::Object(_) => self.to_json5(),
134        }
135    }
136
137    /// Render as compact JSON.
138    pub fn to_json(&self) -> String {
139        let mut out = String::new();
140        self.write_json_inner(&mut out, false);
141        out
142    }
143
144    /// Render as compact JSON in the JSON5 spelling: non-finite floats keep
145    /// their `Infinity` / `-Infinity` / `NaN` literals instead of degrading to
146    /// `null`. This is what the JSONC/JSON5-family emitters use, so converting
147    /// or querying a JSON5 source with a non-finite number preserves it; strict
148    /// JSON must use [`Value::to_json`] (which, like `JSON.stringify`, encodes
149    /// one as `null`).
150    pub fn to_json5(&self) -> String {
151        let mut out = String::new();
152        self.write_json_inner(&mut out, true);
153        out
154    }
155
156    fn write_json_inner(&self, out: &mut String, json5: bool) {
157        match self {
158            Value::Null => out.push_str("null"),
159            Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
160            Value::Int(i) => out.push_str(&i.to_string()),
161            // JSON5 documents may carry a non-finite literal; JSON's grammar
162            // has none, so encoding one there emits `null`, matching
163            // `JSON.stringify`. Only a JSON5 source can supply one (the
164            // mutation path refuses to write one into a strict doc).
165            Value::Float(f) if !f.is_finite() && json5 => out.push_str(&format_f64(*f)),
166            Value::Float(f) if !f.is_finite() => out.push_str("null"),
167            Value::Float(f) => out.push_str(&format_f64(*f)),
168            Value::Str(s) => write_json_string(s, out),
169            Value::Array(a) => {
170                out.push('[');
171                for (i, v) in a.iter().enumerate() {
172                    if i > 0 {
173                        out.push(',');
174                    }
175                    v.write_json_inner(out, json5);
176                }
177                out.push(']');
178            }
179            Value::Object(m) => {
180                out.push('{');
181                for (i, (k, v)) in m.iter().enumerate() {
182                    if i > 0 {
183                        out.push(',');
184                    }
185                    write_json_string(k, out);
186                    out.push(':');
187                    v.write_json_inner(out, json5);
188                }
189                out.push('}');
190            }
191        }
192    }
193}
194
195impl From<bool> for Value {
196    fn from(input: bool) -> Self {
197        Self::Bool(input)
198    }
199}
200
201macro_rules! from_int {
202    ($($t:ty),+) => {$(
203        impl From<$t> for Value {
204            fn from(input: $t) -> Self {
205                Self::Int(input as i64)
206            }
207        }
208    )+};
209}
210// u64/usize are absent on purpose: they do not fit i64 without a lossy cast,
211// and silently wrapping a large count into a negative Int is worse than making
212// the caller choose.
213from_int!(i8, i16, i32, i64, u8, u16, u32, isize);
214
215macro_rules! from_float {
216    ($($t:ty),+) => {$(
217        impl From<$t> for Value {
218            fn from(input: $t) -> Self {
219                Self::Float(input as f64)
220            }
221        }
222    )+};
223}
224from_float!(f32, f64);
225
226/// `None` is JSON's `null`; `Some(v)` is whatever `v` converts to.
227impl<T: Into<Value>> From<Option<T>> for Value {
228    fn from(input: Option<T>) -> Self {
229        input.map_or(Self::Null, Into::into)
230    }
231}
232
233impl<T: Into<Value>> From<Vec<T>> for Value {
234    fn from(input: Vec<T>) -> Self {
235        Self::Array(input.into_iter().map(Into::into).collect())
236    }
237}
238
239impl<T: Into<Value> + Clone> From<&[T]> for Value {
240    fn from(input: &[T]) -> Self {
241        Self::Array(input.iter().cloned().map(Into::into).collect())
242    }
243}
244
245impl<K: Into<String>, V: Into<Value>> FromIterator<(K, V)> for Value {
246    /// Collect key-value pairs into an object, preserving iteration order.
247    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
248        Self::Object(
249            iter.into_iter()
250                .map(|(k, v)| (k.into(), v.into()))
251                .collect(),
252        )
253    }
254}
255
256impl From<&str> for Value {
257    fn from(input: &str) -> Self {
258        Self::Str(input.to_string())
259    }
260}
261
262impl From<String> for Value {
263    fn from(input: String) -> Self {
264        Self::Str(input)
265    }
266}
267
268/// Format an f64 the way jq does: integral values print without a decimal point.
269///
270/// Non-finite values use the JavaScript/JSON5 spelling (`Infinity`, `-Infinity`,
271/// `NaN`) rather than Rust's `inf`/`NaN`, so a JSON5 document's own literals
272/// round-trip through raw output. They are only reachable from a JSON5 source;
273/// arithmetic cannot produce one (division by zero is a hard error).
274fn format_f64(f: f64) -> String {
275    if f.is_nan() {
276        return "NaN".to_string();
277    }
278    if f.is_infinite() {
279        return if f > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
280    }
281    if f.fract() == 0.0 && f.abs() < 1e15 {
282        format!("{}", f as i64)
283    } else {
284        format!("{f}")
285    }
286}
287
288fn write_json_string(s: &str, out: &mut String) {
289    out.push('"');
290    for c in s.chars() {
291        match c {
292            '"' => out.push_str("\\\""),
293            '\\' => out.push_str("\\\\"),
294            '\n' => out.push_str("\\n"),
295            '\r' => out.push_str("\\r"),
296            '\t' => out.push_str("\\t"),
297            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
298            c => out.push(c),
299        }
300    }
301    out.push('"');
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    fn arr(v: &[Value]) -> Value {
309        Value::Array(v.to_vec())
310    }
311    fn obj(pairs: &[(&str, Value)]) -> Value {
312        Value::Object(
313            pairs
314                .iter()
315                .map(|(k, v)| (k.to_string(), v.clone()))
316                .collect(),
317        )
318    }
319
320    #[test]
321    fn total_order_across_types() {
322        // jq's ordering: null < bool < number < string < array < object.
323        let ladder = [
324            Value::Null,
325            Value::Bool(false),
326            Value::Bool(true),
327            Value::Int(-1),
328            Value::Float(2.5),
329            Value::Str("a".into()),
330            arr(&[Value::Int(1)]),
331            obj(&[("k", Value::Int(1))]),
332        ];
333        for i in 0..ladder.len() {
334            for j in 0..ladder.len() {
335                let want = i.cmp(&j);
336                // Equal ranks (the two bools, two numbers) compare by value, so
337                // only assert the strict cross-rank orderings here.
338                if want != Ordering::Equal {
339                    assert_eq!(
340                        ladder[i].order(&ladder[j]),
341                        want,
342                        "{:?} vs {:?}",
343                        ladder[i],
344                        ladder[j]
345                    );
346                }
347            }
348        }
349    }
350
351    #[test]
352    fn numbers_compare_across_int_and_float() {
353        assert!(Value::Int(1).value_eq(&Value::Float(1.0)));
354        assert_eq!(Value::Int(2).order(&Value::Float(2.5)), Ordering::Less);
355        assert_eq!(Value::Float(3.0).order(&Value::Int(3)), Ordering::Equal);
356        assert_eq!(Value::Int(1).as_f64(), Some(1.0));
357        assert_eq!(Value::Str("x".into()).as_f64(), None);
358    }
359
360    #[test]
361    fn arrays_and_objects_order_structurally() {
362        // Arrays compare element-wise, then by length.
363        assert_eq!(
364            arr(&[Value::Int(1), Value::Int(2)]).order(&arr(&[Value::Int(1), Value::Int(3)])),
365            Ordering::Less
366        );
367        assert_eq!(
368            arr(&[Value::Int(1)]).order(&arr(&[Value::Int(1), Value::Int(0)])),
369            Ordering::Less
370        );
371        // Objects compare by sorted keys, then values at those keys.
372        assert_eq!(
373            obj(&[("a", Value::Int(1))]).order(&obj(&[("b", Value::Int(1))])),
374            Ordering::Less
375        );
376        assert_eq!(
377            obj(&[("a", Value::Int(1))]).order(&obj(&[("a", Value::Int(2))])),
378            Ordering::Less
379        );
380        // Key order doesn't affect equality.
381        assert!(
382            obj(&[("a", Value::Int(1)), ("b", Value::Int(2))])
383                .value_eq(&obj(&[("b", Value::Int(2)), ("a", Value::Int(1))]))
384        );
385    }
386
387    #[test]
388    fn non_finite_floats_use_the_json5_spelling_but_encode_as_json_null() {
389        // Raw output round-trips a JSON5 literal...
390        assert_eq!(Value::Float(f64::INFINITY).to_raw_string(), "Infinity");
391        assert_eq!(Value::Float(f64::NEG_INFINITY).to_raw_string(), "-Infinity");
392        assert_eq!(Value::Float(f64::NAN).to_raw_string(), "NaN");
393        // ...while JSON encoding degrades to null, because `Infinity` is not
394        // JSON and emitting it would produce a document nothing can parse.
395        assert_eq!(Value::Float(f64::INFINITY).to_json(), "null");
396        assert_eq!(Value::Float(f64::NAN).to_json(), "null");
397        assert_eq!(
398            obj(&[("a", Value::Float(f64::NEG_INFINITY))]).to_json(),
399            "{\"a\":null}"
400        );
401        // The JSON5 spelling keeps the literal, recursively: a non-finite
402        // nested inside an array or object survives in `to_json5` but still
403        // degrades in `to_json`.
404        assert_eq!(Value::Float(f64::INFINITY).to_json5(), "Infinity");
405        assert_eq!(Value::Float(f64::NEG_INFINITY).to_json5(), "-Infinity");
406        assert_eq!(Value::Float(f64::NAN).to_json5(), "NaN");
407        assert_eq!(
408            arr(&[Value::Float(f64::INFINITY), Value::Float(f64::NAN)]).to_json5(),
409            "[Infinity,NaN]"
410        );
411        assert_eq!(
412            obj(&[("n", Value::Float(f64::NEG_INFINITY))]).to_json5(),
413            "{\"n\":-Infinity}"
414        );
415        assert_eq!(
416            obj(&[("a", Value::Array(vec![Value::Float(f64::INFINITY)]))]).to_json(),
417            "{\"a\":[null]}"
418        );
419        // Finite formatting is unchanged.
420        assert_eq!(Value::Float(1e14).to_json(), "100000000000000");
421        assert_eq!(arr(&[Value::Float(1.5)]).to_json(), "[1.5]");
422        assert_eq!(arr(&[Value::Float(1.5)]).to_json5(), "[1.5]");
423    }
424
425    #[test]
426    fn raw_string_and_truthiness() {
427        assert_eq!(Value::Float(1.0).to_raw_string(), "1");
428        assert_eq!(Value::Float(1.5).to_raw_string(), "1.5");
429        assert_eq!(Value::Null.to_raw_string(), "null");
430        assert_eq!(Value::Bool(true).to_raw_string(), "true");
431        assert_eq!(arr(&[Value::Int(1)]).to_raw_string(), "[1]");
432        assert_eq!(obj(&[("a", Value::Int(1))]).to_raw_string(), "{\"a\":1}");
433        // Only false and null are falsy (jq).
434        assert!(Value::Int(0).is_truthy());
435        assert!(Value::Str("".into()).is_truthy());
436        assert!(!Value::Bool(false).is_truthy());
437        assert!(!Value::Null.is_truthy());
438    }
439
440    #[test]
441    fn json_encoding_escapes_and_floats() {
442        assert_eq!(
443            Value::Str("a\"b\\c\n\t\r".into()).to_json(),
444            "\"a\\\"b\\\\c\\n\\t\\r\""
445        );
446        // A control char below 0x20 becomes a \u escape.
447        assert_eq!(Value::Str("\u{0001}".into()).to_json(), "\"\\u0001\"");
448        // Integral floats print without a decimal point; fractional keep it.
449        assert_eq!(Value::Float(42.0).to_json(), "42");
450        assert_eq!(Value::Float(2.5).to_json(), "2.5");
451        assert_eq!(
452            obj(&[("n", Value::Null), ("xs", arr(&[Value::Bool(true)]))]).to_json(),
453            "{\"n\":null,\"xs\":[true]}"
454        );
455        assert_eq!(arr(&[]).to_json(), "[]");
456    }
457
458    #[test]
459    fn type_names() {
460        assert_eq!(Value::Null.type_name(), "null");
461        assert_eq!(Value::Bool(true).type_name(), "boolean");
462        assert_eq!(Value::Int(1).type_name(), "number");
463        assert_eq!(Value::Float(1.0).type_name(), "number");
464        assert_eq!(Value::Str("x".into()).type_name(), "string");
465        assert_eq!(arr(&[]).type_name(), "array");
466        assert_eq!(obj(&[]).type_name(), "object");
467    }
468}