Skip to main content

type_bridge_contract/
value.rs

1//! Domain-tagged canonical scalar, cardinality, and annotation values.
2
3use std::cmp::Ordering;
4use std::collections::BTreeSet;
5use std::fmt;
6
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use crate::decimal::parse_decimal;
11use crate::diagnostic::{Diagnostic, DiagnosticCategory};
12use crate::limits::MAX_CANONICAL_STRING_BYTES;
13use crate::temporal::{CanonicalDate, CanonicalDateTime, CanonicalDateTimeTz, CanonicalDuration};
14
15/// Validated UTF-8 text bounded by the canonical per-string byte ceiling.
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
17#[serde(transparent)]
18pub struct CanonicalString(String);
19
20impl CanonicalString {
21    /// Construct canonical text, rejecting values above the byte ceiling.
22    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
23        let value = value.into();
24        if value.len() > MAX_CANONICAL_STRING_BYTES {
25            return Err(Diagnostic::stable(
26                DiagnosticCategory::ResourceLimit,
27                "canonical_string_limit_exceeded",
28                "canonical string exceeds the UTF-8 byte ceiling",
29            )
30            .with_detail(
31                "actual_bytes",
32                i64::try_from(value.len()).unwrap_or(i64::MAX),
33            )
34            .with_detail(
35                "maximum_bytes",
36                i64::try_from(MAX_CANONICAL_STRING_BYTES).expect("canonical string limit fits i64"),
37            ));
38        }
39        Ok(Self(value))
40    }
41
42    /// Return the validated text.
43    pub fn as_str(&self) -> &str {
44        &self.0
45    }
46}
47
48impl fmt::Display for CanonicalString {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str(self.as_str())
51    }
52}
53
54impl<'de> Deserialize<'de> for CanonicalString {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: Deserializer<'de>,
58    {
59        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
60    }
61}
62
63/// A finite IEEE-754 double represented by its exact bits.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub struct CanonicalDouble(u64);
66
67impl CanonicalDouble {
68    /// Construct from a finite double, preserving signed zero and subnormals.
69    pub fn new(value: f64) -> Result<Self, Diagnostic> {
70        if value.is_finite() {
71            Ok(Self(value.to_bits()))
72        } else {
73            Err(invalid_scalar("double"))
74        }
75    }
76    /// Construct from finite IEEE bits.
77    pub fn from_bits(bits: u64) -> Result<Self, Diagnostic> {
78        Self::new(f64::from_bits(bits))
79    }
80    /// Return exact IEEE bits.
81    pub const fn bits(self) -> u64 {
82        self.0
83    }
84    /// Return the finite double.
85    pub fn get(self) -> f64 {
86        f64::from_bits(self.0)
87    }
88    /// Return fixed-width lowercase hexadecimal bits.
89    pub fn bits_hex(self) -> String {
90        format!("{:016x}", self.0)
91    }
92}
93
94/// An owned validated canonical decimal spelling.
95#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub struct DecimalValue(String);
97
98impl DecimalValue {
99    /// Validate and normalize TypeQL or driver decimal text.
100    pub fn new(value: impl AsRef<str>) -> Result<Self, Diagnostic> {
101        parse_decimal(value.as_ref())
102            .map(|value| Self(value.canonical_string()))
103            .ok_or_else(|| invalid_scalar("decimal"))
104    }
105    /// Return normalized decimal text without the driver suffix.
106    pub fn as_str(&self) -> &str {
107        &self.0
108    }
109}
110impl fmt::Display for DecimalValue {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.write_str(self.as_str())
113    }
114}
115impl Serialize for DecimalValue {
116    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117    where
118        S: Serializer,
119    {
120        serializer.serialize_str(self.as_str())
121    }
122}
123impl<'de> Deserialize<'de> for DecimalValue {
124    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125    where
126        D: Deserializer<'de>,
127    {
128        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
129    }
130}
131
132fn invalid_scalar(value_type: &'static str) -> Diagnostic {
133    Diagnostic::stable(
134        DiagnosticCategory::InvalidContract,
135        "invalid_canonical_scalar",
136        "scalar value is outside its canonical domain",
137    )
138    .with_detail("value_type", value_type)
139}
140
141/// The closed TypeDB scalar-domain vocabulary.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum ValueTypeTag {
145    /// UTF-8 text.
146    String,
147    /// Signed 64-bit integer.
148    Long,
149    /// Finite IEEE-754 binary64.
150    Double,
151    /// Boolean.
152    Boolean,
153    /// Gregorian date.
154    Date,
155    /// Timezone-free date-time.
156    #[serde(rename = "datetime")]
157    DateTime,
158    /// Timezone-aware date-time.
159    #[serde(rename = "datetime_tz")]
160    DateTimeTz,
161    /// TypeDB decimal.
162    Decimal,
163    /// TypeDB duration.
164    Duration,
165}
166
167impl ValueTypeTag {
168    /// Return the stable contract spelling.
169    pub const fn as_str(self) -> &'static str {
170        match self {
171            Self::String => "string",
172            Self::Long => "long",
173            Self::Double => "double",
174            Self::Boolean => "boolean",
175            Self::Date => "date",
176            Self::DateTime => "datetime",
177            Self::DateTimeTz => "datetime_tz",
178            Self::Decimal => "decimal",
179            Self::Duration => "duration",
180        }
181    }
182}
183
184/// A canonical scalar value with an explicit domain tag.
185#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
186pub enum CanonicalValue {
187    /// UTF-8 text.
188    String(CanonicalString),
189    /// Signed 64-bit integer; JSON uses a decimal string.
190    Long(i64),
191    /// Finite binary64; JSON uses fixed-width IEEE bits.
192    Double(CanonicalDouble),
193    /// Boolean.
194    Boolean(bool),
195    /// Date.
196    Date(CanonicalDate),
197    /// Timezone-free date-time.
198    DateTime(CanonicalDateTime),
199    /// Timezone-aware date-time.
200    DateTimeTz(CanonicalDateTimeTz),
201    /// Decimal.
202    Decimal(DecimalValue),
203    /// Duration.
204    Duration(CanonicalDuration),
205}
206
207impl CanonicalValue {
208    /// Return the closed scalar-domain tag.
209    pub const fn value_type(&self) -> ValueTypeTag {
210        match self {
211            Self::String(_) => ValueTypeTag::String,
212            Self::Long(_) => ValueTypeTag::Long,
213            Self::Double(_) => ValueTypeTag::Double,
214            Self::Boolean(_) => ValueTypeTag::Boolean,
215            Self::Date(_) => ValueTypeTag::Date,
216            Self::DateTime(_) => ValueTypeTag::DateTime,
217            Self::DateTimeTz(_) => ValueTypeTag::DateTimeTz,
218            Self::Decimal(_) => ValueTypeTag::Decimal,
219            Self::Duration(_) => ValueTypeTag::Duration,
220        }
221    }
222
223    /// Compare values within one scalar domain by provider semantics.
224    ///
225    /// This is deliberately separate from representation `Ord`, which remains
226    /// the deterministic identity order used by canonical sets and bytes.
227    /// Compare values only when both belong to the same semantic scalar domain.
228    ///
229    /// Returns `None` for different domains or a domain without semantic ordering.
230    pub fn semantic_cmp_same_domain(&self, other: &Self) -> Option<Ordering> {
231        match (self, other) {
232            (Self::String(left), Self::String(right)) => Some(left.cmp(right)),
233            (Self::Long(left), Self::Long(right)) => Some(left.cmp(right)),
234            (Self::Double(left), Self::Double(right)) => left.get().partial_cmp(&right.get()),
235            (Self::Boolean(left), Self::Boolean(right)) => Some(left.cmp(right)),
236            (Self::Date(left), Self::Date(right)) => Some(left.cmp(right)),
237            (Self::DateTime(left), Self::DateTime(right)) => Some(left.cmp(right)),
238            (Self::DateTimeTz(left), Self::DateTimeTz(right)) => Some(
239                left.semantic_utc_nanoseconds()
240                    .cmp(&right.semantic_utc_nanoseconds()),
241            ),
242            (Self::Decimal(left), Self::Decimal(right)) => {
243                let left = parse_decimal(left.as_str()).expect("DecimalValue is always validated");
244                let right =
245                    parse_decimal(right.as_str()).expect("DecimalValue is always validated");
246                Some(left.compare(&right))
247            }
248            _ => None,
249        }
250    }
251}
252
253#[derive(Serialize, Deserialize)]
254#[serde(tag = "kind", rename_all = "snake_case")]
255enum ValueWire {
256    String {
257        value: CanonicalString,
258    },
259    Long {
260        value: String,
261    },
262    Double {
263        bits: String,
264    },
265    Boolean {
266        value: bool,
267    },
268    Date {
269        value: CanonicalDate,
270    },
271    #[serde(rename = "datetime")]
272    DateTime {
273        value: CanonicalDateTime,
274    },
275    #[serde(rename = "datetime_tz")]
276    DateTimeTz {
277        value: CanonicalDateTimeTz,
278    },
279    Decimal {
280        value: DecimalValue,
281    },
282    Duration {
283        value: CanonicalDuration,
284    },
285}
286
287impl Serialize for CanonicalValue {
288    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
289    where
290        S: Serializer,
291    {
292        let wire = match self {
293            Self::String(value) => ValueWire::String {
294                value: value.clone(),
295            },
296            Self::Long(value) => ValueWire::Long {
297                value: value.to_string(),
298            },
299            Self::Double(value) => ValueWire::Double {
300                bits: value.bits_hex(),
301            },
302            Self::Boolean(value) => ValueWire::Boolean { value: *value },
303            Self::Date(value) => ValueWire::Date { value: *value },
304            Self::DateTime(value) => ValueWire::DateTime { value: *value },
305            Self::DateTimeTz(value) => ValueWire::DateTimeTz {
306                value: value.clone(),
307            },
308            Self::Decimal(value) => ValueWire::Decimal {
309                value: value.clone(),
310            },
311            Self::Duration(value) => ValueWire::Duration { value: *value },
312        };
313        wire.serialize(serializer)
314    }
315}
316
317impl<'de> Deserialize<'de> for CanonicalValue {
318    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
319    where
320        D: Deserializer<'de>,
321    {
322        match ValueWire::deserialize(deserializer)? {
323            ValueWire::String { value } => Ok(Self::String(value)),
324            ValueWire::Boolean { value } => Ok(Self::Boolean(value)),
325            ValueWire::Date { value } => Ok(Self::Date(value)),
326            ValueWire::DateTime { value } => Ok(Self::DateTime(value)),
327            ValueWire::DateTimeTz { value } => Ok(Self::DateTimeTz(value)),
328            ValueWire::Decimal { value } => Ok(Self::Decimal(value)),
329            ValueWire::Duration { value } => Ok(Self::Duration(value)),
330            ValueWire::Long { value } => {
331                let parsed = value.parse::<i64>().map_err(D::Error::custom)?;
332                if parsed.to_string() != value {
333                    return Err(D::Error::custom("long value is not canonical"));
334                }
335                Ok(Self::Long(parsed))
336            }
337            ValueWire::Double { bits } => {
338                if bits.len() != 16
339                    || bits
340                        .bytes()
341                        .any(|b| !b.is_ascii_digit() && !(b'a'..=b'f').contains(&b))
342                {
343                    return Err(D::Error::custom(
344                        "double bits are not canonical lowercase hex",
345                    ));
346                }
347                let bits = u64::from_str_radix(&bits, 16).map_err(D::Error::custom)?;
348                CanonicalDouble::from_bits(bits)
349                    .map(Self::Double)
350                    .map_err(D::Error::custom)
351            }
352        }
353    }
354}
355
356/// A validated cardinality. `None` means an unbounded maximum.
357#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
358pub struct Cardinality {
359    min: u64,
360    max: Option<u64>,
361}
362
363impl Cardinality {
364    /// Construct a cardinality, rejecting inverted and exact-zero ranges.
365    pub fn new(min: u64, max: Option<u64>) -> Result<Self, Diagnostic> {
366        if max.is_some_and(|max| max < min || max == 0) {
367            Err(Diagnostic::stable(
368                DiagnosticCategory::InvalidContract,
369                "invalid_cardinality",
370                "cardinality maximum is below its minimum or exactly zero",
371            ))
372        } else {
373            Ok(Self { min, max })
374        }
375    }
376    /// Return the minimum.
377    pub const fn min(self) -> u64 {
378        self.min
379    }
380    /// Return the optional finite maximum.
381    pub const fn max(self) -> Option<u64> {
382        self.max
383    }
384}
385
386#[derive(Serialize, Deserialize)]
387struct CardinalityWire {
388    kind: CardinalityKind,
389    min: String,
390    max: String,
391}
392#[derive(Serialize, Deserialize)]
393#[serde(rename_all = "snake_case")]
394enum CardinalityKind {
395    Cardinality,
396}
397
398impl Serialize for Cardinality {
399    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
400    where
401        S: Serializer,
402    {
403        CardinalityWire {
404            kind: CardinalityKind::Cardinality,
405            min: self.min.to_string(),
406            max: self
407                .max
408                .map_or_else(|| "unbounded".to_owned(), |max| max.to_string()),
409        }
410        .serialize(serializer)
411    }
412}
413impl<'de> Deserialize<'de> for Cardinality {
414    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
415    where
416        D: Deserializer<'de>,
417    {
418        let wire = CardinalityWire::deserialize(deserializer)?;
419        let min = wire.min.parse::<u64>().map_err(D::Error::custom)?;
420        if min.to_string() != wire.min {
421            return Err(D::Error::custom("cardinality minimum is not canonical"));
422        }
423        let max = if wire.max == "unbounded" {
424            None
425        } else {
426            let max = wire.max.parse::<u64>().map_err(D::Error::custom)?;
427            if max.to_string() != wire.max {
428                return Err(D::Error::custom("cardinality maximum is not canonical"));
429            }
430            Some(max)
431        };
432        Self::new(min, max).map_err(D::Error::custom)
433    }
434}
435
436/// A generic canonical annotation value without freezing annotation fact DTOs.
437#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
438#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
439pub enum AnnotationValue {
440    /// Marker annotation with no payload.
441    Unit,
442    /// One scalar payload.
443    Scalar(CanonicalValue),
444    /// A semantically ordered scalar sequence.
445    Ordered(Vec<CanonicalValue>),
446    /// A semantically unordered deterministically sorted scalar set.
447    Unordered(BTreeSet<CanonicalValue>),
448    /// A cardinality payload.
449    Cardinality(Cardinality),
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn long_decimal_double_and_cardinality_have_binding_safe_shapes() {
458        assert_eq!(
459            serde_json::to_string(&CanonicalValue::Long(9_007_199_254_740_993)).unwrap(),
460            r#"{"kind":"long","value":"9007199254740993"}"#
461        );
462        let decimal = CanonicalValue::Decimal(DecimalValue::new("+001.2300dec").unwrap());
463        assert_eq!(
464            serde_json::to_string(&decimal).unwrap(),
465            r#"{"kind":"decimal","value":"1.23"}"#
466        );
467        let negative_zero = CanonicalValue::Double(CanonicalDouble::new(-0.0).unwrap());
468        assert_eq!(
469            serde_json::to_string(&negative_zero).unwrap(),
470            r#"{"kind":"double","bits":"8000000000000000"}"#
471        );
472        assert!(Cardinality::new(0, Some(0)).is_err());
473    }
474
475    #[test]
476    fn double_policy_rejects_nonfinite_and_preserves_exact_finite_bits() {
477        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
478            assert!(CanonicalDouble::new(value).is_err());
479        }
480
481        let positive_zero = CanonicalDouble::new(0.0).unwrap();
482        let negative_zero = CanonicalDouble::new(-0.0).unwrap();
483        assert_ne!(positive_zero, negative_zero);
484
485        for bits in [0, 0x8000_0000_0000_0000, 1] {
486            let value = CanonicalValue::Double(CanonicalDouble::from_bits(bits).unwrap());
487            let bytes = serde_json::to_vec(&value).unwrap();
488            assert_eq!(
489                serde_json::from_slice::<CanonicalValue>(&bytes).unwrap(),
490                value,
491            );
492        }
493
494        assert!(
495            serde_json::from_str::<CanonicalValue>(
496                r#"{"kind":"double","bits":"7ff0000000000000"}"#
497            )
498            .is_err()
499        );
500    }
501
502    #[test]
503    fn canonical_strings_enforce_the_byte_limit_without_changing_wire_shape() {
504        let boundary = CanonicalString::new("x".repeat(MAX_CANONICAL_STRING_BYTES)).unwrap();
505        assert_eq!(boundary.as_str().len(), MAX_CANONICAL_STRING_BYTES);
506        let error = CanonicalString::new("x".repeat(MAX_CANONICAL_STRING_BYTES + 1)).unwrap_err();
507        assert_eq!(error.category(), DiagnosticCategory::ResourceLimit);
508        assert_eq!(error.code().as_str(), "canonical_string_limit_exceeded");
509        assert_eq!(
510            serde_json::to_string(&CanonicalValue::String(
511                CanonicalString::new("text").unwrap()
512            ))
513            .unwrap(),
514            r#"{"kind":"string","value":"text"}"#,
515        );
516    }
517
518    #[test]
519    fn semantic_order_is_numeric_and_distinct_from_representation_order() {
520        let decimal_two = CanonicalValue::Decimal(DecimalValue::new("2").unwrap());
521        let decimal_ten = CanonicalValue::Decimal(DecimalValue::new("10").unwrap());
522        assert_eq!(
523            decimal_two.semantic_cmp_same_domain(&decimal_ten),
524            Some(Ordering::Less)
525        );
526
527        let negative_zero = CanonicalValue::Double(CanonicalDouble::new(-0.0).unwrap());
528        let positive_zero = CanonicalValue::Double(CanonicalDouble::new(0.0).unwrap());
529        assert_ne!(negative_zero, positive_zero);
530        assert_eq!(
531            negative_zero.semantic_cmp_same_domain(&positive_zero),
532            Some(Ordering::Equal)
533        );
534
535        assert_eq!(
536            CanonicalValue::Long(-10).semantic_cmp_same_domain(&CanonicalValue::Long(-2)),
537            Some(Ordering::Less),
538        );
539        assert_eq!(
540            CanonicalValue::String(CanonicalString::new("alpha").unwrap())
541                .semantic_cmp_same_domain(&CanonicalValue::String(
542                    CanonicalString::new("beta").unwrap()
543                ),),
544            Some(Ordering::Less),
545        );
546        assert_eq!(
547            CanonicalValue::Long(1).semantic_cmp_same_domain(&CanonicalValue::Double(
548                CanonicalDouble::new(1.0).unwrap()
549            )),
550            None,
551        );
552    }
553}