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}