Skip to main content

type_bridge_schema/
assembler.rs

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
29/// Source-language-neutral construction of direct schema facts.
30///
31/// Parsers provide validated contract identities and source spans. The assembler
32/// owns duplicate detection and the forward-reference resolution needed to mint
33/// stable role and playing identities consistently across source languages.
34pub 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    /// Start an assembler for one canonical fact-format version.
50    #[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    /// Add one required capability, retaining duplicate provenance.
68    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    /// Insert one fully identified direct fact.
89    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    /// Insert one struct declaration through the shared declaration namespace.
177    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    /// Queue a related-role declaration whose specialization may target an ancestor.
186    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    /// Queue a related-role declaration with explicit collection semantics.
196    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    /// Queue a player-keyed playing declaration for forward resolution.
235    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    /// Materialize deferred identities and construct the validated declared graph.
251    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(&current) 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}