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::{Label, RoleId, TypeId, TypeKind};
7use type_bridge_contract::schema::{
8 CollectionMode, DeclaredSchema, PlaysFact, PlaysFactId, RelatesFact, RelatesFactId,
9 SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SchemaFactId, SourceSpan, SourcedSchemaFact,
10 StructFact,
11};
12
13use crate::diagnostic::{diagnostic, diagnostic_with_related};
14
15struct PendingRelates {
16 id: RelatesFactId,
17 specializes: Option<(Label, SourceSpan)>,
18 collection_mode: CollectionMode,
19 source: SourceSpan,
20}
21
22struct PendingPlays {
23 player: Label,
24 relation: Label,
25 role: Label,
26 source: SourceSpan,
27}
28
29pub struct FactAssembler {
35 format: FormatVersion,
36 facts: Vec<SourcedSchemaFact>,
37 fact_sources: BTreeMap<SchemaFactId, SourceSpan>,
38 capabilities: CapabilitySet,
39 capability_sources: BTreeMap<CapabilityId, SourceSpan>,
40 declaration_labels: BTreeMap<Label, (SchemaFactId, SourceSpan)>,
41 type_labels: BTreeMap<Label, (TypeId, SourceSpan)>,
42 direct_parents: BTreeMap<TypeId, (TypeId, SourceSpan)>,
43 pending_relates: Vec<PendingRelates>,
44 pending_relates_sources: BTreeMap<RelatesFactId, SourceSpan>,
45 pending_plays: Vec<PendingPlays>,
46}
47
48impl FactAssembler {
49 #[must_use]
51 pub fn new(format: FormatVersion) -> Self {
52 Self {
53 format,
54 facts: Vec::new(),
55 fact_sources: BTreeMap::new(),
56 capabilities: CapabilitySet::new(),
57 capability_sources: BTreeMap::new(),
58 declaration_labels: BTreeMap::new(),
59 type_labels: BTreeMap::new(),
60 direct_parents: BTreeMap::new(),
61 pending_relates: Vec::new(),
62 pending_relates_sources: BTreeMap::new(),
63 pending_plays: Vec::new(),
64 }
65 }
66
67 pub fn require_capability(
69 &mut self,
70 capability: CapabilityId,
71 source: SourceSpan,
72 ) -> Result<(), SchemaDiagnostics> {
73 if let Some(previous) = self.capability_sources.get(&capability) {
74 return Err(diagnostic_with_related(
75 DiagnosticCategory::InvalidContract,
76 "duplicate_required_capability",
77 format!("required capability `{capability}` is declared more than once"),
78 source,
79 previous.clone(),
80 "first capability requirement is here",
81 ));
82 }
83 self.capability_sources.insert(capability.clone(), source);
84 self.capabilities.insert(capability);
85 Ok(())
86 }
87
88 pub fn insert_fact(
90 &mut self,
91 fact: SchemaFact,
92 source: SourceSpan,
93 ) -> Result<(), SchemaDiagnostics> {
94 let id = fact.id();
95 if let Some(previous) = self.fact_sources.get(&id) {
96 return Err(diagnostic_with_related(
97 DiagnosticCategory::InvalidContract,
98 "duplicate_schema_fact",
99 "a direct schema fact is declared more than once",
100 source,
101 previous.clone(),
102 "first declaration is here",
103 ));
104 }
105
106 let declaration_label = match &fact {
107 SchemaFact::Type(type_fact) => Some(type_fact.id().label()),
108 SchemaFact::Struct(struct_fact) => Some(struct_fact.id().label()),
109 _ => None,
110 };
111 if let Some(label) = declaration_label {
112 if let Some((previous_id, previous_source)) = self.declaration_labels.get(label) {
113 let (code, message, related_message) = if matches!(
114 (&id, previous_id),
115 (SchemaFactId::Type(_), SchemaFactId::Type(_))
116 ) {
117 (
118 "duplicate_schema_type_label",
119 format!("schema label `{label}` is declared with more than one type kind"),
120 "first type declaration is here",
121 )
122 } else {
123 (
124 "duplicate_schema_label",
125 format!("schema label `{label}` is declared as both a type and a struct"),
126 "first type or struct declaration is here",
127 )
128 };
129 return Err(diagnostic_with_related(
130 DiagnosticCategory::InvalidContract,
131 code,
132 message,
133 source,
134 previous_source.clone(),
135 related_message,
136 ));
137 }
138 self.declaration_labels
139 .insert(label.clone(), (id.clone(), source.clone()));
140 }
141
142 if let SchemaFact::Type(type_fact) = &fact {
143 let type_id = type_fact.id();
144 self.type_labels
145 .insert(type_id.label().clone(), (type_id.clone(), source.clone()));
146 }
147
148 if let SchemaFact::Sub(sub_fact) = &fact {
149 let subtype = sub_fact.id().subtype();
150 let supertype = sub_fact.id().supertype();
151 if let Some((previous_parent, previous_source)) = self.direct_parents.get(subtype) {
152 if previous_parent != supertype {
153 return Err(diagnostic_with_related(
154 DiagnosticCategory::InvalidContract,
155 "multiple_direct_schema_parents",
156 format!(
157 "schema type `{}` has more than one direct parent",
158 subtype.label()
159 ),
160 source,
161 previous_source.clone(),
162 "first direct parent is declared here",
163 ));
164 }
165 } else {
166 self.direct_parents
167 .insert(subtype.clone(), (supertype.clone(), source.clone()));
168 }
169 }
170
171 self.fact_sources.insert(id, source.clone());
172 self.facts.push(SourcedSchemaFact::new(fact, source));
173 Ok(())
174 }
175
176 pub fn insert_struct(
178 &mut self,
179 fact: StructFact,
180 source: SourceSpan,
181 ) -> Result<(), SchemaDiagnostics> {
182 self.insert_fact(SchemaFact::Struct(fact), source)
183 }
184
185 pub fn insert_relates(
187 &mut self,
188 id: RelatesFactId,
189 specializes: Option<(Label, SourceSpan)>,
190 source: SourceSpan,
191 ) -> Result<(), SchemaDiagnostics> {
192 self.insert_relates_with_collection_mode(id, specializes, CollectionMode::Unordered, source)
193 }
194
195 pub fn insert_relates_with_collection_mode(
197 &mut self,
198 id: RelatesFactId,
199 specializes: Option<(Label, SourceSpan)>,
200 collection_mode: CollectionMode,
201 source: SourceSpan,
202 ) -> Result<(), SchemaDiagnostics> {
203 if let Some(previous) = self.pending_relates_sources.get(&id) {
204 return Err(diagnostic_with_related(
205 DiagnosticCategory::InvalidContract,
206 "duplicate_schema_fact",
207 "a direct related-role fact is declared more than once",
208 source,
209 previous.clone(),
210 "first declaration is here",
211 ));
212 }
213 if let Some(previous) = self.fact_sources.get(&SchemaFactId::Relates(id.clone())) {
214 return Err(diagnostic_with_related(
215 DiagnosticCategory::InvalidContract,
216 "duplicate_schema_fact",
217 "a direct related-role fact is declared more than once",
218 source,
219 previous.clone(),
220 "first declaration is here",
221 ));
222 }
223 self.pending_relates_sources
224 .insert(id.clone(), source.clone());
225 self.pending_relates.push(PendingRelates {
226 id,
227 specializes,
228 collection_mode,
229 source,
230 });
231 Ok(())
232 }
233
234 pub fn insert_plays(
236 &mut self,
237 player: Label,
238 relation: Label,
239 role: Label,
240 source: SourceSpan,
241 ) {
242 self.pending_plays.push(PendingPlays {
243 player,
244 relation,
245 role,
246 source,
247 });
248 }
249
250 pub fn finish(mut self) -> Result<DeclaredSchema, SchemaDiagnostics> {
252 self.materialize_relates()?;
253 self.materialize_plays()?;
254 DeclaredSchema::from_facts(self.format, self.capabilities, self.facts)
255 }
256
257 fn materialize_relates(&mut self) -> Result<(), SchemaDiagnostics> {
258 let declarations = std::mem::take(&mut self.pending_relates);
259 for declaration in &declarations {
260 let specializes = declaration
261 .specializes
262 .as_ref()
263 .map(|(label, source)| {
264 self.resolve_inherited_role(
265 declaration.id.relation(),
266 label,
267 source,
268 &declarations,
269 )
270 })
271 .transpose()?;
272 let fact = RelatesFact::new_with_collection_mode(
273 declaration.id.clone(),
274 specializes,
275 declaration.collection_mode,
276 )
277 .map_err(|error| contract(error, declaration.source.clone()))?;
278 self.insert_fact(SchemaFact::Relates(fact), declaration.source.clone())?;
279 }
280 Ok(())
281 }
282
283 fn materialize_plays(&mut self) -> Result<(), SchemaDiagnostics> {
284 let declarations = std::mem::take(&mut self.pending_plays);
285 for declaration in declarations {
286 let player = self
287 .type_labels
288 .get(&declaration.player)
289 .map(|(id, _)| id.clone())
290 .ok_or_else(|| {
291 diagnostic(
292 DiagnosticCategory::InvalidContract,
293 "unknown_schema_player",
294 format!("unknown playing type `{}`", declaration.player),
295 Some(declaration.source.clone()),
296 )
297 })?;
298 if !matches!(player.kind(), TypeKind::Entity | TypeKind::Relation) {
299 return Err(diagnostic(
300 DiagnosticCategory::InvalidContract,
301 "invalid_schema_player_kind",
302 "only entity and relation types can play roles",
303 Some(declaration.source),
304 ));
305 }
306 let relation = self
307 .type_labels
308 .get(&declaration.relation)
309 .map(|(id, _)| id.clone())
310 .ok_or_else(|| {
311 diagnostic(
312 DiagnosticCategory::InvalidContract,
313 "unknown_schema_relation",
314 format!("unknown relation type `{}`", declaration.relation),
315 Some(declaration.source.clone()),
316 )
317 })?;
318 if relation.kind() != TypeKind::Relation {
319 return Err(diagnostic(
320 DiagnosticCategory::InvalidContract,
321 "invalid_schema_relation_kind",
322 "a playing relation reference must name a relation type",
323 Some(declaration.source),
324 ));
325 }
326 let role = RoleId::new(relation.label().as_str(), declaration.role.as_str())
327 .map_err(|error| contract(error, declaration.source.clone()))?;
328 let id = PlaysFactId::new(player, role)
329 .map_err(|error| contract(error, declaration.source.clone()))?;
330 self.insert_fact(SchemaFact::Plays(PlaysFact::new(id)), declaration.source)?;
331 }
332 Ok(())
333 }
334
335 fn resolve_inherited_role(
336 &self,
337 relation: &TypeId,
338 role_label: &Label,
339 source: &SourceSpan,
340 declarations: &[PendingRelates],
341 ) -> Result<RoleId, SchemaDiagnostics> {
342 let mut current = relation.clone();
343 let mut visited = BTreeSet::new();
344 loop {
345 if !visited.insert(current.clone()) {
346 return Err(diagnostic(
347 DiagnosticCategory::InvalidContract,
348 "schema_inheritance_cycle",
349 "relation inheritance contains a cycle",
350 Some(source.clone()),
351 ));
352 }
353 let Some((parent, _)) = self.direct_parents.get(¤t) else {
354 return Err(diagnostic(
355 DiagnosticCategory::InvalidContract,
356 "invalid_role_specialization",
357 "specialized role is not declared by any ancestor relation",
358 Some(source.clone()),
359 ));
360 };
361 if let Some(role) = declarations.iter().find_map(|candidate| {
362 (candidate.id.relation() == parent && candidate.id.role().label() == role_label)
363 .then(|| candidate.id.role().clone())
364 }) {
365 return Ok(role);
366 }
367 if let Some(role) = self.facts.iter().find_map(|sourced| {
368 let SchemaFact::Relates(fact) = sourced.fact() else {
369 return None;
370 };
371 (fact.id().relation() == parent && fact.id().role().label() == role_label)
372 .then(|| fact.id().role().clone())
373 }) {
374 return Ok(role);
375 }
376 current = parent.clone();
377 }
378 }
379}
380
381fn contract(error: Diagnostic, source: SourceSpan) -> SchemaDiagnostics {
382 SchemaDiagnostics::one(SchemaDiagnostic::new(error, Some(source)))
383}