1use core::fmt;
7use core::marker::PhantomData;
8
9pub use crate::schema::{Schema, SchemaPackage, Unbound, sealed};
10
11#[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#[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#[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#[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#[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#[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#[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#[derive(Clone, Debug, PartialEq)]
207pub enum Never {}
208
209#[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#[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#[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#[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#[doc(hidden)]
408pub trait QueryValued: IntoEncodedScalar {
409 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
446pub enum ThingKind {
447 Entity,
448 Relation,
449}
450
451pub trait Model: sealed::Sealed {
453 type Schema: Schema;
454 const TYPE_ID_JSON: &'static str;
455}
456
457pub trait ThingModel: Model {
459 fn thing_kind() -> ThingKind;
460}
461
462pub trait EntityModel: ThingModel {}
464
465pub trait RelationModel: ThingModel {}
467
468pub 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
483pub trait AbstractModel: ThingModel {}
485
486pub trait TextValued {}
488impl TextValued for String {}
489
490pub trait OrderedValued {}
493
494pub trait NumericValued {
497 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
514pub trait ReferenceModel: ThingModel {
516 fn iid(&self) -> Option<&str>;
517}
518
519#[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#[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#[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#[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#[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#[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#[doc(hidden)]
1030pub trait IntoEncodedCreate: sealed::Sealed {
1031 fn into_encoded_create(self) -> Result<EncodedCreate, ValidationError>;
1032}
1033
1034#[doc(hidden)]
1036pub trait IntoEncodedReference: sealed::Sealed {
1037 fn into_encoded_reference(self) -> Result<EncodedReference, ValidationError>;
1038}
1039
1040#[doc(hidden)]
1042pub trait MaterializeModel: Model + Sized {
1043 fn materialize(row: &HydratedRow, cap: &HydrationCapability) -> Result<Self, ValidationError>;
1044}
1045
1046pub trait ModelFamily: sealed::Sealed {
1048 type Root: ThingModel;
1049 type Schema: Schema;
1050 fn iid(&self) -> &str;
1051}
1052
1053pub trait StructValue: sealed::Sealed {
1055 type Schema: Schema;
1056 const STRUCT_ID_JSON: &'static str;
1057}
1058
1059pub trait NominalUpcast<Target: Model>: Model {}
1061
1062pub trait RoleUpcast<ActiveRole, AncestorRole>: Model {}
1064
1065pub trait RoleTokenCompatible<Owner: RelationModel, Players>: RelationModel {}
1071
1072pub trait RolePlayer<Player: ThingModel> {}
1075
1076#[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#[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#[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#[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#[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#[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}