Skip to main content

type_bridge_schema_codegen/
lib.rs

1//! Deterministic emitters over validated binding-neutral runtime projections.
2
3#![deny(missing_docs)]
4
5use std::collections::BTreeMap;
6
7mod c;
8mod package;
9mod python;
10mod rust;
11mod typescript;
12
13pub use c::CEmitter;
14pub use package::{GeneratedPackage, MIGRATION_HISTORY_BUNDLE_RESOURCE};
15pub use python::PythonEmitter;
16pub use rust::RustEmitter;
17pub use typescript::TypeScriptEmitter;
18
19use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
20use type_bridge_contract::projection::{
21    BindingTarget, ModelProjection, ProjectedAnnotation, ProjectionConfig, RuntimeProjection,
22};
23use type_bridge_contract::schema::{
24    AnnotationFactId, AnnotationKindId, SchemaAnnotationValue, encode_declared_schema,
25};
26use type_bridge_schema::{
27    ResolvedSchema, VerifiedSchemaAuthority, encode_schema_authority, project,
28};
29
30/// Verify that a runtime projection carries the exact feature-selected
31/// handler and fixed-resource evidence emitted for its binding target.
32///
33/// This is the shared package-admission boundary for generated runtimes. It
34/// derives all evidence from the verified schema authority, recomputes the
35/// complete projection, and rejects self-consistent projections forged with a
36/// legacy or foreign emitter ledger.
37pub fn verify_projection_evidence(
38    authority: &VerifiedSchemaAuthority,
39    projection: &RuntimeProjection,
40) -> Result<(), Diagnostic> {
41    let schema = authority.resolved_schema();
42    let reproject =
43        |target,
44         handlers: &[type_bridge_contract::projection::ProjectionHandler],
45         resources: &[type_bridge_contract::projection::CodeResourceDigest]| {
46            project(schema, target, projection.config(), handlers, resources).map_err(|_| {
47            invalid(
48                "schema_codegen_projection_evidence_mismatch",
49                "runtime projection cannot be reproduced from the verified schema authority and emitter evidence",
50            )
51        })
52        };
53    let expected = match projection.target() {
54        BindingTarget::Python => {
55            if projection.config() != &ProjectionConfig::python() {
56                return Err(invalid(
57                    "schema_codegen_projection_evidence_mismatch",
58                    "Python runtime projection carries a foreign target configuration",
59                ));
60            }
61            let emitter = PythonEmitter::new();
62            reproject(
63                BindingTarget::Python,
64                &emitter.generator_handlers_for(schema),
65                &emitter.code_resources_for(schema)?,
66            )?
67        }
68        BindingTarget::TypeScript => {
69            if projection.config() != &ProjectionConfig::typescript() {
70                return Err(invalid(
71                    "schema_codegen_projection_evidence_mismatch",
72                    "TypeScript runtime projection carries a foreign target configuration",
73                ));
74            }
75            let emitter = TypeScriptEmitter::new();
76            reproject(
77                BindingTarget::TypeScript,
78                &emitter.generator_handlers_for(schema),
79                &emitter.code_resources_for(schema)?,
80            )?
81        }
82        BindingTarget::Rust => {
83            if projection.config() != &ProjectionConfig::rust() {
84                return Err(invalid(
85                    "schema_codegen_projection_evidence_mismatch",
86                    "Rust runtime projection carries a foreign target configuration",
87                ));
88            }
89            let emitter = RustEmitter::new();
90            reproject(
91                BindingTarget::Rust,
92                &emitter.generator_handlers_for(schema),
93                &emitter.code_resources_for(schema)?,
94            )?
95        }
96        BindingTarget::C => {
97            if projection.config().c_symbol_prefix().is_none() {
98                return Err(invalid(
99                    "schema_codegen_projection_evidence_mismatch",
100                    "C runtime projection omits its generated link-namespace configuration",
101                ));
102            }
103            let emitter = CEmitter::new();
104            reproject(
105                BindingTarget::C,
106                &emitter.generator_handlers_for(schema),
107                &emitter.code_resources_for(schema)?,
108            )?
109        }
110        _ => {
111            return Err(invalid(
112                "schema_codegen_projection_evidence_mismatch",
113                "runtime projection targets an unsupported generated binding",
114            ));
115        }
116    };
117    if &expected != projection {
118        return Err(invalid(
119            "schema_codegen_projection_evidence_mismatch",
120            "runtime projection differs from the exact feature-selected emitter evidence",
121        ));
122    }
123    Ok(())
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
127struct EmbeddedAuthority {
128    canonical_envelope_json: String,
129    declared_schema_json: String,
130    managed_scope_id: String,
131    semantic_profile_id: String,
132}
133
134fn embedded_authority(
135    projection: &type_bridge_contract::projection::RuntimeProjection,
136    authority: &VerifiedSchemaAuthority,
137) -> Result<EmbeddedAuthority, Diagnostic> {
138    if projection.semantic_fingerprint() != authority.resolved_schema().semantic_fingerprint() {
139        return Err(invalid(
140            "schema_codegen_authority_mismatch",
141            "runtime projection and verified schema authority have different semantic fingerprints",
142        ));
143    }
144
145    let canonical_envelope_json =
146        String::from_utf8(encode_schema_authority(authority)).map_err(|_| {
147            invalid(
148                "schema_codegen_non_utf8_authority",
149                "canonical schema-authority JSON must be UTF-8",
150            )
151        })?;
152    let declared_schema_json =
153        String::from_utf8(encode_declared_schema(authority.declared_schema())?).map_err(|_| {
154            invalid(
155                "schema_codegen_non_utf8_declared_schema",
156                "canonical declared-schema JSON must be UTF-8",
157            )
158        })?;
159
160    Ok(EmbeddedAuthority {
161        canonical_envelope_json,
162        declared_schema_json,
163        managed_scope_id: authority.managed_scope().id().as_str().to_owned(),
164        semantic_profile_id: authority.semantic_profile().id().as_str().to_owned(),
165    })
166}
167
168fn invalid(code: &'static str, message: impl Into<String>) -> Diagnostic {
169    Diagnostic::new(
170        DiagnosticCategory::InvalidContract,
171        DiagnosticCode::new(code).expect("schema-codegen diagnostic code is valid"),
172        message,
173    )
174}
175
176fn resolved_schema_uses_ordered_collections(schema: &ResolvedSchema) -> bool {
177    schema.types().values().any(|model| {
178        model
179            .owns()
180            .values()
181            .any(|owns| !owns.collection_mode().is_unordered())
182            || model
183                .relates()
184                .values()
185                .any(|relates| !relates.collection_mode().is_unordered())
186    })
187}
188
189fn projection_uses_ordered_collections(projection: &RuntimeProjection) -> bool {
190    projection.models().values().any(|model| {
191        model
192            .query_tokens()
193            .fields()
194            .values()
195            .any(|field| !field.multiplicity().collection_mode().is_unordered())
196            || model
197                .query_tokens()
198                .roles()
199                .values()
200                .any(|role| !role.multiplicity().collection_mode().is_unordered())
201    })
202}
203
204fn model_documentation(model: &ModelProjection) -> Option<String> {
205    let type_doc = documentation_annotation(model.declaration().annotations());
206    let direct_sub_doc = model
207        .declaration()
208        .direct_sub()
209        .and_then(|sub| documentation_annotation(sub.annotations()).map(|doc| (sub, doc)));
210
211    match (type_doc, direct_sub_doc) {
212        (None, None) => None,
213        (Some(type_doc), None) => Some(type_doc.to_owned()),
214        (type_doc, Some((sub, sub_doc))) => {
215            let edge_doc = format!(
216                "Direct subtype of `{}`:\n{sub_doc}",
217                sub.id().supertype().label()
218            );
219            Some(match type_doc {
220                Some(type_doc) => format!("{type_doc}\n\n{edge_doc}"),
221                None => edge_doc,
222            })
223        }
224    }
225}
226
227fn documentation_annotation(
228    annotations: &BTreeMap<AnnotationFactId, ProjectedAnnotation>,
229) -> Option<&str> {
230    annotations.values().find_map(|annotation| {
231        if annotation.id().kind() != &AnnotationKindId::Doc {
232            return None;
233        }
234        match annotation.value() {
235            SchemaAnnotationValue::Doc(doc) => Some(doc.as_str()),
236            _ => None,
237        }
238    })
239}