1use std::collections::{BTreeMap, BTreeSet};
2
3use type_bridge_contract::capability::{CapabilityId, CapabilitySet};
4use type_bridge_contract::codec::FormatVersion;
5use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory};
6use type_bridge_contract::id::{
7 AttributeId, FunctionId, Label, RoleId, StructId, TypeId, TypeKind,
8};
9use type_bridge_contract::schema::{
10 AnnotationFact, AnnotationFactId, AnnotationKindId, AnnotationSubjectId, CanonicalValueRange,
11 CanonicalValueSet, DeclaredSchema, DocText, FunctionBody, FunctionFact, FunctionParameter,
12 FunctionReturnElement, FunctionReturnMode, FunctionSignature, OwnsFact, OwnsFactId, PlaysFact,
13 PlaysFactId, RegexPattern, RelatesFact, RelatesFactId, SchemaAnnotationValue, SchemaDiagnostic,
14 SchemaDiagnostics, SchemaFact, SchemaFactId, SourceSpan, SourcedSchemaFact, StructFact,
15 StructField, SubFact, SubFactId, TypeFact, TypeReference, ValueFact, ValueFactId,
16};
17use type_bridge_contract::temporal::{CanonicalDate, CanonicalDateTime, CanonicalDuration};
18use type_bridge_contract::value::{
19 CanonicalDouble, CanonicalString, CanonicalValue, Cardinality, DecimalValue, ValueTypeTag,
20};
21
22use crate::parse_provider_datetime_tz;
23use crate::{FactAssembler, SchemaDocument, SchemaDocumentSet, YamlMapping, YamlNode, YamlScalar};
24
25pub const SCHEMA_V2_FORMAT: &str = "typebridge.schema/v2";
27
28pub fn normalize_documents(
30 documents: &SchemaDocumentSet,
31) -> Result<DeclaredSchema, SchemaDiagnostics> {
32 if documents.is_empty() {
33 return Err(error(
34 "empty_schema_document_set",
35 "a schema document set must contain at least one document",
36 None,
37 ));
38 }
39
40 let mut normalizer = Normalizer::default();
41 for (_, document) in documents.iter() {
42 normalizer.normalize_document(document)?;
43 }
44 normalizer.finish()
45}
46
47#[derive(Default)]
48struct Normalizer {
49 facts: Vec<SourcedSchemaFact>,
50 pending: Vec<PendingAnnotation>,
51 pending_relates: Vec<PendingRelates>,
52 pending_plays: Vec<YamlNode>,
53 capabilities: CapabilitySet,
54 capability_sources: BTreeMap<CapabilityId, SourceSpan>,
55 type_labels: BTreeMap<String, (TypeId, SourceSpan)>,
56}
57
58impl Normalizer {
59 fn normalize_document(&mut self, document: &SchemaDocument) -> Result<(), SchemaDiagnostics> {
60 let root = document.root();
61 check_keys(
62 root,
63 &[
64 "format",
65 "capabilities",
66 "attributes",
67 "entities",
68 "relations",
69 "plays",
70 "functions",
71 "structs",
72 "extensions",
73 ],
74 )?;
75
76 let format = required_entry(root, "format")?;
77 let format = scalar(format.value())?;
78 if format.value() != SCHEMA_V2_FORMAT {
79 return Err(error(
80 "unsupported_schema_document_format",
81 format!("schema document format must be exactly `{SCHEMA_V2_FORMAT}`"),
82 Some(format.span().clone()),
83 ));
84 }
85
86 if let Some(entry) = entry(root, "capabilities") {
87 self.normalize_capabilities(entry.value())?;
88 }
89 if let Some(entry) = entry(root, "extensions") {
90 self.normalize_extensions(entry.value())?;
91 }
92
93 self.normalize_type_section(root, "attributes", TypeKind::Attribute)?;
94 self.normalize_type_section(root, "entities", TypeKind::Entity)?;
95 self.normalize_type_section(root, "relations", TypeKind::Relation)?;
96
97 if let Some(entry) = entry(root, "plays") {
98 self.pending_plays.push(entry.value().clone());
99 }
100 if let Some(entry) = entry(root, "functions") {
101 self.normalize_functions(document, entry.value())?;
102 }
103 if let Some(entry) = entry(root, "structs") {
104 self.normalize_structs(entry.value())?;
105 }
106
107 Ok(())
108 }
109
110 fn normalize_structs(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
111 let declarations = mapping(node)?;
112
113 for declaration in declarations.entries() {
114 let id = contract(
115 StructId::new(declaration.key().value()),
116 declaration.key().span(),
117 )?;
118 let body = mapping(declaration.value())?;
119 check_keys(body, &["fields"])?;
120
121 let fields_node = required_entry(body, "fields")?.value();
122 let fields_sequence = sequence(fields_node)?;
123 let mut fields = Vec::with_capacity(fields_sequence.items().len());
124 let mut field_sources = BTreeMap::<Label, SourceSpan>::new();
125
126 for field_node in fields_sequence.items() {
127 let field_body = mapping(field_node)?;
128 check_keys(field_body, &["name", "type", "optional"])?;
129
130 let name = scalar(required_entry(field_body, "name")?.value())?;
131 let name_id = contract(Label::new(name.value()), name.span())?;
132
133 if let Some(previous) = field_sources.insert(name_id.clone(), name.span().clone()) {
134 return Err(crate::yaml::diagnostic_with_related(
135 DiagnosticCategory::InvalidContract,
136 "duplicate_struct_field",
137 format!("struct field `{}` is declared more than once", name.value()),
138 name.span().clone(),
139 previous,
140 "first field declaration is here",
141 ));
142 }
143
144 let value_type =
145 parse_value_type(scalar(required_entry(field_body, "type")?.value())?)?;
146 let optional = entry(field_body, "optional")
147 .map(|entry| strict_bool(entry.value()))
148 .transpose()?
149 .unwrap_or(false);
150
151 fields.push(StructField::new(name_id, value_type, optional));
152 }
153
154 let fact = contract(StructFact::new(id, fields), body.span())?;
155 self.push(SchemaFact::Struct(fact), body.span().clone());
156 }
157
158 Ok(())
159 }
160
161 fn normalize_capabilities(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
162 let body = mapping(node)?;
163 check_keys(body, &["required"])?;
164 if let Some(required) = entry(body, "required") {
165 let sequence = sequence(required.value())?;
166 for item in sequence.items() {
167 let capability = scalar(item)?;
168 let id = contract(CapabilityId::new(capability.value()), capability.span())?;
169 self.insert_capability(id, capability.span().clone())?;
170 }
171 }
172 Ok(())
173 }
174
175 fn normalize_extensions(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
176 let extensions = mapping(node)?;
177 for extension in extensions.entries() {
178 let id = contract(
179 CapabilityId::new(extension.key().value()),
180 extension.key().span(),
181 )?;
182 let body = mapping(extension.value())?;
183 check_keys(body, &["required"])?;
187 let required = entry(body, "required")
188 .map(|entry| strict_bool(entry.value()))
189 .transpose()?
190 .unwrap_or(false);
191 if required {
192 self.insert_capability(id, extension.key().span().clone())?;
193 }
194 }
195 Ok(())
196 }
197
198 fn insert_capability(
199 &mut self,
200 id: CapabilityId,
201 source: SourceSpan,
202 ) -> Result<(), SchemaDiagnostics> {
203 if let Some(previous) = self.capability_sources.get(&id) {
204 return Err(crate::yaml::diagnostic_with_related(
205 DiagnosticCategory::InvalidContract,
206 "duplicate_required_capability",
207 format!("required capability `{}` is duplicated", id.as_str()),
208 source,
209 previous.clone(),
210 "first requirement is here",
211 ));
212 }
213 self.capabilities.insert(id.clone());
214 self.capability_sources.insert(id, source);
215 Ok(())
216 }
217
218 fn normalize_type_section(
219 &mut self,
220 root: &YamlMapping,
221 section_name: &str,
222 kind: TypeKind,
223 ) -> Result<(), SchemaDiagnostics> {
224 let Some(section) = entry(root, section_name) else {
225 return Ok(());
226 };
227 let section = mapping(section.value())?;
228
229 for declaration in section.entries() {
230 let id = contract(
231 TypeId::new(kind, declaration.key().value()),
232 declaration.key().span(),
233 )?;
234 self.register_type(id.clone(), declaration.key().span().clone())?;
235 self.push(
236 SchemaFact::Type(contract(
237 TypeFact::new(id.clone()),
238 declaration.key().span(),
239 )?),
240 declaration.key().span().clone(),
241 );
242
243 let body = mapping(declaration.value())?;
244 let allowed = match kind {
245 TypeKind::Attribute => {
246 &["sub", "value", "abstract", "independent", "doc", "meta"][..]
247 }
248 TypeKind::Entity => &["sub", "owns", "abstract", "doc", "meta"][..],
249 TypeKind::Relation => &["sub", "owns", "relates", "abstract", "doc", "meta"][..],
250 TypeKind::Struct => &[][..],
251 };
252 check_keys(body, allowed)?;
253
254 self.queue_presence(
255 body,
256 "abstract",
257 AnnotationSubjectId::Type(id.clone()),
258 AnnotationKindId::Abstract,
259 )?;
260 if kind == TypeKind::Attribute {
261 self.queue_presence(
262 body,
263 "independent",
264 AnnotationSubjectId::Type(id.clone()),
265 AnnotationKindId::Independent,
266 )?;
267 }
268 self.queue_doc_meta(body, AnnotationSubjectId::Type(id.clone()))?;
269
270 if let Some(sub) = entry(body, "sub") {
271 let (parent, annotations) = match sub.value() {
272 YamlNode::Scalar(parent) => (parent, None),
273 YamlNode::Mapping(expanded) => {
274 check_keys(expanded, &["type", "doc", "meta"])?;
275 let parent = scalar(required_entry(expanded, "type")?.value())?;
276 (parent, Some(expanded))
277 }
278 YamlNode::Sequence(sequence) => {
279 return Err(error(
280 "invalid_schema_sub_shape",
281 "sub must be a scalar or mapping",
282 Some(sequence.span().clone()),
283 ));
284 }
285 };
286 let parent_id = contract(TypeId::new(kind, parent.value()), parent.span())?;
287 let sub_id = contract(SubFactId::new(id.clone(), parent_id), parent.span())?;
288 self.push(
289 SchemaFact::Sub(SubFact::new(sub_id.clone())),
290 parent.span().clone(),
291 );
292 if let Some(annotations) = annotations {
293 self.queue_doc_meta(annotations, AnnotationSubjectId::Sub(sub_id))?;
294 }
295 }
296
297 if kind == TypeKind::Attribute
298 && let Some(value) = entry(body, "value")
299 {
300 self.normalize_value(&id, value.value())?;
301 }
302 if matches!(kind, TypeKind::Entity | TypeKind::Relation)
303 && let Some(owns) = entry(body, "owns")
304 {
305 self.normalize_owns(&id, owns.value())?;
306 }
307 if kind == TypeKind::Relation
308 && let Some(relates) = entry(body, "relates")
309 {
310 self.normalize_relates(&id, relates.value())?;
311 }
312 }
313 Ok(())
314 }
315
316 fn register_type(&mut self, id: TypeId, source: SourceSpan) -> Result<(), SchemaDiagnostics> {
317 let label = id.label().as_str().to_owned();
318 if let Some((_, previous)) = self.type_labels.get(&label) {
319 return Err(crate::yaml::diagnostic_with_related(
320 DiagnosticCategory::InvalidContract,
321 "duplicate_schema_type_label",
322 format!("schema type label `{label}` is duplicated"),
323 source,
324 previous.clone(),
325 "first type label is here",
326 ));
327 }
328 self.type_labels.insert(label, (id, source));
329 Ok(())
330 }
331
332 fn normalize_value(
333 &mut self,
334 attribute: &TypeId,
335 node: &YamlNode,
336 ) -> Result<(), SchemaDiagnostics> {
337 let (value_type, source, annotations) = match node {
338 YamlNode::Scalar(value) => (parse_value_type(value)?, value.span().clone(), None),
339 YamlNode::Mapping(body) => {
340 check_keys(body, &["type", "regex", "range", "values"])?;
341 let value = required_entry(body, "type")?;
342 let value = scalar(value.value())?;
343 (parse_value_type(value)?, value.span().clone(), Some(body))
344 }
345 YamlNode::Sequence(sequence) => {
346 return Err(error(
347 "invalid_schema_value_shape",
348 "attribute value must be a scalar or mapping",
349 Some(sequence.span().clone()),
350 ));
351 }
352 };
353
354 let attribute_id = contract(AttributeId::new(attribute.label().as_str()), &source)?;
355 let value_id = ValueFactId::new(attribute_id);
356 self.push(
357 SchemaFact::Value(ValueFact::new(value_id.clone(), value_type)),
358 source,
359 );
360
361 if let Some(body) = annotations {
362 self.queue_value_annotations(body, AnnotationSubjectId::Value(value_id))?;
363 }
364 Ok(())
365 }
366
367 fn normalize_owns(&mut self, owner: &TypeId, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
368 for named in named_bodies(node)? {
369 let attribute = contract(AttributeId::new(named.name.value()), named.name.span())?;
370 let id = contract(OwnsFactId::new(owner.clone(), attribute), &named.source)?;
371 self.push(
372 SchemaFact::Owns(OwnsFact::new(id.clone())),
373 named.source.clone(),
374 );
375
376 if let Some(body) = &named.body {
377 check_keys(
378 body,
379 &[
380 "key", "unique", "card", "regex", "range", "values", "doc", "meta",
381 ],
382 )?;
383 let subject = AnnotationSubjectId::Owns(id);
384 self.queue_presence(body, "key", subject.clone(), AnnotationKindId::Key)?;
385 self.queue_presence(body, "unique", subject.clone(), AnnotationKindId::Unique)?;
386 self.queue_if_present(
387 body,
388 "card",
389 subject.clone(),
390 AnnotationKindId::Card,
391 PendingInput::Card,
392 )?;
393 self.queue_value_annotations(body, subject.clone())?;
394 self.queue_doc_meta(body, subject)?;
395 }
396 }
397 Ok(())
398 }
399
400 fn normalize_relates(
401 &mut self,
402 relation: &TypeId,
403 node: &YamlNode,
404 ) -> Result<(), SchemaDiagnostics> {
405 for named in named_bodies(node)? {
406 let role = contract(
407 RoleId::new(relation.label().as_str(), named.name.value()),
408 named.name.span(),
409 )?;
410 let specializes = if let Some(body) = &named.body {
411 if let Some(specializes) = entry(body, "as") {
412 let specializes = scalar(specializes.value())?;
413 Some((specializes.value().to_owned(), specializes.span().clone()))
414 } else {
415 None
416 }
417 } else {
418 None
419 };
420 let id = contract(RelatesFactId::new(relation.clone(), role), &named.source)?;
421 self.pending_relates.push(PendingRelates {
422 id: id.clone(),
423 specializes,
424 source: named.source.clone(),
425 });
426
427 if let Some(body) = &named.body {
428 check_keys(body, &["as", "abstract", "card", "doc", "meta"])?;
429 let subject = AnnotationSubjectId::Relates(id);
430 self.queue_presence(
431 body,
432 "abstract",
433 subject.clone(),
434 AnnotationKindId::Abstract,
435 )?;
436 self.queue_if_present(
437 body,
438 "card",
439 subject.clone(),
440 AnnotationKindId::Card,
441 PendingInput::Card,
442 )?;
443 self.queue_doc_meta(body, subject)?;
444 }
445 }
446 Ok(())
447 }
448
449 fn normalize_plays(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
450 let players = mapping(node)?;
451 for player_entry in players.entries() {
452 let Some((player, _)) = self.type_labels.get(player_entry.key().value()).cloned()
453 else {
454 return Err(error(
455 "unknown_schema_player",
456 "root plays declaration references an unknown player type",
457 Some(player_entry.key().span().clone()),
458 ));
459 };
460 if !matches!(player.kind(), TypeKind::Entity | TypeKind::Relation) {
461 return Err(error(
462 "invalid_schema_player_kind",
463 "only entity and relation types may play roles",
464 Some(player_entry.key().span().clone()),
465 ));
466 }
467 let relations = mapping(player_entry.value())?;
468 for relation_entry in relations.entries() {
469 let Some((relation, _)) =
470 self.type_labels.get(relation_entry.key().value()).cloned()
471 else {
472 return Err(error(
473 "unknown_schema_relation",
474 "root plays declaration references an unknown relation type",
475 Some(relation_entry.key().span().clone()),
476 ));
477 };
478 if relation.kind() != TypeKind::Relation {
479 return Err(error(
480 "invalid_schema_relation_kind",
481 "root plays relation key must name a relation type",
482 Some(relation_entry.key().span().clone()),
483 ));
484 }
485 for named in named_bodies(relation_entry.value())? {
486 let role = contract(
487 RoleId::new(relation.label().as_str(), named.name.value()),
488 named.name.span(),
489 )?;
490 let id = contract(PlaysFactId::new(player.clone(), role), &named.source)?;
491 self.push(
492 SchemaFact::Plays(PlaysFact::new(id.clone())),
493 named.source.clone(),
494 );
495 if let Some(body) = &named.body {
496 check_keys(body, &["card", "doc", "meta"])?;
497 let subject = AnnotationSubjectId::Plays(id);
498 self.queue_if_present(
499 body,
500 "card",
501 subject.clone(),
502 AnnotationKindId::Card,
503 PendingInput::Card,
504 )?;
505 self.queue_doc_meta(body, subject)?;
506 }
507 }
508 }
509 }
510 Ok(())
511 }
512
513 fn normalize_functions(
514 &mut self,
515 _document: &SchemaDocument,
516 node: &YamlNode,
517 ) -> Result<(), SchemaDiagnostics> {
518 let functions = mapping(node)?;
519 for declaration in functions.entries() {
520 let body = mapping(declaration.value())?;
521 check_keys(body, &["parameters", "returns", "body", "doc", "meta"])?;
522 let typeql_body = mapping(required_entry(body, "body")?.value())?;
523 check_keys(typeql_body, &["typeql"])?;
524 let body_text = scalar(required_entry(typeql_body, "typeql")?.value())?;
525
526 let mut parameters_out = Vec::new();
527 let mut parameter_sources = BTreeMap::<Label, SourceSpan>::new();
528 if let Some(parameters) = entry(body, "parameters") {
529 for parameter_node in sequence(parameters.value())?.items() {
530 let parameter = mapping(parameter_node)?;
531 check_keys(parameter, &["name", "type"])?;
532 let name = scalar(required_entry(parameter, "name")?.value())?;
533 let name_id = contract(Label::new(name.value()), name.span())?;
534 if let Some(previous) =
535 parameter_sources.insert(name_id.clone(), name.span().clone())
536 {
537 return Err(crate::diagnostic::diagnostic_with_related(
538 DiagnosticCategory::InvalidContract,
539 "duplicate_function_parameter",
540 format!("function parameter `{}` is duplicated", name.value()),
541 name.span().clone(),
542 previous,
543 "first parameter declaration is here",
544 ));
545 }
546 let type_token = scalar(required_entry(parameter, "type")?.value())?;
547 let type_ref = contract(
548 TypeReference::from_token(type_token.value()),
549 type_token.span(),
550 )?;
551 parameters_out.push(FunctionParameter::new(name_id, type_ref));
552 }
553 }
554
555 let returns = mapping(required_entry(body, "returns")?.value())?;
556 check_keys(returns, &["stream"])?;
557 let stream = sequence(required_entry(returns, "stream")?.value())?;
558 let mut return_elements = Vec::with_capacity(stream.items().len());
559 for element in stream.items() {
560 let type_token = scalar(element)?;
561 let type_ref = contract(
562 TypeReference::from_token(type_token.value()),
563 type_token.span(),
564 )?;
565 return_elements.push(FunctionReturnElement::new(type_ref, false));
566 }
567
568 let id = contract(
569 FunctionId::new(declaration.key().value()),
570 declaration.key().span(),
571 )?;
572 let returns = contract(FunctionReturnMode::stream(return_elements), returns.span())?;
573 let signature = contract(FunctionSignature::new(parameters_out, returns), body.span())?;
574 let function_body = contract(FunctionBody::new(body_text.value()), body_text.span())?;
575 self.push(
576 SchemaFact::Function(FunctionFact::new(id.clone(), signature, function_body)),
577 body.span().clone(),
578 );
579 self.queue_doc_meta(body, AnnotationSubjectId::Function(id))?;
580 }
581 Ok(())
582 }
583
584 fn queue_presence(
585 &mut self,
586 body: &YamlMapping,
587 key: &str,
588 subject: AnnotationSubjectId,
589 kind: AnnotationKindId,
590 ) -> Result<(), SchemaDiagnostics> {
591 let Some(entry) = entry(body, key) else {
592 return Ok(());
593 };
594 if !strict_bool(entry.value())? {
595 return Err(error(
596 "false_presence_annotation",
597 "presence annotations must be omitted rather than set to false",
598 Some(entry.value().span().clone()),
599 ));
600 }
601 self.pending.push(PendingAnnotation {
602 subject,
603 kind,
604 input: PendingInput::Presence,
605 source: entry.value().span().clone(),
606 });
607 Ok(())
608 }
609
610 fn queue_if_present(
611 &mut self,
612 body: &YamlMapping,
613 key: &str,
614 subject: AnnotationSubjectId,
615 kind: AnnotationKindId,
616 input: fn(YamlNode) -> PendingInput,
617 ) -> Result<(), SchemaDiagnostics> {
618 if let Some(entry) = entry(body, key) {
619 self.pending.push(PendingAnnotation {
620 subject,
621 kind,
622 input: input(entry.value().clone()),
623 source: entry.value().span().clone(),
624 });
625 }
626 Ok(())
627 }
628
629 fn queue_value_annotations(
630 &mut self,
631 body: &YamlMapping,
632 subject: AnnotationSubjectId,
633 ) -> Result<(), SchemaDiagnostics> {
634 self.queue_if_present(
635 body,
636 "regex",
637 subject.clone(),
638 AnnotationKindId::Regex,
639 PendingInput::Regex,
640 )?;
641 self.queue_if_present(
642 body,
643 "range",
644 subject.clone(),
645 AnnotationKindId::Range,
646 PendingInput::Range,
647 )?;
648 self.queue_if_present(
649 body,
650 "values",
651 subject,
652 AnnotationKindId::Values,
653 PendingInput::Values,
654 )
655 }
656
657 fn queue_doc_meta(
658 &mut self,
659 body: &YamlMapping,
660 subject: AnnotationSubjectId,
661 ) -> Result<(), SchemaDiagnostics> {
662 self.queue_if_present(
663 body,
664 "doc",
665 subject.clone(),
666 AnnotationKindId::Doc,
667 PendingInput::Doc,
668 )?;
669 if let Some(meta) = entry(body, "meta") {
670 let meta = mapping(meta.value())?;
671 for item in meta.entries() {
672 let kind = contract(
673 AnnotationKindId::meta(item.key().value()),
674 item.key().span(),
675 )?;
676 self.pending.push(PendingAnnotation {
677 subject: subject.clone(),
678 kind,
679 input: PendingInput::Meta(item.value().clone()),
680 source: item.value().span().clone(),
681 });
682 }
683 }
684 Ok(())
685 }
686
687 fn push(&mut self, fact: SchemaFact, source: SourceSpan) {
688 self.facts.push(SourcedSchemaFact::new(fact, source));
689 }
690
691 fn finish(mut self) -> Result<DeclaredSchema, SchemaDiagnostics> {
692 self.materialize_relates()?;
693 for plays in std::mem::take(&mut self.pending_plays) {
694 self.normalize_plays(&plays)?;
695 }
696 let mut assembler = FactAssembler::new(FormatVersion::V1);
697 for (capability, source) in &self.capability_sources {
698 assembler.require_capability(capability.clone(), source.clone())?;
699 }
700 for sourced in &self.facts {
701 match sourced.fact() {
702 SchemaFact::Struct(fact) => {
703 assembler.insert_struct(fact.clone(), sourced.source().clone())?;
704 }
705 fact => {
706 assembler.insert_fact(fact.clone(), sourced.source().clone())?;
707 }
708 }
709 }
710 let structural = assembler.finish()?;
711 for pending in self.pending {
712 self.facts.push(pending.resolve(&structural)?);
713 }
714 DeclaredSchema::from_facts(FormatVersion::V1, self.capabilities, self.facts)
715 }
716
717 fn materialize_relates(&mut self) -> Result<(), SchemaDiagnostics> {
718 let declarations = std::mem::take(&mut self.pending_relates);
719 for declaration in &declarations {
720 let specializes = declaration
721 .specializes
722 .as_ref()
723 .map(|(label, source)| {
724 self.resolve_inherited_role(
725 declaration.id.relation(),
726 label,
727 source,
728 &declarations,
729 )
730 })
731 .transpose()?;
732 self.push(
733 SchemaFact::Relates(contract(
734 RelatesFact::new(declaration.id.clone(), specializes),
735 &declaration.source,
736 )?),
737 declaration.source.clone(),
738 );
739 }
740 Ok(())
741 }
742
743 fn resolve_inherited_role(
744 &self,
745 relation: &TypeId,
746 role_label: &str,
747 source: &SourceSpan,
748 declarations: &[PendingRelates],
749 ) -> Result<RoleId, SchemaDiagnostics> {
750 let mut current = relation.clone();
751 let mut visited = BTreeSet::new();
752 loop {
753 if !visited.insert(current.clone()) {
754 return Err(error(
755 "schema_inheritance_cycle",
756 "relation inheritance contains a cycle",
757 Some(source.clone()),
758 ));
759 }
760 let Some(parent) = self.direct_parent(¤t) else {
761 return Err(error(
762 "invalid_role_specialization",
763 "specialized role is not declared by any ancestor relation",
764 Some(source.clone()),
765 ));
766 };
767 if let Some(role) = declarations.iter().find_map(|candidate| {
768 (candidate.id.relation() == &parent
769 && candidate.id.role().label().as_str() == role_label)
770 .then(|| candidate.id.role().clone())
771 }) {
772 return Ok(role);
773 }
774 current = parent;
775 }
776 }
777
778 fn direct_parent(&self, subtype: &TypeId) -> Option<TypeId> {
779 self.facts.iter().find_map(|sourced| {
780 let SchemaFact::Sub(sub) = sourced.fact() else {
781 return None;
782 };
783 (sub.id().subtype() == subtype).then(|| sub.id().supertype().clone())
784 })
785 }
786}
787
788struct PendingRelates {
789 id: RelatesFactId,
790 specializes: Option<(String, SourceSpan)>,
791 source: SourceSpan,
792}
793
794struct PendingAnnotation {
795 subject: AnnotationSubjectId,
796 kind: AnnotationKindId,
797 input: PendingInput,
798 source: SourceSpan,
799}
800
801enum PendingInput {
802 Presence,
803 Card(YamlNode),
804 Regex(YamlNode),
805 Range(YamlNode),
806 Values(YamlNode),
807 Doc(YamlNode),
808 Meta(YamlNode),
809}
810
811impl PendingAnnotation {
812 fn resolve(self, schema: &DeclaredSchema) -> Result<SourcedSchemaFact, SchemaDiagnostics> {
813 let value = match self.input {
814 PendingInput::Presence => SchemaAnnotationValue::Presence,
815 PendingInput::Card(node) => {
816 SchemaAnnotationValue::Cardinality(parse_cardinality(&node)?)
817 }
818 PendingInput::Regex(node) => SchemaAnnotationValue::Regex(contract(
819 RegexPattern::new(scalar(&node)?.value()),
820 node.span(),
821 )?),
822 PendingInput::Range(node) => {
823 let value_type = subject_value_type(schema, &self.subject, &self.source)?;
824 SchemaAnnotationValue::Range(parse_range(&node, value_type)?)
825 }
826 PendingInput::Values(node) => {
827 let value_type = subject_value_type(schema, &self.subject, &self.source)?;
828 SchemaAnnotationValue::Values(parse_values(&node, value_type)?)
829 }
830 PendingInput::Doc(node) => SchemaAnnotationValue::Doc(contract(
831 DocText::new(scalar(&node)?.value()),
832 node.span(),
833 )?),
834 PendingInput::Meta(node) => {
835 let scalar = scalar(&node)?;
836 SchemaAnnotationValue::Meta(CanonicalValue::String(contract(
837 CanonicalString::new(scalar.value()),
838 scalar.span(),
839 )?))
840 }
841 };
842 let fact = contract(
843 AnnotationFact::new(AnnotationFactId::new(self.subject, self.kind), value),
844 &self.source,
845 )?;
846 Ok(SourcedSchemaFact::new(
847 SchemaFact::Annotation(fact),
848 self.source,
849 ))
850 }
851}
852
853#[derive(Clone)]
854struct NamedBody {
855 name: YamlScalar,
856 body: Option<YamlMapping>,
857 source: SourceSpan,
858}
859
860fn named_bodies(node: &YamlNode) -> Result<Vec<NamedBody>, SchemaDiagnostics> {
861 match node {
862 YamlNode::Mapping(mapping) => mapping.entries().iter().map(named_mapping_entry).collect(),
863 YamlNode::Sequence(sequence) => sequence
864 .items()
865 .iter()
866 .map(|item| match item {
867 YamlNode::Scalar(name) => Ok(NamedBody {
868 name: name.clone(),
869 body: None,
870 source: name.span().clone(),
871 }),
872 YamlNode::Mapping(mapping) if mapping.entries().len() == 1 => {
873 named_mapping_entry(&mapping.entries()[0])
874 }
875 other => Err(error(
876 "invalid_named_schema_fact",
877 "named facts must be scalars or single-entry mappings",
878 Some(other.span().clone()),
879 )),
880 })
881 .collect(),
882 YamlNode::Scalar(value) => Err(error(
883 "invalid_named_schema_fact_collection",
884 "named facts must be a sequence or mapping",
885 Some(value.span().clone()),
886 )),
887 }
888}
889
890fn named_mapping_entry(entry: &crate::YamlMappingEntry) -> Result<NamedBody, SchemaDiagnostics> {
891 let (body, source) = match entry.value() {
892 YamlNode::Mapping(body) => (Some(body.clone()), body.span().clone()),
893 other => {
894 return Err(error(
895 "invalid_named_schema_fact_body",
896 "expanded named fact bodies must be mappings",
897 Some(other.span().clone()),
898 ));
899 }
900 };
901 Ok(NamedBody {
902 name: entry.key().clone(),
903 body,
904 source,
905 })
906}
907
908fn subject_value_type(
909 schema: &DeclaredSchema,
910 subject: &AnnotationSubjectId,
911 source: &SourceSpan,
912) -> Result<ValueTypeTag, SchemaDiagnostics> {
913 let mut attribute = match subject {
914 AnnotationSubjectId::Value(id) => id.attribute().clone(),
915 AnnotationSubjectId::Owns(id) => id.attribute().clone(),
916 _ => {
917 return Err(error(
918 "annotation_value_domain_unavailable",
919 "value annotation subject has no attribute domain",
920 Some(source.clone()),
921 ));
922 }
923 };
924 let mut visited = BTreeSet::new();
925 loop {
926 let type_id = contract(
927 TypeId::new(TypeKind::Attribute, attribute.label().as_str()),
928 source,
929 )?;
930 if !visited.insert(type_id.clone()) {
931 return Err(error(
932 "schema_value_inheritance_cycle",
933 "attribute value inheritance contains a cycle",
934 Some(source.clone()),
935 ));
936 }
937 let value_id = ValueFactId::new(attribute.clone());
938 if let Some(SchemaFact::Value(value)) = schema.fact(&SchemaFactId::Value(value_id)) {
939 return Ok(value.value_type());
940 }
941 let Some(parent) = schema.facts().find_map(|fact| {
942 let SchemaFact::Sub(sub) = fact else {
943 return None;
944 };
945 (sub.id().subtype() == &type_id && sub.id().supertype().kind() == TypeKind::Attribute)
946 .then(|| sub.id().supertype().clone())
947 }) else {
948 return Err(error(
949 "schema_value_domain_missing",
950 "attribute has no direct or inherited value domain",
951 Some(source.clone()),
952 ));
953 };
954 attribute = contract(AttributeId::new(parent.label().as_str()), source)?;
955 }
956}
957
958fn parse_cardinality(node: &YamlNode) -> Result<Cardinality, SchemaDiagnostics> {
959 match node {
960 YamlNode::Scalar(value) => {
961 let exact = canonical_u64(value)?;
962 contract(Cardinality::new(exact, Some(exact)), value.span())
963 }
964 YamlNode::Mapping(body) => {
965 check_keys(body, &["min", "max"])?;
966 let min = canonical_u64(scalar(required_entry(body, "min")?.value())?)?;
967 let max = entry(body, "max")
968 .map(|entry| canonical_u64(scalar(entry.value())?))
969 .transpose()?;
970 contract(Cardinality::new(min, max), body.span())
971 }
972 YamlNode::Sequence(sequence) => Err(error(
973 "invalid_cardinality_shape",
974 "cardinality must be an integer or a min/max mapping",
975 Some(sequence.span().clone()),
976 )),
977 }
978}
979
980fn parse_values(
981 node: &YamlNode,
982 value_type: ValueTypeTag,
983) -> Result<CanonicalValueSet, SchemaDiagnostics> {
984 let values = sequence(node)?;
985 let canonical = values
986 .items()
987 .iter()
988 .map(|item| canonical_value(value_type, scalar(item)?))
989 .collect::<Result<Vec<_>, _>>()?;
990 contract(CanonicalValueSet::new(canonical), values.span())
991}
992
993fn parse_range(
994 node: &YamlNode,
995 value_type: ValueTypeTag,
996) -> Result<CanonicalValueRange, SchemaDiagnostics> {
997 let body = mapping(node)?;
998 check_keys(body, &["min", "max"])?;
999 let lower = entry(body, "min")
1000 .map(|entry| canonical_value(value_type, scalar(entry.value())?))
1001 .transpose()?;
1002 let upper = entry(body, "max")
1003 .map(|entry| canonical_value(value_type, scalar(entry.value())?))
1004 .transpose()?;
1005 contract(CanonicalValueRange::new(lower, upper), body.span())
1006}
1007
1008fn canonical_value(
1009 value_type: ValueTypeTag,
1010 scalar: &YamlScalar,
1011) -> Result<CanonicalValue, SchemaDiagnostics> {
1012 let spelling = scalar.value();
1013 match value_type {
1014 ValueTypeTag::String => Ok(CanonicalValue::String(contract(
1015 CanonicalString::new(spelling),
1016 scalar.span(),
1017 )?)),
1018 ValueTypeTag::Long => {
1019 let value = spelling.parse::<i64>().map_err(|_| {
1020 error(
1021 "invalid_integer_value",
1022 "integer annotation value is not an i64",
1023 Some(scalar.span().clone()),
1024 )
1025 })?;
1026 if value.to_string() != spelling {
1027 return Err(error(
1028 "non_canonical_integer_value",
1029 "integer annotation value is not canonically spelled",
1030 Some(scalar.span().clone()),
1031 ));
1032 }
1033 Ok(CanonicalValue::Long(value))
1034 }
1035 ValueTypeTag::Double => {
1036 let value = spelling.parse::<f64>().map_err(|_| {
1037 error(
1038 "invalid_double_value",
1039 "double annotation value is not an f64",
1040 Some(scalar.span().clone()),
1041 )
1042 })?;
1043 Ok(CanonicalValue::Double(contract(
1044 CanonicalDouble::new(value),
1045 scalar.span(),
1046 )?))
1047 }
1048 ValueTypeTag::Boolean => match spelling {
1049 "true" => Ok(CanonicalValue::Boolean(true)),
1050 "false" => Ok(CanonicalValue::Boolean(false)),
1051 _ => Err(error(
1052 "invalid_boolean_value",
1053 "boolean annotation value must be exactly `true` or `false`",
1054 Some(scalar.span().clone()),
1055 )),
1056 },
1057 ValueTypeTag::Date => Ok(CanonicalValue::Date(contract(
1058 spelling.parse::<CanonicalDate>(),
1059 scalar.span(),
1060 )?)),
1061 ValueTypeTag::DateTime => Ok(CanonicalValue::DateTime(contract(
1062 spelling.parse::<CanonicalDateTime>(),
1063 scalar.span(),
1064 )?)),
1065 ValueTypeTag::DateTimeTz => Ok(CanonicalValue::DateTimeTz(contract(
1066 parse_provider_datetime_tz(spelling),
1067 scalar.span(),
1068 )?)),
1069 ValueTypeTag::Decimal => Ok(CanonicalValue::Decimal(contract(
1070 DecimalValue::new(spelling),
1071 scalar.span(),
1072 )?)),
1073 ValueTypeTag::Duration => Ok(CanonicalValue::Duration(contract(
1074 spelling.parse::<CanonicalDuration>(),
1075 scalar.span(),
1076 )?)),
1077 }
1078}
1079
1080fn parse_value_type(value: &YamlScalar) -> Result<ValueTypeTag, SchemaDiagnostics> {
1081 match value.value() {
1082 "string" => Ok(ValueTypeTag::String),
1083 "integer" => Ok(ValueTypeTag::Long),
1084 "double" => Ok(ValueTypeTag::Double),
1085 "boolean" => Ok(ValueTypeTag::Boolean),
1086 "date" => Ok(ValueTypeTag::Date),
1087 "datetime" => Ok(ValueTypeTag::DateTime),
1088 "datetime-tz" => Ok(ValueTypeTag::DateTimeTz),
1089 "decimal" => Ok(ValueTypeTag::Decimal),
1090 "duration" => Ok(ValueTypeTag::Duration),
1091 _ => Err(error(
1092 "unknown_schema_value_type",
1093 "value type must use a canonical TypeQL value-type token",
1094 Some(value.span().clone()),
1095 )),
1096 }
1097}
1098
1099fn canonical_u64(value: &YamlScalar) -> Result<u64, SchemaDiagnostics> {
1100 let parsed = value.value().parse::<u64>().map_err(|_| {
1101 error(
1102 "invalid_unsigned_integer",
1103 "value must be an unsigned integer",
1104 Some(value.span().clone()),
1105 )
1106 })?;
1107 if parsed.to_string() != value.value() {
1108 return Err(error(
1109 "non_canonical_unsigned_integer",
1110 "unsigned integer is not canonically spelled",
1111 Some(value.span().clone()),
1112 ));
1113 }
1114 Ok(parsed)
1115}
1116
1117fn strict_bool(node: &YamlNode) -> Result<bool, SchemaDiagnostics> {
1118 let value = scalar(node)?;
1119 match value.value() {
1120 "true" => Ok(true),
1121 "false" => Ok(false),
1122 _ => Err(error(
1123 "invalid_schema_boolean",
1124 "schema boolean must be exactly `true` or `false`",
1125 Some(value.span().clone()),
1126 )),
1127 }
1128}
1129
1130fn check_keys(mapping: &YamlMapping, allowed: &[&str]) -> Result<(), SchemaDiagnostics> {
1131 for entry in mapping.entries() {
1132 if !allowed.contains(&entry.key().value()) {
1133 return Err(error(
1134 "unknown_schema_document_key",
1135 format!("unknown schema key `{}`", entry.key().value()),
1136 Some(entry.key().span().clone()),
1137 ));
1138 }
1139 }
1140 Ok(())
1141}
1142
1143fn entry<'a>(mapping: &'a YamlMapping, key: &str) -> Option<&'a crate::YamlMappingEntry> {
1144 mapping
1145 .entries()
1146 .iter()
1147 .find(|entry| entry.key().value() == key)
1148}
1149
1150fn required_entry<'a>(
1151 mapping: &'a YamlMapping,
1152 key: &str,
1153) -> Result<&'a crate::YamlMappingEntry, SchemaDiagnostics> {
1154 entry(mapping, key).ok_or_else(|| {
1155 error(
1156 "missing_schema_document_key",
1157 format!("required schema key `{key}` is missing"),
1158 Some(mapping.span().clone()),
1159 )
1160 })
1161}
1162
1163fn scalar(node: &YamlNode) -> Result<&YamlScalar, SchemaDiagnostics> {
1164 node.as_scalar().ok_or_else(|| {
1165 error(
1166 "schema_scalar_required",
1167 "schema value must be a scalar",
1168 Some(node.span().clone()),
1169 )
1170 })
1171}
1172
1173fn mapping(node: &YamlNode) -> Result<&YamlMapping, SchemaDiagnostics> {
1174 node.as_mapping().ok_or_else(|| {
1175 error(
1176 "schema_mapping_required",
1177 "schema value must be a mapping",
1178 Some(node.span().clone()),
1179 )
1180 })
1181}
1182
1183fn sequence(node: &YamlNode) -> Result<&crate::YamlSequence, SchemaDiagnostics> {
1184 node.as_sequence().ok_or_else(|| {
1185 error(
1186 "schema_sequence_required",
1187 "schema value must be a sequence",
1188 Some(node.span().clone()),
1189 )
1190 })
1191}
1192
1193fn contract<T>(result: Result<T, Diagnostic>, span: &SourceSpan) -> Result<T, SchemaDiagnostics> {
1194 result.map_err(|diagnostic| {
1195 SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, Some(span.clone())))
1196 })
1197}
1198
1199fn error(
1200 code: &'static str,
1201 message: impl Into<String>,
1202 primary: Option<SourceSpan>,
1203) -> SchemaDiagnostics {
1204 crate::yaml::diagnostic(DiagnosticCategory::InvalidContract, code, message, primary)
1205}