Skip to main content

riffdb_types/
value.rs

1//! The closed canonical value algebra shared by semantic boundaries.
2
3use std::fmt;
4
5use crate::limits::{
6    MAX_BYTES_VALUE_BYTES, MAX_LIST_ENTRIES, MAX_NESTING_DEPTH, MAX_RECORD_FIELDS, MAX_STRING_BYTES,
7};
8use crate::{CanonicalVector, Date, Decimal, EnumTypeId, EnumVariantId, FieldId, Money, Timestamp};
9
10/// A value in RiffDB's closed, deterministic transactional value algebra.
11///
12/// Constructors for variable-sized variants enforce process hard limits. The
13/// canonical encoder validates again so direct enum construction cannot bypass
14/// a durable-boundary check.
15#[derive(Clone, Eq, PartialEq)]
16pub enum CanonicalValue {
17    /// Explicit optional absence.
18    Null,
19    /// Boolean value.
20    Bool(bool),
21    /// Signed 64-bit integer.
22    I64(i64),
23    /// Unsigned 64-bit integer.
24    U64(u64),
25    /// Checked fixed-scale decimal.
26    Decimal(Decimal),
27    /// Currency-qualified checked decimal.
28    Money(Money),
29    /// Exact UTF-8 text without implicit normalization.
30    String(CanonicalString),
31    /// Exact opaque bytes.
32    Bytes(CanonicalBytes),
33    /// UTC timestamp supplied by an input or deterministic transaction context.
34    Timestamp(Timestamp),
35    /// Calendar date represented as days since the Unix epoch.
36    Date(Date),
37    /// UUID network-order bytes.
38    Uuid([u8; 16]),
39    /// Stable enum type and variant IDs. Display names are not canonical.
40    Enum {
41        /// Stable enum type ID.
42        type_id: EnumTypeId,
43        /// Stable enum variant ID.
44        variant_id: EnumVariantId,
45    },
46    /// Bounded ordered values.
47    List(CanonicalList),
48    /// Fields in strictly increasing stable-ID order.
49    Record(CanonicalRecord),
50    /// A fixed-dimension f32 vector for nearest-neighbor search (ADR-0091).
51    Vector(CanonicalVector),
52}
53
54impl crate::InspectOwnedCapacityV1 for CanonicalValue {
55    fn inspect_owned_capacity(
56        &self,
57        inspection: &mut crate::OwnedCapacityInspectionV1,
58    ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
59        match self {
60            Self::String(value) => inspection.visit(value),
61            Self::Bytes(value) => inspection.visit(value),
62            Self::List(value) => inspection.visit(value),
63            Self::Record(value) => inspection.visit(value),
64            Self::Vector(value) => inspection.visit(value),
65            Self::Null
66            | Self::Bool(_)
67            | Self::I64(_)
68            | Self::U64(_)
69            | Self::Decimal(_)
70            | Self::Money(_)
71            | Self::Timestamp(_)
72            | Self::Date(_)
73            | Self::Uuid(_)
74            | Self::Enum {
75                type_id: _,
76                variant_id: _,
77            } => Ok(()),
78        }
79    }
80}
81
82impl fmt::Debug for CanonicalValue {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::Null => formatter.write_str("CanonicalValue::Null"),
86            Self::Bool(_) => formatter.write_str("CanonicalValue::Bool([REDACTED])"),
87            Self::I64(_) => formatter.write_str("CanonicalValue::I64([REDACTED])"),
88            Self::U64(_) => formatter.write_str("CanonicalValue::U64([REDACTED])"),
89            Self::Decimal(_) => formatter.write_str("CanonicalValue::Decimal([REDACTED])"),
90            Self::Money(_) => formatter.write_str("CanonicalValue::Money([REDACTED])"),
91            Self::String(value) => formatter
92                .debug_tuple("CanonicalValue::String")
93                .field(value)
94                .finish(),
95            Self::Bytes(value) => formatter
96                .debug_tuple("CanonicalValue::Bytes")
97                .field(value)
98                .finish(),
99            Self::Timestamp(_) => formatter.write_str("CanonicalValue::Timestamp([REDACTED])"),
100            Self::Date(_) => formatter.write_str("CanonicalValue::Date([REDACTED])"),
101            Self::Uuid(_) => formatter.write_str("CanonicalValue::Uuid([REDACTED])"),
102            Self::Enum { .. } => formatter.write_str("CanonicalValue::Enum([REDACTED])"),
103            Self::List(values) => formatter
104                .debug_struct("CanonicalValue::List")
105                .field("length", &values.len())
106                .finish(),
107            Self::Record(record) => formatter
108                .debug_struct("CanonicalValue::Record")
109                .field("field_count", &record.len())
110                .finish(),
111            Self::Vector(vector) => formatter
112                .debug_struct("CanonicalValue::Vector")
113                .field("dimension", &vector.dimension())
114                .finish(),
115        }
116    }
117}
118
119impl CanonicalValue {
120    /// Constructs bounded exact UTF-8 text.
121    pub fn string(value: impl Into<String>) -> Result<Self, ValueError> {
122        let value = value.into();
123        Ok(Self::String(CanonicalString::new(value)?))
124    }
125
126    /// Constructs bounded exact bytes.
127    pub fn bytes(value: impl Into<Vec<u8>>) -> Result<Self, ValueError> {
128        let value = value.into();
129        Ok(Self::Bytes(CanonicalBytes::new(value)?))
130    }
131
132    /// Constructs a bounded list without changing its order.
133    pub fn list(values: Vec<Self>) -> Result<Self, ValueError> {
134        Ok(Self::List(CanonicalList::new(values)?))
135    }
136
137    /// Constructs a record, sorting fields by stable ID and rejecting duplicates.
138    pub fn record(fields: Vec<(FieldId, Self)>) -> Result<Self, ValueError> {
139        Ok(Self::Record(CanonicalRecord::new(fields)?))
140    }
141
142    fn nesting_depth(&self) -> usize {
143        match self {
144            Self::List(list) => list
145                .values()
146                .iter()
147                .map(Self::nesting_depth)
148                .max()
149                .map_or(0, |depth| depth + 1),
150            Self::Record(record) => record
151                .fields()
152                .iter()
153                .map(|(_, value)| value.nesting_depth())
154                .max()
155                .map_or(0, |depth| depth + 1),
156            _ => 0,
157        }
158    }
159}
160
161/// Exact bounded UTF-8 text for a canonical value.
162#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub struct CanonicalString(String);
164crate::inspect_owned_capacity_tuple_v1!(CanonicalString, String);
165
166impl fmt::Debug for CanonicalString {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter
169            .debug_struct("CanonicalString")
170            .field("value", &"[REDACTED]")
171            .field("length", &self.len())
172            .finish()
173    }
174}
175
176impl CanonicalString {
177    /// Validates and constructs exact UTF-8 text without normalization.
178    pub fn new(value: impl Into<String>) -> Result<Self, ValueError> {
179        let value = value.into();
180        if value.len() > MAX_STRING_BYTES {
181            return Err(ValueError::StringTooLong {
182                actual: value.len(),
183                maximum: MAX_STRING_BYTES,
184            });
185        }
186        Ok(Self(value))
187    }
188
189    /// Borrows the exact UTF-8 text.
190    pub fn as_str(&self) -> &str {
191        &self.0
192    }
193
194    /// Returns the UTF-8 byte length.
195    pub fn len(&self) -> usize {
196        self.0.len()
197    }
198
199    /// Returns whether the text is empty.
200    pub fn is_empty(&self) -> bool {
201        self.0.is_empty()
202    }
203
204    /// Consumes the wrapper and returns the owned UTF-8 string.
205    #[must_use]
206    pub fn into_string(self) -> String {
207        self.0
208    }
209}
210
211/// Exact bounded opaque bytes for a canonical value.
212#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
213pub struct CanonicalBytes(Vec<u8>);
214crate::inspect_owned_capacity_tuple_v1!(CanonicalBytes, Vec<u8>);
215
216impl fmt::Debug for CanonicalBytes {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        formatter
219            .debug_struct("CanonicalBytes")
220            .field("value", &"[REDACTED]")
221            .field("length", &self.len())
222            .finish()
223    }
224}
225
226impl CanonicalBytes {
227    /// Validates and constructs an exact byte value.
228    pub fn new(value: impl Into<Vec<u8>>) -> Result<Self, ValueError> {
229        let value = value.into();
230        if value.len() > MAX_BYTES_VALUE_BYTES {
231            return Err(ValueError::BytesTooLong {
232                actual: value.len(),
233                maximum: MAX_BYTES_VALUE_BYTES,
234            });
235        }
236        Ok(Self(value))
237    }
238
239    /// Borrows the exact bytes.
240    pub fn as_bytes(&self) -> &[u8] {
241        &self.0
242    }
243
244    /// Returns the byte length.
245    pub fn len(&self) -> usize {
246        self.0.len()
247    }
248
249    /// Returns whether the value is empty.
250    pub fn is_empty(&self) -> bool {
251        self.0.is_empty()
252    }
253
254    /// Consumes the wrapper and returns the owned byte vector.
255    #[must_use]
256    pub fn into_vec(self) -> Vec<u8> {
257        self.0
258    }
259}
260
261/// A bounded ordered list of canonical values.
262#[derive(Clone, Eq, PartialEq)]
263pub struct CanonicalList(Vec<CanonicalValue>);
264crate::inspect_owned_capacity_tuple_v1!(CanonicalList, Vec<CanonicalValue>);
265
266impl fmt::Debug for CanonicalList {
267    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
268        formatter
269            .debug_struct("CanonicalList")
270            .field("length", &self.len())
271            .finish()
272    }
273}
274
275impl CanonicalList {
276    /// Validates and constructs a bounded list without changing its order.
277    pub fn new(values: Vec<CanonicalValue>) -> Result<Self, ValueError> {
278        if values.len() > MAX_LIST_ENTRIES {
279            return Err(ValueError::TooManyListEntries {
280                actual: values.len(),
281                maximum: MAX_LIST_ENTRIES,
282            });
283        }
284        validate_child_depth(values.iter())?;
285        Ok(Self(values))
286    }
287
288    /// Borrows the ordered values.
289    pub fn values(&self) -> &[CanonicalValue] {
290        &self.0
291    }
292
293    /// Returns the number of values.
294    pub fn len(&self) -> usize {
295        self.0.len()
296    }
297
298    /// Returns whether the list contains no values.
299    pub fn is_empty(&self) -> bool {
300        self.0.is_empty()
301    }
302
303    /// Consumes the list and returns the ordered values.
304    #[must_use]
305    pub fn into_values(self) -> Vec<CanonicalValue> {
306        self.0
307    }
308}
309
310/// A record whose fields are canonicalized by stable field ID.
311#[derive(Clone, Eq, PartialEq)]
312pub struct CanonicalRecord {
313    fields: Vec<(FieldId, CanonicalValue)>,
314}
315crate::inspect_owned_capacity_struct_v1!(CanonicalRecord {
316    owned: [fields],
317    inline: []
318});
319
320impl fmt::Debug for CanonicalRecord {
321    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
322        formatter
323            .debug_struct("CanonicalRecord")
324            .field("field_count", &self.len())
325            .finish()
326    }
327}
328
329impl CanonicalRecord {
330    /// Sorts fields by stable ID and rejects duplicate IDs.
331    pub fn new(mut fields: Vec<(FieldId, CanonicalValue)>) -> Result<Self, ValueError> {
332        if fields.len() > MAX_RECORD_FIELDS {
333            return Err(ValueError::TooManyRecordFields {
334                actual: fields.len(),
335                maximum: MAX_RECORD_FIELDS,
336            });
337        }
338
339        fields.sort_by_key(|(field_id, _)| field_id.get());
340        if let Some(field_id) = fields
341            .windows(2)
342            .find_map(|pair| (pair[0].0 == pair[1].0).then_some(pair[0].0))
343        {
344            return Err(ValueError::DuplicateRecordField { field_id });
345        }
346        validate_child_depth(fields.iter().map(|(_, value)| value))?;
347
348        Ok(Self { fields })
349    }
350
351    pub(crate) fn from_canonical_fields(fields: Vec<(FieldId, CanonicalValue)>) -> Self {
352        debug_assert!(fields.windows(2).all(|pair| pair[0].0 < pair[1].0));
353        Self { fields }
354    }
355
356    /// Returns fields in strictly increasing stable-ID order.
357    pub fn fields(&self) -> &[(FieldId, CanonicalValue)] {
358        &self.fields
359    }
360
361    /// Returns the number of fields.
362    pub fn len(&self) -> usize {
363        self.fields.len()
364    }
365
366    /// Returns whether the record contains no fields.
367    pub fn is_empty(&self) -> bool {
368        self.fields.is_empty()
369    }
370
371    /// Consumes the record and returns fields in stable-ID order.
372    #[must_use]
373    pub fn into_fields(self) -> Vec<(FieldId, CanonicalValue)> {
374        self.fields
375    }
376}
377
378/// A bounded-value construction failure.
379#[derive(Clone, Debug, Eq, PartialEq)]
380pub enum ValueError {
381    /// A string exceeds the process hard limit.
382    StringTooLong {
383        /// Observed UTF-8 byte length.
384        actual: usize,
385        /// Maximum permitted UTF-8 byte length.
386        maximum: usize,
387    },
388    /// A byte value exceeds the process hard limit.
389    BytesTooLong {
390        /// Observed byte length.
391        actual: usize,
392        /// Maximum permitted byte length.
393        maximum: usize,
394    },
395    /// A list exceeds the process hard entry limit.
396    TooManyListEntries {
397        /// Observed entry count.
398        actual: usize,
399        /// Maximum permitted entry count.
400        maximum: usize,
401    },
402    /// A record exceeds the process hard field limit.
403    TooManyRecordFields {
404        /// Observed field count.
405        actual: usize,
406        /// Maximum permitted field count.
407        maximum: usize,
408    },
409    /// Two record fields use the same stable field ID.
410    DuplicateRecordField {
411        /// Duplicated stable field ID.
412        field_id: FieldId,
413    },
414    /// A list or record would exceed the canonical nesting limit.
415    NestingTooDeep {
416        /// Attempted depth below the root value.
417        depth: usize,
418        /// Maximum depth below the root value.
419        maximum: usize,
420    },
421}
422
423impl fmt::Display for ValueError {
424    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
425        match self {
426            Self::StringTooLong { actual, maximum } => {
427                write!(formatter, "string has {actual} bytes; maximum is {maximum}")
428            }
429            Self::BytesTooLong { actual, maximum } => {
430                write!(
431                    formatter,
432                    "byte value has {actual} bytes; maximum is {maximum}"
433                )
434            }
435            Self::TooManyListEntries { actual, maximum } => {
436                write!(formatter, "list has {actual} entries; maximum is {maximum}")
437            }
438            Self::TooManyRecordFields { actual, maximum } => {
439                write!(
440                    formatter,
441                    "record has {actual} fields; maximum is {maximum}"
442                )
443            }
444            Self::DuplicateRecordField { field_id } => {
445                write!(
446                    formatter,
447                    "record contains duplicate field ID {}",
448                    field_id.get()
449                )
450            }
451            Self::NestingTooDeep { depth, maximum } => {
452                write!(formatter, "nesting depth is {depth}; maximum is {maximum}")
453            }
454        }
455    }
456}
457
458impl std::error::Error for ValueError {}
459
460fn validate_child_depth<'a>(
461    children: impl IntoIterator<Item = &'a CanonicalValue>,
462) -> Result<(), ValueError> {
463    let depth = children
464        .into_iter()
465        .map(CanonicalValue::nesting_depth)
466        .max()
467        .map_or(0, |depth| depth + 1);
468    if depth > MAX_NESTING_DEPTH {
469        return Err(ValueError::NestingTooDeep {
470            depth,
471            maximum: MAX_NESTING_DEPTH,
472        });
473    }
474    Ok(())
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn record_constructor_sorts_and_rejects_duplicates() {
483        let record = CanonicalRecord::new(vec![
484            (FieldId::new(9).expect("nonzero"), CanonicalValue::Null),
485            (
486                FieldId::new(2).expect("nonzero"),
487                CanonicalValue::Bool(true),
488            ),
489        ])
490        .expect("valid record");
491        assert_eq!(
492            record
493                .fields()
494                .iter()
495                .map(|(id, _)| id.get())
496                .collect::<Vec<_>>(),
497            [2, 9]
498        );
499
500        assert_eq!(
501            CanonicalRecord::new(vec![
502                (FieldId::new(2).expect("nonzero"), CanonicalValue::Null),
503                (FieldId::new(2).expect("nonzero"), CanonicalValue::Null),
504            ]),
505            Err(ValueError::DuplicateRecordField {
506                field_id: FieldId::new(2).expect("nonzero")
507            })
508        );
509    }
510
511    #[test]
512    fn canonical_debug_output_redacts_exact_values() {
513        let secret = "raw-idempotency-key-canary";
514        let values = [
515            CanonicalValue::string(secret).expect("bounded string"),
516            CanonicalValue::bytes(secret.as_bytes().to_vec()).expect("bounded bytes"),
517            CanonicalValue::I64(4_242_424_242),
518        ];
519
520        for value in values {
521            let output = format!("{value:?}");
522            assert!(!output.contains(secret));
523            assert!(!output.contains("4242424242"));
524        }
525    }
526}