Skip to main content

hyphae_query/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::BTreeMap;
4
5/// Canonical structured value used by the deterministic reference executor.
6///
7/// Variant declaration order is the normative cross-type sort order. Binary
8/// floating point is intentionally absent.
9#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
10pub enum Value {
11    /// Explicit null.
12    Null,
13    /// Boolean value.
14    Boolean(bool),
15    /// Signed 64-bit integer.
16    Integer(i64),
17    /// UTF-8 string.
18    String(String),
19    /// Opaque binary bytes.
20    Bytes(Vec<u8>),
21    /// Ordered array.
22    Array(Vec<Self>),
23    /// Object ordered by UTF-8 field name.
24    Object(BTreeMap<String, Self>),
25}
26
27/// Exact object-field path. An empty path selects the root value.
28#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
29pub struct FieldPath {
30    segments: Vec<String>,
31}
32
33impl FieldPath {
34    /// Creates a path from exact object field names.
35    pub fn new<I, S>(segments: I) -> Self
36    where
37        I: IntoIterator<Item = S>,
38        S: Into<String>,
39    {
40        Self {
41            segments: segments.into_iter().map(Into::into).collect(),
42        }
43    }
44
45    /// Creates a single-field path.
46    pub fn field(name: impl Into<String>) -> Self {
47        Self::new([name.into()])
48    }
49
50    /// Returns the exact path segments.
51    pub fn segments(&self) -> &[String] {
52        &self.segments
53    }
54
55    /// Resolves this path, returning `None` for a missing field or traversal
56    /// through a non-object value.
57    pub fn resolve<'value>(&self, root: &'value Value) -> Option<&'value Value> {
58        let mut current = root;
59        for segment in &self.segments {
60            let Value::Object(object) = current else {
61                return None;
62            };
63            current = object.get(segment)?;
64        }
65        Some(current)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use std::collections::BTreeMap;
72
73    use super::{FieldPath, Value};
74
75    #[test]
76    fn field_path_distinguishes_missing_from_explicit_null() {
77        let root = Value::Object(BTreeMap::from([
78            (
79                "nested".to_owned(),
80                Value::Object(BTreeMap::from([("value".to_owned(), Value::Null)])),
81            ),
82            ("scalar".to_owned(), Value::Integer(7)),
83        ]));
84
85        assert_eq!(
86            FieldPath::new(["nested", "value"]).resolve(&root),
87            Some(&Value::Null)
88        );
89        assert_eq!(FieldPath::new(["nested", "missing"]).resolve(&root), None);
90        assert_eq!(FieldPath::new(["scalar", "child"]).resolve(&root), None);
91        assert_eq!(FieldPath::default().resolve(&root), Some(&root));
92    }
93
94    #[test]
95    fn variant_order_is_the_normative_total_order() {
96        let values = [
97            Value::Null,
98            Value::Boolean(false),
99            Value::Integer(-1),
100            Value::String(String::new()),
101            Value::Bytes(Vec::new()),
102            Value::Array(Vec::new()),
103            Value::Object(BTreeMap::new()),
104        ];
105        assert!(values.windows(2).all(|pair| pair[0] < pair[1]));
106    }
107}