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 Card,
462 Regex,
464 Range,
466 Values,
468 Doc,
470 Meta(Label),
472}
473
474impl AnnotationKindId {
475 pub fn meta(key: impl Into<String>) -> Result<Self, Diagnostic> {
477 Label::new(key).map(Self::Meta)
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
483pub struct AnnotationFactId {
484 subject: AnnotationSubjectId,
485 kind: AnnotationKindId,
486}
487
488impl AnnotationFactId {
489 pub const fn new(subject: AnnotationSubjectId, kind: AnnotationKindId) -> Self {
491 Self { subject, kind }
492 }
493
494 pub const fn subject(&self) -> &AnnotationSubjectId {
496 &self.subject
497 }
498
499 pub const fn kind(&self) -> &AnnotationKindId {
501 &self.kind
502 }
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
507#[serde(transparent)]
508pub struct RegexPattern(String);
509
510impl RegexPattern {
511 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
513 let value = value.into();
514 if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
515 return Err(schema_diagnostic(
516 DiagnosticCategory::InvalidContract,
517 "invalid_regex_annotation",
518 "regex annotation text must be non-empty and bounded",
519 ));
520 }
521 Ok(Self(value))
522 }
523
524 pub fn as_str(&self) -> &str {
526 &self.0
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
532#[serde(transparent)]
533pub struct DocText(String);
534
535impl DocText {
536 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
538 let value = value.into();
539 if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
540 return Err(schema_diagnostic(
541 DiagnosticCategory::InvalidContract,
542 "invalid_doc_annotation",
543 "documentation text must be non-empty and bounded",
544 ));
545 }
546 Ok(Self(value))
547 }
548
549 pub fn as_str(&self) -> &str {
551 &self.0
552 }
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
557#[serde(transparent)]
558pub struct CanonicalValueSet(BTreeSet<CanonicalValue>);
559
560#[derive(Debug, Clone, PartialEq, Eq)]
562pub enum CanonicalValueSetViolation {
563 Empty,
565 MemberLimitExceeded {
567 maximum: usize,
569 first_excess_index: usize,
571 },
572 MixedDomain {
574 expected: ValueTypeTag,
576 actual: ValueTypeTag,
578 member_index: usize,
580 },
581 Duplicate {
583 first_index: usize,
585 duplicate_index: usize,
587 },
588}
589
590impl CanonicalValueSetViolation {
591 pub fn into_diagnostic(self) -> Diagnostic {
593 match self {
594 Self::Empty => schema_diagnostic(
595 DiagnosticCategory::InvalidContract,
596 "empty_values_annotation",
597 "values annotations must contain at least one value",
598 ),
599 Self::MemberLimitExceeded {
600 maximum,
601 first_excess_index,
602 } => schema_diagnostic(
603 DiagnosticCategory::ResourceLimit,
604 "values_annotation_member_limit_exceeded",
605 "values annotation exceeds the raw member ceiling",
606 )
607 .with_detail(
608 "maximum_members",
609 i64::try_from(maximum).expect("collection limit fits i64"),
610 )
611 .with_detail(
612 "first_excess_index",
613 i64::try_from(first_excess_index).expect("collection index fits i64"),
614 ),
615 Self::MixedDomain {
616 expected,
617 actual,
618 member_index,
619 } => schema_diagnostic(
620 DiagnosticCategory::InvalidContract,
621 "mixed_values_annotation_domain",
622 "values annotations require one exact scalar domain",
623 )
624 .with_detail("expected_value_type", expected.as_str())
625 .with_detail("actual_value_type", actual.as_str())
626 .with_detail(
627 "member_index",
628 i64::try_from(member_index).expect("collection index fits i64"),
629 ),
630 Self::Duplicate {
631 first_index,
632 duplicate_index,
633 } => schema_diagnostic(
634 DiagnosticCategory::InvalidContract,
635 "duplicate_values_annotation_value",
636 "values annotations cannot contain duplicates",
637 )
638 .with_detail(
639 "first_index",
640 i64::try_from(first_index).expect("collection index fits i64"),
641 )
642 .with_detail(
643 "duplicate_index",
644 i64::try_from(duplicate_index).expect("collection index fits i64"),
645 ),
646 }
647 }
648}
649
650impl CanonicalValueSet {
651 pub fn new(values: impl IntoIterator<Item = CanonicalValue>) -> Result<Self, Diagnostic> {
653 Self::new_detailed(values).map_err(CanonicalValueSetViolation::into_diagnostic)
654 }
655
656 pub fn new_detailed(
658 values: impl IntoIterator<Item = CanonicalValue>,
659 ) -> Result<Self, CanonicalValueSetViolation> {
660 let mut positions = BTreeMap::new();
661 let mut value_type = None;
662 for (member_index, value) in values.into_iter().enumerate() {
663 if member_index >= MAX_CANONICAL_COLLECTION_LEN {
664 return Err(CanonicalValueSetViolation::MemberLimitExceeded {
665 maximum: MAX_CANONICAL_COLLECTION_LEN,
666 first_excess_index: member_index,
667 });
668 }
669 if let Some(expected) = value_type {
670 if expected != value.value_type() {
671 return Err(CanonicalValueSetViolation::MixedDomain {
672 expected,
673 actual: value.value_type(),
674 member_index,
675 });
676 }
677 } else {
678 value_type = Some(value.value_type());
679 }
680 if let Some(first_index) = positions.get(&value) {
681 return Err(CanonicalValueSetViolation::Duplicate {
682 first_index: *first_index,
683 duplicate_index: member_index,
684 });
685 }
686 positions.insert(value, member_index);
687 }
688 if positions.is_empty() {
689 return Err(CanonicalValueSetViolation::Empty);
690 }
691 Ok(Self(positions.into_keys().collect()))
692 }
693
694 pub fn iter(&self) -> impl ExactSizeIterator<Item = &CanonicalValue> {
696 self.0.iter()
697 }
698}
699
700#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
702pub struct CanonicalValueRange {
703 lower: Option<CanonicalValue>,
704 upper: Option<CanonicalValue>,
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub enum CanonicalValueRangeViolation {
710 Empty,
712 MixedDomain {
714 lower: ValueTypeTag,
716 upper: ValueTypeTag,
718 },
719 UnsupportedDomain {
721 value_type: ValueTypeTag,
723 },
724 InvalidBounds {
726 ordering: Ordering,
728 },
729}
730
731impl CanonicalValueRangeViolation {
732 pub fn into_diagnostic(self) -> Diagnostic {
734 match self {
735 Self::Empty => schema_diagnostic(
736 DiagnosticCategory::InvalidContract,
737 "empty_range_annotation",
738 "range annotations require at least one bound",
739 ),
740 Self::MixedDomain { lower, upper } => schema_diagnostic(
741 DiagnosticCategory::InvalidContract,
742 "mixed_range_annotation_domain",
743 "range bounds require one exact scalar domain",
744 )
745 .with_detail("lower_value_type", lower.as_str())
746 .with_detail("upper_value_type", upper.as_str()),
747 Self::UnsupportedDomain { value_type } => schema_diagnostic(
748 DiagnosticCategory::InvalidContract,
749 "unsupported_range_annotation_domain",
750 "range annotations require an ordered scalar domain",
751 )
752 .with_detail("value_type", value_type.as_str()),
753 Self::InvalidBounds { ordering } => schema_diagnostic(
754 DiagnosticCategory::InvalidContract,
755 "invalid_range_annotation_bounds",
756 "range lower bounds must be strictly less than upper bounds",
757 )
758 .with_detail(
759 "ordering",
760 match ordering {
761 Ordering::Less => "less",
762 Ordering::Equal => "equal",
763 Ordering::Greater => "greater",
764 },
765 ),
766 }
767 }
768}
769
770impl CanonicalValueRange {
771 pub fn new(
773 lower: Option<CanonicalValue>,
774 upper: Option<CanonicalValue>,
775 ) -> Result<Self, Diagnostic> {
776 Self::new_detailed(lower, upper).map_err(CanonicalValueRangeViolation::into_diagnostic)
777 }
778
779 pub fn new_detailed(
781 lower: Option<CanonicalValue>,
782 upper: Option<CanonicalValue>,
783 ) -> Result<Self, CanonicalValueRangeViolation> {
784 if lower.is_none() && upper.is_none() {
785 return Err(CanonicalValueRangeViolation::Empty);
786 }
787 if let (Some(lower), Some(upper)) = (&lower, &upper)
788 && lower.value_type() != upper.value_type()
789 {
790 return Err(CanonicalValueRangeViolation::MixedDomain {
791 lower: lower.value_type(),
792 upper: upper.value_type(),
793 });
794 }
795 let value_type = lower
796 .as_ref()
797 .or(upper.as_ref())
798 .expect("non-empty range has one bound")
799 .value_type();
800 if matches!(value_type, ValueTypeTag::Duration) {
801 return Err(CanonicalValueRangeViolation::UnsupportedDomain { value_type });
802 }
803 if let (Some(lower), Some(upper)) = (&lower, &upper) {
804 let ordering = lower
805 .semantic_cmp_same_domain(upper)
806 .expect("every supported exact domain has semantic ordering");
807 if ordering != Ordering::Less {
808 return Err(CanonicalValueRangeViolation::InvalidBounds { ordering });
809 }
810 }
811 Ok(Self { lower, upper })
812 }
813
814 pub const fn lower(&self) -> Option<&CanonicalValue> {
816 self.lower.as_ref()
817 }
818
819 pub const fn upper(&self) -> Option<&CanonicalValue> {
821 self.upper.as_ref()
822 }
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
827#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
828pub enum SchemaAnnotationValue {
829 Presence,
831 Cardinality(Cardinality),
833 Regex(RegexPattern),
835 Range(CanonicalValueRange),
837 Values(CanonicalValueSet),
839 Doc(DocText),
841 Meta(CanonicalValue),
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
847pub struct TypeFact {
848 id: TypeId,
849}
850
851impl TypeFact {
852 pub fn new(id: TypeId) -> Result<Self, Diagnostic> {
854 if id.kind() == TypeKind::Struct {
855 return Err(schema_diagnostic(
856 DiagnosticCategory::InvalidContract,
857 "invalid_type_fact_kind",
858 "struct existence uses StructFact",
859 ));
860 }
861 if value_type_tag(id.label().as_str()).is_some() {
862 return Err(schema_diagnostic(
863 DiagnosticCategory::InvalidContract,
864 "reserved_schema_type_label",
865 "schema type labels cannot collide with built-in value-type tokens",
866 ));
867 }
868 Ok(Self { id })
869 }
870
871 pub const fn id(&self) -> &TypeId {
873 &self.id
874 }
875}
876
877#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
879pub struct SubFact {
880 id: SubFactId,
881}
882
883impl SubFact {
884 pub const fn new(id: SubFactId) -> Self {
886 Self { id }
887 }
888
889 pub const fn id(&self) -> &SubFactId {
891 &self.id
892 }
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
897pub struct ValueFact {
898 id: ValueFactId,
899 value_type: ValueTypeTag,
900}
901
902impl ValueFact {
903 pub const fn new(id: ValueFactId, value_type: ValueTypeTag) -> Self {
905 Self { id, value_type }
906 }
907
908 pub const fn id(&self) -> &ValueFactId {
910 &self.id
911 }
912
913 pub const fn value_type(&self) -> ValueTypeTag {
915 self.value_type
916 }
917}
918
919#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
921pub struct OwnsFact {
922 id: OwnsFactId,
923}
924
925impl OwnsFact {
926 pub const fn new(id: OwnsFactId) -> Self {
928 Self { id }
929 }
930
931 pub const fn id(&self) -> &OwnsFactId {
933 &self.id
934 }
935}
936
937#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
939pub struct RelatesFact {
940 id: RelatesFactId,
941 specializes: Option<RoleId>,
942}
943
944impl RelatesFact {
945 pub fn new(id: RelatesFactId, specializes: Option<RoleId>) -> Result<Self, Diagnostic> {
947 if specializes.as_ref().is_some_and(|role| role == id.role()) {
948 return Err(schema_diagnostic(
949 DiagnosticCategory::InvalidContract,
950 "self_specializing_role",
951 "a role cannot specialize itself",
952 ));
953 }
954 Ok(Self { id, specializes })
955 }
956
957 pub const fn id(&self) -> &RelatesFactId {
959 &self.id
960 }
961
962 pub const fn specializes(&self) -> Option<&RoleId> {
964 self.specializes.as_ref()
965 }
966}
967
968#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
970pub struct PlaysFact {
971 id: PlaysFactId,
972}
973
974impl PlaysFact {
975 pub const fn new(id: PlaysFactId) -> Self {
977 Self { id }
978 }
979
980 pub const fn id(&self) -> &PlaysFactId {
982 &self.id
983 }
984}
985
986#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
988pub struct AnnotationFact {
989 id: AnnotationFactId,
990 value: SchemaAnnotationValue,
991}
992
993impl AnnotationFact {
994 pub fn new(id: AnnotationFactId, value: SchemaAnnotationValue) -> Result<Self, Diagnostic> {
996 validate_annotation(id.subject(), id.kind(), &value)?;
997 Ok(Self { id, value })
998 }
999
1000 pub const fn id(&self) -> &AnnotationFactId {
1002 &self.id
1003 }
1004
1005 pub const fn value(&self) -> &SchemaAnnotationValue {
1007 &self.value
1008 }
1009}
1010
1011#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1013#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1014pub enum TypeReference {
1015 Value(ValueTypeTag),
1017 Schema(Label),
1019}
1020
1021impl TypeReference {
1022 pub fn from_token(value: impl Into<String>) -> Result<Self, Diagnostic> {
1024 let value = value.into();
1025 Ok(value_type_tag(&value)
1026 .map(Self::Value)
1027 .unwrap_or(Self::Schema(Label::new(value)?)))
1028 }
1029
1030 pub const fn schema_label(&self) -> Option<&Label> {
1032 match self {
1033 Self::Value(_) => None,
1034 Self::Schema(label) => Some(label),
1035 }
1036 }
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1041pub struct FunctionParameter {
1042 name: Label,
1043 type_ref: TypeReference,
1044}
1045
1046impl FunctionParameter {
1047 pub const fn new(name: Label, type_ref: TypeReference) -> Self {
1049 Self { name, type_ref }
1050 }
1051
1052 pub const fn name(&self) -> &Label {
1054 &self.name
1055 }
1056
1057 pub const fn type_ref(&self) -> &TypeReference {
1059 &self.type_ref
1060 }
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1065pub struct FunctionReturnElement {
1066 type_ref: TypeReference,
1067 optional: bool,
1068}
1069
1070impl FunctionReturnElement {
1071 pub const fn new(type_ref: TypeReference, optional: bool) -> Self {
1073 Self { type_ref, optional }
1074 }
1075
1076 pub const fn type_ref(&self) -> &TypeReference {
1078 &self.type_ref
1079 }
1080
1081 pub const fn optional(&self) -> bool {
1083 self.optional
1084 }
1085}
1086
1087#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1089#[serde(tag = "kind", content = "elements", rename_all = "snake_case")]
1090pub enum FunctionReturnMode {
1091 Scalar(FunctionReturnElement),
1093 Tuple(Vec<FunctionReturnElement>),
1095 Stream(Vec<FunctionReturnElement>),
1097}
1098
1099impl FunctionReturnMode {
1100 pub const fn scalar(element: FunctionReturnElement) -> Self {
1102 Self::Scalar(element)
1103 }
1104
1105 pub fn tuple(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1107 if !(2..=MAX_CANONICAL_COLLECTION_LEN).contains(&elements.len()) {
1108 return Err(schema_diagnostic(
1109 DiagnosticCategory::InvalidContract,
1110 "invalid_function_tuple_return",
1111 "tuple function returns require between two and the collection limit elements",
1112 ));
1113 }
1114 Ok(Self::Tuple(elements))
1115 }
1116
1117 pub fn stream(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1119 if elements.is_empty() || elements.len() > MAX_CANONICAL_COLLECTION_LEN {
1120 return Err(schema_diagnostic(
1121 DiagnosticCategory::InvalidContract,
1122 "invalid_function_stream_return",
1123 "stream function returns require a non-empty bounded element list",
1124 ));
1125 }
1126 Ok(Self::Stream(elements))
1127 }
1128
1129 pub fn elements(&self) -> &[FunctionReturnElement] {
1131 match self {
1132 Self::Scalar(element) => std::slice::from_ref(element),
1133 Self::Tuple(elements) | Self::Stream(elements) => elements,
1134 }
1135 }
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1140pub struct FunctionSignature {
1141 parameters: Vec<FunctionParameter>,
1142 returns: FunctionReturnMode,
1143}
1144
1145impl FunctionSignature {
1146 pub fn new(
1148 parameters: Vec<FunctionParameter>,
1149 returns: FunctionReturnMode,
1150 ) -> Result<Self, Diagnostic> {
1151 if parameters.len() > MAX_CANONICAL_COLLECTION_LEN {
1152 return Err(schema_diagnostic(
1153 DiagnosticCategory::ResourceLimit,
1154 "too_many_function_parameters",
1155 "function parameter count exceeds the canonical collection limit",
1156 ));
1157 }
1158 let mut names = BTreeSet::new();
1159 if parameters
1160 .iter()
1161 .any(|parameter| !names.insert(parameter.name().clone()))
1162 {
1163 return Err(schema_diagnostic(
1164 DiagnosticCategory::InvalidContract,
1165 "duplicate_function_parameter",
1166 "function parameter names must be unique",
1167 ));
1168 }
1169 Ok(Self {
1170 parameters,
1171 returns,
1172 })
1173 }
1174
1175 pub fn parameters(&self) -> &[FunctionParameter] {
1177 &self.parameters
1178 }
1179
1180 pub const fn returns(&self) -> &FunctionReturnMode {
1182 &self.returns
1183 }
1184}
1185
1186#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1188#[serde(transparent)]
1189pub struct FunctionBody(String);
1190
1191impl FunctionBody {
1192 pub fn new(text: impl Into<String>) -> Result<Self, Diagnostic> {
1194 let text = text.into();
1195 if text.is_empty() || text.len() > MAX_CANONICAL_STRING_BYTES {
1196 return Err(schema_diagnostic(
1197 DiagnosticCategory::InvalidContract,
1198 "invalid_function_body",
1199 "decoded function body must be non-empty and bounded",
1200 ));
1201 }
1202 Ok(Self(text))
1203 }
1204
1205 pub fn text(&self) -> &str {
1207 &self.0
1208 }
1209}
1210
1211#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1213pub struct FunctionFact {
1214 id: FunctionId,
1215 signature: FunctionSignature,
1216 body: FunctionBody,
1217}
1218
1219impl FunctionFact {
1220 pub const fn new(id: FunctionId, signature: FunctionSignature, body: FunctionBody) -> Self {
1222 Self {
1223 id,
1224 signature,
1225 body,
1226 }
1227 }
1228
1229 pub const fn id(&self) -> &FunctionId {
1231 &self.id
1232 }
1233
1234 pub const fn signature(&self) -> &FunctionSignature {
1236 &self.signature
1237 }
1238
1239 pub const fn body(&self) -> &FunctionBody {
1241 &self.body
1242 }
1243
1244 pub fn schema_references(&self) -> impl Iterator<Item = &Label> {
1246 self.signature
1247 .parameters()
1248 .iter()
1249 .filter_map(|parameter| parameter.type_ref().schema_label())
1250 .chain(
1251 self.signature
1252 .returns()
1253 .elements()
1254 .iter()
1255 .filter_map(|element| element.type_ref().schema_label()),
1256 )
1257 }
1258}
1259
1260#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1262pub struct StructField {
1263 name: Label,
1264 value_type: ValueTypeTag,
1265 optional: bool,
1266}
1267
1268impl StructField {
1269 pub const fn new(name: Label, value_type: ValueTypeTag, optional: bool) -> Self {
1271 Self {
1272 name,
1273 value_type,
1274 optional,
1275 }
1276 }
1277
1278 pub const fn name(&self) -> &Label {
1280 &self.name
1281 }
1282
1283 pub const fn value_type(&self) -> ValueTypeTag {
1285 self.value_type
1286 }
1287
1288 pub const fn optional(&self) -> bool {
1290 self.optional
1291 }
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1296pub struct StructFact {
1297 id: StructId,
1298 fields: Vec<StructField>,
1299}
1300
1301impl StructFact {
1302 pub fn new(id: StructId, fields: Vec<StructField>) -> Result<Self, Diagnostic> {
1304 if value_type_tag(id.label().as_str()).is_some() {
1305 return Err(schema_diagnostic(
1306 DiagnosticCategory::InvalidContract,
1307 "reserved_schema_type_label",
1308 "struct labels cannot collide with built-in value-type tokens",
1309 ));
1310 }
1311 if fields.is_empty() {
1312 return Err(schema_diagnostic(
1313 DiagnosticCategory::InvalidContract,
1314 "empty_struct_fields",
1315 "struct declarations require at least one field",
1316 ));
1317 }
1318 if fields.len() > MAX_CANONICAL_COLLECTION_LEN {
1319 return Err(schema_diagnostic(
1320 DiagnosticCategory::ResourceLimit,
1321 "too_many_struct_fields",
1322 "struct field count exceeds the canonical collection limit",
1323 ));
1324 }
1325
1326 let mut names = BTreeSet::new();
1327 for field in &fields {
1328 if !names.insert(field.name().clone()) {
1329 return Err(schema_diagnostic(
1330 DiagnosticCategory::InvalidContract,
1331 "duplicate_struct_field",
1332 "struct field names must be unique within the struct",
1333 ));
1334 }
1335 }
1336
1337 Ok(Self { id, fields })
1338 }
1339
1340 pub const fn id(&self) -> &StructId {
1342 &self.id
1343 }
1344
1345 pub fn fields(&self) -> &[StructField] {
1347 &self.fields
1348 }
1349}
1350
1351#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1353#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1354pub enum SchemaFactId {
1355 Type(TypeId),
1357 Sub(SubFactId),
1359 Value(ValueFactId),
1361 Owns(OwnsFactId),
1363 Relates(RelatesFactId),
1365 Plays(PlaysFactId),
1367 Annotation(AnnotationFactId),
1369 Function(FunctionId),
1371 Struct(StructId),
1373}
1374
1375#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1377#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1378pub enum SchemaFact {
1379 Type(TypeFact),
1381 Sub(SubFact),
1383 Value(ValueFact),
1385 Owns(OwnsFact),
1387 Relates(RelatesFact),
1389 Plays(PlaysFact),
1391 Annotation(AnnotationFact),
1393 Function(FunctionFact),
1395 Struct(StructFact),
1397}
1398
1399impl SchemaFact {
1400 pub fn id(&self) -> SchemaFactId {
1402 match self {
1403 Self::Type(fact) => SchemaFactId::Type(fact.id().clone()),
1404 Self::Sub(fact) => SchemaFactId::Sub(fact.id().clone()),
1405 Self::Value(fact) => SchemaFactId::Value(fact.id().clone()),
1406 Self::Owns(fact) => SchemaFactId::Owns(fact.id().clone()),
1407 Self::Relates(fact) => SchemaFactId::Relates(fact.id().clone()),
1408 Self::Plays(fact) => SchemaFactId::Plays(fact.id().clone()),
1409 Self::Annotation(fact) => SchemaFactId::Annotation(fact.id().clone()),
1410 Self::Function(fact) => SchemaFactId::Function(fact.id().clone()),
1411 Self::Struct(fact) => SchemaFactId::Struct(fact.id().clone()),
1412 }
1413 }
1414}
1415
1416#[derive(Debug, Clone, PartialEq, Eq)]
1418pub struct SourcedSchemaFact {
1419 fact: SchemaFact,
1420 source: SourceSpan,
1421}
1422
1423impl SourcedSchemaFact {
1424 pub const fn new(fact: SchemaFact, source: SourceSpan) -> Self {
1426 Self { fact, source }
1427 }
1428
1429 pub const fn fact(&self) -> &SchemaFact {
1431 &self.fact
1432 }
1433
1434 pub const fn source(&self) -> &SourceSpan {
1436 &self.source
1437 }
1438}
1439
1440#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1442#[serde(transparent)]
1443pub struct DocumentFingerprint(Fingerprint);
1444
1445impl DocumentFingerprint {
1446 pub fn compute(source: &[u8]) -> Result<Self, Diagnostic> {
1448 Ok(Self(Fingerprint::compute(
1449 FingerprintDomain::new("typebridge.schema.document")?,
1450 CanonicalizationVersion::new("typebridge.raw-utf8/v1")?,
1451 None,
1452 source,
1453 )))
1454 }
1455
1456 pub const fn as_fingerprint(&self) -> &Fingerprint {
1458 &self.0
1459 }
1460}
1461
1462#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1464#[serde(transparent)]
1465pub struct DeclaredIdentityFingerprint(Fingerprint);
1466
1467impl DeclaredIdentityFingerprint {
1468 fn compute(canonical_bytes: &[u8]) -> Result<Self, Diagnostic> {
1469 Ok(Self(Fingerprint::compute(
1470 FingerprintDomain::new("typebridge.schema.declared-identity")?,
1471 CanonicalizationVersion::new("typebridge.schema-canonical-json/v1")?,
1472 None,
1473 canonical_bytes,
1474 )))
1475 }
1476
1477 pub const fn as_fingerprint(&self) -> &Fingerprint {
1479 &self.0
1480 }
1481
1482 pub(crate) fn from_wire(fingerprint: Fingerprint) -> Result<Self, Diagnostic> {
1483 if fingerprint.domain().as_str() != "typebridge.schema.declared-identity"
1484 || fingerprint.canonicalization().as_str() != "typebridge.schema-canonical-json/v1"
1485 || fingerprint.semantic_profile().is_some()
1486 {
1487 return Err(Diagnostic::stable(
1488 DiagnosticCategory::Integrity,
1489 "invalid_declared_identity_fingerprint",
1490 "declared identity fingerprint metadata is invalid",
1491 ));
1492 }
1493 Ok(Self(fingerprint))
1494 }
1495}
1496
1497#[derive(Debug, Clone, PartialEq, Eq)]
1499pub struct DeclaredSchema {
1500 format: FormatVersion,
1501 required_capabilities: CapabilitySet,
1502 facts: BTreeMap<SchemaFactId, SchemaFact>,
1503 provenance: BTreeMap<SchemaFactId, SourceSpan>,
1504 fingerprint: DeclaredIdentityFingerprint,
1505}
1506
1507impl Serialize for DeclaredSchema {
1508 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1509 where
1510 S: Serializer,
1511 {
1512 #[derive(Serialize)]
1513 struct TrustedDeclaredSchemaView<'a> {
1514 declared_identity: &'a DeclaredIdentityFingerprint,
1515 facts: Vec<&'a SchemaFact>,
1516 format_version: FormatVersion,
1517 required_capabilities: &'a CapabilitySet,
1518 }
1519
1520 TrustedDeclaredSchemaView {
1521 declared_identity: &self.fingerprint,
1522 facts: self.facts.values().collect(),
1523 format_version: self.format,
1524 required_capabilities: &self.required_capabilities,
1525 }
1526 .serialize(serializer)
1527 }
1528}
1529
1530impl DeclaredSchema {
1531 pub fn from_facts(
1533 format: FormatVersion,
1534 required_capabilities: CapabilitySet,
1535 sourced_facts: impl IntoIterator<Item = SourcedSchemaFact>,
1536 ) -> Result<Self, SchemaDiagnostics> {
1537 ensure_format_version(format, FormatVersion::V1)
1538 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1539
1540 let mut facts = BTreeMap::new();
1541 let mut provenance = BTreeMap::<SchemaFactId, SourceSpan>::new();
1542 let mut diagnostics = Vec::new();
1543 for sourced in sourced_facts {
1544 let id = sourced.fact.id();
1545 if let Some(previous) = provenance.get(&id) {
1546 diagnostics.push(
1547 SchemaDiagnostic::new(
1548 schema_diagnostic(
1549 DiagnosticCategory::InvalidContract,
1550 "duplicate_schema_fact",
1551 "a direct schema fact is declared more than once",
1552 ),
1553 Some(sourced.source.clone()),
1554 )
1555 .with_related(DiagnosticLabel::new(
1556 previous.clone(),
1557 "first declaration is here",
1558 )),
1559 );
1560 continue;
1561 }
1562 provenance.insert(id.clone(), sourced.source);
1563 facts.insert(id, sourced.fact);
1564 }
1565
1566 if diagnostics.is_empty() {
1567 validate_references(&facts, &provenance, &mut diagnostics);
1568 validate_annotation_combinations(&facts, &provenance, &mut diagnostics);
1569 validate_annotation_value_domains(&facts, &provenance, &mut diagnostics);
1570 }
1571 if !diagnostics.is_empty() {
1572 return Err(SchemaDiagnostics::from_vec(diagnostics));
1573 }
1574
1575 let canonical =
1576 canonical_declared_identity_bytes(format, &required_capabilities, &facts)
1577 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1578 let fingerprint = DeclaredIdentityFingerprint::compute(&canonical)
1579 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1580 Ok(Self {
1581 format,
1582 required_capabilities,
1583 facts,
1584 provenance,
1585 fingerprint,
1586 })
1587 }
1588
1589 pub const fn format(&self) -> FormatVersion {
1591 self.format
1592 }
1593
1594 pub const fn required_capabilities(&self) -> &CapabilitySet {
1596 &self.required_capabilities
1597 }
1598
1599 pub fn fact(&self, id: &SchemaFactId) -> Option<&SchemaFact> {
1601 self.facts.get(id)
1602 }
1603
1604 pub fn facts(&self) -> impl ExactSizeIterator<Item = &SchemaFact> {
1606 self.facts.values()
1607 }
1608
1609 pub fn source(&self, id: &SchemaFactId) -> Option<&SourceSpan> {
1611 self.provenance.get(id)
1612 }
1613
1614 pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
1616 canonical_declared_identity_bytes(self.format, &self.required_capabilities, &self.facts)
1617 }
1618
1619 pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
1621 &self.fingerprint
1622 }
1623}
1624
1625pub fn encode_declared_schema(schema: &DeclaredSchema) -> Result<Vec<u8>, Diagnostic> {
1627 crate::declared_schema_wire::encode_declared_schema(schema)
1628}
1629
1630pub fn decode_declared_schema(bytes: &[u8]) -> Result<DeclaredSchema, Diagnostic> {
1632 crate::declared_schema_wire::decode_declared_schema(bytes)
1633}
1634
1635#[derive(Serialize)]
1636struct DeclaredIdentityView<'a> {
1637 format_version: FormatVersion,
1638 required_capabilities: &'a CapabilitySet,
1639 facts: Vec<&'a SchemaFact>,
1640}
1641
1642fn canonical_declared_identity_bytes(
1643 format: FormatVersion,
1644 required_capabilities: &CapabilitySet,
1645 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1646) -> Result<Vec<u8>, Diagnostic> {
1647 to_canonical_json(&DeclaredIdentityView {
1648 format_version: format,
1649 required_capabilities,
1650 facts: facts.values().collect(),
1651 })
1652}
1653
1654fn validate_annotation(
1655 subject: &AnnotationSubjectId,
1656 kind: &AnnotationKindId,
1657 value: &SchemaAnnotationValue,
1658) -> Result<(), Diagnostic> {
1659 let payload_matches = matches!(
1660 (kind, value),
1661 (
1662 AnnotationKindId::Abstract
1663 | AnnotationKindId::Independent
1664 | AnnotationKindId::Key
1665 | AnnotationKindId::Unique,
1666 SchemaAnnotationValue::Presence
1667 ) | (
1668 AnnotationKindId::Card,
1669 SchemaAnnotationValue::Cardinality(_)
1670 ) | (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_))
1671 | (AnnotationKindId::Range, SchemaAnnotationValue::Range(_))
1672 | (AnnotationKindId::Values, SchemaAnnotationValue::Values(_))
1673 | (AnnotationKindId::Doc, SchemaAnnotationValue::Doc(_))
1674 | (
1675 AnnotationKindId::Meta(_),
1676 SchemaAnnotationValue::Meta(CanonicalValue::String(_))
1677 )
1678 );
1679 if !payload_matches {
1680 return Err(schema_diagnostic(
1681 DiagnosticCategory::InvalidContract,
1682 "invalid_annotation_payload",
1683 "annotation kind and payload do not agree",
1684 ));
1685 }
1686 let subject_matches = match kind {
1687 AnnotationKindId::Abstract => match subject {
1688 AnnotationSubjectId::Type(id) => matches!(
1689 id.kind(),
1690 TypeKind::Entity | TypeKind::Relation | TypeKind::Attribute
1691 ),
1692 AnnotationSubjectId::Relates(_) => true,
1693 AnnotationSubjectId::Sub(_)
1694 | AnnotationSubjectId::Value(_)
1695 | AnnotationSubjectId::Owns(_)
1696 | AnnotationSubjectId::Plays(_)
1697 | AnnotationSubjectId::Function(_) => false,
1698 },
1699 AnnotationKindId::Independent => matches!(
1700 subject,
1701 AnnotationSubjectId::Type(id) if id.kind() == TypeKind::Attribute
1702 ),
1703 AnnotationKindId::Key | AnnotationKindId::Unique => {
1704 matches!(subject, AnnotationSubjectId::Owns(_))
1705 }
1706 AnnotationKindId::Card => matches!(
1707 subject,
1708 AnnotationSubjectId::Owns(_)
1709 | AnnotationSubjectId::Relates(_)
1710 | AnnotationSubjectId::Plays(_)
1711 ),
1712 AnnotationKindId::Regex | AnnotationKindId::Range | AnnotationKindId::Values => {
1713 matches!(
1714 subject,
1715 AnnotationSubjectId::Value(_) | AnnotationSubjectId::Owns(_)
1716 )
1717 }
1718 AnnotationKindId::Doc | AnnotationKindId::Meta(_) => matches!(
1719 subject,
1720 AnnotationSubjectId::Type(_)
1721 | AnnotationSubjectId::Sub(_)
1722 | AnnotationSubjectId::Owns(_)
1723 | AnnotationSubjectId::Relates(_)
1724 | AnnotationSubjectId::Plays(_)
1725 | AnnotationSubjectId::Function(_)
1726 ),
1727 };
1728 if !subject_matches {
1729 return Err(schema_diagnostic(
1730 DiagnosticCategory::InvalidContract,
1731 "invalid_annotation_subject",
1732 "annotation kind does not apply to this schema subject",
1733 ));
1734 }
1735 Ok(())
1736}
1737
1738fn validate_annotation_value_domains(
1739 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1740 provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1741 diagnostics: &mut Vec<SchemaDiagnostic>,
1742) {
1743 for (fact_id, fact) in facts {
1744 let SchemaFact::Annotation(annotation) = fact else {
1745 continue;
1746 };
1747
1748 let kind = annotation.id().kind();
1749 if !matches!(
1750 kind,
1751 AnnotationKindId::Key
1752 | AnnotationKindId::Unique
1753 | AnnotationKindId::Regex
1754 | AnnotationKindId::Range
1755 | AnnotationKindId::Values
1756 ) {
1757 continue;
1758 }
1759
1760 let Some((value_type, value_fact_id)) =
1761 annotation_subject_value_type(annotation.id().subject(), facts)
1762 else {
1763 diagnostics.push(SchemaDiagnostic::new(
1764 schema_diagnostic(
1765 DiagnosticCategory::InvalidContract,
1766 "unknown_annotation_value_domain",
1767 "annotation subject has no resolvable attribute value domain",
1768 ),
1769 provenance.get(fact_id).cloned(),
1770 ));
1771 continue;
1772 };
1773
1774 let valid = match (kind, annotation.value()) {
1775 (AnnotationKindId::Key | AnnotationKindId::Unique, _) => {
1776 value_type != ValueTypeTag::Double
1777 }
1778 (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_)) => {
1779 value_type == ValueTypeTag::String
1780 }
1781 (AnnotationKindId::Range, SchemaAnnotationValue::Range(range)) => {
1782 value_type != ValueTypeTag::Duration
1783 && range
1784 .lower()
1785 .into_iter()
1786 .chain(range.upper())
1787 .all(|bound| bound.value_type() == value_type)
1788 }
1789 (AnnotationKindId::Values, SchemaAnnotationValue::Values(values)) => {
1790 values.iter().all(|value| value.value_type() == value_type)
1791 }
1792 _ => false,
1793 };
1794
1795 if !valid {
1796 let mut diagnostic = SchemaDiagnostic::new(
1797 schema_diagnostic(
1798 DiagnosticCategory::InvalidContract,
1799 "invalid_annotation_value_domain",
1800 "annotation payload is incompatible with the attribute value domain",
1801 ),
1802 provenance.get(fact_id).cloned(),
1803 );
1804 if let Some(value_source) = provenance.get(&value_fact_id) {
1805 diagnostic = diagnostic.with_related(DiagnosticLabel::new(
1806 value_source.clone(),
1807 "attribute value domain is declared here",
1808 ));
1809 }
1810 diagnostics.push(diagnostic);
1811 }
1812 }
1813}
1814
1815fn annotation_subject_value_type(
1816 subject: &AnnotationSubjectId,
1817 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1818) -> Option<(ValueTypeTag, SchemaFactId)> {
1819 let mut attribute = match subject {
1820 AnnotationSubjectId::Value(id) => id.attribute().clone(),
1821 AnnotationSubjectId::Owns(id) => id.attribute().clone(),
1822 AnnotationSubjectId::Type(_)
1823 | AnnotationSubjectId::Sub(_)
1824 | AnnotationSubjectId::Relates(_)
1825 | AnnotationSubjectId::Plays(_)
1826 | AnnotationSubjectId::Function(_) => return None,
1827 };
1828 let mut visited = BTreeSet::new();
1829
1830 loop {
1831 let attribute_type = TypeId::new(TypeKind::Attribute, attribute.label().as_str()).ok()?;
1832 if !visited.insert(attribute_type.clone()) {
1833 return None;
1834 }
1835
1836 let value_fact_id = ValueFactId::new(attribute.clone());
1837 let schema_fact_id = SchemaFactId::Value(value_fact_id);
1838 if let Some(SchemaFact::Value(value)) = facts.get(&schema_fact_id) {
1839 return Some((value.value_type(), schema_fact_id));
1840 }
1841
1842 let supertype = facts.values().find_map(|fact| {
1843 let SchemaFact::Sub(sub) = fact else {
1844 return None;
1845 };
1846 (sub.id().subtype() == &attribute_type
1847 && sub.id().supertype().kind() == TypeKind::Attribute)
1848 .then(|| sub.id().supertype().clone())
1849 })?;
1850 attribute = AttributeId::new(supertype.label().as_str()).ok()?;
1851 }
1852}
1853
1854fn validate_references(
1855 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1856 provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1857 diagnostics: &mut Vec<SchemaDiagnostic>,
1858) {
1859 let type_ids = facts
1860 .keys()
1861 .filter_map(|id| match id {
1862 SchemaFactId::Type(id) => Some(id.clone()),
1863 _ => None,
1864 })
1865 .collect::<BTreeSet<_>>();
1866 let role_ids = facts
1867 .keys()
1868 .filter_map(|id| match id {
1869 SchemaFactId::Relates(id) => Some(id.role().clone()),
1870 _ => None,
1871 })
1872 .collect::<BTreeSet<_>>();
1873 let struct_labels = facts
1874 .keys()
1875 .filter_map(|id| match id {
1876 SchemaFactId::Struct(id) => Some(id.label().clone()),
1877 _ => None,
1878 })
1879 .collect::<BTreeSet<_>>();
1880
1881 for (id, fact) in facts {
1882 let valid = match fact {
1883 SchemaFact::Type(_) | SchemaFact::Struct(_) => true,
1884 SchemaFact::Function(fact) => fact.schema_references().all(|label| {
1885 type_ids.iter().any(|id| id.label() == label) || struct_labels.contains(label)
1886 }),
1887 SchemaFact::Sub(fact) => {
1888 type_ids.contains(fact.id().subtype()) && type_ids.contains(fact.id().supertype())
1889 }
1890 SchemaFact::Value(fact) => type_ids.contains(&attribute_type_id(fact.id().attribute())),
1891 SchemaFact::Owns(fact) => {
1892 type_ids.contains(fact.id().owner())
1893 && type_ids.contains(&attribute_type_id(fact.id().attribute()))
1894 }
1895 SchemaFact::Relates(fact) => {
1896 type_ids.contains(fact.id().relation())
1897 && fact
1898 .specializes()
1899 .is_none_or(|role| role_ids.contains(role))
1900 }
1901 SchemaFact::Plays(fact) => {
1902 type_ids.contains(fact.id().player()) && role_ids.contains(fact.id().role())
1903 }
1904 SchemaFact::Annotation(fact) => {
1905 facts.contains_key(&subject_fact_id(fact.id().subject()))
1906 }
1907 };
1908 if !valid {
1909 diagnostics.push(SchemaDiagnostic::new(
1910 schema_diagnostic(
1911 DiagnosticCategory::InvalidContract,
1912 "unknown_schema_fact_reference",
1913 "schema fact references a declaration that does not exist",
1914 ),
1915 provenance.get(id).cloned(),
1916 ));
1917 }
1918 }
1919}
1920
1921fn validate_annotation_combinations(
1922 facts: &BTreeMap<SchemaFactId, SchemaFact>,
1923 provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1924 diagnostics: &mut Vec<SchemaDiagnostic>,
1925) {
1926 let mut by_subject = BTreeMap::<AnnotationSubjectId, BTreeSet<AnnotationKindId>>::new();
1927 for fact in facts.values() {
1928 if let SchemaFact::Annotation(annotation) = fact {
1929 by_subject
1930 .entry(annotation.id().subject().clone())
1931 .or_default()
1932 .insert(annotation.id().kind().clone());
1933 }
1934 }
1935 for (subject, kinds) in by_subject {
1936 if kinds.contains(&AnnotationKindId::Key)
1937 && (kinds.contains(&AnnotationKindId::Unique)
1938 || kinds.contains(&AnnotationKindId::Card))
1939 {
1940 let key_id =
1941 SchemaFactId::Annotation(AnnotationFactId::new(subject, AnnotationKindId::Key));
1942 diagnostics.push(SchemaDiagnostic::new(
1943 schema_diagnostic(
1944 DiagnosticCategory::InvalidContract,
1945 "key_annotation_conflict",
1946 "key cannot be combined with unique or cardinality",
1947 ),
1948 provenance.get(&key_id).cloned(),
1949 ));
1950 }
1951 }
1952}
1953
1954fn attribute_type_id(attribute: &AttributeId) -> TypeId {
1955 TypeId::new(TypeKind::Attribute, attribute.label().as_str())
1956 .expect("validated attribute labels always form attribute type identities")
1957}
1958
1959fn subject_fact_id(subject: &AnnotationSubjectId) -> SchemaFactId {
1960 match subject {
1961 AnnotationSubjectId::Type(id) => SchemaFactId::Type(id.clone()),
1962 AnnotationSubjectId::Sub(id) => SchemaFactId::Sub(id.clone()),
1963 AnnotationSubjectId::Value(id) => SchemaFactId::Value(id.clone()),
1964 AnnotationSubjectId::Owns(id) => SchemaFactId::Owns(id.clone()),
1965 AnnotationSubjectId::Relates(id) => SchemaFactId::Relates(id.clone()),
1966 AnnotationSubjectId::Plays(id) => SchemaFactId::Plays(id.clone()),
1967 AnnotationSubjectId::Function(id) => SchemaFactId::Function(id.clone()),
1968 }
1969}
1970
1971fn value_type_tag(value: &str) -> Option<ValueTypeTag> {
1972 match value {
1973 "string" => Some(ValueTypeTag::String),
1974 "integer" => Some(ValueTypeTag::Long),
1975 "double" => Some(ValueTypeTag::Double),
1976 "boolean" => Some(ValueTypeTag::Boolean),
1977 "date" => Some(ValueTypeTag::Date),
1978 "datetime" => Some(ValueTypeTag::DateTime),
1979 "datetime-tz" => Some(ValueTypeTag::DateTimeTz),
1980 "decimal" => Some(ValueTypeTag::Decimal),
1981 "duration" => Some(ValueTypeTag::Duration),
1982 _ => None,
1983 }
1984}
1985
1986fn schema_diagnostic(
1987 category: DiagnosticCategory,
1988 code: &'static str,
1989 message: &'static str,
1990) -> Diagnostic {
1991 Diagnostic::stable(category, code, message)
1992}