Skip to main content

panproto_inst/
value.rs

1//! Value types and field presence for W-type instances.
2//!
3//! [`Value`] represents the leaf data in an instance tree, while
4//! [`FieldPresence`] distinguishes between present, null, and absent
5//! fields in the W-type model.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11/// Field presence in a W-type instance node.
12///
13/// Distinguishes between a field that is present with a value,
14/// explicitly null, or absent (not provided).
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
16pub enum FieldPresence {
17    /// The field is present with the given value.
18    Present(Value),
19    /// The field is explicitly null.
20    Null,
21    /// The field is absent (not provided).
22    Absent,
23}
24
25impl FieldPresence {
26    /// Returns `true` if the field is present (not null or absent).
27    #[must_use]
28    pub const fn is_present(&self) -> bool {
29        matches!(self, Self::Present(_))
30    }
31
32    /// Returns `true` if the field is absent.
33    #[must_use]
34    pub const fn is_absent(&self) -> bool {
35        matches!(self, Self::Absent)
36    }
37
38    /// Returns `true` if the field is null.
39    #[must_use]
40    pub const fn is_null(&self) -> bool {
41        matches!(self, Self::Null)
42    }
43
44    /// Returns the inner value if present.
45    #[must_use]
46    pub const fn as_value(&self) -> Option<&Value> {
47        match self {
48            Self::Present(v) => Some(v),
49            Self::Null | Self::Absent => None,
50        }
51    }
52}
53
54/// A concrete data value in an instance.
55///
56/// This is the ADT of *leaf-or-opaque* data carried by a W-type node.
57/// It mirrors the free term algebra of JSON-like values and forms a
58/// faithful round-trip target for any schema-unanchored data that
59/// parses into the instance (e.g. values landing in `extra_fields`).
60///
61/// Category-theoretically, the variants partition into:
62///
63/// - **Primitive atoms** ([`Self::Bool`], [`Self::Int`], [`Self::Float`],
64///   [`Self::Str`], [`Self::Bytes`], [`Self::CidLink`], [`Self::Blob`],
65///   [`Self::Token`], [`Self::Null`]) — generators of the ADT.
66/// - **Records** ([`Self::Opaque`], [`Self::Unknown`]) — finite products
67///   indexed by string field names (heterogeneous, unordered).
68/// - **Lists** ([`Self::List`]) — the free monoid / list object, an
69///   ordered collection with anonymous positions. This is the list
70///   constructor needed to faithfully embed JSON arrays and, more
71///   generally, any ordered-collection leaf value.
72///
73/// The `Unknown` and `List` variants together give the enum closure
74/// under the two fundamental JSON constructors (object and array) so
75/// that values with no schema anchor still round-trip losslessly.
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77pub enum Value {
78    /// Boolean value.
79    Bool(bool),
80    /// 64-bit signed integer.
81    Int(i64),
82    /// 64-bit floating-point number.
83    Float(f64),
84    /// UTF-8 string.
85    Str(String),
86    /// Raw bytes.
87    Bytes(Vec<u8>),
88    /// A content-identifier link (CID).
89    CidLink(String),
90    /// A blob reference.
91    Blob {
92        /// Reference identifier.
93        ref_: String,
94        /// MIME type.
95        mime: String,
96        /// Size in bytes.
97        size: u64,
98    },
99    /// A token (enum variant name).
100    Token(String),
101    /// Explicit null.
102    Null,
103    /// An opaque typed value (protocol-specific extension).
104    Opaque {
105        /// The type identifier.
106        type_: String,
107        /// Opaque fields.
108        fields: HashMap<String, Self>,
109    },
110    /// An unknown record value: a finite product of name/value pairs.
111    /// Used for schema-unanchored objects that must round-trip.
112    Unknown(HashMap<String, Self>),
113    /// An ordered list of values: the free-monoid list object over
114    /// `Value`. Used for schema-unanchored arrays and for transforms
115    /// that operate on ordered collections carried in `extra_fields`.
116    List(Vec<Self>),
117}
118
119impl Value {
120    /// Returns a human-readable type name for this value.
121    #[must_use]
122    pub const fn type_name(&self) -> &'static str {
123        match self {
124            Self::Bool(_) => "bool",
125            Self::Int(_) => "int",
126            Self::Float(_) => "float",
127            Self::Str(_) => "str",
128            Self::Bytes(_) => "bytes",
129            Self::CidLink(_) => "cid-link",
130            Self::Blob { .. } => "blob",
131            Self::Token(_) => "token",
132            Self::Null => "null",
133            Self::Opaque { .. } => "opaque",
134            Self::Unknown(_) => "unknown",
135            Self::List(_) => "list",
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn field_presence_predicates() {
146        let present = FieldPresence::Present(Value::Int(42));
147        assert!(present.is_present());
148        assert!(!present.is_null());
149        assert!(!present.is_absent());
150
151        let null = FieldPresence::Null;
152        assert!(null.is_null());
153
154        let absent = FieldPresence::Absent;
155        assert!(absent.is_absent());
156    }
157
158    #[test]
159    fn value_type_names() {
160        assert_eq!(Value::Bool(true).type_name(), "bool");
161        assert_eq!(Value::Str("hello".into()).type_name(), "str");
162        assert_eq!(Value::Null.type_name(), "null");
163        assert_eq!(Value::List(Vec::new()).type_name(), "list");
164        assert_eq!(Value::Unknown(HashMap::new()).type_name(), "unknown");
165    }
166
167    #[test]
168    fn value_list_round_trip_via_serde() -> Result<(), serde_json::Error> {
169        // A Value::List of mixed primitives should survive a JSON round
170        // trip through its derived Serde impl.
171        let original = Value::List(vec![
172            Value::Int(1),
173            Value::Str("two".into()),
174            Value::Bool(true),
175        ]);
176        let json = serde_json::to_string(&original)?;
177        let restored: Value = serde_json::from_str(&json)?;
178        assert_eq!(original, restored);
179        Ok(())
180    }
181
182    #[test]
183    fn value_list_is_free_monoid_over_values() {
184        // Concatenation of two Value::List instances is itself a
185        // Value::List (monoid closure under +). Empty list is the
186        // identity element (neutrality on both sides).
187        let a = Value::List(vec![Value::Int(1), Value::Int(2)]);
188        let b = Value::List(vec![Value::Int(3)]);
189        let empty = Value::List(Vec::new());
190
191        let concat = |xs: &Value, ys: &Value| match (xs, ys) {
192            (Value::List(x), Value::List(y)) => {
193                let mut out = x.clone();
194                out.extend(y.iter().cloned());
195                Value::List(out)
196            }
197            _ => panic!("expected lists"),
198        };
199
200        assert_eq!(
201            concat(&a, &b),
202            Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
203        );
204        assert_eq!(concat(&empty, &a), a, "left identity");
205        assert_eq!(concat(&a, &empty), a, "right identity");
206    }
207
208    #[test]
209    fn field_presence_as_value() {
210        let present = FieldPresence::Present(Value::Int(42));
211        assert_eq!(present.as_value(), Some(&Value::Int(42)));
212
213        let null = FieldPresence::Null;
214        assert_eq!(null.as_value(), None);
215    }
216}