Skip to main content

type_bridge/
__codegen.rs

1//! Generated-code SPI for TypeBridge.
2//!
3//! Internal primitives and traits required by generated schema crates.
4//! Hand-written trait implementations are not schema evidence.
5
6use core::fmt;
7use core::marker::PhantomData;
8
9pub use crate::schema::{Schema, SchemaPackage, Unbound, sealed};
10
11/// An owned, nestable validation path.
12#[derive(Clone, Debug, Eq, PartialEq, Default)]
13pub struct ValidationPath {
14    segments: Vec<String>,
15}
16
17impl ValidationPath {
18    #[must_use]
19    pub fn root() -> Self {
20        Self {
21            segments: Vec::new(),
22        }
23    }
24
25    #[must_use]
26    pub fn join(&self, segment: impl Into<String>) -> Self {
27        let mut s = self.segments.clone();
28        s.push(segment.into());
29        Self { segments: s }
30    }
31
32    #[must_use]
33    pub fn join_index(&self, index: usize) -> Self {
34        let mut s = self.segments.clone();
35        if let Some(last) = s.last_mut() {
36            last.push_str(&format!("[{index}]"));
37        } else {
38            s.push(format!("[{index}]"));
39        }
40        Self { segments: s }
41    }
42
43    #[must_use]
44    pub fn path(&self) -> String {
45        self.segments.join(".")
46    }
47}
48
49/// A stable generated-input validation failure.
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct ValidationError {
52    path: String,
53    code: String,
54}
55
56impl ValidationError {
57    #[must_use]
58    pub fn new(path: impl Into<String>, code: impl Into<String>) -> Self {
59        Self {
60            path: path.into(),
61            code: code.into(),
62        }
63    }
64
65    #[must_use]
66    pub fn field(&self) -> &str {
67        &self.path
68    }
69
70    #[must_use]
71    pub fn code(&self) -> &str {
72        &self.code
73    }
74}
75
76impl fmt::Display for ValidationError {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        if self.path.is_empty() {
79            write!(formatter, "{}", self.code)
80        } else {
81            write!(formatter, "{}: {}", self.path, self.code)
82        }
83    }
84}
85
86impl std::error::Error for ValidationError {}
87
88/// An exact resolved cardinality.
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct Cardinality {
91    min: u64,
92    max: Option<u64>,
93}
94
95impl Cardinality {
96    #[must_use]
97    pub const fn new(min: u64, max: Option<u64>) -> Self {
98        Self { min, max }
99    }
100
101    #[must_use]
102    pub const fn min(self) -> u64 {
103        self.min
104    }
105
106    #[must_use]
107    pub const fn max(self) -> Option<u64> {
108        self.max
109    }
110}
111
112/// A statically required scalar value.
113#[derive(Clone, Debug, PartialEq)]
114pub struct Required<T>(T);
115
116impl<T> Required<T> {
117    #[must_use]
118    pub const fn new(value: T) -> Self {
119        Self(value)
120    }
121
122    #[must_use]
123    pub const fn get(&self) -> &T {
124        &self.0
125    }
126
127    #[must_use]
128    pub const fn value(&self) -> &T {
129        &self.0
130    }
131}
132
133impl<T> core::ops::Deref for Required<T> {
134    type Target = T;
135    fn deref(&self) -> &Self::Target {
136        &self.0
137    }
138}
139
140/// An optional scalar value.
141#[derive(Clone, Debug, PartialEq)]
142pub struct Optional<T>(Option<T>);
143
144impl<T> Optional<T> {
145    #[must_use]
146    pub const fn new(value: Option<T>) -> Self {
147        Self(value)
148    }
149
150    #[must_use]
151    pub const fn as_ref(&self) -> Option<&T> {
152        self.0.as_ref()
153    }
154}
155
156/// A sequence checked against its exact resolved cardinality.
157#[derive(Clone, Debug, PartialEq)]
158pub struct Sequence<T> {
159    values: Vec<T>,
160    cardinality: Cardinality,
161}
162
163impl<T> Sequence<T> {
164    pub fn try_new(
165        values: Vec<T>,
166        cardinality: Cardinality,
167        path: &ValidationPath,
168    ) -> Result<Self, ValidationError> {
169        let length = u64::try_from(values.len())
170            .map_err(|_| ValidationError::new(path.path(), "cardinality_overflow"))?;
171        if length < cardinality.min() || cardinality.max().is_some_and(|maximum| length > maximum) {
172            return Err(ValidationError::new(path.path(), "cardinality_violation"));
173        }
174        Ok(Self {
175            values,
176            cardinality,
177        })
178    }
179
180    #[must_use]
181    pub fn as_slice(&self) -> &[T] {
182        &self.values
183    }
184
185    #[must_use]
186    pub const fn cardinality(&self) -> Cardinality {
187        self.cardinality
188    }
189}
190
191/// A binary sum used to preserve exact heterogeneous model forms.
192#[derive(Clone, Debug, PartialEq)]
193pub enum Either<L, R> {
194    Left(L),
195    Right(R),
196}
197
198impl<L: sealed::Sealed, R: sealed::Sealed> sealed::Sealed for Either<L, R> {}
199
200impl<L: Model<Schema = S>, R: Model<Schema = S>, S: Schema> Model for Either<L, R> {
201    type Schema = S;
202    const TYPE_ID_JSON: &'static str = "either";
203}
204
205/// The uninhabited projection of an empty accepted-player set.
206#[derive(Clone, Debug, PartialEq)]
207pub enum Never {}
208
209/// A finite, normalized floating-point value stored as IEEE 754 bits.
210#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
211pub struct CanonicalDouble(u64);
212
213impl CanonicalDouble {
214    pub fn try_new(value: f64) -> Result<Self, ValidationError> {
215        if value.is_nan() || value.is_infinite() {
216            return Err(ValidationError::new("", "noncanonical_double"));
217        }
218        Ok(Self(value.to_bits()))
219    }
220
221    pub fn try_from_bits(bits: u64) -> Result<Self, ValidationError> {
222        let val = f64::from_bits(bits);
223        if val.is_nan() || val.is_infinite() {
224            return Err(ValidationError::new("", "noncanonical_double"));
225        }
226        Ok(Self(bits))
227    }
228
229    #[must_use]
230    pub fn get(self) -> f64 {
231        f64::from_bits(self.0)
232    }
233
234    #[must_use]
235    pub const fn to_bits(self) -> u64 {
236        self.0
237    }
238}
239
240macro_rules! canonical_scalar {
241    ($name:ident, $code:expr, $parse_expr:expr) => {
242        #[derive(Clone, Debug, Eq, PartialEq, Hash)]
243        pub struct $name(String);
244
245        impl $name {
246            pub fn try_new(value: impl Into<String>) -> Result<Self, ValidationError> {
247                let s = value.into();
248                if type_bridge_contract::value::CanonicalString::new(&s).is_err() {
249                    return Err(ValidationError::new("", "string_limit_exceeded"));
250                }
251                let check_fn: fn(&str) -> bool = $parse_expr;
252                if !check_fn(&s) {
253                    return Err(ValidationError::new("", $code));
254                }
255                Ok(Self(s))
256            }
257
258            #[must_use]
259            pub fn as_str(&self) -> &str {
260                &self.0
261            }
262        }
263    };
264}
265
266canonical_scalar!(Decimal, "noncanonical_decimal", |s| {
267    if let Some(dec) = type_bridge_contract::decimal::parse_decimal(s) {
268        dec.canonical_string() == s
269    } else {
270        false
271    }
272});
273
274canonical_scalar!(Date, "noncanonical_date", |s| {
275    if let Ok(d) = s.parse::<type_bridge_contract::temporal::CanonicalDate>() {
276        d.to_string() == s
277    } else {
278        false
279    }
280});
281
282canonical_scalar!(DateTime, "noncanonical_datetime", |s| {
283    if let Ok(dt) = s.parse::<type_bridge_contract::temporal::CanonicalDateTime>() {
284        dt.to_string() == s
285    } else {
286        false
287    }
288});
289
290canonical_scalar!(DateTimeTz, "noncanonical_datetime_tz", |s| {
291    if let Ok(dtz) = s.parse::<type_bridge_contract::temporal::CanonicalDateTimeTz>() {
292        dtz.to_string() == s
293    } else {
294        false
295    }
296});
297
298canonical_scalar!(Duration, "noncanonical_duration", |s| {
299    if let Ok(dur) = s.parse::<type_bridge_contract::temporal::CanonicalDuration>() {
300        dur.to_string() == s
301    } else {
302        false
303    }
304});
305
306/// An opaque capability token required for model materialization.
307#[doc(hidden)]
308#[derive(Clone, Copy, Debug, Eq, PartialEq)]
309pub struct HydrationCapability {
310    _private: (),
311}
312
313impl HydrationCapability {
314    pub(crate) const fn new() -> Self {
315        Self { _private: () }
316    }
317}
318
319/// Test-support materializer entry point available under test-harness feature.
320#[cfg(feature = "test-harness")]
321#[doc(hidden)]
322pub fn materialize_model_for_test<M: MaterializeModel>(
323    row: &HydratedRow,
324) -> Result<M, ValidationError> {
325    let cap = HydrationCapability::new();
326    M::materialize(row, &cap)
327}
328
329/// Conversion trait into an `EncodedScalar`.
330#[doc(hidden)]
331pub trait IntoEncodedScalar {
332    #[allow(clippy::wrong_self_convention)]
333    fn into_encoded_scalar(&self) -> EncodedScalar;
334}
335
336impl IntoEncodedScalar for String {
337    fn into_encoded_scalar(&self) -> EncodedScalar {
338        EncodedScalar::String(self.clone())
339    }
340}
341
342impl IntoEncodedScalar for i64 {
343    fn into_encoded_scalar(&self) -> EncodedScalar {
344        EncodedScalar::Long(*self)
345    }
346}
347
348impl IntoEncodedScalar for CanonicalDouble {
349    fn into_encoded_scalar(&self) -> EncodedScalar {
350        EncodedScalar::Double(*self)
351    }
352}
353
354impl IntoEncodedScalar for bool {
355    fn into_encoded_scalar(&self) -> EncodedScalar {
356        EncodedScalar::Boolean(*self)
357    }
358}
359
360impl IntoEncodedScalar for Decimal {
361    fn into_encoded_scalar(&self) -> EncodedScalar {
362        EncodedScalar::Decimal(self.clone())
363    }
364}
365
366impl IntoEncodedScalar for Date {
367    fn into_encoded_scalar(&self) -> EncodedScalar {
368        EncodedScalar::Date(self.clone())
369    }
370}
371
372impl IntoEncodedScalar for DateTime {
373    fn into_encoded_scalar(&self) -> EncodedScalar {
374        EncodedScalar::DateTime(self.clone())
375    }
376}
377
378impl IntoEncodedScalar for DateTimeTz {
379    fn into_encoded_scalar(&self) -> EncodedScalar {
380        EncodedScalar::DateTimeTz(self.clone())
381    }
382}
383
384impl IntoEncodedScalar for Duration {
385    fn into_encoded_scalar(&self) -> EncodedScalar {
386        EncodedScalar::Duration(self.clone())
387    }
388}
389
390impl IntoEncodedScalar for EncodedScalar {
391    fn into_encoded_scalar(&self) -> EncodedScalar {
392        self.clone()
393    }
394}
395
396impl<T: IntoEncodedScalar> IntoEncodedScalar for &T {
397    fn into_encoded_scalar(&self) -> EncodedScalar {
398        (*self).into_encoded_scalar()
399    }
400}
401
402/// A generated value wrapper's canonical scalar query domain.
403///
404/// Generated schema crates implement this marker for attribute wrappers so
405/// equality operands can preserve their scalar domain without exposing engine
406/// value DTOs.
407#[doc(hidden)]
408pub trait QueryValued: IntoEncodedScalar {
409    /// The canonical scalar type represented by this value.
410    type Domain;
411}
412
413impl QueryValued for String {
414    type Domain = String;
415}
416impl QueryValued for i64 {
417    type Domain = i64;
418}
419impl QueryValued for CanonicalDouble {
420    type Domain = CanonicalDouble;
421}
422impl QueryValued for bool {
423    type Domain = bool;
424}
425impl QueryValued for Decimal {
426    type Domain = Decimal;
427}
428impl QueryValued for Date {
429    type Domain = Date;
430}
431impl QueryValued for DateTime {
432    type Domain = DateTime;
433}
434impl QueryValued for DateTimeTz {
435    type Domain = DateTimeTz;
436}
437impl QueryValued for Duration {
438    type Domain = Duration;
439}
440impl<T: QueryValued> QueryValued for &T {
441    type Domain = T::Domain;
442}
443
444/// Target thing category: Entity or Relation.
445#[derive(Clone, Copy, Debug, Eq, PartialEq)]
446pub enum ThingKind {
447    Entity,
448    Relation,
449}
450
451/// A sealed generated schema model associated with its schema marker `S`.
452pub trait Model: sealed::Sealed {
453    type Schema: Schema;
454    const TYPE_ID_JSON: &'static str;
455}
456
457/// An entity or relation schema model.
458pub trait ThingModel: Model {
459    fn thing_kind() -> ThingKind;
460}
461
462/// An entity schema model.
463pub trait EntityModel: ThingModel {}
464
465/// A relation schema model.
466pub trait RelationModel: ThingModel {}
467
468/// A complete materialized generated model with a mandatory canonical IID.
469pub trait CompleteModel: ThingModel + MaterializeModel {
470    type Create: IntoEncodedCreate;
471    fn iid(&self) -> &str;
472}
473
474#[doc(hidden)]
475pub trait SubtypeRootModel: ThingModel {
476    type Subtypes;
477    fn __tb_dispatch_subtype(
478        row: &HydratedRow,
479        cap: &HydrationCapability,
480    ) -> Result<Self::Subtypes, ValidationError>;
481}
482
483/// An abstract generated model marker.
484pub trait AbstractModel: ThingModel {}
485
486/// Marker for generated attribute values whose canonical domain is text.
487pub trait TextValued {}
488impl TextValued for String {}
489
490/// Marker for generated attribute values whose canonical domain admits
491/// canonical ordering for range comparisons.
492pub trait OrderedValued {}
493
494/// Marker for generated attribute values whose canonical domain admits
495/// numeric reduction, carrying the canonical reduced scalar domain.
496pub trait NumericValued {
497    /// The canonical scalar domain of domain-preserving reductions.
498    type Reduced;
499}
500impl NumericValued for i64 {
501    type Reduced = i64;
502}
503impl NumericValued for CanonicalDouble {
504    type Reduced = f64;
505}
506impl OrderedValued for i64 {}
507impl OrderedValued for CanonicalDouble {}
508impl OrderedValued for Date {}
509impl OrderedValued for DateTime {}
510impl OrderedValued for DateTimeTz {}
511impl OrderedValued for Decimal {}
512impl OrderedValued for Duration {}
513
514/// A nonrecursive generated reference model. Key-based references may not have an IID initially.
515pub trait ReferenceModel: ThingModel {
516    fn iid(&self) -> Option<&str>;
517}
518
519/// A closed client scalar value over all nine canonical domains.
520#[doc(hidden)]
521#[derive(Clone, Debug, PartialEq)]
522pub enum EncodedScalar {
523    String(String),
524    Long(i64),
525    Double(CanonicalDouble),
526    Decimal(Decimal),
527    Boolean(bool),
528    Date(Date),
529    DateTime(DateTime),
530    DateTimeTz(DateTimeTz),
531    Duration(Duration),
532}
533
534impl EncodedScalar {
535    pub fn as_string(&self) -> Option<&str> {
536        match self {
537            Self::String(s) => Some(s.as_str()),
538            _ => None,
539        }
540    }
541
542    pub fn as_long(&self) -> Option<i64> {
543        match self {
544            Self::Long(n) => Some(*n),
545            _ => None,
546        }
547    }
548
549    pub fn as_double(&self) -> Option<CanonicalDouble> {
550        match self {
551            Self::Double(d) => Some(*d),
552            _ => None,
553        }
554    }
555
556    pub fn as_boolean(&self) -> Option<bool> {
557        match self {
558            Self::Boolean(b) => Some(*b),
559            _ => None,
560        }
561    }
562
563    pub fn as_decimal(&self) -> Option<&Decimal> {
564        match self {
565            Self::Decimal(d) => Some(d),
566            _ => None,
567        }
568    }
569
570    pub fn as_date(&self) -> Option<&Date> {
571        match self {
572            Self::Date(d) => Some(d),
573            _ => None,
574        }
575    }
576
577    pub fn as_datetime(&self) -> Option<&DateTime> {
578        match self {
579            Self::DateTime(d) => Some(d),
580            _ => None,
581        }
582    }
583
584    pub fn as_datetime_tz(&self) -> Option<&DateTimeTz> {
585        match self {
586            Self::DateTimeTz(d) => Some(d),
587            _ => None,
588        }
589    }
590
591    pub fn as_duration(&self) -> Option<&Duration> {
592        match self {
593            Self::Duration(d) => Some(d),
594            _ => None,
595        }
596    }
597
598    fn to_canonical_value(
599        &self,
600        path: &ValidationPath,
601    ) -> Result<type_bridge_contract::value::CanonicalValue, ValidationError> {
602        use type_bridge_contract::temporal::{
603            CanonicalDate, CanonicalDateTime, CanonicalDateTimeTz, CanonicalDuration,
604        };
605        use type_bridge_contract::value::{CanonicalDouble, CanonicalString, CanonicalValue};
606        match self {
607            Self::String(s) => CanonicalString::new(s)
608                .map(CanonicalValue::String)
609                .map_err(|_| ValidationError::new(path.path(), "string_limit_exceeded")),
610            Self::Long(n) => Ok(CanonicalValue::Long(*n)),
611            Self::Double(d) => CanonicalDouble::new(d.get())
612                .map(CanonicalValue::Double)
613                .map_err(|_| ValidationError::new(path.path(), "noncanonical_double")),
614            Self::Boolean(b) => Ok(CanonicalValue::Boolean(*b)),
615            Self::Decimal(d) => {
616                if type_bridge_contract::decimal::parse_decimal(d.as_str()).is_some() {
617                    type_bridge_contract::value::DecimalValue::new(d.as_str())
618                        .map(CanonicalValue::Decimal)
619                        .map_err(|_| ValidationError::new(path.path(), "noncanonical_decimal"))
620                } else {
621                    Err(ValidationError::new(path.path(), "noncanonical_decimal"))
622                }
623            }
624            Self::Date(d) => d
625                .as_str()
626                .parse::<CanonicalDate>()
627                .map(CanonicalValue::Date)
628                .map_err(|_| ValidationError::new(path.path(), "noncanonical_date")),
629            Self::DateTime(dt) => dt
630                .as_str()
631                .parse::<CanonicalDateTime>()
632                .map(CanonicalValue::DateTime)
633                .map_err(|_| ValidationError::new(path.path(), "noncanonical_datetime")),
634            Self::DateTimeTz(dtz) => dtz
635                .as_str()
636                .parse::<CanonicalDateTimeTz>()
637                .map(CanonicalValue::DateTimeTz)
638                .map_err(|_| ValidationError::new(path.path(), "noncanonical_datetime_tz")),
639            Self::Duration(dur) => dur
640                .as_str()
641                .parse::<CanonicalDuration>()
642                .map(CanonicalValue::Duration)
643                .map_err(|_| ValidationError::new(path.path(), "noncanonical_duration")),
644        }
645    }
646}
647
648pub fn validate_canonical_string(
649    value: &str,
650    path: &ValidationPath,
651) -> Result<(), ValidationError> {
652    if type_bridge_contract::value::CanonicalString::new(value).is_err() {
653        return Err(ValidationError::new(path.path(), "string_limit_exceeded"));
654    }
655    Ok(())
656}
657
658pub fn prefix_validation_path(
659    err: ValidationError,
660    parent_path: &ValidationPath,
661) -> ValidationError {
662    let sub_path = err.field();
663    let full_path = if sub_path.is_empty() || sub_path == "value" {
664        parent_path.path()
665    } else {
666        format!("{}.{}", parent_path.path(), sub_path)
667    };
668    ValidationError::new(full_path, err.code())
669}
670
671/// A document-hidden closed constraint descriptor for attribute value and owns-edge validation.
672#[doc(hidden)]
673#[derive(Clone, Debug, PartialEq)]
674pub struct ConstraintDescriptor {
675    range_min: Option<EncodedScalar>,
676    range_max: Option<EncodedScalar>,
677    regex: Option<&'static str>,
678    values: Option<Vec<EncodedScalar>>,
679}
680
681impl ConstraintDescriptor {
682    #[must_use]
683    pub fn new(
684        range_min: Option<EncodedScalar>,
685        range_max: Option<EncodedScalar>,
686        regex: Option<&'static str>,
687        values: Option<Vec<EncodedScalar>>,
688    ) -> Self {
689        Self {
690            range_min,
691            range_max,
692            regex,
693            values,
694        }
695    }
696
697    pub fn validate(
698        &self,
699        value: &EncodedScalar,
700        path: &ValidationPath,
701    ) -> Result<(), ValidationError> {
702        if let Some(pattern) = self.regex {
703            let Some(s) = value.as_string() else {
704                return Err(ValidationError::new(path.path(), "wrong_scalar_domain"));
705            };
706            let re = regex::Regex::new(pattern)
707                .map_err(|_| ValidationError::new(path.path(), "invalid_regex_pattern"))?;
708            if !re.is_match(s) {
709                return Err(ValidationError::new(path.path(), "regex_violation"));
710            }
711        }
712
713        let canonical_val = value.to_canonical_value(path)?;
714
715        if let Some(allowed) = &self.values {
716            let mut found = false;
717            for item in allowed {
718                let allowed_canon = item.to_canonical_value(path)?;
719                if canonical_val.value_type() != allowed_canon.value_type() {
720                    return Err(ValidationError::new(path.path(), "wrong_scalar_domain"));
721                }
722                let equal = match canonical_val.semantic_cmp_same_domain(&allowed_canon) {
723                    Some(std::cmp::Ordering::Equal) => true,
724                    Some(_) => false,
725                    None => canonical_val == allowed_canon,
726                };
727                if equal {
728                    found = true;
729                    break;
730                }
731            }
732            if !found {
733                return Err(ValidationError::new(path.path(), "values_violation"));
734            }
735        }
736        if let Some(min) = &self.range_min {
737            let min_canon = min.to_canonical_value(path)?;
738            let cmp = canonical_val
739                .semantic_cmp_same_domain(&min_canon)
740                .ok_or_else(|| ValidationError::new(path.path(), "wrong_scalar_domain"))?;
741            if cmp == std::cmp::Ordering::Less {
742                return Err(ValidationError::new(path.path(), "range_violation"));
743            }
744        }
745        if let Some(max) = &self.range_max {
746            let max_canon = max.to_canonical_value(path)?;
747            let cmp = canonical_val
748                .semantic_cmp_same_domain(&max_canon)
749                .ok_or_else(|| ValidationError::new(path.path(), "wrong_scalar_domain"))?;
750            if cmp == std::cmp::Ordering::Greater {
751                return Err(ValidationError::new(path.path(), "range_violation"));
752            }
753        }
754        Ok(())
755    }
756}
757
758/// A transport-neutral encoded IID-or-typed-key reference.
759#[doc(hidden)]
760#[derive(Clone, Debug, PartialEq)]
761pub struct EncodedReference {
762    type_id_json: &'static str,
763    iid: Option<String>,
764    keys: Vec<(&'static str, EncodedScalar)>,
765}
766
767impl EncodedReference {
768    pub fn try_new(
769        type_id_json: &'static str,
770        iid: Option<String>,
771        keys: Vec<(&'static str, EncodedScalar)>,
772        path: &ValidationPath,
773    ) -> Result<Self, ValidationError> {
774        if iid.as_deref().is_some_and(|value| value.trim().is_empty()) {
775            return Err(ValidationError::new(path.join("iid").path(), "empty_iid"));
776        }
777        let mut seen = std::collections::BTreeSet::new();
778        for (index, (token, _)) in keys.iter().enumerate() {
779            if !seen.insert(*token) {
780                return Err(ValidationError::new(
781                    path.join("keys").join_index(index).path(),
782                    "duplicate_reference_key",
783                ));
784            }
785        }
786        if iid.is_none() && keys.is_empty() {
787            return Err(ValidationError::new(
788                path.path(),
789                "missing_reference_identity",
790            ));
791        }
792        if iid.is_none() && keys.len() > 1 {
793            return Err(ValidationError::new(
794                path.path(),
795                "multiple_reference_keys_without_iid",
796            ));
797        }
798        Ok(Self {
799            type_id_json,
800            iid,
801            keys,
802        })
803    }
804
805    #[must_use]
806    pub const fn type_id_json(&self) -> &'static str {
807        self.type_id_json
808    }
809
810    #[must_use]
811    pub fn iid(&self) -> Option<&str> {
812        self.iid.as_deref()
813    }
814
815    #[must_use]
816    pub fn keys(&self) -> &[(&'static str, EncodedScalar)] {
817        &self.keys
818    }
819}
820
821/// Ordered encoded owned fields and active roles for a thing creation payload.
822#[doc(hidden)]
823#[derive(Clone, Debug, PartialEq)]
824pub struct EncodedCreate {
825    type_id_json: &'static str,
826    fields: Vec<(&'static str, Vec<EncodedScalar>)>,
827    roles: Vec<(&'static str, Vec<EncodedReference>)>,
828}
829
830impl EncodedCreate {
831    #[must_use]
832    pub const fn new(
833        type_id_json: &'static str,
834        fields: Vec<(&'static str, Vec<EncodedScalar>)>,
835        roles: Vec<(&'static str, Vec<EncodedReference>)>,
836    ) -> Self {
837        Self {
838            type_id_json,
839            fields,
840            roles,
841        }
842    }
843
844    #[must_use]
845    pub const fn type_id_json(&self) -> &'static str {
846        self.type_id_json
847    }
848
849    #[must_use]
850    pub fn fields(&self) -> &[(&'static str, Vec<EncodedScalar>)] {
851        &self.fields
852    }
853
854    #[must_use]
855    pub fn roles(&self) -> &[(&'static str, Vec<EncodedReference>)] {
856        &self.roles
857    }
858}
859
860/// Nonrecursive player reference evidence carried by a hydrated role.
861#[doc(hidden)]
862#[derive(Clone, Debug, PartialEq)]
863pub struct HydratedPlayer {
864    type_id_json: String,
865    iid: Option<String>,
866    keys: Vec<(String, EncodedScalar)>,
867}
868
869impl HydratedPlayer {
870    #[must_use]
871    pub fn new(
872        type_id_json: &'static str,
873        iid: Option<String>,
874        keys: Vec<(&'static str, EncodedScalar)>,
875    ) -> Self {
876        Self {
877            type_id_json: type_id_json.to_owned(),
878            iid,
879            keys: keys
880                .into_iter()
881                .map(|(identity, value)| (identity.to_owned(), value))
882                .collect(),
883        }
884    }
885
886    #[must_use]
887    #[allow(dead_code)]
888    pub(crate) fn from_owned(
889        type_id_json: String,
890        iid: Option<String>,
891        keys: Vec<(String, EncodedScalar)>,
892    ) -> Self {
893        Self {
894            type_id_json,
895            iid,
896            keys,
897        }
898    }
899
900    #[must_use]
901    pub fn type_id_json(&self) -> &str {
902        &self.type_id_json
903    }
904
905    #[must_use]
906    pub fn iid(&self) -> Option<&str> {
907        self.iid.as_deref()
908    }
909
910    #[must_use]
911    pub fn keys(&self) -> &[(String, EncodedScalar)] {
912        &self.keys
913    }
914}
915
916/// A transport-neutral hydrated thing with exact concrete type identity, mandatory IID, owned-value evidence, and role-player evidence.
917#[doc(hidden)]
918#[derive(Clone, Debug, PartialEq)]
919pub struct HydratedRow {
920    type_id_json: String,
921    iid: String,
922    fields: Vec<(String, Vec<EncodedScalar>)>,
923    roles: Vec<(String, Vec<HydratedPlayer>)>,
924}
925
926impl HydratedRow {
927    #[must_use]
928    pub fn new(
929        type_id_json: &'static str,
930        iid: String,
931        fields: Vec<(&'static str, Vec<EncodedScalar>)>,
932        roles: Vec<(&'static str, Vec<HydratedPlayer>)>,
933    ) -> Self {
934        Self {
935            type_id_json: type_id_json.to_owned(),
936            iid,
937            fields: fields
938                .into_iter()
939                .map(|(identity, values)| (identity.to_owned(), values))
940                .collect(),
941            roles: roles
942                .into_iter()
943                .map(|(identity, players)| (identity.to_owned(), players))
944                .collect(),
945        }
946    }
947
948    #[must_use]
949    #[allow(dead_code)]
950    pub(crate) fn from_owned(
951        type_id_json: String,
952        iid: String,
953        fields: Vec<(String, Vec<EncodedScalar>)>,
954        roles: Vec<(String, Vec<HydratedPlayer>)>,
955    ) -> Self {
956        Self {
957            type_id_json,
958            iid,
959            fields,
960            roles,
961        }
962    }
963
964    #[must_use]
965    pub fn type_id_json(&self) -> &str {
966        &self.type_id_json
967    }
968
969    #[must_use]
970    pub fn iid(&self) -> &str {
971        &self.iid
972    }
973
974    #[must_use]
975    pub fn fields(&self) -> &[(String, Vec<EncodedScalar>)] {
976        &self.fields
977    }
978
979    #[must_use]
980    pub fn roles(&self) -> &[(String, Vec<HydratedPlayer>)] {
981        &self.roles
982    }
983
984    pub fn validate_shape(
985        &self,
986        expected_type_id: &'static str,
987        expected_fields: &[&'static str],
988        expected_roles: &[&'static str],
989        path: &ValidationPath,
990    ) -> Result<(), ValidationError> {
991        if self.type_id_json != expected_type_id {
992            return Err(ValidationError::new(
993                path.path(),
994                "wrong_concrete_model_type",
995            ));
996        }
997        let mut seen_fields = std::collections::BTreeSet::new();
998        for (k, _) in &self.fields {
999            if !seen_fields.insert(k.as_str()) {
1000                return Err(ValidationError::new(
1001                    path.path(),
1002                    "duplicate_scalar_evidence",
1003                ));
1004            }
1005            if !expected_fields.contains(&k.as_str()) {
1006                return Err(ValidationError::new(
1007                    path.path(),
1008                    "unexpected_field_evidence",
1009                ));
1010            }
1011        }
1012        let mut seen_roles = std::collections::BTreeSet::new();
1013        for (r, _) in &self.roles {
1014            if !seen_roles.insert(r.as_str()) {
1015                return Err(ValidationError::new(path.path(), "duplicate_role_evidence"));
1016            }
1017            if !expected_roles.contains(&r.as_str()) {
1018                return Err(ValidationError::new(
1019                    path.path(),
1020                    "unexpected_role_evidence",
1021                ));
1022            }
1023        }
1024        Ok(())
1025    }
1026}
1027
1028/// Lowering trait from a generated create payload into an `EncodedCreate`.
1029#[doc(hidden)]
1030pub trait IntoEncodedCreate: sealed::Sealed {
1031    fn into_encoded_create(self) -> Result<EncodedCreate, ValidationError>;
1032}
1033
1034/// Lowering trait from a generated reference into an `EncodedReference`.
1035#[doc(hidden)]
1036pub trait IntoEncodedReference: sealed::Sealed {
1037    fn into_encoded_reference(self) -> Result<EncodedReference, ValidationError>;
1038}
1039
1040/// Materializing trait for generated complete read models or families from a `HydratedRow`.
1041#[doc(hidden)]
1042pub trait MaterializeModel: Model + Sized {
1043    fn materialize(row: &HydratedRow, cap: &HydrationCapability) -> Result<Self, ValidationError>;
1044}
1045
1046/// A closed subtype family enum representing a concrete descendant closure for a root `Root`.
1047pub trait ModelFamily: sealed::Sealed {
1048    type Root: ThingModel;
1049    type Schema: Schema;
1050    fn iid(&self) -> &str;
1051}
1052
1053/// A sealed generated struct value associated with its schema marker `S`.
1054pub trait StructValue: sealed::Sealed {
1055    type Schema: Schema;
1056    const STRUCT_ID_JSON: &'static str;
1057}
1058
1059/// A resolver-proven nominal upcast relation.
1060pub trait NominalUpcast<Target: Model>: Model {}
1061
1062/// A resolver-proven specialized-role upcast relation.
1063pub trait RoleUpcast<ActiveRole, AncestorRole>: Model {}
1064
1065/// Positive generated evidence that one role token is active on a relation.
1066///
1067/// Unlike nominal inheritance, this relation is intentionally subtractive:
1068/// generated specializing relations do not implement compatibility for the
1069/// specialized-away ancestor role.
1070pub trait RoleTokenCompatible<Owner: RelationModel, Players>: RelationModel {}
1071
1072/// Positive generated evidence that a role's exact player union admits one
1073/// bound player model.
1074pub trait RolePlayer<Player: ThingModel> {}
1075
1076/// A schema type/query token branded by its exact owner.
1077#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1078pub struct TypeToken<Owner: Model> {
1079    type_id_json: &'static str,
1080    metadata_json: &'static str,
1081    marker: PhantomData<fn() -> Owner>,
1082}
1083
1084impl<Owner: Model> TypeToken<Owner> {
1085    #[must_use]
1086    pub const fn new(type_id_json: &'static str, metadata_json: &'static str) -> Self {
1087        Self {
1088            type_id_json,
1089            metadata_json,
1090            marker: PhantomData,
1091        }
1092    }
1093
1094    #[must_use]
1095    pub const fn type_id_json(self) -> &'static str {
1096        self.type_id_json
1097    }
1098
1099    #[must_use]
1100    pub const fn metadata_json(self) -> &'static str {
1101        self.metadata_json
1102    }
1103}
1104
1105/// An owned-field token branded by owner and value model.
1106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1107pub struct FieldToken<Owner: Model, Value> {
1108    owns_id_json: &'static str,
1109    metadata_json: &'static str,
1110    marker: PhantomData<fn() -> (Owner, Value)>,
1111}
1112
1113impl<Owner: Model, Value> FieldToken<Owner, Value> {
1114    #[must_use]
1115    pub const fn new(owns_id_json: &'static str, metadata_json: &'static str) -> Self {
1116        Self {
1117            owns_id_json,
1118            metadata_json,
1119            marker: PhantomData,
1120        }
1121    }
1122
1123    #[must_use]
1124    pub const fn owns_id_json(self) -> &'static str {
1125        self.owns_id_json
1126    }
1127
1128    #[must_use]
1129    pub const fn metadata_json(self) -> &'static str {
1130        self.metadata_json
1131    }
1132}
1133
1134/// A related-role token branded by owner and exact accepted-player enum.
1135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1136pub struct RoleToken<Owner: Model, Players> {
1137    role_id_json: &'static str,
1138    metadata_json: &'static str,
1139    marker: PhantomData<fn() -> (Owner, Players)>,
1140}
1141
1142impl<Owner: Model, Players> RoleToken<Owner, Players> {
1143    #[must_use]
1144    pub const fn new(role_id_json: &'static str, metadata_json: &'static str) -> Self {
1145        Self {
1146            role_id_json,
1147            metadata_json,
1148            marker: PhantomData,
1149        }
1150    }
1151
1152    #[must_use]
1153    pub const fn role_id_json(self) -> &'static str {
1154        self.role_id_json
1155    }
1156
1157    #[must_use]
1158    pub const fn metadata_json(self) -> &'static str {
1159        self.metadata_json
1160    }
1161}
1162
1163/// A playing-fact token branded by player, role owner, and accepted-player enum.
1164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1165pub struct PlaysToken<Player: Model, Owner: Model, Players> {
1166    plays_id_json: &'static str,
1167    metadata_json: &'static str,
1168    #[allow(clippy::type_complexity)]
1169    marker: PhantomData<fn() -> (Player, Owner, Players)>,
1170}
1171
1172impl<Player: Model, Owner: Model, Players> PlaysToken<Player, Owner, Players> {
1173    #[must_use]
1174    pub const fn new(plays_id_json: &'static str, metadata_json: &'static str) -> Self {
1175        Self {
1176            plays_id_json,
1177            metadata_json,
1178            marker: PhantomData,
1179        }
1180    }
1181
1182    #[must_use]
1183    pub const fn plays_id_json(self) -> &'static str {
1184        self.plays_id_json
1185    }
1186
1187    #[must_use]
1188    pub const fn metadata_json(self) -> &'static str {
1189        self.metadata_json
1190    }
1191}
1192
1193/// A typed schema-function token branded by schema `S`.
1194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1195pub struct FunctionToken<S: Schema, Arguments, Output> {
1196    function_id: &'static str,
1197    metadata_json: &'static str,
1198    marker: PhantomData<fn(Arguments) -> (S, Output)>,
1199}
1200
1201impl<S: Schema, Arguments, Output> FunctionToken<S, Arguments, Output> {
1202    #[must_use]
1203    pub const fn new(function_id: &'static str, metadata_json: &'static str) -> Self {
1204        Self {
1205            function_id,
1206            metadata_json,
1207            marker: PhantomData,
1208        }
1209    }
1210
1211    #[must_use]
1212    pub const fn function_id(self) -> &'static str {
1213        self.function_id
1214    }
1215
1216    #[must_use]
1217    pub const fn metadata_json(self) -> &'static str {
1218        self.metadata_json
1219    }
1220}
1221
1222/// A typed asynchronous stream result marker.
1223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1224pub struct Stream<T>(PhantomData<fn() -> T>);
1225
1226#[cfg(test)]
1227mod tests {
1228    use super::*;
1229
1230    #[test]
1231    fn canonical_scalar_wrappers_do_not_expose_lexical_ordering() {
1232        let source = include_str!("__codegen.rs");
1233        let (_, macro_and_invocations) = source
1234            .split_once("macro_rules! canonical_scalar")
1235            .expect("canonical scalar macro remains present");
1236        let (macro_body, _) = macro_and_invocations
1237            .split_once("canonical_scalar!(Decimal")
1238            .expect("canonical scalar invocations remain present");
1239        assert!(macro_body.contains("#[derive(Clone, Debug, Eq, PartialEq, Hash)]"));
1240        assert!(!macro_body.contains("Ord"));
1241        assert!(!macro_body.contains("PartialOrd"));
1242    }
1243
1244    #[test]
1245    fn scalar_domain_wrappers_and_validation() {
1246        let path = ValidationPath::root().join("test");
1247
1248        let double_ok = CanonicalDouble::try_new(3.125).unwrap();
1249        assert_eq!(double_ok.get(), 3.125);
1250        assert_eq!(
1251            CanonicalDouble::try_new(f64::NAN).unwrap_err().code(),
1252            "noncanonical_double"
1253        );
1254        assert_eq!(
1255            CanonicalDouble::try_new(f64::INFINITY).unwrap_err().code(),
1256            "noncanonical_double"
1257        );
1258        let neg_zero = CanonicalDouble::try_new(-0.0).unwrap();
1259        let pos_zero = CanonicalDouble::try_new(0.0).unwrap();
1260        assert_ne!(neg_zero.to_bits(), pos_zero.to_bits());
1261
1262        let dec = Decimal::try_new("123.45").unwrap();
1263        assert_eq!(dec.as_str(), "123.45");
1264        assert_eq!(
1265            Decimal::try_new("123.4500").unwrap_err().code(),
1266            "noncanonical_decimal"
1267        );
1268
1269        let date = Date::try_new("2026-07-28").unwrap();
1270        assert_eq!(date.as_str(), "2026-07-28");
1271        assert_eq!(
1272            Date::try_new("2026-7-28").unwrap_err().code(),
1273            "noncanonical_date"
1274        );
1275
1276        let dt = DateTime::try_new("2026-07-28T03:55:00").unwrap();
1277        assert_eq!(dt.as_str(), "2026-07-28T03:55:00");
1278
1279        let dtz = DateTimeTz::try_new("2026-07-28T03:55:00Z").unwrap();
1280        assert_eq!(dtz.as_str(), "2026-07-28T03:55:00Z");
1281
1282        let dur = Duration::try_new("P1D").unwrap();
1283        assert_eq!(dur.as_str(), "P1D");
1284
1285        let seq = Sequence::try_new(vec![1, 2], Cardinality::new(1, Some(3)), &path).unwrap();
1286        assert_eq!(seq.as_slice(), &[1, 2]);
1287
1288        let seq_err = Sequence::try_new(vec![1, 2, 3, 4], Cardinality::new(1, Some(3)), &path);
1289        assert_eq!(seq_err.unwrap_err().code(), "cardinality_violation");
1290
1291        let constraint = ConstraintDescriptor::new(
1292            Some(EncodedScalar::String("a".to_owned())),
1293            Some(EncodedScalar::String("z".to_owned())),
1294            Some("^a.*z$"),
1295            None,
1296        );
1297        let s_val = EncodedScalar::String("abcz".to_owned());
1298        assert_eq!(constraint.validate(&s_val, &path), Ok(()));
1299    }
1300}