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