1use std::cmp::Ordering;
8use std::collections::{BTreeMap, BTreeSet};
9use std::error::Error;
10use std::fmt;
11
12use serde::{Serialize, Serializer};
13
14use crate::capability::CapabilitySet;
15use crate::codec::{FormatVersion, ensure_format_version, to_canonical_json};
16use crate::diagnostic::{Diagnostic, DiagnosticCategory};
17use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
18use crate::id::{AttributeId, FunctionId, Label, RoleId, StructId, TypeId, TypeKind};
19use crate::limits::{MAX_CANONICAL_COLLECTION_LEN, MAX_CANONICAL_STRING_BYTES};
20use crate::value::{CanonicalValue, Cardinality, ValueTypeTag};
21
22pub use crate::managed_scope::{
23 ManagedScopeBinding, ManagedScopeId, ManagedScopeProfileBinding,
24 ManagedScopeProfileFingerprint, ManagedScopeProfileId, SemanticProfileFingerprint,
25};
26pub use crate::schema_delta::{
27 ManagedFactSelection, ManagedSchemaState, PatchFormatVersion, SchemaDelta, SchemaOperation,
28 SchemaOperationKind, decode_schema_delta, encode_schema_delta,
29};
30pub use crate::schema_fingerprint::{
31 ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
32 SchemaDocumentSetFingerprint, SemanticSchemaFingerprint,
33};
34pub use crate::semantic_profile::{InterfaceKind, SemanticProfile};
35
36pub const MAX_DOCUMENT_ID_BYTES: usize = 4096;
38
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
41#[serde(transparent)]
42pub struct DocumentId(String);
43
44impl DocumentId {
45 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
47 let value = value.into();
48 let valid_segments = value
49 .split('/')
50 .all(|segment| !segment.is_empty() && segment != "." && segment != "..");
51 if value.is_empty()
52 || value.len() > MAX_DOCUMENT_ID_BYTES
53 || value.starts_with('/')
54 || value.contains('\\')
55 || value.contains('\0')
56 || !valid_segments
57 {
58 return Err(schema_diagnostic(
59 DiagnosticCategory::InvalidContract,
60 "invalid_schema_document_id",
61 "schema document identifiers must be normalized relative paths",
62 )
63 .with_detail("document", value));
64 }
65 Ok(Self(value))
66 }
67
68 pub fn as_str(&self) -> &str {
70 &self.0
71 }
72}
73
74impl fmt::Display for DocumentId {
75 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76 formatter.write_str(self.as_str())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
82pub struct SourceSpan {
83 document: DocumentId,
84 byte_start: u64,
85 byte_end: u64,
86 line: u32,
87 column: u32,
88 end_line: u32,
89 end_column: u32,
90}
91
92impl SourceSpan {
93 #[allow(clippy::too_many_arguments)]
95 pub fn new(
96 document: DocumentId,
97 byte_start: u64,
98 byte_end: u64,
99 line: u32,
100 column: u32,
101 end_line: u32,
102 end_column: u32,
103 ) -> Result<Self, Diagnostic> {
104 if byte_start > byte_end
105 || line == 0
106 || column == 0
107 || end_line == 0
108 || end_column == 0
109 || (end_line, end_column) < (line, column)
110 {
111 return Err(schema_diagnostic(
112 DiagnosticCategory::InvalidContract,
113 "invalid_schema_source_span",
114 "schema source spans must be ordered and one-based",
115 ));
116 }
117 Ok(Self {
118 document,
119 byte_start,
120 byte_end,
121 line,
122 column,
123 end_line,
124 end_column,
125 })
126 }
127
128 pub fn document(&self) -> &DocumentId {
130 &self.document
131 }
132
133 pub const fn byte_start(&self) -> u64 {
135 self.byte_start
136 }
137
138 pub const fn byte_end(&self) -> u64 {
140 self.byte_end
141 }
142
143 pub const fn line(&self) -> u32 {
145 self.line
146 }
147
148 pub const fn column(&self) -> u32 {
150 self.column
151 }
152
153 pub const fn end_line(&self) -> u32 {
155 self.end_line
156 }
157
158 pub const fn end_column(&self) -> u32 {
160 self.end_column
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
166pub struct DiagnosticLabel {
167 span: SourceSpan,
168 message: String,
169}
170
171impl DiagnosticLabel {
172 pub fn new(span: SourceSpan, message: impl Into<String>) -> Self {
174 Self {
175 span,
176 message: message.into(),
177 }
178 }
179
180 pub const fn span(&self) -> &SourceSpan {
182 &self.span
183 }
184
185 pub fn message(&self) -> &str {
187 &self.message
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
193pub struct SchemaDiagnostic {
194 diagnostic: Diagnostic,
195 primary: Option<SourceSpan>,
196 related: Vec<DiagnosticLabel>,
197}
198
199impl SchemaDiagnostic {
200 pub fn new(diagnostic: Diagnostic, primary: Option<SourceSpan>) -> Self {
202 Self {
203 diagnostic,
204 primary,
205 related: Vec::new(),
206 }
207 }
208
209 pub fn with_related(mut self, label: DiagnosticLabel) -> Self {
211 self.related.push(label);
212 self
213 }
214
215 pub const fn diagnostic(&self) -> &Diagnostic {
217 &self.diagnostic
218 }
219
220 pub fn primary(&self) -> Option<&SourceSpan> {
222 self.primary.as_ref()
223 }
224
225 pub fn related(&self) -> &[DiagnosticLabel] {
227 &self.related
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SchemaDiagnostics(Vec<SchemaDiagnostic>);
234
235impl SchemaDiagnostics {
236 pub fn one(diagnostic: SchemaDiagnostic) -> Self {
238 Self(vec![diagnostic])
239 }
240
241 pub fn from_vec(diagnostics: Vec<SchemaDiagnostic>) -> Self {
243 Self(diagnostics)
244 }
245
246 pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaDiagnostic> {
248 self.0.iter()
249 }
250
251 pub fn len(&self) -> usize {
253 self.0.len()
254 }
255
256 pub fn is_empty(&self) -> bool {
258 self.0.is_empty()
259 }
260
261 pub fn into_vec(self) -> Vec<SchemaDiagnostic> {
263 self.0
264 }
265}
266
267impl fmt::Display for SchemaDiagnostics {
268 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
269 if let Some(first) = self.0.first() {
270 write!(formatter, "{}", first.diagnostic())?;
271 if self.0.len() > 1 {
272 write!(formatter, " (and {} more)", self.0.len() - 1)?;
273 }
274 Ok(())
275 } else {
276 formatter.write_str("schema validation failed")
277 }
278 }
279}
280
281impl Error for SchemaDiagnostics {}
282
283#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
285pub struct SubFactId {
286 subtype: TypeId,
287 supertype: TypeId,
288}
289
290impl SubFactId {
291 pub fn new(subtype: TypeId, supertype: TypeId) -> Result<Self, Diagnostic> {
293 if subtype.kind() == TypeKind::Struct
294 || supertype.kind() == TypeKind::Struct
295 || subtype.kind() != supertype.kind()
296 || subtype == supertype
297 {
298 return Err(schema_diagnostic(
299 DiagnosticCategory::InvalidContract,
300 "invalid_sub_fact",
301 "subtype edges require distinct types of the same non-struct kind",
302 ));
303 }
304 Ok(Self { subtype, supertype })
305 }
306
307 pub const fn subtype(&self) -> &TypeId {
309 &self.subtype
310 }
311
312 pub const fn supertype(&self) -> &TypeId {
314 &self.supertype
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
320#[serde(transparent)]
321pub struct ValueFactId(AttributeId);
322
323impl ValueFactId {
324 pub const fn new(attribute: AttributeId) -> Self {
326 Self(attribute)
327 }
328
329 pub const fn attribute(&self) -> &AttributeId {
331 &self.0
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
337pub struct OwnsFactId {
338 owner: TypeId,
339 attribute: AttributeId,
340}
341
342impl OwnsFactId {
343 pub fn new(owner: TypeId, attribute: AttributeId) -> Result<Self, Diagnostic> {
345 if !matches!(owner.kind(), TypeKind::Entity | TypeKind::Relation) {
346 return Err(schema_diagnostic(
347 DiagnosticCategory::InvalidContract,
348 "invalid_owns_owner",
349 "only entity and relation types can own attributes",
350 ));
351 }
352 Ok(Self { owner, attribute })
353 }
354
355 pub const fn owner(&self) -> &TypeId {
357 &self.owner
358 }
359
360 pub const fn attribute(&self) -> &AttributeId {
362 &self.attribute
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
368pub struct RelatesFactId {
369 relation: TypeId,
370 role: RoleId,
371}
372
373impl RelatesFactId {
374 pub fn new(relation: TypeId, role: RoleId) -> Result<Self, Diagnostic> {
376 if relation.kind() != TypeKind::Relation || relation.label() != role.declaring_relation() {
377 return Err(schema_diagnostic(
378 DiagnosticCategory::InvalidContract,
379 "invalid_relates_identity",
380 "a related role must be declared by its relation type",
381 ));
382 }
383 Ok(Self { relation, role })
384 }
385
386 pub const fn relation(&self) -> &TypeId {
388 &self.relation
389 }
390
391 pub const fn role(&self) -> &RoleId {
393 &self.role
394 }
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
399pub struct PlaysFactId {
400 player: TypeId,
401 role: RoleId,
402}
403
404impl PlaysFactId {
405 pub fn new(player: TypeId, role: RoleId) -> Result<Self, Diagnostic> {
407 if !matches!(player.kind(), TypeKind::Entity | TypeKind::Relation) {
408 return Err(schema_diagnostic(
409 DiagnosticCategory::InvalidContract,
410 "invalid_plays_player",
411 "only entity and relation types can play roles",
412 ));
413 }
414 Ok(Self { player, role })
415 }
416
417 pub const fn player(&self) -> &TypeId {
419 &self.player
420 }
421
422 pub const fn role(&self) -> &RoleId {
424 &self.role
425 }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
430#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
431pub enum AnnotationSubjectId {
432 Type(TypeId),
434 Sub(SubFactId),
436 Value(ValueFactId),
438 Owns(OwnsFactId),
440 Relates(RelatesFactId),
442 Plays(PlaysFactId),
444 Function(FunctionId),
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
450#[serde(tag = "kind", content = "key", rename_all = "snake_case")]
451pub enum AnnotationKindId {
452 Abstract,
454 Independent,
456 Key,
458 Unique,
460 Distinct,
462 Card,
464 Regex,
466 Range,
468 Values,
470 Doc,
472 Meta(Label),
474}
475
476impl AnnotationKindId {
477 pub fn meta(key: impl Into<String>) -> Result<Self, Diagnostic> {
479 Label::new(key).map(Self::Meta)
480 }
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
485pub struct AnnotationFactId {
486 subject: AnnotationSubjectId,
487 kind: AnnotationKindId,
488}
489
490impl AnnotationFactId {
491 pub const fn new(subject: AnnotationSubjectId, kind: AnnotationKindId) -> Self {
493 Self { subject, kind }
494 }
495
496 pub const fn subject(&self) -> &AnnotationSubjectId {
498 &self.subject
499 }
500
501 pub const fn kind(&self) -> &AnnotationKindId {
503 &self.kind
504 }
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
509#[serde(transparent)]
510pub struct RegexPattern(String);
511
512impl RegexPattern {
513 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
515 let value = value.into();
516 if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
517 return Err(schema_diagnostic(
518 DiagnosticCategory::InvalidContract,
519 "invalid_regex_annotation",
520 "regex annotation text must be non-empty and bounded",
521 ));
522 }
523 Ok(Self(value))
524 }
525
526 pub fn as_str(&self) -> &str {
528 &self.0
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
534#[serde(transparent)]
535pub struct DocText(String);
536
537impl DocText {
538 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
540 let value = value.into();
541 if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
542 return Err(schema_diagnostic(
543 DiagnosticCategory::InvalidContract,
544 "invalid_doc_annotation",
545 "documentation text must be non-empty and bounded",
546 ));
547 }
548 Ok(Self(value))
549 }
550
551 pub fn as_str(&self) -> &str {
553 &self.0
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
559#[serde(transparent)]
560pub struct CanonicalValueSet(BTreeSet<CanonicalValue>);
561
562#[derive(Debug, Clone, PartialEq, Eq)]
564pub enum CanonicalValueSetViolation {
565 Empty,
567 MemberLimitExceeded {
569 maximum: usize,
571 first_excess_index: usize,
573 },
574 MixedDomain {
576 expected: ValueTypeTag,
578 actual: ValueTypeTag,
580 member_index: usize,
582 },
583 Duplicate {
585 first_index: usize,
587 duplicate_index: usize,
589 },
590}
591
592impl CanonicalValueSetViolation {
593 pub fn into_diagnostic(self) -> Diagnostic {
595 match self {
596 Self::Empty => schema_diagnostic(
597 DiagnosticCategory::InvalidContract,
598 "empty_values_annotation",
599 "values annotations must contain at least one value",
600 ),
601 Self::MemberLimitExceeded {
602 maximum,
603 first_excess_index,
604 } => schema_diagnostic(
605 DiagnosticCategory::ResourceLimit,
606 "values_annotation_member_limit_exceeded",
607 "values annotation exceeds the raw member ceiling",
608 )
609 .with_detail(
610 "maximum_members",
611 i64::try_from(maximum).expect("collection limit fits i64"),
612 )
613 .with_detail(
614 "first_excess_index",
615 i64::try_from(first_excess_index).expect("collection index fits i64"),
616 ),
617 Self::MixedDomain {
618 expected,
619 actual,
620 member_index,
621 } => schema_diagnostic(
622 DiagnosticCategory::InvalidContract,
623 "mixed_values_annotation_domain",
624 "values annotations require one exact scalar domain",
625 )
626 .with_detail("expected_value_type", expected.as_str())
627 .with_detail("actual_value_type", actual.as_str())
628 .with_detail(
629 "member_index",
630 i64::try_from(member_index).expect("collection index fits i64"),
631 ),
632 Self::Duplicate {
633 first_index,
634 duplicate_index,
635 } => schema_diagnostic(
636 DiagnosticCategory::InvalidContract,
637 "duplicate_values_annotation_value",
638 "values annotations cannot contain duplicates",
639 )
640 .with_detail(
641 "first_index",
642 i64::try_from(first_index).expect("collection index fits i64"),
643 )
644 .with_detail(
645 "duplicate_index",
646 i64::try_from(duplicate_index).expect("collection index fits i64"),
647 ),
648 }
649 }
650}
651
652impl CanonicalValueSet {
653 pub fn new(values: impl IntoIterator<Item = CanonicalValue>) -> Result<Self, Diagnostic> {
655 Self::new_detailed(values).map_err(CanonicalValueSetViolation::into_diagnostic)
656 }
657
658 pub fn new_detailed(
660 values: impl IntoIterator<Item = CanonicalValue>,
661 ) -> Result<Self, CanonicalValueSetViolation> {
662 let mut positions = BTreeMap::new();
663 let mut value_type = None;
664 for (member_index, value) in values.into_iter().enumerate() {
665 if member_index >= MAX_CANONICAL_COLLECTION_LEN {
666 return Err(CanonicalValueSetViolation::MemberLimitExceeded {
667 maximum: MAX_CANONICAL_COLLECTION_LEN,
668 first_excess_index: member_index,
669 });
670 }
671 if let Some(expected) = value_type {
672 if expected != value.value_type() {
673 return Err(CanonicalValueSetViolation::MixedDomain {
674 expected,
675 actual: value.value_type(),
676 member_index,
677 });
678 }
679 } else {
680 value_type = Some(value.value_type());
681 }
682 if let Some(first_index) = positions.get(&value) {
683 return Err(CanonicalValueSetViolation::Duplicate {
684 first_index: *first_index,
685 duplicate_index: member_index,
686 });
687 }
688 positions.insert(value, member_index);
689 }
690 if positions.is_empty() {
691 return Err(CanonicalValueSetViolation::Empty);
692 }
693 Ok(Self(positions.into_keys().collect()))
694 }
695
696 pub fn iter(&self) -> impl ExactSizeIterator<Item = &CanonicalValue> {
698 self.0.iter()
699 }
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
704pub struct CanonicalValueRange {
705 lower: Option<CanonicalValue>,
706 upper: Option<CanonicalValue>,
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq)]
711pub enum CanonicalValueRangeViolation {
712 Empty,
714 MixedDomain {
716 lower: ValueTypeTag,
718 upper: ValueTypeTag,
720 },
721 UnsupportedDomain {
723 value_type: ValueTypeTag,
725 },
726 InvalidBounds {
728 ordering: Ordering,
730 },
731}
732
733impl CanonicalValueRangeViolation {
734 pub fn into_diagnostic(self) -> Diagnostic {
736 match self {
737 Self::Empty => schema_diagnostic(
738 DiagnosticCategory::InvalidContract,
739 "empty_range_annotation",
740 "range annotations require at least one bound",
741 ),
742 Self::MixedDomain { lower, upper } => schema_diagnostic(
743 DiagnosticCategory::InvalidContract,
744 "mixed_range_annotation_domain",
745 "range bounds require one exact scalar domain",
746 )
747 .with_detail("lower_value_type", lower.as_str())
748 .with_detail("upper_value_type", upper.as_str()),
749 Self::UnsupportedDomain { value_type } => schema_diagnostic(
750 DiagnosticCategory::InvalidContract,
751 "unsupported_range_annotation_domain",
752 "range annotations require an ordered scalar domain",
753 )
754 .with_detail("value_type", value_type.as_str()),
755 Self::InvalidBounds { ordering } => schema_diagnostic(
756 DiagnosticCategory::InvalidContract,
757 "invalid_range_annotation_bounds",
758 "range lower bounds must be strictly less than upper bounds",
759 )
760 .with_detail(
761 "ordering",
762 match ordering {
763 Ordering::Less => "less",
764 Ordering::Equal => "equal",
765 Ordering::Greater => "greater",
766 },
767 ),
768 }
769 }
770}
771
772impl CanonicalValueRange {
773 pub fn new(
775 lower: Option<CanonicalValue>,
776 upper: Option<CanonicalValue>,
777 ) -> Result<Self, Diagnostic> {
778 Self::new_detailed(lower, upper).map_err(CanonicalValueRangeViolation::into_diagnostic)
779 }
780
781 pub fn new_detailed(
783 lower: Option<CanonicalValue>,
784 upper: Option<CanonicalValue>,
785 ) -> Result<Self, CanonicalValueRangeViolation> {
786 if lower.is_none() && upper.is_none() {
787 return Err(CanonicalValueRangeViolation::Empty);
788 }
789 if let (Some(lower), Some(upper)) = (&lower, &upper)
790 && lower.value_type() != upper.value_type()
791 {
792 return Err(CanonicalValueRangeViolation::MixedDomain {
793 lower: lower.value_type(),
794 upper: upper.value_type(),
795 });
796 }
797 let value_type = lower
798 .as_ref()
799 .or(upper.as_ref())
800 .expect("non-empty range has one bound")
801 .value_type();
802 if matches!(value_type, ValueTypeTag::Duration) {
803 return Err(CanonicalValueRangeViolation::UnsupportedDomain { value_type });
804 }
805 if let (Some(lower), Some(upper)) = (&lower, &upper) {
806 let ordering = lower
807 .semantic_cmp_same_domain(upper)
808 .expect("every supported exact domain has semantic ordering");
809 if ordering != Ordering::Less {
810 return Err(CanonicalValueRangeViolation::InvalidBounds { ordering });
811 }
812 }
813 Ok(Self { lower, upper })
814 }
815
816 pub const fn lower(&self) -> Option<&CanonicalValue> {
818 self.lower.as_ref()
819 }
820
821 pub const fn upper(&self) -> Option<&CanonicalValue> {
823 self.upper.as_ref()
824 }
825}
826
827#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
829#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
830pub enum SchemaAnnotationValue {
831 Presence,
833 Cardinality(Cardinality),
835 Regex(RegexPattern),
837 Range(CanonicalValueRange),
839 Values(CanonicalValueSet),
841 Doc(DocText),
843 Meta(CanonicalValue),
845}
846
847#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
849pub struct TypeFact {
850 id: TypeId,
851}
852
853impl TypeFact {
854 pub fn new(id: TypeId) -> Result<Self, Diagnostic> {
856 if id.kind() == TypeKind::Struct {
857 return Err(schema_diagnostic(
858 DiagnosticCategory::InvalidContract,
859 "invalid_type_fact_kind",
860 "struct existence uses StructFact",
861 ));
862 }
863 if value_type_tag(id.label().as_str()).is_some() {
864 return Err(schema_diagnostic(
865 DiagnosticCategory::InvalidContract,
866 "reserved_schema_type_label",
867 "schema type labels cannot collide with built-in value-type tokens",
868 ));
869 }
870 Ok(Self { id })
871 }
872
873 pub const fn id(&self) -> &TypeId {
875 &self.id
876 }
877}
878
879#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
881pub struct SubFact {
882 id: SubFactId,
883}
884
885impl SubFact {
886 pub const fn new(id: SubFactId) -> Self {
888 Self { id }
889 }
890
891 pub const fn id(&self) -> &SubFactId {
893 &self.id
894 }
895}
896
897#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
899pub struct ValueFact {
900 id: ValueFactId,
901 value_type: ValueTypeTag,
902}
903
904impl ValueFact {
905 pub const fn new(id: ValueFactId, value_type: ValueTypeTag) -> Self {
907 Self { id, value_type }
908 }
909
910 pub const fn id(&self) -> &ValueFactId {
912 &self.id
913 }
914
915 pub const fn value_type(&self) -> ValueTypeTag {
917 self.value_type
918 }
919}
920
921#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
923#[serde(rename_all = "snake_case")]
924pub enum CollectionMode {
925 #[default]
927 Unordered,
928 OrderedList,
930}
931
932impl CollectionMode {
933 #[must_use]
935 pub const fn is_unordered(&self) -> bool {
936 matches!(self, Self::Unordered)
937 }
938}
939
940#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
942pub struct OwnsFact {
943 id: OwnsFactId,
944 #[serde(skip_serializing_if = "CollectionMode::is_unordered")]
945 collection_mode: CollectionMode,
946}
947
948impl OwnsFact {
949 pub const fn new(id: OwnsFactId) -> Self {
951 Self::new_with_collection_mode(id, CollectionMode::Unordered)
952 }
953
954 pub const fn new_with_collection_mode(id: OwnsFactId, collection_mode: CollectionMode) -> Self {
956 Self {
957 id,
958 collection_mode,
959 }
960 }
961
962 #[must_use]
964 pub const fn with_collection_mode(mut self, collection_mode: CollectionMode) -> Self {
965 self.collection_mode = collection_mode;
966 self
967 }
968
969 pub const fn id(&self) -> &OwnsFactId {
971 &self.id
972 }
973
974 pub const fn collection_mode(&self) -> CollectionMode {
976 self.collection_mode
977 }
978}
979
980#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
982pub struct RelatesFact {
983 id: RelatesFactId,
984 specializes: Option<RoleId>,
985 #[serde(skip_serializing_if = "CollectionMode::is_unordered")]
986 collection_mode: CollectionMode,
987}
988
989impl RelatesFact {
990 pub fn new(id: RelatesFactId, specializes: Option<RoleId>) -> Result<Self, Diagnostic> {
992 Self::new_with_collection_mode(id, specializes, CollectionMode::Unordered)
993 }
994
995 pub fn new_with_collection_mode(
997 id: RelatesFactId,
998 specializes: Option<RoleId>,
999 collection_mode: CollectionMode,
1000 ) -> Result<Self, Diagnostic> {
1001 if specializes.as_ref().is_some_and(|role| role == id.role()) {
1002 return Err(schema_diagnostic(
1003 DiagnosticCategory::InvalidContract,
1004 "self_specializing_role",
1005 "a role cannot specialize itself",
1006 ));
1007 }
1008 Ok(Self {
1009 id,
1010 specializes,
1011 collection_mode,
1012 })
1013 }
1014
1015 #[must_use]
1017 pub const fn with_collection_mode(mut self, collection_mode: CollectionMode) -> Self {
1018 self.collection_mode = collection_mode;
1019 self
1020 }
1021
1022 pub const fn id(&self) -> &RelatesFactId {
1024 &self.id
1025 }
1026
1027 pub const fn specializes(&self) -> Option<&RoleId> {
1029 self.specializes.as_ref()
1030 }
1031
1032 pub const fn collection_mode(&self) -> CollectionMode {
1034 self.collection_mode
1035 }
1036}
1037
1038#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1040pub struct PlaysFact {
1041 id: PlaysFactId,
1042}
1043
1044impl PlaysFact {
1045 pub const fn new(id: PlaysFactId) -> Self {
1047 Self { id }
1048 }
1049
1050 pub const fn id(&self) -> &PlaysFactId {
1052 &self.id
1053 }
1054}
1055
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1058pub struct AnnotationFact {
1059 id: AnnotationFactId,
1060 value: SchemaAnnotationValue,
1061}
1062
1063impl AnnotationFact {
1064 pub fn new(id: AnnotationFactId, value: SchemaAnnotationValue) -> Result<Self, Diagnostic> {
1066 validate_annotation(id.subject(), id.kind(), &value)?;
1067 Ok(Self { id, value })
1068 }
1069
1070 pub const fn id(&self) -> &AnnotationFactId {
1072 &self.id
1073 }
1074
1075 pub const fn value(&self) -> &SchemaAnnotationValue {
1077 &self.value
1078 }
1079}
1080
1081#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1083#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1084pub enum TypeReference {
1085 Value(ValueTypeTag),
1087 Schema(Label),
1089}
1090
1091impl TypeReference {
1092 pub fn from_token(value: impl Into<String>) -> Result<Self, Diagnostic> {
1094 let value = value.into();
1095 Ok(value_type_tag(&value)
1096 .map(Self::Value)
1097 .unwrap_or(Self::Schema(Label::new(value)?)))
1098 }
1099
1100 pub const fn schema_label(&self) -> Option<&Label> {
1102 match self {
1103 Self::Value(_) => None,
1104 Self::Schema(label) => Some(label),
1105 }
1106 }
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1111pub struct FunctionParameter {
1112 name: Label,
1113 type_ref: TypeReference,
1114}
1115
1116impl FunctionParameter {
1117 pub const fn new(name: Label, type_ref: TypeReference) -> Self {
1119 Self { name, type_ref }
1120 }
1121
1122 pub const fn name(&self) -> &Label {
1124 &self.name
1125 }
1126
1127 pub const fn type_ref(&self) -> &TypeReference {
1129 &self.type_ref
1130 }
1131}
1132
1133#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1135pub struct FunctionReturnElement {
1136 type_ref: TypeReference,
1137 optional: bool,
1138}
1139
1140impl FunctionReturnElement {
1141 pub const fn new(type_ref: TypeReference, optional: bool) -> Self {
1143 Self { type_ref, optional }
1144 }
1145
1146 pub const fn type_ref(&self) -> &TypeReference {
1148 &self.type_ref
1149 }
1150
1151 pub const fn optional(&self) -> bool {
1153 self.optional
1154 }
1155}
1156
1157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1159#[serde(tag = "kind", content = "elements", rename_all = "snake_case")]
1160pub enum FunctionReturnMode {
1161 Scalar(FunctionReturnElement),
1163 Tuple(Vec<FunctionReturnElement>),
1165 Stream(Vec<FunctionReturnElement>),
1167}
1168
1169impl FunctionReturnMode {
1170 pub const fn scalar(element: FunctionReturnElement) -> Self {
1172 Self::Scalar(element)
1173 }
1174
1175 pub fn tuple(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1177 if !(2..=MAX_CANONICAL_COLLECTION_LEN).contains(&elements.len()) {
1178 return Err(schema_diagnostic(
1179 DiagnosticCategory::InvalidContract,
1180 "invalid_function_tuple_return",
1181 "tuple function returns require between two and the collection limit elements",
1182 ));
1183 }
1184 Ok(Self::Tuple(elements))
1185 }
1186
1187 pub fn stream(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1189 if elements.is_empty() || elements.len() > MAX_CANONICAL_COLLECTION_LEN {
1190 return Err(schema_diagnostic(
1191 DiagnosticCategory::InvalidContract,
1192 "invalid_function_stream_return",
1193 "stream function returns require a non-empty bounded element list",
1194 ));
1195 }
1196 Ok(Self::Stream(elements))
1197 }
1198
1199 pub fn elements(&self) -> &[FunctionReturnElement] {
1201 match self {
1202 Self::Scalar(element) => std::slice::from_ref(element),
1203 Self::Tuple(elements) | Self::Stream(elements) => elements,
1204 }
1205 }
1206}
1207
1208#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1210pub struct FunctionSignature {
1211 parameters: Vec<FunctionParameter>,
1212 returns: FunctionReturnMode,
1213}
1214
1215impl FunctionSignature {
1216 pub fn new(
1218 parameters: Vec<FunctionParameter>,
1219 returns: FunctionReturnMode,
1220 ) -> Result<Self, Diagnostic> {
1221 if parameters.len() > MAX_CANONICAL_COLLECTION_LEN {
1222 return Err(schema_diagnostic(
1223 DiagnosticCategory::ResourceLimit,
1224 "too_many_function_parameters",
1225 "function parameter count exceeds the canonical collection limit",
1226 ));
1227 }
1228 let mut names = BTreeSet::new();
1229 if parameters
1230 .iter()
1231 .any(|parameter| !names.insert(parameter.name().clone()))
1232 {
1233 return Err(schema_diagnostic(
1234 DiagnosticCategory::InvalidContract,
1235 "duplicate_function_parameter",
1236 "function parameter names must be unique",
1237 ));
1238 }
1239 Ok(Self {
1240 parameters,
1241 returns,
1242 })
1243 }
1244
1245 pub fn parameters(&self) -> &[FunctionParameter] {
1247 &self.parameters
1248 }
1249
1250 pub const fn returns(&self) -> &FunctionReturnMode {
1252 &self.returns
1253 }
1254}
1255
1256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1258#[serde(transparent)]
1259pub struct FunctionBody(String);
1260
1261impl FunctionBody {
1262 pub fn new(text: impl Into<String>) -> Result<Self, Diagnostic> {
1264 let text = text.into();
1265 if text.is_empty() || text.len() > MAX_CANONICAL_STRING_BYTES {
1266 return Err(schema_diagnostic(
1267 DiagnosticCategory::InvalidContract,
1268 "invalid_function_body",
1269 "decoded function body must be non-empty and bounded",
1270 ));
1271 }
1272 Ok(Self(text))
1273 }
1274
1275 pub fn text(&self) -> &str {
1277 &self.0
1278 }
1279}
1280
1281#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1283pub struct FunctionFact {
1284 id: FunctionId,
1285 signature: FunctionSignature,
1286 body: FunctionBody,
1287}
1288
1289impl FunctionFact {
1290 pub const fn new(id: FunctionId, signature: FunctionSignature, body: FunctionBody) -> Self {
1292 Self {
1293 id,
1294 signature,
1295 body,
1296 }
1297 }
1298
1299 pub const fn id(&self) -> &FunctionId {
1301 &self.id
1302 }
1303
1304 pub const fn signature(&self) -> &FunctionSignature {
1306 &self.signature
1307 }
1308
1309 pub const fn body(&self) -> &FunctionBody {
1311 &self.body
1312 }
1313
1314 pub fn schema_references(&self) -> impl Iterator<Item = &Label> {
1316 self.signature
1317 .parameters()
1318 .iter()
1319 .filter_map(|parameter| parameter.type_ref().schema_label())
1320 .chain(
1321 self.signature
1322 .returns()
1323 .elements()
1324 .iter()
1325 .filter_map(|element| element.type_ref().schema_label()),
1326 )
1327 }
1328}
1329
1330#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1332pub struct StructField {
1333 name: Label,
1334 value_type: ValueTypeTag,
1335 optional: bool,
1336}
1337
1338impl StructField {
1339 pub const fn new(name: Label, value_type: ValueTypeTag, optional: bool) -> Self {
1341 Self {
1342 name,
1343 value_type,
1344 optional,
1345 }
1346 }
1347
1348 pub const fn name(&self) -> &Label {
1350 &self.name
1351 }
1352
1353 pub const fn value_type(&self) -> ValueTypeTag {
1355 self.value_type
1356 }
1357
1358 pub const fn optional(&self) -> bool {
1360 self.optional
1361 }
1362}
1363
1364#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1366pub struct StructFact {
1367 id: StructId,
1368 fields: Vec<StructField>,
1369}
1370
1371impl StructFact {
1372 pub fn new(id: StructId, fields: Vec<StructField>) -> Result<Self, Diagnostic> {
1374 if value_type_tag(id.label().as_str()).is_some() {
1375 return Err(schema_diagnostic(
1376 DiagnosticCategory::InvalidContract,
1377 "reserved_schema_type_label",
1378 "struct labels cannot collide with built-in value-type tokens",
1379 ));
1380 }
1381 if fields.is_empty() {
1382 return Err(schema_diagnostic(
1383 DiagnosticCategory::InvalidContract,
1384 "empty_struct_fields",
1385 "struct declarations require at least one field",
1386 ));
1387 }
1388 if fields.len() > MAX_CANONICAL_COLLECTION_LEN {
1389 return Err(schema_diagnostic(
1390 DiagnosticCategory::ResourceLimit,
1391 "too_many_struct_fields",
1392 "struct field count exceeds the canonical collection limit",
1393 ));
1394 }
1395
1396 let mut names = BTreeSet::new();
1397 for field in &fields {
1398 if !names.insert(field.name().clone()) {
1399 return Err(schema_diagnostic(
1400 DiagnosticCategory::InvalidContract,
1401 "duplicate_struct_field",
1402 "struct field names must be unique within the struct",
1403 ));
1404 }
1405 }
1406
1407 Ok(Self { id, fields })
1408 }
1409
1410 pub const fn id(&self) -> &StructId {
1412 &self.id
1413 }
1414
1415 pub fn fields(&self) -> &[StructField] {
1417 &self.fields
1418 }
1419}
1420
1421#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1423#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1424pub enum SchemaFactId {
1425 Type(TypeId),
1427 Sub(SubFactId),
1429 Value(ValueFactId),
1431 Owns(OwnsFactId),
1433 Relates(RelatesFactId),
1435 Plays(PlaysFactId),
1437 Annotation(AnnotationFactId),
1439 Function(FunctionId),
1441 Struct(StructId),
1443}
1444
1445#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1447#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1448pub enum SchemaFact {
1449 Type(TypeFact),
1451 Sub(SubFact),
1453 Value(ValueFact),
1455 Owns(OwnsFact),
1457 Relates(RelatesFact),
1459 Plays(PlaysFact),
1461 Annotation(AnnotationFact),
1463 Function(FunctionFact),
1465 Struct(StructFact),
1467}
1468
1469impl SchemaFact {
1470 pub fn id(&self) -> SchemaFactId {
1472 match self {
1473 Self::Type(fact) => SchemaFactId::Type(fact.id().clone()),
1474 Self::Sub(fact) => SchemaFactId::Sub(fact.id().clone()),
1475 Self::Value(fact) => SchemaFactId::Value(fact.id().clone()),
1476 Self::Owns(fact) => SchemaFactId::Owns(fact.id().clone()),
1477 Self::Relates(fact) => SchemaFactId::Relates(fact.id().clone()),
1478 Self::Plays(fact) => SchemaFactId::Plays(fact.id().clone()),
1479 Self::Annotation(fact) => SchemaFactId::Annotation(fact.id().clone()),
1480 Self::Function(fact) => SchemaFactId::Function(fact.id().clone()),
1481 Self::Struct(fact) => SchemaFactId::Struct(fact.id().clone()),
1482 }
1483 }
1484}
1485
1486#[derive(Debug, Clone, PartialEq, Eq)]
1488pub struct SourcedSchemaFact {
1489 fact: SchemaFact,
1490 source: SourceSpan,
1491}
1492
1493impl SourcedSchemaFact {
1494 pub const fn new(fact: SchemaFact, source: SourceSpan) -> Self {
1496 Self { fact, source }
1497 }
1498
1499 pub const fn fact(&self) -> &SchemaFact {
1501 &self.fact
1502 }
1503
1504 pub const fn source(&self) -> &SourceSpan {
1506 &self.source
1507 }
1508}
1509
1510#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1512#[serde(transparent)]
1513pub struct DocumentFingerprint(Fingerprint);
1514
1515impl DocumentFingerprint {
1516 pub fn compute(source: &[u8]) -> Result<Self, Diagnostic> {
1518 Ok(Self(Fingerprint::compute(
1519 FingerprintDomain::new("typebridge.schema.document")?,
1520 CanonicalizationVersion::new("typebridge.raw-utf8/v1")?,
1521 None,
1522 source,
1523 )))
1524 }
1525
1526 pub const fn as_fingerprint(&self) -> &Fingerprint {
1528 &self.0
1529 }
1530}
1531
1532#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1534#[serde(transparent)]
1535pub struct DeclaredIdentityFingerprint(Fingerprint);
1536
1537impl DeclaredIdentityFingerprint {
1538 fn compute(canonical_bytes: &[u8]) -> Result<Self, Diagnostic> {
1539 Ok(Self(Fingerprint::compute(
1540 FingerprintDomain::new("typebridge.schema.declared-identity")?,
1541 CanonicalizationVersion::new("typebridge.schema-canonical-json/v1")?,
1542 None,
1543 canonical_bytes,
1544 )))
1545 }
1546
1547 pub const fn as_fingerprint(&self) -> &Fingerprint {
1549 &self.0
1550 }
1551
1552 pub(crate) fn from_wire(fingerprint: Fingerprint) -> Result<Self, Diagnostic> {
1553 if fingerprint.domain().as_str() != "typebridge.schema.declared-identity"
1554 || fingerprint.canonicalization().as_str() != "typebridge.schema-canonical-json/v1"
1555 || fingerprint.semantic_profile().is_some()
1556 {
1557 return Err(Diagnostic::stable(
1558 DiagnosticCategory::Integrity,
1559 "invalid_declared_identity_fingerprint",
1560 "declared identity fingerprint metadata is invalid",
1561 ));
1562 }
1563 Ok(Self(fingerprint))
1564 }
1565}
1566
1567#[derive(Debug, Clone, PartialEq, Eq)]
1569pub struct DeclaredSchema {
1570 format: FormatVersion,
1571 required_capabilities: CapabilitySet,
1572 facts: BTreeMap<SchemaFactId, SchemaFact>,
1573 provenance: BTreeMap<SchemaFactId, SourceSpan>,
1574 fingerprint: DeclaredIdentityFingerprint,
1575}
1576
1577impl Serialize for DeclaredSchema {
1578 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1579 where
1580 S: Serializer,
1581 {
1582 #[derive(Serialize)]
1583 struct TrustedDeclaredSchemaView<'a> {
1584 declared_identity: &'a DeclaredIdentityFingerprint,
1585 facts: Vec<&'a SchemaFact>,
1586 format_version: FormatVersion,
1587 required_capabilities: &'a CapabilitySet,
1588 }
1589
1590 TrustedDeclaredSchemaView {
1591 declared_identity: &self.fingerprint,
1592 facts: self.facts.values().collect(),
1593 format_version: self.format,
1594 required_capabilities: &self.required_capabilities,
1595 }
1596 .serialize(serializer)
1597 }
1598}
1599
1600impl DeclaredSchema {
1601 pub fn from_facts(
1603 format: FormatVersion,
1604 required_capabilities: CapabilitySet,
1605 sourced_facts: impl IntoIterator<Item = SourcedSchemaFact>,
1606 ) -> Result<Self, SchemaDiagnostics> {
1607 ensure_format_version(format, FormatVersion::V1)
1608 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1609
1610 let mut facts = BTreeMap::new();
1611 let mut provenance = BTreeMap::<SchemaFactId, SourceSpan>::new();
1612 let mut diagnostics = Vec::new();
1613 for sourced in sourced_facts {
1614 let id = sourced.fact.id();
1615 if let Some(previous) = provenance.get(&id) {
1616 diagnostics.push(
1617 SchemaDiagnostic::new(
1618 schema_diagnostic(
1619 DiagnosticCategory::InvalidContract,
1620 "duplicate_schema_fact",
1621 "a direct schema fact is declared more than once",
1622 ),
1623 Some(sourced.source.clone()),
1624 )
1625 .with_related(DiagnosticLabel::new(
1626 previous.clone(),
1627 "first declaration is here",
1628 )),
1629 );
1630 continue;
1631 }
1632 provenance.insert(id.clone(), sourced.source);
1633 facts.insert(id, sourced.fact);
1634 }
1635
1636 if diagnostics.is_empty() {
1637 validate_references(&facts, &provenance, &mut diagnostics);
1638 validate_annotation_combinations(&facts, &provenance, &mut diagnostics);
1639 validate_annotation_value_domains(&facts, &provenance, &mut diagnostics);
1640 }
1641 if !diagnostics.is_empty() {
1642 return Err(SchemaDiagnostics::from_vec(diagnostics));
1643 }
1644
1645 let canonical =
1646 canonical_declared_identity_bytes(format, &required_capabilities, &facts)
1647 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1648 let fingerprint = DeclaredIdentityFingerprint::compute(&canonical)
1649 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1650 Ok(Self {
1651 format,
1652 required_capabilities,
1653 facts,
1654 provenance,
1655 fingerprint,
1656 })
1657 }
1658
1659 pub const fn format(&self) -> FormatVersion {
1661 self.format
1662 }
1663
1664 pub const fn required_capabilities(&self) -> &CapabilitySet {
1666 &self.required_capabilities
1667 }
1668
1669 pub fn fact(&self, id: &SchemaFactId) -> Option<&SchemaFact> {
1671 self.facts.get(id)
1672 }
1673
1674 pub fn facts(&self) -> impl ExactSizeIterator<Item = &SchemaFact> {
1676 self.facts.values()
1677 }
1678
1679 pub fn source(&self, id: &SchemaFactId) -> Option<&SourceSpan> {
1681 self.provenance.get(id)
1682 }
1683
1684 pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
1686 canonical_declared_identity_bytes(self.format, &self.required_capabilities, &self.facts)
1687 }
1688
1689 pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
1691 &self.fingerprint
1692 }
1693}
1694
1695pub fn encode_declared_schema(schema: &DeclaredSchema) -> Result<Vec<u8>, Diagnostic> {
1697 crate::declared_schema_wire::encode_declared_schema(schema)
1698}
1699
1700pub fn decode_declared_schema(bytes: &[u8]) -> Result<DeclaredSchema, Diagnostic> {
1702 crate::declared_schema_wire::decode_declared_schema(bytes)
1703}
1704
1705#[derive(Serialize)]
1706struct DeclaredIdentityView<'a> {
1707 format_version: FormatVersion,
1708 required_capabilities: &'a CapabilitySet,
1709 facts: Vec<&'a SchemaFact>,
1710}
1711
1712fn canonical_declared_identity_bytes(
1713 format: FormatVersion,
1714 required_capabilities: &CapabilitySet,
1715 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1716) -> Result<Vec<u8>, Diagnostic> {
1717 to_canonical_json(&DeclaredIdentityView {
1718 format_version: format,
1719 required_capabilities,
1720 facts: facts.values().collect(),
1721 })
1722}
1723
1724fn validate_annotation(
1725 subject: &AnnotationSubjectId,
1726 kind: &AnnotationKindId,
1727 value: &SchemaAnnotationValue,
1728) -> Result<(), Diagnostic> {
1729 let payload_matches = matches!(
1730 (kind, value),
1731 (
1732 AnnotationKindId::Abstract
1733 | AnnotationKindId::Independent
1734 | AnnotationKindId::Key
1735 | AnnotationKindId::Unique
1736 | AnnotationKindId::Distinct,
1737 SchemaAnnotationValue::Presence
1738 ) | (
1739 AnnotationKindId::Card,
1740 SchemaAnnotationValue::Cardinality(_)
1741 ) | (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_))
1742 | (AnnotationKindId::Range, SchemaAnnotationValue::Range(_))
1743 | (AnnotationKindId::Values, SchemaAnnotationValue::Values(_))
1744 | (AnnotationKindId::Doc, SchemaAnnotationValue::Doc(_))
1745 | (
1746 AnnotationKindId::Meta(_),
1747 SchemaAnnotationValue::Meta(CanonicalValue::String(_))
1748 )
1749 );
1750 if !payload_matches {
1751 return Err(schema_diagnostic(
1752 DiagnosticCategory::InvalidContract,
1753 "invalid_annotation_payload",
1754 "annotation kind and payload do not agree",
1755 ));
1756 }
1757 let subject_matches = match kind {
1758 AnnotationKindId::Abstract => match subject {
1759 AnnotationSubjectId::Type(id) => matches!(
1760 id.kind(),
1761 TypeKind::Entity | TypeKind::Relation | TypeKind::Attribute
1762 ),
1763 AnnotationSubjectId::Relates(_) => true,
1764 AnnotationSubjectId::Sub(_)
1765 | AnnotationSubjectId::Value(_)
1766 | AnnotationSubjectId::Owns(_)
1767 | AnnotationSubjectId::Plays(_)
1768 | AnnotationSubjectId::Function(_) => false,
1769 },
1770 AnnotationKindId::Independent => matches!(
1771 subject,
1772 AnnotationSubjectId::Type(id) if id.kind() == TypeKind::Attribute
1773 ),
1774 AnnotationKindId::Key | AnnotationKindId::Unique => {
1775 matches!(subject, AnnotationSubjectId::Owns(_))
1776 }
1777 AnnotationKindId::Distinct => matches!(
1778 subject,
1779 AnnotationSubjectId::Owns(_) | AnnotationSubjectId::Relates(_)
1780 ),
1781 AnnotationKindId::Card => matches!(
1782 subject,
1783 AnnotationSubjectId::Owns(_)
1784 | AnnotationSubjectId::Relates(_)
1785 | AnnotationSubjectId::Plays(_)
1786 ),
1787 AnnotationKindId::Regex | AnnotationKindId::Range | AnnotationKindId::Values => {
1788 matches!(
1789 subject,
1790 AnnotationSubjectId::Value(_) | AnnotationSubjectId::Owns(_)
1791 )
1792 }
1793 AnnotationKindId::Doc | AnnotationKindId::Meta(_) => matches!(
1794 subject,
1795 AnnotationSubjectId::Type(_)
1796 | AnnotationSubjectId::Sub(_)
1797 | AnnotationSubjectId::Owns(_)
1798 | AnnotationSubjectId::Relates(_)
1799 | AnnotationSubjectId::Plays(_)
1800 | AnnotationSubjectId::Function(_)
1801 ),
1802 };
1803 if !subject_matches {
1804 return Err(schema_diagnostic(
1805 DiagnosticCategory::InvalidContract,
1806 "invalid_annotation_subject",
1807 "annotation kind does not apply to this schema subject",
1808 ));
1809 }
1810 Ok(())
1811}
1812
1813fn validate_annotation_value_domains(
1814 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1815 provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1816 diagnostics: &mut Vec<SchemaDiagnostic>,
1817) {
1818 for (fact_id, fact) in facts {
1819 let SchemaFact::Annotation(annotation) = fact else {
1820 continue;
1821 };
1822
1823 let kind = annotation.id().kind();
1824 if !matches!(
1825 kind,
1826 AnnotationKindId::Key
1827 | AnnotationKindId::Unique
1828 | AnnotationKindId::Regex
1829 | AnnotationKindId::Range
1830 | AnnotationKindId::Values
1831 ) {
1832 continue;
1833 }
1834
1835 let Some((value_type, value_fact_id)) =
1836 annotation_subject_value_type(annotation.id().subject(), facts)
1837 else {
1838 diagnostics.push(SchemaDiagnostic::new(
1839 schema_diagnostic(
1840 DiagnosticCategory::InvalidContract,
1841 "unknown_annotation_value_domain",
1842 "annotation subject has no resolvable attribute value domain",
1843 ),
1844 provenance.get(fact_id).cloned(),
1845 ));
1846 continue;
1847 };
1848
1849 let valid = match (kind, annotation.value()) {
1850 (AnnotationKindId::Key | AnnotationKindId::Unique, _) => {
1851 value_type != ValueTypeTag::Double
1852 }
1853 (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_)) => {
1854 value_type == ValueTypeTag::String
1855 }
1856 (AnnotationKindId::Range, SchemaAnnotationValue::Range(range)) => {
1857 value_type != ValueTypeTag::Duration
1858 && range
1859 .lower()
1860 .into_iter()
1861 .chain(range.upper())
1862 .all(|bound| bound.value_type() == value_type)
1863 }
1864 (AnnotationKindId::Values, SchemaAnnotationValue::Values(values)) => {
1865 values.iter().all(|value| value.value_type() == value_type)
1866 }
1867 _ => false,
1868 };
1869
1870 if !valid {
1871 let mut diagnostic = SchemaDiagnostic::new(
1872 schema_diagnostic(
1873 DiagnosticCategory::InvalidContract,
1874 "invalid_annotation_value_domain",
1875 "annotation payload is incompatible with the attribute value domain",
1876 ),
1877 provenance.get(fact_id).cloned(),
1878 );
1879 if let Some(value_source) = provenance.get(&value_fact_id) {
1880 diagnostic = diagnostic.with_related(DiagnosticLabel::new(
1881 value_source.clone(),
1882 "attribute value domain is declared here",
1883 ));
1884 }
1885 diagnostics.push(diagnostic);
1886 }
1887 }
1888}
1889
1890fn annotation_subject_value_type(
1891 subject: &AnnotationSubjectId,
1892 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1893) -> Option<(ValueTypeTag, SchemaFactId)> {
1894 let mut attribute = match subject {
1895 AnnotationSubjectId::Value(id) => id.attribute().clone(),
1896 AnnotationSubjectId::Owns(id) => id.attribute().clone(),
1897 AnnotationSubjectId::Type(_)
1898 | AnnotationSubjectId::Sub(_)
1899 | AnnotationSubjectId::Relates(_)
1900 | AnnotationSubjectId::Plays(_)
1901 | AnnotationSubjectId::Function(_) => return None,
1902 };
1903 let mut visited = BTreeSet::new();
1904
1905 loop {
1906 let attribute_type = TypeId::new(TypeKind::Attribute, attribute.label().as_str()).ok()?;
1907 if !visited.insert(attribute_type.clone()) {
1908 return None;
1909 }
1910
1911 let value_fact_id = ValueFactId::new(attribute.clone());
1912 let schema_fact_id = SchemaFactId::Value(value_fact_id);
1913 if let Some(SchemaFact::Value(value)) = facts.get(&schema_fact_id) {
1914 return Some((value.value_type(), schema_fact_id));
1915 }
1916
1917 let supertype = facts.values().find_map(|fact| {
1918 let SchemaFact::Sub(sub) = fact else {
1919 return None;
1920 };
1921 (sub.id().subtype() == &attribute_type
1922 && sub.id().supertype().kind() == TypeKind::Attribute)
1923 .then(|| sub.id().supertype().clone())
1924 })?;
1925 attribute = AttributeId::new(supertype.label().as_str()).ok()?;
1926 }
1927}
1928
1929fn validate_references(
1930 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1931 provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1932 diagnostics: &mut Vec<SchemaDiagnostic>,
1933) {
1934 let type_ids = facts
1935 .keys()
1936 .filter_map(|id| match id {
1937 SchemaFactId::Type(id) => Some(id.clone()),
1938 _ => None,
1939 })
1940 .collect::<BTreeSet<_>>();
1941 let role_ids = facts
1942 .keys()
1943 .filter_map(|id| match id {
1944 SchemaFactId::Relates(id) => Some(id.role().clone()),
1945 _ => None,
1946 })
1947 .collect::<BTreeSet<_>>();
1948 let struct_labels = facts
1949 .keys()
1950 .filter_map(|id| match id {
1951 SchemaFactId::Struct(id) => Some(id.label().clone()),
1952 _ => None,
1953 })
1954 .collect::<BTreeSet<_>>();
1955
1956 for (id, fact) in facts {
1957 let valid = match fact {
1958 SchemaFact::Type(_) | SchemaFact::Struct(_) => true,
1959 SchemaFact::Function(fact) => fact.schema_references().all(|label| {
1960 type_ids.iter().any(|id| id.label() == label) || struct_labels.contains(label)
1961 }),
1962 SchemaFact::Sub(fact) => {
1963 type_ids.contains(fact.id().subtype()) && type_ids.contains(fact.id().supertype())
1964 }
1965 SchemaFact::Value(fact) => type_ids.contains(&attribute_type_id(fact.id().attribute())),
1966 SchemaFact::Owns(fact) => {
1967 type_ids.contains(fact.id().owner())
1968 && type_ids.contains(&attribute_type_id(fact.id().attribute()))
1969 }
1970 SchemaFact::Relates(fact) => {
1971 type_ids.contains(fact.id().relation())
1972 && fact
1973 .specializes()
1974 .is_none_or(|role| role_ids.contains(role))
1975 }
1976 SchemaFact::Plays(fact) => {
1977 type_ids.contains(fact.id().player()) && role_ids.contains(fact.id().role())
1978 }
1979 SchemaFact::Annotation(fact) => {
1980 facts.contains_key(&subject_fact_id(fact.id().subject()))
1981 }
1982 };
1983 if !valid {
1984 diagnostics.push(SchemaDiagnostic::new(
1985 schema_diagnostic(
1986 DiagnosticCategory::InvalidContract,
1987 "unknown_schema_fact_reference",
1988 "schema fact references a declaration that does not exist",
1989 ),
1990 provenance.get(id).cloned(),
1991 ));
1992 }
1993 }
1994}
1995
1996fn validate_annotation_combinations(
1997 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1998 provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1999 diagnostics: &mut Vec<SchemaDiagnostic>,
2000) {
2001 let mut by_subject = BTreeMap::<AnnotationSubjectId, BTreeSet<AnnotationKindId>>::new();
2002 for fact in facts.values() {
2003 if let SchemaFact::Annotation(annotation) = fact {
2004 by_subject
2005 .entry(annotation.id().subject().clone())
2006 .or_default()
2007 .insert(annotation.id().kind().clone());
2008 }
2009 }
2010 for (subject, kinds) in by_subject {
2011 if kinds.contains(&AnnotationKindId::Distinct) {
2012 let subject_id = subject_fact_id(&subject);
2013 let ordered = match facts.get(&subject_id) {
2014 Some(SchemaFact::Owns(fact)) => {
2015 fact.collection_mode() == CollectionMode::OrderedList
2016 }
2017 Some(SchemaFact::Relates(fact)) => {
2018 fact.collection_mode() == CollectionMode::OrderedList
2019 }
2020 _ => true,
2021 };
2022 if !ordered {
2023 let distinct_id = SchemaFactId::Annotation(AnnotationFactId::new(
2024 subject.clone(),
2025 AnnotationKindId::Distinct,
2026 ));
2027 let mut diagnostic = SchemaDiagnostic::new(
2028 schema_diagnostic(
2029 DiagnosticCategory::InvalidContract,
2030 "distinct_requires_ordered_collection",
2031 "distinct applies only to an ordered ownership or related-role collection",
2032 ),
2033 provenance.get(&distinct_id).cloned(),
2034 );
2035 if let Some(subject_source) = provenance.get(&subject_id) {
2036 diagnostic = diagnostic.with_related(DiagnosticLabel::new(
2037 subject_source.clone(),
2038 "unordered collection fact is declared here",
2039 ));
2040 }
2041 diagnostics.push(diagnostic);
2042 }
2043 }
2044 if kinds.contains(&AnnotationKindId::Key)
2045 && (kinds.contains(&AnnotationKindId::Unique)
2046 || kinds.contains(&AnnotationKindId::Card))
2047 {
2048 let key_id =
2049 SchemaFactId::Annotation(AnnotationFactId::new(subject, AnnotationKindId::Key));
2050 diagnostics.push(SchemaDiagnostic::new(
2051 schema_diagnostic(
2052 DiagnosticCategory::InvalidContract,
2053 "key_annotation_conflict",
2054 "key cannot be combined with unique or cardinality",
2055 ),
2056 provenance.get(&key_id).cloned(),
2057 ));
2058 }
2059 }
2060}
2061
2062fn attribute_type_id(attribute: &AttributeId) -> TypeId {
2063 TypeId::new(TypeKind::Attribute, attribute.label().as_str())
2064 .expect("validated attribute labels always form attribute type identities")
2065}
2066
2067fn subject_fact_id(subject: &AnnotationSubjectId) -> SchemaFactId {
2068 match subject {
2069 AnnotationSubjectId::Type(id) => SchemaFactId::Type(id.clone()),
2070 AnnotationSubjectId::Sub(id) => SchemaFactId::Sub(id.clone()),
2071 AnnotationSubjectId::Value(id) => SchemaFactId::Value(id.clone()),
2072 AnnotationSubjectId::Owns(id) => SchemaFactId::Owns(id.clone()),
2073 AnnotationSubjectId::Relates(id) => SchemaFactId::Relates(id.clone()),
2074 AnnotationSubjectId::Plays(id) => SchemaFactId::Plays(id.clone()),
2075 AnnotationSubjectId::Function(id) => SchemaFactId::Function(id.clone()),
2076 }
2077}
2078
2079fn value_type_tag(value: &str) -> Option<ValueTypeTag> {
2080 match value {
2081 "string" => Some(ValueTypeTag::String),
2082 "integer" => Some(ValueTypeTag::Long),
2083 "double" => Some(ValueTypeTag::Double),
2084 "boolean" => Some(ValueTypeTag::Boolean),
2085 "date" => Some(ValueTypeTag::Date),
2086 "datetime" => Some(ValueTypeTag::DateTime),
2087 "datetime-tz" => Some(ValueTypeTag::DateTimeTz),
2088 "decimal" => Some(ValueTypeTag::Decimal),
2089 "duration" => Some(ValueTypeTag::Duration),
2090 _ => None,
2091 }
2092}
2093
2094fn schema_diagnostic(
2095 category: DiagnosticCategory,
2096 code: &'static str,
2097 message: &'static str,
2098) -> Diagnostic {
2099 Diagnostic::stable(category, code, message)
2100}