1use std::collections::{BTreeMap, BTreeSet};
2
3use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory};
4use type_bridge_contract::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
5use type_bridge_contract::id::{AttributeId, RoleId, TypeId, TypeKind};
6use type_bridge_contract::projection::{
7 BindingTarget, CNamingPolicy, CodeResourceDigest, CompleteReadProjection,
8 CreateFieldProjection, CreateProjection, CreateRoleProjection, DeclarationProjection,
9 DeclaredRoleProjection, DirectSubProjection, EmissionPlan, FieldTokenProjection,
10 FunctionParameterProjection, FunctionProjection, FunctionReturnElementProjection,
11 FunctionReturnProjection, ModelProjection, PlayingProjection, ProjectedAnnotation,
12 ProjectedModelForm, ProjectedModelUse, ProjectedMultiplicity, ProjectedTypeRef,
13 ProjectionConfig, ProjectionHandler, QueryTokenProjection, ReadFieldProjection,
14 ReadRoleProjection, ReferenceReadProjection, RoleTokenProjection, RuntimeProjection,
15 RustCreatePolicy, StructFieldProjection, StructProjection, TYPESCRIPT_MODEL_RESERVED_NAMES,
16 TargetIdentifier,
17};
18use type_bridge_contract::schema::{
19 AnnotationFactId, AnnotationKindId, AnnotationSubjectId, FunctionReturnMode, OwnsFactId,
20 RelatesFactId, SchemaAnnotationValue, SchemaDiagnostic, SchemaDiagnostics, TypeReference,
21 ValueFactId,
22};
23
24use crate::resolve::{EffectiveRelates, ResolvedSchema, ResolvedType};
25
26const C_MAX_TARGET_IDENTIFIER_BYTES: usize = 255;
27const C_IDENTIFIER_DIGEST_DOMAIN: &str = "typebridge.binding.c-identifier";
28const C_IDENTIFIER_CANONICALIZATION: &str = "typebridge.c-identifier/v1";
29
30fn no_source(error: Diagnostic) -> SchemaDiagnostics {
31 SchemaDiagnostics::one(SchemaDiagnostic::new(error, None))
32}
33
34fn projection_error(code: &'static str, message: impl Into<String>) -> SchemaDiagnostics {
35 crate::diagnostic::diagnostic(DiagnosticCategory::InvalidContract, code, message, None)
36}
37
38fn python_class_name(label: &str) -> String {
39 label
40 .replace('_', "-")
41 .split('-')
42 .map(|part| {
43 let mut chars = part.chars();
44 match chars.next() {
45 Some(first) => {
46 let mut output = first.to_uppercase().collect::<String>();
47 let rest = chars.as_str();
48 let uniform = part.chars().all(char::is_uppercase)
49 || part.chars().all(char::is_lowercase);
50 if uniform {
51 output.push_str(&rest.to_lowercase());
52 } else {
53 output.push_str(rest);
54 }
55 output
56 }
57 None => String::new(),
58 }
59 })
60 .collect()
61}
62
63fn python_member_name(label: &str) -> String {
64 label.replace('-', "_")
65}
66
67fn typescript_member_name(label: &str) -> String {
68 let class_name = python_class_name(label);
69 let mut chars = class_name.chars();
70 chars.next().map_or_else(String::new, |first| {
71 first.to_lowercase().chain(chars).collect()
72 })
73}
74
75fn rust_member_name(label: &str) -> String {
76 label.replace('-', "_").to_ascii_lowercase()
77}
78
79fn c_v1_escape(label: &str) -> String {
80 let mut escaped = String::with_capacity(label.len());
81 for byte in label.bytes() {
82 match byte {
83 b'a'..=b'y' | b'A'..=b'Z' | b'0'..=b'9' => escaped.push(char::from(byte)),
84 b'z' => escaped.push_str("zz"),
85 b'-' => escaped.push_str("zh"),
86 b'_' => escaped.push_str("zu"),
87 _ => escaped.push_str(&format!("zx{byte:02x}")),
88 }
89 }
90 escaped
91}
92
93fn c_identifier(value: String) -> Result<TargetIdentifier, SchemaDiagnostics> {
94 let bounded = if value.len() <= C_MAX_TARGET_IDENTIFIER_BYTES {
95 value
96 } else {
97 let digest = Fingerprint::compute(
98 FingerprintDomain::new(C_IDENTIFIER_DIGEST_DOMAIN)
99 .expect("the built-in C identifier fingerprint domain is valid"),
100 CanonicalizationVersion::new(C_IDENTIFIER_CANONICALIZATION)
101 .expect("the built-in C identifier canonicalization is valid"),
102 None,
103 value.as_bytes(),
104 )
105 .digest()
106 .to_hex();
107 let mut stem_bytes = C_MAX_TARGET_IDENTIFIER_BYTES - digest.len() - 1;
108 while value.as_bytes().get(stem_bytes - 1) == Some(&b'_') {
109 stem_bytes -= 1;
110 }
111 format!("{}_{}", &value[..stem_bytes], digest)
112 };
113 TargetIdentifier::c(bounded).map_err(no_source)
114}
115
116fn python_identifier(value: String) -> Result<TargetIdentifier, SchemaDiagnostics> {
117 if matches!(value.as_str(), "iid" | "model_config" | "model_fields")
118 || value.starts_with("__") && value.ends_with("__")
119 {
120 return Err(projection_error(
121 "reserved_python_projection_identifier",
122 "projected Python name collides with generated runtime state",
123 ));
124 }
125 TargetIdentifier::python(value).map_err(no_source)
126}
127
128fn typescript_identifier(value: String) -> Result<TargetIdentifier, SchemaDiagnostics> {
129 if matches!(
130 value.as_str(),
131 "iid"
132 | "typeToken"
133 | "fields"
134 | "roles"
135 | "plays"
136 | "create"
137 | "reference"
138 | "prototype"
139 | "__proto__"
140 ) {
141 return Err(projection_error(
142 "reserved_typescript_projection_identifier",
143 "projected TypeScript name collides with generated runtime state",
144 ));
145 }
146 TargetIdentifier::typescript(value).map_err(no_source)
147}
148
149fn rust_identifier(value: String) -> Result<TargetIdentifier, SchemaDiagnostics> {
150 TargetIdentifier::rust(value).map_err(no_source)
151}
152
153struct ProjectionNamer<'a> {
154 target: BindingTarget,
155 c_symbol_prefix: Option<&'a str>,
156}
157
158impl<'a> ProjectionNamer<'a> {
159 fn new(target: BindingTarget, config: &'a ProjectionConfig) -> Result<Self, SchemaDiagnostics> {
160 let c_symbol_prefix = if target == BindingTarget::C {
161 if config.c_naming_policy() != Some(CNamingPolicy::TypeBridgeV1) {
162 return Err(projection_error(
163 "unsupported_c_naming_policy",
164 "C projection does not recognize the configured naming policy",
165 ));
166 }
167 Some(
168 config
169 .c_symbol_prefix()
170 .ok_or_else(|| {
171 projection_error(
172 "missing_c_symbol_prefix",
173 "C projection configuration omits its symbol prefix",
174 )
175 })?
176 .as_str(),
177 )
178 } else {
179 None
180 };
181 Ok(Self {
182 target,
183 c_symbol_prefix,
184 })
185 }
186
187 fn c_symbol_prefix(&self) -> &str {
188 self.c_symbol_prefix
189 .expect("C naming is only requested for a validated C configuration")
190 }
191
192 fn c_label_identifier(&self, label: &str) -> Result<TargetIdentifier, SchemaDiagnostics> {
193 c_identifier(format!("{}_{}", self.c_symbol_prefix(), c_v1_escape(label)))
194 }
195
196 fn class_identifier(&self, label: &str) -> Result<TargetIdentifier, SchemaDiagnostics> {
197 let value = python_class_name(label);
198 if self.target == BindingTarget::Python {
199 python_identifier(value)
200 } else if self.target == BindingTarget::TypeScript {
201 typescript_identifier(value)
202 } else if self.target == BindingTarget::Rust {
203 rust_identifier(value)
204 } else if self.target == BindingTarget::C {
205 self.c_label_identifier(label)
206 } else {
207 Err(projection_error(
208 "unsupported_binding_projection_target",
209 "schema projection does not support this binding target",
210 ))
211 }
212 }
213
214 fn member_identifier(&self, label: &str) -> Result<TargetIdentifier, SchemaDiagnostics> {
215 if self.target == BindingTarget::Python {
216 python_identifier(python_member_name(label))
217 } else if self.target == BindingTarget::TypeScript {
218 typescript_identifier(typescript_member_name(label))
219 } else if self.target == BindingTarget::Rust {
220 rust_identifier(rust_member_name(label))
221 } else if self.target == BindingTarget::C {
222 self.c_label_identifier(label)
223 } else {
224 Err(projection_error(
225 "unsupported_binding_projection_target",
226 "schema projection does not support this binding target",
227 ))
228 }
229 }
230
231 fn model_member_identifier(&self, label: &str) -> Result<TargetIdentifier, SchemaDiagnostics> {
232 if self.target == BindingTarget::TypeScript {
233 let mut name = typescript_member_name(label);
234 if TYPESCRIPT_MODEL_RESERVED_NAMES.contains(&name.as_str()) {
235 name.push('_');
238 }
239 TargetIdentifier::typescript(name).map_err(no_source)
240 } else {
241 self.member_identifier(label)
242 }
243 }
244
245 fn reference_identifier(
246 &self,
247 name: &TargetIdentifier,
248 ) -> Result<TargetIdentifier, SchemaDiagnostics> {
249 if self.target == BindingTarget::Python {
250 python_identifier(format!("{}Ref", name.as_str()))
251 } else if self.target == BindingTarget::TypeScript {
252 typescript_identifier(format!("{}Ref", name.as_str()))
253 } else if self.target == BindingTarget::Rust {
254 rust_identifier(format!("{}Ref", name.as_str()))
255 } else if self.target == BindingTarget::C {
256 c_identifier(format!("{}_ref", name.as_str()))
257 } else {
258 Err(projection_error(
259 "unsupported_binding_projection_target",
260 "schema projection does not support this binding target",
261 ))
262 }
263 }
264
265 fn query_token_identifier(
266 &self,
267 model_name: &TargetIdentifier,
268 ) -> Result<Option<TargetIdentifier>, SchemaDiagnostics> {
269 if self.target == BindingTarget::Rust {
270 rust_identifier(format!("{}Type", model_name.as_str())).map(Some)
271 } else if self.target == BindingTarget::C {
272 c_identifier(format!("{}_type", model_name.as_str())).map(Some)
273 } else {
274 Ok(None)
275 }
276 }
277
278 fn player_union_identifier(
279 &self,
280 model_name: &TargetIdentifier,
281 role_label: &str,
282 ) -> Result<Option<TargetIdentifier>, SchemaDiagnostics> {
283 if self.target == BindingTarget::Rust {
284 rust_identifier(format!(
285 "{}{}Player",
286 model_name.as_str(),
287 python_class_name(role_label),
288 ))
289 .map(Some)
290 } else if self.target == BindingTarget::C {
291 c_identifier(format!(
292 "{}_{}_player",
293 model_name.as_str(),
294 c_v1_escape(role_label),
295 ))
296 .map(Some)
297 } else {
298 Ok(None)
299 }
300 }
301
302 fn create_identifier(
303 &self,
304 model_name: &TargetIdentifier,
305 ) -> Result<Option<TargetIdentifier>, SchemaDiagnostics> {
306 if self.target == BindingTarget::Rust {
307 rust_identifier(format!("{}Create", model_name.as_str())).map(Some)
308 } else if self.target == BindingTarget::C {
309 c_identifier(format!("{}_create", model_name.as_str())).map(Some)
310 } else {
311 Ok(None)
312 }
313 }
314
315 fn playing_identifier(
316 &self,
317 player_label: &str,
318 relation_label: &str,
319 role_label: &str,
320 ) -> Result<TargetIdentifier, SchemaDiagnostics> {
321 if self.target == BindingTarget::C {
322 c_identifier(format!(
323 "{}_plays_{}_relation_{}_role_{}",
324 self.c_symbol_prefix(),
325 c_v1_escape(player_label),
326 c_v1_escape(relation_label),
327 c_v1_escape(role_label),
328 ))
329 } else {
330 self.member_identifier(&format!(
331 "plays-{player_label}-{relation_label}-{role_label}"
332 ))
333 }
334 }
335
336 fn c_schema_package_identifier(&self) -> Result<Option<TargetIdentifier>, SchemaDiagnostics> {
337 if self.target == BindingTarget::C {
338 c_identifier(format!("{}_schema_package_open", self.c_symbol_prefix())).map(Some)
339 } else {
340 Ok(None)
341 }
342 }
343}
344
345#[derive(Default)]
346struct NameRegistry {
347 names: BTreeMap<(String, String), String>,
348}
349
350impl NameRegistry {
351 fn insert(
352 &mut self,
353 namespace: impl Into<String>,
354 name: &TargetIdentifier,
355 identity: impl Into<String>,
356 ) -> Result<(), SchemaDiagnostics> {
357 let key = (namespace.into(), name.as_str().to_owned());
358 let identity = identity.into();
359 if let Some(previous) = self.names.get(&key) {
360 if previous != &identity {
361 return Err(projection_error(
362 "projection_name_collision",
363 format!(
364 "projected name `{}` in namespace `{}` collides between `{}` and `{}`",
365 key.1, key.0, previous, identity
366 ),
367 ));
368 }
369 } else {
370 self.names.insert(key, identity);
371 }
372 Ok(())
373 }
374}
375
376fn annotations(
377 subject: AnnotationSubjectId,
378 values: &BTreeMap<AnnotationKindId, SchemaAnnotationValue>,
379) -> Result<BTreeMap<AnnotationFactId, ProjectedAnnotation>, SchemaDiagnostics> {
380 values
381 .iter()
382 .map(|(kind, value)| {
383 let id = AnnotationFactId::new(subject.clone(), kind.clone());
384 ProjectedAnnotation::new(id.clone(), value.clone())
385 .map(|annotation| (id, annotation))
386 .map_err(no_source)
387 })
388 .collect()
389}
390
391fn effective_relates_annotation_subject(
392 relates: &EffectiveRelates,
393) -> Result<AnnotationSubjectId, SchemaDiagnostics> {
394 let role = RoleId::new(
395 relates.id().relation().label().as_str(),
396 relates.id().role().label().as_str(),
397 )
398 .map_err(no_source)?;
399 RelatesFactId::new(relates.id().relation().clone(), role)
400 .map(AnnotationSubjectId::Relates)
401 .map_err(no_source)
402}
403
404fn ordered_owns(resolved: &ResolvedType) -> Vec<OwnsFactId> {
405 let mut ordered = Vec::new();
406 let mut seen = BTreeSet::new();
407 for attribute in resolved.owned_attribute_order() {
408 if let Some(owns) = resolved.owns().get(attribute) {
409 seen.insert(attribute.clone());
410 ordered.push(owns.id().clone());
411 }
412 }
413 ordered.extend(
414 resolved
415 .owns()
416 .iter()
417 .filter(|(attribute, _)| !seen.contains(*attribute))
418 .map(|(_, owns)| owns.id().clone()),
419 );
420 ordered
421}
422
423fn read_model_use(id: &TypeId) -> Result<ProjectedModelUse, SchemaDiagnostics> {
424 let form = match id.kind() {
425 TypeKind::Entity => ProjectedModelForm::Complete,
426 TypeKind::Relation => ProjectedModelForm::Reference,
427 TypeKind::Attribute | TypeKind::Struct => {
428 return Err(projection_error(
429 "invalid_projection_reference",
430 "only entity and relation types can be projected as role players",
431 ));
432 }
433 };
434 Ok(ProjectedModelUse::new(id.clone(), form))
435}
436
437fn create_model_uses(id: &TypeId) -> Result<BTreeSet<ProjectedModelUse>, SchemaDiagnostics> {
438 match id.kind() {
439 TypeKind::Entity => Ok(BTreeSet::from([ProjectedModelUse::new(
440 id.clone(),
441 ProjectedModelForm::Complete,
442 )])),
443 TypeKind::Relation => Ok(BTreeSet::from([
444 ProjectedModelUse::new(id.clone(), ProjectedModelForm::Complete),
445 ProjectedModelUse::new(id.clone(), ProjectedModelForm::Reference),
446 ])),
447 TypeKind::Attribute | TypeKind::Struct => Err(projection_error(
448 "invalid_projection_reference",
449 "only entity and relation types can be projected as role players",
450 )),
451 }
452}
453
454fn immediate_specialization(
455 resolved: &ResolvedType,
456 relates: &EffectiveRelates,
457) -> Result<Option<RoleId>, SchemaDiagnostics> {
458 if relates.replaced_roles().is_empty() {
459 return Ok(None);
460 }
461 let mut ranked = relates
462 .replaced_roles()
463 .iter()
464 .filter_map(|role| {
465 resolved
466 .supertypes()
467 .iter()
468 .position(|parent| parent.label() == role.declaring_relation())
469 .map(|distance| (distance, role.clone()))
470 })
471 .collect::<Vec<_>>();
472 ranked.sort();
473 let Some((distance, role)) = ranked.first().cloned() else {
474 return Err(projection_error(
475 "invalid_projection_reference",
476 "specialized role has no declaring relation in the resolved ancestor chain",
477 ));
478 };
479 if ranked.get(1).is_some_and(|next| next.0 == distance) {
480 return Err(projection_error(
481 "ambiguous_role_specialization_projection",
482 "more than one replaced role is nearest to the specializing relation",
483 ));
484 }
485 Ok(Some(role))
486}
487
488fn resolve_type_reference(
489 reference: &TypeReference,
490 resolved: &ResolvedSchema,
491) -> Result<ProjectedTypeRef, SchemaDiagnostics> {
492 match reference {
493 TypeReference::Value(value) => Ok(ProjectedTypeRef::Scalar(*value)),
494 TypeReference::Schema(label) => {
495 let models = resolved
496 .types()
497 .keys()
498 .filter(|id| id.label() == label)
499 .cloned()
500 .collect::<Vec<_>>();
501 let structures = resolved
502 .structs()
503 .keys()
504 .filter(|id| id.label() == label)
505 .cloned()
506 .collect::<Vec<_>>();
507 match (models.as_slice(), structures.as_slice()) {
508 ([model], []) => Ok(ProjectedTypeRef::Model(ProjectedModelUse::new(
509 model.clone(),
510 ProjectedModelForm::Complete,
511 ))),
512 ([], [structure]) => Ok(ProjectedTypeRef::Struct(structure.clone())),
513 ([], []) => Err(projection_error(
514 "unknown_projection_type_reference",
515 "function signature references an unknown projected type",
516 )),
517 _ => Err(projection_error(
518 "ambiguous_projection_type_reference",
519 "function signature label resolves to more than one projected type",
520 )),
521 }
522 }
523 }
524}
525
526fn function_return(
527 returns: &FunctionReturnMode,
528 resolved: &ResolvedSchema,
529) -> Result<FunctionReturnProjection, SchemaDiagnostics> {
530 let project = |element: &type_bridge_contract::schema::FunctionReturnElement| {
531 Ok(FunctionReturnElementProjection::new(
532 resolve_type_reference(element.type_ref(), resolved)?,
533 element.optional(),
534 ))
535 };
536 match returns {
537 FunctionReturnMode::Scalar(element) => {
538 Ok(FunctionReturnProjection::Scalar(project(element)?))
539 }
540 FunctionReturnMode::Tuple(elements) => elements
541 .iter()
542 .map(project)
543 .collect::<Result<Vec<_>, _>>()
544 .map(FunctionReturnProjection::Tuple),
545 FunctionReturnMode::Stream(elements) => elements
546 .iter()
547 .map(project)
548 .collect::<Result<Vec<_>, _>>()
549 .map(FunctionReturnProjection::Stream),
550 }
551}
552
553fn link_components(resolved: &ResolvedSchema) -> Result<Vec<BTreeSet<TypeId>>, SchemaDiagnostics> {
554 let components = resolved.dependency_graph().strongly_connected_components();
555 let mut membership = BTreeMap::new();
556 for (index, component) in components.iter().enumerate() {
557 for member in component {
558 if membership.insert(member.clone(), index).is_some() {
559 return Err(projection_error(
560 "invalid_projection_emission_plan",
561 "resolver SCC partition repeats a model identity",
562 ));
563 }
564 }
565 }
566 if membership.len() != resolved.types().len()
567 || resolved
568 .types()
569 .keys()
570 .any(|id| !membership.contains_key(id))
571 {
572 return Err(projection_error(
573 "invalid_projection_emission_plan",
574 "resolver SCC partition does not cover every model",
575 ));
576 }
577 let mut dependents = vec![BTreeSet::<usize>::new(); components.len()];
578 let mut indegree = vec![0usize; components.len()];
579 for id in resolved.types().keys() {
580 let source = membership[id];
581 for dependency in resolved
582 .dependency_graph()
583 .dependencies(id)
584 .into_iter()
585 .flatten()
586 {
587 let target = membership[dependency];
588 if source != target && dependents[target].insert(source) {
589 indegree[source] += 1;
590 }
591 }
592 }
593 let minimum = components
594 .iter()
595 .map(|component| {
596 component
597 .first()
598 .cloned()
599 .expect("resolver SCCs are non-empty")
600 })
601 .collect::<Vec<_>>();
602 let mut ready = indegree
603 .iter()
604 .enumerate()
605 .filter(|(_, degree)| **degree == 0)
606 .map(|(index, _)| (minimum[index].clone(), index))
607 .collect::<BTreeSet<_>>();
608 let mut ordered = Vec::with_capacity(components.len());
609 while let Some((_, index)) = ready.pop_first() {
610 ordered.push(components[index].clone());
611 for dependent in &dependents[index] {
612 indegree[*dependent] -= 1;
613 if indegree[*dependent] == 0 {
614 ready.insert((minimum[*dependent].clone(), *dependent));
615 }
616 }
617 }
618 if ordered.len() != components.len() {
619 return Err(projection_error(
620 "invalid_projection_emission_plan",
621 "resolver SCC condensation graph is cyclic",
622 ));
623 }
624 Ok(ordered)
625}
626
627pub fn project(
629 resolved: &ResolvedSchema,
630 target: BindingTarget,
631 config: &ProjectionConfig,
632 handlers: &[ProjectionHandler],
633 resources: &[CodeResourceDigest],
634) -> Result<RuntimeProjection, SchemaDiagnostics> {
635 if config.target() != target {
636 return Err(projection_error(
637 "projection_config_target_mismatch",
638 "projection configuration belongs to a different target",
639 ));
640 }
641 config.validate_type_name_overrides().map_err(no_source)?;
642 for item in config.type_name_overrides() {
643 if !resolved.types().contains_key(item.type_id()) {
644 return Err(projection_error(
645 "unknown_type_name_override",
646 format!(
647 "type-name override refers to unknown type {:?}",
648 item.type_id()
649 ),
650 ));
651 }
652 }
653 let namer = ProjectionNamer::new(target, config)?;
654 let mut names = NameRegistry::default();
655 if let Some(descriptor_name) = namer.c_schema_package_identifier()? {
656 names.insert("root", &descriptor_name, "runtime:c-schema-package-open")?;
657 }
658 let mut models = BTreeMap::new();
659 let mut playing_facts = BTreeMap::new();
660
661 for (id, resolved_type) in resolved.types() {
662 let target_name = match config
663 .type_name_overrides()
664 .iter()
665 .find(|item| item.type_id() == id)
666 {
667 Some(item) => item.name().clone(),
668 None => namer.class_identifier(id.label().as_str())?,
669 };
670 names.insert("root", &target_name, format!("model:{id:?}"))?;
671 let reference_name = if matches!(id.kind(), TypeKind::Entity | TypeKind::Relation) {
672 let reference = namer.reference_identifier(&target_name)?;
673 names.insert("root", &reference, format!("reference:{id:?}"))?;
674 Some(reference)
675 } else {
676 None
677 };
678 let query_token_name = if let Some(name) = namer.query_token_identifier(&target_name)? {
679 names.insert("root", &name, format!("query-token:{id:?}"))?;
680 Some(name)
681 } else {
682 None
683 };
684 let type_annotations = annotations(
685 AnnotationSubjectId::Type(id.clone()),
686 resolved_type.annotations(),
687 )?;
688 let ordered_field_ids = ordered_owns(resolved_type);
689 let mut field_tokens = BTreeMap::new();
690 let mut direct_fields = Vec::new();
691 let mut create_fields = Vec::new();
692 let mut read_fields = Vec::new();
693 for owns_id in &ordered_field_ids {
694 let owns = resolved_type
695 .owns()
696 .get(owns_id.attribute())
697 .ok_or_else(|| {
698 projection_error(
699 "invalid_projection_reference",
700 "ordered ownership is absent from the effective ownership map",
701 )
702 })?;
703 let name = namer.model_member_identifier(owns.id().attribute().label().as_str())?;
704 names.insert(
705 format!("model:{id:?}"),
706 &name,
707 format!("field:{:?}", owns.id()),
708 )?;
709 let declaring_id = match owns.origin().declared() {
710 type_bridge_contract::schema::SchemaFactId::Owns(fact_id) => fact_id.clone(),
711 _ => {
712 return Err(projection_error(
713 "invalid_projection_reference",
714 "declared resolution origin for owns fact is not an Owns fact ID",
715 ));
716 }
717 };
718 let multiplicity =
719 ProjectedMultiplicity::new(owns.cardinality(), owns.collection_mode());
720 let token = FieldTokenProjection::new(
721 owns.id().clone(),
722 declaring_id,
723 name,
724 multiplicity,
725 owns.is_key(),
726 owns.is_unique(),
727 annotations(
728 AnnotationSubjectId::Owns(owns.id().clone()),
729 owns.annotations(),
730 )?,
731 )
732 .map_err(no_source)?;
733 let attribute_model =
734 TypeId::new(TypeKind::Attribute, owns.id().attribute().label().as_str())
735 .map_err(no_source)?;
736 let value = ProjectedTypeRef::Model(ProjectedModelUse::new(
737 attribute_model,
738 ProjectedModelForm::Complete,
739 ));
740 if owns.origin().is_direct() {
741 direct_fields.push(owns.id().clone());
742 }
743 create_fields.push(CreateFieldProjection::new(
744 owns.id().clone(),
745 value.clone(),
746 multiplicity,
747 ));
748 read_fields.push(ReadFieldProjection::new(
749 owns.id().clone(),
750 value,
751 multiplicity,
752 ));
753 field_tokens.insert(owns.id().clone(), token);
754 }
755
756 let replaced = resolved_type
757 .relates()
758 .values()
759 .flat_map(|relates| relates.replaced_roles().iter().cloned())
760 .collect::<BTreeSet<_>>();
761 let mut role_tokens = BTreeMap::new();
762 let mut direct_roles = BTreeMap::new();
763 let mut create_roles = BTreeMap::new();
764 let mut read_roles = BTreeMap::new();
765 let mut create_enabled = resolved_type.is_constructible() && !resolved_type.is_abstract();
766 for (role_id, relates) in resolved_type.relates() {
767 if replaced.contains(role_id) {
768 continue;
769 }
770 let name = namer.model_member_identifier(role_id.label().as_str())?;
771 names.insert(format!("model:{id:?}"), &name, format!("role:{role_id:?}"))?;
772 let resolved_role = resolved.roles().get(role_id).ok_or_else(|| {
773 projection_error(
774 "invalid_projection_reference",
775 "effective relates role is absent from the resolved role index",
776 )
777 })?;
778 let specializes = immediate_specialization(resolved_type, relates)?;
779 let multiplicity =
780 ProjectedMultiplicity::new(relates.cardinality(), relates.collection_mode());
781 let player_union_name = if let Some(name) =
782 namer.player_union_identifier(&target_name, role_id.label().as_str())?
783 {
784 names.insert("root", &name, format!("player-union:{id:?}:{role_id:?}"))?;
785 Some(name)
786 } else {
787 None
788 };
789 let mut token = RoleTokenProjection::new(
790 id.clone(),
791 role_id.clone(),
792 name,
793 resolved_role.accepted_players().clone(),
794 specializes.clone(),
795 multiplicity,
796 relates.is_abstract(),
797 annotations(
798 effective_relates_annotation_subject(relates)?,
799 relates.annotations(),
800 )?,
801 )
802 .map_err(no_source)?;
803 if let Some(name) = player_union_name {
804 token = token.with_player_union_target_name(name);
805 }
806 let read_players = resolved_role
807 .accepted_players()
808 .iter()
809 .map(read_model_use)
810 .collect::<Result<BTreeSet<_>, _>>()?;
811 let mut create_players = BTreeSet::new();
812 for player in resolved_role.accepted_players() {
813 create_players.extend(create_model_uses(player)?);
814 }
815 if relates.origin().is_direct() {
816 direct_roles.insert(
817 role_id.clone(),
818 DeclaredRoleProjection::new(role_id.clone(), specializes),
819 );
820 }
821 if relates.accepts_players_at_effective_scope() {
822 if create_players.is_empty() && multiplicity.required() {
823 create_enabled = false;
824 } else if !create_players.is_empty() {
825 create_roles.insert(
826 role_id.clone(),
827 CreateRoleProjection::new(role_id.clone(), create_players, multiplicity)
828 .map_err(no_source)?,
829 );
830 }
831 }
832 read_roles.insert(
833 role_id.clone(),
834 ReadRoleProjection::new(role_id.clone(), read_players, multiplicity)
835 .map_err(no_source)?,
836 );
837 role_tokens.insert(role_id.clone(), token);
838 }
839
840 let mut direct_plays = BTreeSet::new();
841 for plays in resolved_type.plays().values() {
842 if plays.origin().is_direct() {
843 direct_plays.insert(plays.id().clone());
844 }
845 let plays_name = namer.playing_identifier(
846 id.label().as_str(),
847 plays.id().role().declaring_relation().as_str(),
848 plays.id().role().label().as_str(),
849 )?;
850 names.insert("root", &plays_name, format!("plays:{:?}", plays.id()))?;
851 let projected = PlayingProjection::new(
852 plays.id().clone(),
853 plays.id().role().clone(),
854 ProjectedMultiplicity::from_cardinality(plays.cardinality()),
855 annotations(
856 AnnotationSubjectId::Plays(plays.id().clone()),
857 plays.annotations(),
858 )?,
859 )
860 .map_err(no_source)?
861 .with_target_name(plays_name);
862 if playing_facts
863 .insert(plays.id().clone(), projected)
864 .is_some()
865 {
866 return Err(projection_error(
867 "invalid_runtime_projection_map",
868 "an effective playing identity appears more than once",
869 ));
870 }
871 }
872
873 let key_fields = ordered_field_ids
874 .iter()
875 .filter(|owns| resolved_type.key_attributes().contains(owns.attribute()))
876 .cloned()
877 .collect();
878 let value_annotations = if let Some(value) = resolved_type.value_type() {
879 let attribute = AttributeId::new(id.label().as_str()).map_err(no_source)?;
880 annotations(
881 AnnotationSubjectId::Value(ValueFactId::new(attribute)),
882 value.annotations(),
883 )?
884 } else {
885 BTreeMap::new()
886 };
887 let direct_sub = resolved_type
888 .direct_sub()
889 .map(|sub| {
890 let annotations = annotations(
891 AnnotationSubjectId::Sub(sub.id().clone()),
892 sub.annotations(),
893 )?;
894 DirectSubProjection::new(
895 sub.id().clone(),
896 sub.origin().declared().clone(),
897 annotations,
898 )
899 .map_err(no_source)
900 })
901 .transpose()?;
902 let declaration = DeclarationProjection::new(
903 resolved_type.supertypes().first().cloned(),
904 resolved_type.value_type().map(|value| value.value_type()),
905 resolved_type.is_abstract(),
906 resolved_type.is_constructible(),
907 type_annotations,
908 direct_fields,
909 direct_roles,
910 direct_plays,
911 )
912 .map_err(no_source)?
913 .with_direct_sub(direct_sub)
914 .map_err(no_source)?
915 .with_value_annotations(value_annotations)
916 .map_err(no_source)?;
917 let create_target_name = if create_enabled {
918 if target == BindingTarget::Rust
919 && config.rust_create_policy() != Some(RustCreatePolicy::ValidatedInputV1)
920 {
921 return Err(projection_error(
922 "unsupported_rust_create_policy",
923 "Rust projection does not recognize the configured create policy",
924 ));
925 }
926 if let Some(name) = namer.create_identifier(&target_name)? {
927 names.insert("root", &name, format!("create:{id:?}"))?;
928 Some(name)
929 } else {
930 None
931 }
932 } else {
933 None
934 };
935 let mut create = CreateProjection::new(create_enabled, create_fields, create_roles)
936 .map_err(no_source)?;
937 if let Some(name) = create_target_name {
938 create = create.with_target_name(name);
939 }
940 let role_upcasts = resolved_type
941 .relates()
942 .iter()
943 .filter_map(|(role, relates)| {
944 if relates.replaced_roles().is_empty() {
945 return None;
946 }
947 let mut ancestors = relates.replaced_roles().iter().cloned().collect::<Vec<_>>();
948 ancestors.sort_by_key(|ancestor| {
949 resolved_type
950 .supertypes()
951 .iter()
952 .position(|parent| parent.label() == ancestor.declaring_relation())
953 .unwrap_or(usize::MAX)
954 });
955 Some((role.clone(), ancestors))
956 })
957 .collect();
958 let complete_read = CompleteReadProjection::new(
959 read_fields,
960 read_roles,
961 resolved_type.supertypes().to_vec(),
962 )
963 .map_err(no_source)?
964 .with_role_upcasts(role_upcasts)
965 .map_err(no_source)?;
966 let reference_read =
967 ReferenceReadProjection::new(reference_name, key_fields).map_err(no_source)?;
968 let mut query_tokens =
969 QueryTokenProjection::new(id.clone(), field_tokens, role_tokens).map_err(no_source)?;
970 if let Some(name) = query_token_name {
971 query_tokens = query_tokens.with_target_name(name);
972 }
973 let model = ModelProjection::new(
974 id.clone(),
975 target_name,
976 declaration,
977 create,
978 complete_read,
979 reference_read,
980 query_tokens,
981 )
982 .map_err(no_source)?;
983 models.insert(id.clone(), model);
984 }
985
986 let mut structs = BTreeMap::new();
987 for (id, structure) in resolved.structs() {
988 let target_name = namer.class_identifier(id.label().as_str())?;
989 names.insert("root", &target_name, format!("struct:{id:?}"))?;
990 let mut fields = Vec::new();
991 for field in structure.fields() {
992 let target_field = namer.member_identifier(field.name().as_str())?;
993 names.insert(
994 format!("struct:{id:?}"),
995 &target_field,
996 format!("field:{:?}", field.name()),
997 )?;
998 fields.push(StructFieldProjection::new(
999 field.name().clone(),
1000 target_field,
1001 field.value_type(),
1002 field.optional(),
1003 ));
1004 }
1005 structs.insert(
1006 id.clone(),
1007 StructProjection::new(id.clone(), target_name, fields).map_err(no_source)?,
1008 );
1009 }
1010
1011 let mut functions = BTreeMap::new();
1012 for (id, function) in resolved.functions() {
1013 let target_name = namer.member_identifier(id.label().as_str())?;
1014 names.insert("root", &target_name, format!("function:{id:?}"))?;
1015 let mut parameters = Vec::new();
1016 for parameter in function.declaration().signature().parameters() {
1017 let target_parameter = namer.member_identifier(parameter.name().as_str())?;
1018 names.insert(
1019 format!("function:{id:?}"),
1020 &target_parameter,
1021 format!("parameter:{:?}", parameter.name()),
1022 )?;
1023 parameters.push(FunctionParameterProjection::new(
1024 parameter.name().clone(),
1025 target_parameter,
1026 resolve_type_reference(parameter.type_ref(), resolved)?,
1027 ));
1028 }
1029 functions.insert(
1030 id.clone(),
1031 FunctionProjection::new(
1032 id.clone(),
1033 target_name,
1034 parameters,
1035 function_return(function.declaration().signature().returns(), resolved)?,
1036 )
1037 .map_err(no_source)?
1038 .with_annotations(annotations(
1039 AnnotationSubjectId::Function(id.clone()),
1040 function.annotations(),
1041 )?)
1042 .map_err(no_source)?,
1043 );
1044 }
1045
1046 let mut shells = resolved.types().values().collect::<Vec<_>>();
1047 shells.sort_by_key(|model| (model.supertypes().len(), model.id().clone()));
1048 let emission = EmissionPlan::new(
1049 shells.into_iter().map(|model| model.id().clone()).collect(),
1050 link_components(resolved)?,
1051 structs.keys().cloned().collect(),
1052 functions.keys().cloned().collect(),
1053 )
1054 .map_err(no_source)?;
1055 RuntimeProjection::try_new(
1056 target,
1057 config.clone(),
1058 resolved.semantic_fingerprint().clone(),
1059 handlers,
1060 resources,
1061 models,
1062 structs,
1063 functions,
1064 playing_facts,
1065 emission,
1066 )
1067 .map_err(no_source)
1068}