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