use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Serialize, Serializer};
use crate::codec::{FormatVersion, to_canonical_json};
use crate::diagnostic::{Diagnostic, DiagnosticCategory};
use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
use crate::id::{AttributeId, FunctionId, Label, RoleId, StructId, TypeId, TypeKind};
use crate::limits::MAX_CANONICAL_COLLECTION_LEN;
use crate::schema::{
AnnotationFact, AnnotationFactId, AnnotationKindId, AnnotationSubjectId, CollectionMode,
OwnsFactId, PlaysFactId, RelatesFactId, SchemaAnnotationValue, SchemaFactId, SubFactId,
ValueFactId,
};
use crate::schema_fingerprint::SemanticSchemaFingerprint;
use crate::value::{Cardinality, ValueTypeTag};
const MAX_PROJECTION_COMPONENT_ID_BYTES: usize = 255;
const PYTHON_GENERATOR_HANDLER_ID: &str = "typebridge.generator.python";
const TYPESCRIPT_GENERATOR_HANDLER_ID: &str = "typebridge.generator.typescript";
const RUST_GENERATOR_HANDLER_ID: &str = "typebridge.generator.rust";
const C_GENERATOR_HANDLER_ID: &str = "typebridge.generator.c";
const CODE_RESOURCE_DOMAIN: &str = "typebridge.binding.code-resource";
const RAW_BYTES_CANONICALIZATION: &str = "typebridge.raw-bytes/v1";
const BINDING_PROJECTION_DOMAIN: &str = "typebridge.binding.projection";
const BINDING_PROJECTION_CANONICALIZATION: &str = "typebridge.binding-projection/v1";
const BINDING_PROJECTION_CONTENT_DOMAIN: &str = "typebridge.binding.projection-content";
const MAX_TARGET_IDENTIFIER_BYTES: usize = 255;
const MAX_C_SYMBOL_PREFIX_BYTES: usize = 63;
pub const TYPESCRIPT_MODEL_RESERVED_NAMES: &[&str] = &[
"__proto__",
"completeRead",
"constructor",
"create",
"declaration",
"fields",
"id",
"iid",
"metadata",
"manager",
"name",
"plays",
"prototype",
"reference",
"roles",
"typeKey",
"typeToken",
"valueType",
];
pub const TYPE_BRIDGE_C_ABI_MAJOR: u32 = 1;
pub const TYPE_BRIDGE_C_ABI_MINOR: u32 = 6;
pub const TYPE_BRIDGE_PROJECTED_TOKEN_VERSION: u32 = 1;
pub const TYPE_BRIDGE_C_CREATE_FIELD_MAX: usize = 1_020;
pub const TYPE_BRIDGE_C_CREATE_ROLE_MAX: usize = 1_020;
pub const TYPE_BRIDGE_C_CREATE_MEMBER_MAX: usize = 1_020;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum ProjectedTokenKind {
Model,
Field,
Role,
Function,
Struct,
Attribute,
}
impl ProjectedTokenKind {
#[must_use]
pub const fn as_u32(self) -> u32 {
match self {
Self::Model => 1,
Self::Field => 2,
Self::Role => 3,
Self::Function => 4,
Self::Struct => 5,
Self::Attribute => 6,
}
}
#[must_use]
pub const fn from_u32(value: u32) -> Option<Self> {
match value {
1 => Some(Self::Model),
2 => Some(Self::Field),
3 => Some(Self::Role),
4 => Some(Self::Function),
5 => Some(Self::Struct),
6 => Some(Self::Attribute),
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ProjectedTokenIdentity {
Model(TypeId),
Field {
owner: TypeId,
field: OwnsFactId,
},
Role {
owner: TypeId,
role: RoleId,
},
Function(FunctionId),
Struct(StructId),
Attribute(AttributeId),
}
impl ProjectedTokenIdentity {
#[must_use]
pub const fn kind(&self) -> ProjectedTokenKind {
match self {
Self::Model(_) => ProjectedTokenKind::Model,
Self::Field { .. } => ProjectedTokenKind::Field,
Self::Role { .. } => ProjectedTokenKind::Role,
Self::Function(_) => ProjectedTokenKind::Function,
Self::Struct(_) => ProjectedTokenKind::Struct,
Self::Attribute(_) => ProjectedTokenKind::Attribute,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BindingTarget {
Python,
#[serde(rename = "typescript")]
TypeScript,
Rust,
C,
}
impl BindingTarget {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Python => "python",
Self::TypeScript => "typescript",
Self::Rust => "rust",
Self::C => "c",
}
}
const fn required_generator_handler_id(self) -> &'static str {
match self {
Self::Python => PYTHON_GENERATOR_HANDLER_ID,
Self::TypeScript => TYPESCRIPT_GENERATOR_HANDLER_ID,
Self::Rust => RUST_GENERATOR_HANDLER_ID,
Self::C => C_GENERATOR_HANDLER_ID,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum PythonNamingPolicy {
#[serde(rename = "typebridge.python/v1")]
TypeBridgeV1,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum TypeScriptNamingPolicy {
#[serde(rename = "typebridge.typescript/v1")]
TypeBridgeV1,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum RustNamingPolicy {
#[serde(rename = "typebridge.rust/v1")]
TypeBridgeV1,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum RustCreatePolicy {
#[serde(rename = "typebridge.rust.validated-create-input/v1")]
ValidatedInputV1,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum CNamingPolicy {
#[serde(rename = "typebridge.c/v1")]
TypeBridgeV1,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct CSymbolPrefix(String);
impl CSymbolPrefix {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
let uses_runtime_namespace = value == "type_bridge" || value.starts_with("type_bridge_");
let portable_path_component = value
.bytes()
.all(|byte| byte == b'_' || byte.is_ascii_lowercase() || byte.is_ascii_digit())
&& !value.ends_with('_')
&& !is_windows_device_name(&value);
if !is_valid_c_identifier(&value, MAX_C_SYMBOL_PREFIX_BYTES)
|| !portable_path_component
|| uses_runtime_namespace
{
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"invalid_c_symbol_prefix",
"C symbol prefix must be a bounded lowercase portable path outside the reserved TypeBridge runtime namespace",
));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CSymbolPrefix {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct TypeNameOverride {
type_id: TypeId,
name: TargetIdentifier,
}
impl TypeNameOverride {
#[must_use]
pub const fn type_id(&self) -> &TypeId {
&self.type_id
}
#[must_use]
pub const fn name(&self) -> &TargetIdentifier {
&self.name
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "binding")]
#[non_exhaustive]
pub enum ProjectionConfig {
#[serde(rename = "python")]
#[non_exhaustive]
Python {
naming_policy: PythonNamingPolicy,
#[serde(skip_serializing_if = "Vec::is_empty")]
type_name_overrides: Vec<TypeNameOverride>,
},
#[serde(rename = "typescript")]
#[non_exhaustive]
TypeScript {
naming_policy: TypeScriptNamingPolicy,
#[serde(skip_serializing_if = "Vec::is_empty")]
type_name_overrides: Vec<TypeNameOverride>,
},
#[serde(rename = "rust")]
#[non_exhaustive]
Rust {
naming_policy: RustNamingPolicy,
#[serde(skip_serializing_if = "Vec::is_empty")]
type_name_overrides: Vec<TypeNameOverride>,
create_policy: RustCreatePolicy,
},
#[serde(rename = "c")]
#[non_exhaustive]
C {
naming_policy: CNamingPolicy,
#[serde(skip_serializing_if = "Vec::is_empty")]
type_name_overrides: Vec<TypeNameOverride>,
symbol_prefix: CSymbolPrefix,
},
}
impl ProjectionConfig {
#[must_use]
pub const fn python() -> Self {
Self::Python {
naming_policy: PythonNamingPolicy::TypeBridgeV1,
type_name_overrides: Vec::new(),
}
}
#[must_use]
pub const fn typescript() -> Self {
Self::TypeScript {
naming_policy: TypeScriptNamingPolicy::TypeBridgeV1,
type_name_overrides: Vec::new(),
}
}
#[must_use]
pub const fn rust() -> Self {
Self::Rust {
naming_policy: RustNamingPolicy::TypeBridgeV1,
type_name_overrides: Vec::new(),
create_policy: RustCreatePolicy::ValidatedInputV1,
}
}
#[must_use]
pub fn c(symbol_prefix: CSymbolPrefix) -> Self {
Self::C {
naming_policy: CNamingPolicy::TypeBridgeV1,
type_name_overrides: Vec::new(),
symbol_prefix,
}
}
pub fn with_type_name_override(
mut self,
type_id: TypeId,
name: impl Into<String>,
) -> Result<Self, Diagnostic> {
let name = match self.target() {
BindingTarget::Python => TargetIdentifier::python(name)?,
BindingTarget::TypeScript => TargetIdentifier::typescript(name)?,
BindingTarget::Rust => TargetIdentifier::rust(name)?,
BindingTarget::C => TargetIdentifier::c(name)?,
};
let overrides = match &mut self {
Self::Python {
type_name_overrides,
..
}
| Self::TypeScript {
type_name_overrides,
..
}
| Self::Rust {
type_name_overrides,
..
}
| Self::C {
type_name_overrides,
..
} => type_name_overrides,
};
ensure_collection_limit(overrides.len() + 1, "too_many_type_name_overrides")?;
match overrides.binary_search_by(|item| item.type_id.cmp(&type_id)) {
Ok(_) => {
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"duplicate_type_name_override",
"a canonical type identity has more than one type-name override",
));
}
Err(index) => overrides.insert(index, TypeNameOverride { type_id, name }),
}
Ok(self)
}
pub fn validate_type_name_overrides(&self) -> Result<(), Diagnostic> {
let mut validated = match self {
Self::Python { .. } => Self::python(),
Self::TypeScript { .. } => Self::typescript(),
Self::Rust { .. } => Self::rust(),
Self::C { symbol_prefix, .. } => Self::c(symbol_prefix.clone()),
};
for item in self.type_name_overrides() {
validated =
validated.with_type_name_override(item.type_id.clone(), item.name.as_str())?;
}
if validated.type_name_overrides() != self.type_name_overrides() {
return Err(invalid_projection(
"unordered_type_name_overrides",
"type-name overrides must be sorted by canonical type identity",
));
}
Ok(())
}
#[must_use]
pub fn type_name_overrides(&self) -> &[TypeNameOverride] {
match self {
Self::Python {
type_name_overrides,
..
}
| Self::TypeScript {
type_name_overrides,
..
}
| Self::Rust {
type_name_overrides,
..
}
| Self::C {
type_name_overrides,
..
} => type_name_overrides,
}
}
#[must_use]
pub const fn target(&self) -> BindingTarget {
match self {
Self::Python { .. } => BindingTarget::Python,
Self::TypeScript { .. } => BindingTarget::TypeScript,
Self::Rust { .. } => BindingTarget::Rust,
Self::C { .. } => BindingTarget::C,
}
}
#[must_use]
pub const fn python_naming_policy(&self) -> Option<PythonNamingPolicy> {
match self {
Self::Python { naming_policy, .. } => Some(*naming_policy),
Self::TypeScript { .. } | Self::Rust { .. } | Self::C { .. } => None,
}
}
#[must_use]
pub const fn typescript_naming_policy(&self) -> Option<TypeScriptNamingPolicy> {
match self {
Self::TypeScript { naming_policy, .. } => Some(*naming_policy),
Self::Python { .. } | Self::Rust { .. } | Self::C { .. } => None,
}
}
#[must_use]
pub const fn rust_naming_policy(&self) -> Option<RustNamingPolicy> {
match self {
Self::Rust { naming_policy, .. } => Some(*naming_policy),
Self::Python { .. } | Self::TypeScript { .. } | Self::C { .. } => None,
}
}
#[must_use]
pub const fn rust_create_policy(&self) -> Option<RustCreatePolicy> {
match self {
Self::Rust { create_policy, .. } => Some(*create_policy),
Self::Python { .. } | Self::TypeScript { .. } | Self::C { .. } => None,
}
}
#[must_use]
pub const fn c_naming_policy(&self) -> Option<CNamingPolicy> {
match self {
Self::C { naming_policy, .. } => Some(*naming_policy),
Self::Python { .. } | Self::TypeScript { .. } | Self::Rust { .. } => None,
}
}
#[must_use]
pub const fn c_symbol_prefix(&self) -> Option<&CSymbolPrefix> {
match self {
Self::C { symbol_prefix, .. } => Some(symbol_prefix),
Self::Python { .. } | Self::TypeScript { .. } | Self::Rust { .. } => None,
}
}
}
fn validate_component_id(value: String) -> Result<String, Diagnostic> {
let segments = value.split('.').collect::<Vec<_>>();
let valid_segment = |segment: &str| {
let mut bytes = segment.bytes();
bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
&& bytes.all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')
})
};
if value.len() <= MAX_PROJECTION_COMPONENT_ID_BYTES
&& segments.len() >= 2
&& segments.iter().all(|segment| valid_segment(segment))
{
Ok(value)
} else {
Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"malformed_projection_component_id",
"projection component ID must be a bounded lowercase namespaced identifier",
))
}
}
macro_rules! projection_component_id {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
Ok(Self(validate_component_id(value.into())?))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
};
}
projection_component_id!(
ProjectionHandlerId,
"A stable identity for one generator or projection handler."
);
projection_component_id!(
CodeResourceId,
"A stable identity for exact code-resource bytes referenced during emission."
);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct ProjectionHandlerVersion(u16);
impl ProjectionHandlerVersion {
pub const V1: Self = Self(1);
pub const V2: Self = Self(2);
pub const V3: Self = Self(3);
pub fn new(value: u16) -> Result<Self, Diagnostic> {
if value == 0 {
Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"invalid_projection_handler_version",
"projection handler version must be nonzero",
))
} else {
Ok(Self(value))
}
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ProjectionHandler {
id: ProjectionHandlerId,
version: ProjectionHandlerVersion,
}
impl ProjectionHandler {
pub fn new(id: impl Into<String>, version: u16) -> Result<Self, Diagnostic> {
Ok(Self {
id: ProjectionHandlerId::new(id)?,
version: ProjectionHandlerVersion::new(version)?,
})
}
#[must_use]
pub fn python_v1() -> Self {
Self {
id: ProjectionHandlerId::new(PYTHON_GENERATOR_HANDLER_ID)
.expect("the built-in Python generator ID is valid"),
version: ProjectionHandlerVersion::V1,
}
}
#[must_use]
pub fn python_v2() -> Self {
Self {
id: ProjectionHandlerId::new(PYTHON_GENERATOR_HANDLER_ID)
.expect("the built-in Python generator ID is valid"),
version: ProjectionHandlerVersion::V2,
}
}
#[must_use]
pub fn typescript_v1() -> Self {
Self {
id: ProjectionHandlerId::new(TYPESCRIPT_GENERATOR_HANDLER_ID)
.expect("built-in TypeScript projection handler ID is valid"),
version: ProjectionHandlerVersion::V1,
}
}
#[must_use]
pub fn typescript_v2() -> Self {
Self {
id: ProjectionHandlerId::new(TYPESCRIPT_GENERATOR_HANDLER_ID)
.expect("built-in TypeScript projection handler ID is valid"),
version: ProjectionHandlerVersion::V2,
}
}
#[must_use]
pub fn rust_v1() -> Self {
Self {
id: ProjectionHandlerId::new(RUST_GENERATOR_HANDLER_ID)
.expect("built-in Rust projection handler ID is valid"),
version: ProjectionHandlerVersion::V1,
}
}
#[must_use]
pub fn rust_v2() -> Self {
Self {
id: ProjectionHandlerId::new(RUST_GENERATOR_HANDLER_ID)
.expect("built-in Rust projection handler ID is valid"),
version: ProjectionHandlerVersion::V2,
}
}
#[must_use]
pub fn c_v1() -> Self {
Self {
id: ProjectionHandlerId::new(C_GENERATOR_HANDLER_ID)
.expect("the built-in C generator ID is valid"),
version: ProjectionHandlerVersion::V1,
}
}
#[must_use]
pub fn c_v2() -> Self {
Self {
id: ProjectionHandlerId::new(C_GENERATOR_HANDLER_ID)
.expect("the built-in C generator ID is valid"),
version: ProjectionHandlerVersion::V2,
}
}
#[must_use]
pub fn c_v3() -> Self {
Self {
id: ProjectionHandlerId::new(C_GENERATOR_HANDLER_ID)
.expect("the built-in C generator ID is valid"),
version: ProjectionHandlerVersion::V3,
}
}
#[must_use]
pub const fn id(&self) -> &ProjectionHandlerId {
&self.id
}
#[must_use]
pub const fn version(&self) -> ProjectionHandlerVersion {
self.version
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CodeResourceDigest {
id: CodeResourceId,
content_fingerprint: Fingerprint,
}
impl CodeResourceDigest {
pub fn from_bytes(id: impl Into<String>, bytes: &[u8]) -> Result<Self, Diagnostic> {
Ok(Self {
id: CodeResourceId::new(id)?,
content_fingerprint: Fingerprint::compute(
FingerprintDomain::new(CODE_RESOURCE_DOMAIN)?,
CanonicalizationVersion::new(RAW_BYTES_CANONICALIZATION)?,
None,
bytes,
),
})
}
#[must_use]
pub const fn id(&self) -> &CodeResourceId {
&self.id
}
#[must_use]
pub const fn content_fingerprint(&self) -> &Fingerprint {
&self.content_fingerprint
}
}
#[derive(Serialize)]
struct BindingProjectionView<'a> {
format_version: FormatVersion,
target: BindingTarget,
semantic_schema_fingerprint: &'a SemanticSchemaFingerprint,
config: &'a ProjectionConfig,
generator_handlers: Vec<&'a ProjectionHandler>,
referenced_code_resources: Vec<&'a CodeResourceDigest>,
}
fn ordered_handlers(
target: BindingTarget,
handlers: &[ProjectionHandler],
) -> Result<Vec<&ProjectionHandler>, Diagnostic> {
let mut ordered = handlers.iter().collect::<Vec<_>>();
ordered.sort_by(|left, right| match left.id().cmp(right.id()) {
Ordering::Equal => left.version().cmp(&right.version()),
ordering => ordering,
});
if ordered.windows(2).any(|pair| pair[0].id() == pair[1].id()) {
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"duplicate_projection_handler_id",
"a projection handler identity may appear only once",
));
}
if !ordered
.iter()
.any(|handler| handler.id().as_str() == target.required_generator_handler_id())
{
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"missing_target_projection_handler",
"projection fingerprint inputs omit the target's generator handler",
));
}
Ok(ordered)
}
fn ordered_resources(
resources: &[CodeResourceDigest],
) -> Result<Vec<&CodeResourceDigest>, Diagnostic> {
let mut ordered = resources.iter().collect::<Vec<_>>();
ordered.sort_by(|left, right| left.id().cmp(right.id()));
if ordered.windows(2).any(|pair| pair[0].id() == pair[1].id()) {
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"duplicate_projection_resource_id",
"a referenced code-resource identity may appear only once",
));
}
Ok(ordered)
}
pub fn canonical_binding_projection_bytes(
target: BindingTarget,
semantic_schema: &SemanticSchemaFingerprint,
config: &ProjectionConfig,
handlers: &[ProjectionHandler],
resources: &[CodeResourceDigest],
) -> Result<Vec<u8>, Diagnostic> {
if config.target() != target {
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"projection_config_target_mismatch",
"projection configuration belongs to a different binding target",
));
}
let view = BindingProjectionView {
format_version: FormatVersion::V1,
target,
semantic_schema_fingerprint: semantic_schema,
config,
generator_handlers: ordered_handlers(target, handlers)?,
referenced_code_resources: ordered_resources(resources)?,
};
to_canonical_json(&view)
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(transparent)]
pub struct BindingProjectionFingerprint(Fingerprint);
impl BindingProjectionFingerprint {
pub fn compute(
target: BindingTarget,
semantic_schema: &SemanticSchemaFingerprint,
config: &ProjectionConfig,
handlers: &[ProjectionHandler],
resources: &[CodeResourceDigest],
) -> Result<Self, Diagnostic> {
let canonical = canonical_binding_projection_bytes(
target,
semantic_schema,
config,
handlers,
resources,
)?;
let semantic_profile = semantic_schema
.as_fingerprint()
.semantic_profile()
.cloned()
.ok_or_else(|| {
Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"projection_semantic_profile_missing",
"semantic schema fingerprint does not carry its semantic profile",
)
})?;
Ok(Self(Fingerprint::compute(
FingerprintDomain::new(BINDING_PROJECTION_DOMAIN)?,
CanonicalizationVersion::new(BINDING_PROJECTION_CANONICALIZATION)?,
Some(semantic_profile),
&canonical,
)))
}
#[must_use]
pub const fn as_fingerprint(&self) -> &Fingerprint {
&self.0
}
pub fn compute_with_projection(
target: BindingTarget,
semantic_schema: &SemanticSchemaFingerprint,
config: &ProjectionConfig,
handlers: &[ProjectionHandler],
resources: &[CodeResourceDigest],
canonical_projection: &[u8],
) -> Result<Self, Diagnostic> {
#[derive(Serialize)]
struct CompleteProjectionView<'a> {
inputs: BindingProjectionView<'a>,
projection_content: Fingerprint,
}
if config.target() != target {
return Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"projection_config_target_mismatch",
"projection configuration belongs to a different binding target",
));
}
let inputs = BindingProjectionView {
format_version: FormatVersion::V1,
target,
semantic_schema_fingerprint: semantic_schema,
config,
generator_handlers: ordered_handlers(target, handlers)?,
referenced_code_resources: ordered_resources(resources)?,
};
let projection_content = Fingerprint::compute(
FingerprintDomain::new(BINDING_PROJECTION_CONTENT_DOMAIN)?,
CanonicalizationVersion::new(BINDING_PROJECTION_CANONICALIZATION)?,
semantic_schema.as_fingerprint().semantic_profile().cloned(),
canonical_projection,
);
let canonical = to_canonical_json(&CompleteProjectionView {
inputs,
projection_content,
})?;
let semantic_profile = semantic_schema
.as_fingerprint()
.semantic_profile()
.cloned()
.ok_or_else(|| {
Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"projection_semantic_profile_missing",
"semantic schema fingerprint does not carry its semantic profile",
)
})?;
Ok(Self(Fingerprint::compute(
FingerprintDomain::new(BINDING_PROJECTION_DOMAIN)?,
CanonicalizationVersion::new(BINDING_PROJECTION_CANONICALIZATION)?,
Some(semantic_profile),
&canonical,
)))
}
}
fn serialize_map_values<S, K, V>(map: &BTreeMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
V: Serialize,
{
map.values().collect::<Vec<_>>().serialize(serializer)
}
#[derive(Serialize)]
struct RoleUpcastEntry<'a> {
role: &'a RoleId,
ancestors: &'a [RoleId],
}
fn serialize_role_upcasts<S>(
map: &BTreeMap<RoleId, Vec<RoleId>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
map.iter()
.map(|(role, ancestors)| RoleUpcastEntry { role, ancestors })
.collect::<Vec<_>>()
.serialize(serializer)
}
fn invalid_projection(code: &'static str, message: &'static str) -> Diagnostic {
Diagnostic::stable(DiagnosticCategory::InvalidContract, code, message)
}
fn ensure_collection_limit(length: usize, code: &'static str) -> Result<(), Diagnostic> {
if length > MAX_CANONICAL_COLLECTION_LEN {
Err(Diagnostic::stable(
DiagnosticCategory::ResourceLimit,
code,
"projection collection exceeds the canonical collection limit",
))
} else {
Ok(())
}
}
fn validate_c_create_limits(models: &BTreeMap<TypeId, ModelProjection>) -> Result<(), Diagnostic> {
for model in models.values() {
if model.create().target_name().is_none() {
continue;
}
let field_count = model.create().fields().len();
let role_count = model.create().roles().len();
if field_count > TYPE_BRIDGE_C_CREATE_FIELD_MAX {
return Err(Diagnostic::stable(
DiagnosticCategory::ResourceLimit,
"c_projection_create_field_limit_exceeded",
"C create fields exceed the 1020-member translation ceiling",
));
}
if role_count > TYPE_BRIDGE_C_CREATE_ROLE_MAX {
return Err(Diagnostic::stable(
DiagnosticCategory::ResourceLimit,
"c_projection_create_role_limit_exceeded",
"C create roles exceed the 1020-member translation ceiling",
));
}
if field_count
.checked_add(role_count)
.is_none_or(|count| count > TYPE_BRIDGE_C_CREATE_MEMBER_MAX)
{
return Err(Diagnostic::stable(
DiagnosticCategory::ResourceLimit,
"c_projection_create_member_limit_exceeded",
"combined C create fields and roles exceed the 1020-member translation ceiling",
));
}
}
Ok(())
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct TargetIdentifier(String);
impl TargetIdentifier {
pub fn python(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
let mut bytes = value.bytes();
let valid = value.len() <= MAX_TARGET_IDENTIFIER_BYTES
&& bytes
.next()
.is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic())
&& bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric());
if !valid || is_python_keyword(&value) {
return Err(invalid_projection(
"invalid_python_projection_identifier",
"projected Python name is not a bounded non-keyword identifier",
));
}
Ok(Self(value))
}
pub fn typescript(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
let mut bytes = value.bytes();
let valid = value.len() <= MAX_TARGET_IDENTIFIER_BYTES
&& bytes
.next()
.is_some_and(|byte| byte == b'_' || byte == b'$' || byte.is_ascii_alphabetic())
&& bytes.all(|byte| byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric());
if !valid || is_typescript_keyword(&value) {
return Err(invalid_projection(
"invalid_typescript_projection_identifier",
"projected TypeScript name is not a bounded non-keyword identifier",
));
}
Ok(Self(value))
}
pub fn rust(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
let mut bytes = value.bytes();
let valid = value != "_"
&& value.len() <= MAX_TARGET_IDENTIFIER_BYTES
&& bytes
.next()
.is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic())
&& bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric());
if !valid || is_rust_keyword(&value) {
return Err(invalid_projection(
"invalid_rust_projection_identifier",
"projected Rust name is not a bounded non-keyword ASCII identifier",
));
}
Ok(Self(value))
}
pub fn c(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
if !is_valid_c_identifier(&value, MAX_TARGET_IDENTIFIER_BYTES) {
return Err(invalid_projection(
"invalid_c_projection_identifier",
"projected C name is not a bounded, non-keyword, non-reserved ASCII identifier",
));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
fn is_python_keyword(value: &str) -> bool {
matches!(
value,
"False"
| "None"
| "True"
| "and"
| "as"
| "assert"
| "async"
| "await"
| "break"
| "case"
| "class"
| "continue"
| "def"
| "del"
| "elif"
| "else"
| "except"
| "finally"
| "for"
| "from"
| "global"
| "if"
| "import"
| "in"
| "is"
| "lambda"
| "match"
| "nonlocal"
| "not"
| "or"
| "pass"
| "raise"
| "return"
| "try"
| "while"
| "with"
| "yield"
)
}
fn is_typescript_keyword(value: &str) -> bool {
matches!(
value,
"abstract"
| "any"
| "as"
| "asserts"
| "async"
| "await"
| "bigint"
| "boolean"
| "break"
| "case"
| "catch"
| "class"
| "const"
| "constructor"
| "continue"
| "debugger"
| "declare"
| "default"
| "delete"
| "do"
| "else"
| "enum"
| "export"
| "extends"
| "false"
| "finally"
| "for"
| "from"
| "function"
| "get"
| "if"
| "implements"
| "import"
| "in"
| "infer"
| "instanceof"
| "interface"
| "is"
| "keyof"
| "let"
| "module"
| "namespace"
| "never"
| "new"
| "null"
| "number"
| "object"
| "of"
| "out"
| "override"
| "package"
| "private"
| "protected"
| "public"
| "readonly"
| "require"
| "return"
| "satisfies"
| "set"
| "static"
| "string"
| "super"
| "switch"
| "symbol"
| "this"
| "throw"
| "true"
| "try"
| "type"
| "typeof"
| "undefined"
| "unique"
| "unknown"
| "using"
| "var"
| "void"
| "while"
| "with"
| "yield"
)
}
fn is_rust_keyword(value: &str) -> bool {
matches!(
value,
"Self"
| "abstract"
| "as"
| "async"
| "await"
| "become"
| "box"
| "break"
| "const"
| "continue"
| "crate"
| "do"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "final"
| "fn"
| "for"
| "gen"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "macro"
| "match"
| "mod"
| "move"
| "mut"
| "override"
| "priv"
| "pub"
| "ref"
| "return"
| "self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "try"
| "type"
| "typeof"
| "unsafe"
| "unsized"
| "use"
| "virtual"
| "where"
| "while"
| "yield"
)
}
fn is_valid_c_identifier(value: &str, max_bytes: usize) -> bool {
let mut bytes = value.bytes();
value.len() <= max_bytes
&& bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic())
&& bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric())
&& !value.contains("__")
&& !is_c_keyword(value)
}
fn is_windows_device_name(value: &str) -> bool {
matches!(
value,
"aux"
| "clock$"
| "con"
| "nul"
| "prn"
| "com1"
| "com2"
| "com3"
| "com4"
| "com5"
| "com6"
| "com7"
| "com8"
| "com9"
| "lpt1"
| "lpt2"
| "lpt3"
| "lpt4"
| "lpt5"
| "lpt6"
| "lpt7"
| "lpt8"
| "lpt9"
)
}
fn is_c_keyword(value: &str) -> bool {
matches!(
value,
"_Alignas"
| "_Alignof"
| "_Atomic"
| "_BitInt"
| "_Bool"
| "_Complex"
| "_Decimal128"
| "_Decimal32"
| "_Decimal64"
| "_Generic"
| "_Imaginary"
| "_Noreturn"
| "_Static_assert"
| "_Thread_local"
| "alignas"
| "alignof"
| "auto"
| "bool"
| "break"
| "case"
| "char"
| "const"
| "constexpr"
| "continue"
| "default"
| "do"
| "double"
| "else"
| "enum"
| "extern"
| "false"
| "float"
| "for"
| "goto"
| "if"
| "inline"
| "int"
| "long"
| "nullptr"
| "register"
| "restrict"
| "return"
| "short"
| "signed"
| "sizeof"
| "static"
| "static_assert"
| "struct"
| "switch"
| "thread_local"
| "true"
| "typedef"
| "typeof"
| "typeof_unqual"
| "union"
| "unsigned"
| "void"
| "volatile"
| "while"
)
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectedModelForm {
Complete,
Reference,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct ProjectedModelUse {
id: TypeId,
form: ProjectedModelForm,
}
impl ProjectedModelUse {
#[must_use]
pub const fn new(id: TypeId, form: ProjectedModelForm) -> Self {
Self { id, form }
}
#[must_use]
pub const fn id(&self) -> &TypeId {
&self.id
}
#[must_use]
pub const fn form(&self) -> ProjectedModelForm {
self.form
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum ProjectedTypeRef {
Scalar(ValueTypeTag),
Model(ProjectedModelUse),
Struct(StructId),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectedContainer {
Scalar,
Sequence,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub struct ProjectedMultiplicity {
cardinality: Cardinality,
required: bool,
container: ProjectedContainer,
#[serde(skip_serializing_if = "CollectionMode::is_unordered")]
collection_mode: CollectionMode,
}
impl ProjectedMultiplicity {
#[must_use]
pub const fn from_cardinality(cardinality: Cardinality) -> Self {
Self::new(cardinality, CollectionMode::Unordered)
}
#[must_use]
pub const fn new(cardinality: Cardinality, collection_mode: CollectionMode) -> Self {
let container = match collection_mode {
CollectionMode::OrderedList => ProjectedContainer::Sequence,
CollectionMode::Unordered => match cardinality.max() {
Some(0 | 1) => ProjectedContainer::Scalar,
Some(_) | None => ProjectedContainer::Sequence,
},
};
Self {
cardinality,
required: cardinality.min() > 0,
container,
collection_mode,
}
}
#[must_use]
pub const fn cardinality(&self) -> Cardinality {
self.cardinality
}
#[must_use]
pub const fn required(&self) -> bool {
self.required
}
#[must_use]
pub const fn container(&self) -> ProjectedContainer {
self.container
}
#[must_use]
pub const fn collection_mode(&self) -> CollectionMode {
self.collection_mode
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ProjectedAnnotation {
id: AnnotationFactId,
value: SchemaAnnotationValue,
}
impl ProjectedAnnotation {
pub fn new(id: AnnotationFactId, value: SchemaAnnotationValue) -> Result<Self, Diagnostic> {
AnnotationFact::new(id.clone(), value.clone())?;
Ok(Self { id, value })
}
#[must_use]
pub const fn id(&self) -> &AnnotationFactId {
&self.id
}
#[must_use]
pub const fn value(&self) -> &SchemaAnnotationValue {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FieldTokenProjection {
id: OwnsFactId,
declaring_id: OwnsFactId,
target_name: TargetIdentifier,
multiplicity: ProjectedMultiplicity,
key: bool,
unique: bool,
#[serde(serialize_with = "serialize_map_values")]
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
}
impl FieldTokenProjection {
pub fn new(
id: OwnsFactId,
declaring_id: OwnsFactId,
target_name: TargetIdentifier,
multiplicity: ProjectedMultiplicity,
key: bool,
unique: bool,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
) -> Result<Self, Diagnostic> {
if id.attribute() != declaring_id.attribute() {
return Err(invalid_projection(
"invalid_projection_reference",
"effective owns fact attribute does not match declaring owns fact attribute",
));
}
if annotations.iter().any(|(key, value)| {
key != value.id()
|| !matches!(
value.id().subject(),
AnnotationSubjectId::Owns(subject) if subject == &id
)
}) {
return Err(invalid_projection(
"invalid_projected_owns_annotation",
"owns annotations require matching exact effective owns subjects",
));
}
if multiplicity.collection_mode().is_unordered()
&& annotations
.keys()
.any(|id| id.kind() == &AnnotationKindId::Distinct)
{
return Err(invalid_projection(
"distinct_requires_ordered_collection",
"distinct applies only to an ordered ownership collection",
));
}
ensure_collection_limit(annotations.len(), "too_many_projected_annotations")?;
Ok(Self {
id,
declaring_id,
target_name,
multiplicity,
key,
unique,
annotations,
})
}
#[must_use]
pub const fn id(&self) -> &OwnsFactId {
&self.id
}
#[must_use]
pub const fn declaring_id(&self) -> &OwnsFactId {
&self.declaring_id
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
#[must_use]
pub const fn is_key(&self) -> bool {
self.key
}
#[must_use]
pub const fn is_unique(&self) -> bool {
self.unique
}
#[must_use]
pub const fn annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.annotations
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct RoleTokenProjection {
owner: TypeId,
role: RoleId,
target_name: TargetIdentifier,
#[serde(skip_serializing_if = "Option::is_none")]
player_union_target_name: Option<TargetIdentifier>,
accepted_players: BTreeSet<TypeId>,
specializes: Option<RoleId>,
multiplicity: ProjectedMultiplicity,
is_abstract: bool,
#[serde(serialize_with = "serialize_map_values")]
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
}
impl RoleTokenProjection {
#[allow(clippy::too_many_arguments)]
pub fn new(
owner: TypeId,
role: RoleId,
target_name: TargetIdentifier,
accepted_players: BTreeSet<TypeId>,
specializes: Option<RoleId>,
multiplicity: ProjectedMultiplicity,
is_abstract: bool,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
) -> Result<Self, Diagnostic> {
if owner.kind() != TypeKind::Relation
|| accepted_players
.iter()
.any(|id| !matches!(id.kind(), TypeKind::Entity | TypeKind::Relation))
{
return Err(invalid_projection(
"invalid_projected_role_token",
"role tokens require a relation owner and entity/relation players",
));
}
let effective_role = RoleId::new(
owner.label().as_str().to_owned(),
role.label().as_str().to_owned(),
)?;
let effective_subject =
AnnotationSubjectId::Relates(RelatesFactId::new(owner.clone(), effective_role)?);
if annotations
.iter()
.any(|(key, value)| key != value.id() || value.id().subject() != &effective_subject)
{
return Err(invalid_projection(
"invalid_projected_relates_annotation",
"relates annotations require matching exact effective relates subjects",
));
}
if multiplicity.collection_mode().is_unordered()
&& annotations
.keys()
.any(|id| id.kind() == &AnnotationKindId::Distinct)
{
return Err(invalid_projection(
"distinct_requires_ordered_collection",
"distinct applies only to an ordered related-role collection",
));
}
ensure_collection_limit(accepted_players.len(), "too_many_projected_role_players")?;
ensure_collection_limit(annotations.len(), "too_many_projected_annotations")?;
Ok(Self {
owner,
role,
target_name,
player_union_target_name: None,
accepted_players,
specializes,
multiplicity,
is_abstract,
annotations,
})
}
#[must_use]
pub fn with_player_union_target_name(mut self, target_name: TargetIdentifier) -> Self {
self.player_union_target_name = Some(target_name);
self
}
#[must_use]
pub const fn owner(&self) -> &TypeId {
&self.owner
}
#[must_use]
pub const fn role(&self) -> &RoleId {
&self.role
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub const fn player_union_target_name(&self) -> Option<&TargetIdentifier> {
self.player_union_target_name.as_ref()
}
#[must_use]
pub const fn accepted_players(&self) -> &BTreeSet<TypeId> {
&self.accepted_players
}
#[must_use]
pub const fn specializes(&self) -> Option<&RoleId> {
self.specializes.as_ref()
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
#[must_use]
pub const fn is_abstract(&self) -> bool {
self.is_abstract
}
#[must_use]
pub const fn annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.annotations
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DeclaredRoleProjection {
role: RoleId,
specializes: Option<RoleId>,
}
impl DeclaredRoleProjection {
#[must_use]
pub const fn new(role: RoleId, specializes: Option<RoleId>) -> Self {
Self { role, specializes }
}
#[must_use]
pub const fn role(&self) -> &RoleId {
&self.role
}
#[must_use]
pub const fn specializes(&self) -> Option<&RoleId> {
self.specializes.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DirectSubProjection {
id: SubFactId,
origin: SchemaFactId,
#[serde(serialize_with = "serialize_map_values")]
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
}
impl DirectSubProjection {
pub fn new(
id: SubFactId,
origin: SchemaFactId,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
) -> Result<Self, Diagnostic> {
if origin != SchemaFactId::Sub(id.clone()) {
return Err(invalid_projection(
"invalid_projected_sub_origin",
"projected subtype origin must identify its exact direct edge",
));
}
if annotations.iter().any(|(key, value)| {
key != value.id()
|| !matches!(key.subject(), AnnotationSubjectId::Sub(subject) if subject == &id)
}) {
return Err(invalid_projection(
"invalid_projected_sub_annotation",
"subtype annotations require matching exact edge subjects",
));
}
ensure_collection_limit(annotations.len(), "too_many_projected_annotations")?;
Ok(Self {
id,
origin,
annotations,
})
}
#[must_use]
pub const fn id(&self) -> &SubFactId {
&self.id
}
#[must_use]
pub const fn origin(&self) -> &SchemaFactId {
&self.origin
}
#[must_use]
pub const fn annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.annotations
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DeclarationProjection {
parent: Option<TypeId>,
direct_sub: Option<DirectSubProjection>,
value_type: Option<ValueTypeTag>,
is_abstract: bool,
is_constructible: bool,
#[serde(serialize_with = "serialize_map_values")]
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
#[serde(serialize_with = "serialize_map_values")]
value_annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
direct_fields: Vec<OwnsFactId>,
#[serde(serialize_with = "serialize_map_values")]
direct_roles: BTreeMap<RoleId, DeclaredRoleProjection>,
direct_plays: BTreeSet<PlaysFactId>,
}
impl DeclarationProjection {
#[allow(clippy::too_many_arguments)]
pub fn new(
parent: Option<TypeId>,
value_type: Option<ValueTypeTag>,
is_abstract: bool,
is_constructible: bool,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
direct_fields: Vec<OwnsFactId>,
direct_roles: BTreeMap<RoleId, DeclaredRoleProjection>,
direct_plays: BTreeSet<PlaysFactId>,
) -> Result<Self, Diagnostic> {
for length in [
annotations.len(),
direct_fields.len(),
direct_roles.len(),
direct_plays.len(),
] {
ensure_collection_limit(length, "projection_declaration_limit_exceeded")?;
}
Ok(Self {
parent,
direct_sub: None,
value_type,
is_abstract,
is_constructible,
annotations,
value_annotations: BTreeMap::new(),
direct_fields,
direct_roles,
direct_plays,
})
}
pub fn with_direct_sub(
mut self,
direct_sub: Option<DirectSubProjection>,
) -> Result<Self, Diagnostic> {
if direct_sub
.as_ref()
.is_some_and(|sub| self.parent.as_ref() != Some(sub.id().supertype()))
{
return Err(invalid_projection(
"invalid_projected_sub_parent",
"projected direct subtype edge must match the nominal parent",
));
}
self.direct_sub = direct_sub;
Ok(self)
}
pub fn with_value_annotations(
mut self,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
) -> Result<Self, Diagnostic> {
if annotations.iter().any(|(key, value)| {
key != value.id() || !matches!(key.subject(), AnnotationSubjectId::Value(_))
}) {
return Err(invalid_projection(
"invalid_projected_value_annotation",
"attribute value annotations require matching effective value subjects",
));
}
ensure_collection_limit(annotations.len(), "too_many_projected_annotations")?;
self.value_annotations = annotations;
Ok(self)
}
#[must_use]
pub const fn parent(&self) -> Option<&TypeId> {
self.parent.as_ref()
}
#[must_use]
pub const fn direct_sub(&self) -> Option<&DirectSubProjection> {
self.direct_sub.as_ref()
}
#[must_use]
pub const fn value_type(&self) -> Option<ValueTypeTag> {
self.value_type
}
#[must_use]
pub const fn is_abstract(&self) -> bool {
self.is_abstract
}
#[must_use]
pub const fn is_constructible(&self) -> bool {
self.is_constructible
}
#[must_use]
pub const fn annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.annotations
}
#[must_use]
pub const fn value_annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.value_annotations
}
#[must_use]
pub fn direct_fields(&self) -> &[OwnsFactId] {
&self.direct_fields
}
#[must_use]
pub const fn direct_roles(&self) -> &BTreeMap<RoleId, DeclaredRoleProjection> {
&self.direct_roles
}
#[must_use]
pub const fn direct_plays(&self) -> &BTreeSet<PlaysFactId> {
&self.direct_plays
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CreateFieldProjection {
token: OwnsFactId,
value: ProjectedTypeRef,
multiplicity: ProjectedMultiplicity,
}
impl CreateFieldProjection {
#[must_use]
pub const fn new(
token: OwnsFactId,
value: ProjectedTypeRef,
multiplicity: ProjectedMultiplicity,
) -> Self {
Self {
token,
value,
multiplicity,
}
}
#[must_use]
pub const fn token(&self) -> &OwnsFactId {
&self.token
}
#[must_use]
pub const fn value(&self) -> &ProjectedTypeRef {
&self.value
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CreateRoleProjection {
role: RoleId,
players: BTreeSet<ProjectedModelUse>,
multiplicity: ProjectedMultiplicity,
}
impl CreateRoleProjection {
pub fn new(
role: RoleId,
players: BTreeSet<ProjectedModelUse>,
multiplicity: ProjectedMultiplicity,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(players.len(), "too_many_projected_role_players")?;
Ok(Self {
role,
players,
multiplicity,
})
}
#[must_use]
pub const fn role(&self) -> &RoleId {
&self.role
}
#[must_use]
pub const fn players(&self) -> &BTreeSet<ProjectedModelUse> {
&self.players
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CreateProjection {
#[serde(skip_serializing_if = "Option::is_none")]
target_name: Option<TargetIdentifier>,
enabled: bool,
fields: Vec<CreateFieldProjection>,
#[serde(serialize_with = "serialize_map_values")]
roles: BTreeMap<RoleId, CreateRoleProjection>,
}
impl CreateProjection {
pub fn new(
enabled: bool,
fields: Vec<CreateFieldProjection>,
roles: BTreeMap<RoleId, CreateRoleProjection>,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(fields.len(), "too_many_projected_create_fields")?;
ensure_collection_limit(roles.len(), "too_many_projected_create_roles")?;
Ok(Self {
target_name: None,
enabled,
fields,
roles,
})
}
#[must_use]
pub fn with_target_name(mut self, target_name: TargetIdentifier) -> Self {
self.target_name = Some(target_name);
self
}
#[must_use]
pub const fn target_name(&self) -> Option<&TargetIdentifier> {
self.target_name.as_ref()
}
#[must_use]
pub const fn enabled(&self) -> bool {
self.enabled
}
#[must_use]
pub fn fields(&self) -> &[CreateFieldProjection] {
&self.fields
}
#[must_use]
pub const fn roles(&self) -> &BTreeMap<RoleId, CreateRoleProjection> {
&self.roles
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ReadFieldProjection {
token: OwnsFactId,
value: ProjectedTypeRef,
multiplicity: ProjectedMultiplicity,
}
impl ReadFieldProjection {
#[must_use]
pub const fn new(
token: OwnsFactId,
value: ProjectedTypeRef,
multiplicity: ProjectedMultiplicity,
) -> Self {
Self {
token,
value,
multiplicity,
}
}
#[must_use]
pub const fn token(&self) -> &OwnsFactId {
&self.token
}
#[must_use]
pub const fn value(&self) -> &ProjectedTypeRef {
&self.value
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ReadRoleProjection {
role: RoleId,
players: BTreeSet<ProjectedModelUse>,
multiplicity: ProjectedMultiplicity,
}
impl ReadRoleProjection {
pub fn new(
role: RoleId,
players: BTreeSet<ProjectedModelUse>,
multiplicity: ProjectedMultiplicity,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(players.len(), "too_many_projected_role_players")?;
Ok(Self {
role,
players,
multiplicity,
})
}
#[must_use]
pub const fn role(&self) -> &RoleId {
&self.role
}
#[must_use]
pub const fn players(&self) -> &BTreeSet<ProjectedModelUse> {
&self.players
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CompleteReadProjection {
fields: Vec<ReadFieldProjection>,
#[serde(serialize_with = "serialize_map_values")]
roles: BTreeMap<RoleId, ReadRoleProjection>,
nominal_upcasts: Vec<TypeId>,
#[serde(serialize_with = "serialize_role_upcasts")]
role_upcasts: BTreeMap<RoleId, Vec<RoleId>>,
}
impl CompleteReadProjection {
pub fn new(
fields: Vec<ReadFieldProjection>,
roles: BTreeMap<RoleId, ReadRoleProjection>,
nominal_upcasts: Vec<TypeId>,
) -> Result<Self, Diagnostic> {
for length in [fields.len(), roles.len(), nominal_upcasts.len()] {
ensure_collection_limit(length, "projection_read_limit_exceeded")?;
}
Ok(Self {
fields,
roles,
nominal_upcasts,
role_upcasts: BTreeMap::new(),
})
}
pub fn with_role_upcasts(
mut self,
role_upcasts: BTreeMap<RoleId, Vec<RoleId>>,
) -> Result<Self, Diagnostic> {
if role_upcasts.iter().any(|(role, ancestors)| {
!self.roles.contains_key(role)
|| ancestors.is_empty()
|| ancestors.iter().collect::<BTreeSet<_>>().len() != ancestors.len()
}) {
return Err(invalid_projection(
"invalid_projected_role_upcast",
"role upcasts require an active role and unique non-empty ancestor roles",
));
}
ensure_collection_limit(role_upcasts.len(), "projection_read_limit_exceeded")?;
self.role_upcasts = role_upcasts;
Ok(self)
}
#[must_use]
pub fn fields(&self) -> &[ReadFieldProjection] {
&self.fields
}
#[must_use]
pub const fn roles(&self) -> &BTreeMap<RoleId, ReadRoleProjection> {
&self.roles
}
#[must_use]
pub fn nominal_upcasts(&self) -> &[TypeId] {
&self.nominal_upcasts
}
#[must_use]
pub const fn role_upcasts(&self) -> &BTreeMap<RoleId, Vec<RoleId>> {
&self.role_upcasts
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceConstructionPolicy {
IidOnly,
KeyFallback,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ReferenceReadProjection {
target_name: Option<TargetIdentifier>,
key_fields: Vec<OwnsFactId>,
construction_policy: ReferenceConstructionPolicy,
}
impl ReferenceReadProjection {
pub fn new(
target_name: Option<TargetIdentifier>,
key_fields: Vec<OwnsFactId>,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(key_fields.len(), "too_many_projected_reference_keys")?;
let mut unique = BTreeSet::new();
if key_fields.iter().any(|id| !unique.insert(id)) {
return Err(invalid_projection(
"duplicate_projected_reference_key",
"reference key identities must be unique",
));
}
let construction_policy = if key_fields.is_empty() {
ReferenceConstructionPolicy::IidOnly
} else {
ReferenceConstructionPolicy::KeyFallback
};
Ok(Self {
target_name,
key_fields,
construction_policy,
})
}
#[must_use]
pub const fn target_name(&self) -> Option<&TargetIdentifier> {
self.target_name.as_ref()
}
#[must_use]
pub fn key_fields(&self) -> &[OwnsFactId] {
&self.key_fields
}
#[must_use]
pub const fn construction_policy(&self) -> ReferenceConstructionPolicy {
self.construction_policy
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct QueryTokenProjection {
type_id: TypeId,
#[serde(skip_serializing_if = "Option::is_none")]
target_name: Option<TargetIdentifier>,
#[serde(serialize_with = "serialize_map_values")]
fields: BTreeMap<OwnsFactId, FieldTokenProjection>,
#[serde(serialize_with = "serialize_map_values")]
roles: BTreeMap<RoleId, RoleTokenProjection>,
}
impl QueryTokenProjection {
pub fn new(
type_id: TypeId,
fields: BTreeMap<OwnsFactId, FieldTokenProjection>,
roles: BTreeMap<RoleId, RoleTokenProjection>,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(fields.len(), "too_many_projected_field_tokens")?;
ensure_collection_limit(roles.len(), "too_many_projected_role_tokens")?;
Ok(Self {
type_id,
target_name: None,
fields,
roles,
})
}
#[must_use]
pub fn with_target_name(mut self, target_name: TargetIdentifier) -> Self {
self.target_name = Some(target_name);
self
}
#[must_use]
pub const fn type_id(&self) -> &TypeId {
&self.type_id
}
#[must_use]
pub const fn target_name(&self) -> Option<&TargetIdentifier> {
self.target_name.as_ref()
}
#[must_use]
pub const fn fields(&self) -> &BTreeMap<OwnsFactId, FieldTokenProjection> {
&self.fields
}
#[must_use]
pub const fn roles(&self) -> &BTreeMap<RoleId, RoleTokenProjection> {
&self.roles
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ModelProjection {
id: TypeId,
target_name: TargetIdentifier,
declaration: DeclarationProjection,
create: CreateProjection,
complete_read: CompleteReadProjection,
reference_read: ReferenceReadProjection,
query_tokens: QueryTokenProjection,
}
impl ModelProjection {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: TypeId,
target_name: TargetIdentifier,
declaration: DeclarationProjection,
create: CreateProjection,
complete_read: CompleteReadProjection,
reference_read: ReferenceReadProjection,
query_tokens: QueryTokenProjection,
) -> Result<Self, Diagnostic> {
let exact_required_scalar = |multiplicity: ProjectedMultiplicity| {
multiplicity.required()
&& multiplicity.container() == ProjectedContainer::Scalar
&& multiplicity.cardinality().min() == 1
&& multiplicity.cardinality().max() == Some(1)
};
let reference_keys_valid = reference_read.key_fields().iter().all(|key| {
let Some(token) = query_tokens.fields().get(key) else {
return false;
};
if !token.is_key() || !exact_required_scalar(token.multiplicity()) {
return false;
}
let mut complete = complete_read
.fields()
.iter()
.filter(|field| field.token() == key);
let Some(field) = complete.next() else {
return false;
};
if complete.next().is_some() || !exact_required_scalar(field.multiplicity()) {
return false;
}
matches!(
field.value(),
ProjectedTypeRef::Model(value)
if value.form() == ProjectedModelForm::Complete
&& value.id().kind() == TypeKind::Attribute
&& value.id().label() == key.attribute().label()
)
});
if (!reference_read.key_fields().is_empty() && reference_read.target_name().is_none())
|| !reference_keys_valid
{
return Err(invalid_projection(
"invalid_projected_reference_key",
"reference keys require exact required-scalar complete/query key facets",
));
}
let field_multiplicities_match = create.fields().iter().all(|field| {
query_tokens
.fields()
.get(field.token())
.is_some_and(|token| token.multiplicity() == field.multiplicity())
}) && complete_read.fields().iter().all(|field| {
query_tokens
.fields()
.get(field.token())
.is_some_and(|token| token.multiplicity() == field.multiplicity())
});
let role_multiplicities_match = create.roles().iter().all(|(id, role)| {
query_tokens
.roles()
.get(id)
.is_some_and(|token| token.multiplicity() == role.multiplicity())
}) && complete_read.roles().iter().all(|(id, role)| {
query_tokens
.roles()
.get(id)
.is_some_and(|token| token.multiplicity() == role.multiplicity())
});
if !field_multiplicities_match || !role_multiplicities_match {
return Err(invalid_projection(
"projected_multiplicity_mismatch",
"create, complete-read, and query-token facets require exact multiplicity equality",
));
}
if query_tokens.type_id() != &id
|| match (declaration.parent(), declaration.direct_sub()) {
(None, None) => false,
(Some(parent), Some(sub)) => {
sub.id().subtype() != &id || sub.id().supertype() != parent
}
(None, Some(_)) | (Some(_), None) => true,
}
|| query_tokens
.fields()
.values()
.any(|field| field.id().owner() != &id)
|| query_tokens
.roles()
.values()
.any(|role| role.owner() != &id)
|| create
.fields()
.iter()
.any(|field| !query_tokens.fields().contains_key(field.token()))
|| complete_read
.fields()
.iter()
.any(|field| !query_tokens.fields().contains_key(field.token()))
|| create
.roles()
.iter()
.any(|(id, role)| id != role.role() || !query_tokens.roles().contains_key(id))
|| complete_read
.roles()
.iter()
.any(|(id, role)| id != role.role() || !query_tokens.roles().contains_key(id))
{
return Err(invalid_projection(
"invalid_model_projection_reference",
"model facets contain a mismatched owner or token reference",
));
}
Ok(Self {
id,
target_name,
declaration,
create,
complete_read,
reference_read,
query_tokens,
})
}
#[must_use]
pub const fn id(&self) -> &TypeId {
&self.id
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub const fn declaration(&self) -> &DeclarationProjection {
&self.declaration
}
#[must_use]
pub const fn create(&self) -> &CreateProjection {
&self.create
}
#[must_use]
pub const fn complete_read(&self) -> &CompleteReadProjection {
&self.complete_read
}
#[must_use]
pub const fn reference_read(&self) -> &ReferenceReadProjection {
&self.reference_read
}
#[must_use]
pub const fn query_tokens(&self) -> &QueryTokenProjection {
&self.query_tokens
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct StructFieldProjection {
name: Label,
target_name: TargetIdentifier,
value_type: ValueTypeTag,
optional: bool,
}
impl StructFieldProjection {
#[must_use]
pub const fn new(
name: Label,
target_name: TargetIdentifier,
value_type: ValueTypeTag,
optional: bool,
) -> Self {
Self {
name,
target_name,
value_type,
optional,
}
}
#[must_use]
pub const fn name(&self) -> &Label {
&self.name
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub const fn value_type(&self) -> ValueTypeTag {
self.value_type
}
#[must_use]
pub const fn optional(&self) -> bool {
self.optional
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct StructProjection {
id: StructId,
target_name: TargetIdentifier,
fields: Vec<StructFieldProjection>,
}
impl StructProjection {
pub fn new(
id: StructId,
target_name: TargetIdentifier,
fields: Vec<StructFieldProjection>,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(fields.len(), "too_many_projected_struct_fields")?;
Ok(Self {
id,
target_name,
fields,
})
}
#[must_use]
pub const fn id(&self) -> &StructId {
&self.id
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub fn fields(&self) -> &[StructFieldProjection] {
&self.fields
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FunctionParameterProjection {
name: Label,
target_name: TargetIdentifier,
type_ref: ProjectedTypeRef,
}
impl FunctionParameterProjection {
#[must_use]
pub const fn new(
name: Label,
target_name: TargetIdentifier,
type_ref: ProjectedTypeRef,
) -> Self {
Self {
name,
target_name,
type_ref,
}
}
#[must_use]
pub const fn name(&self) -> &Label {
&self.name
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub const fn type_ref(&self) -> &ProjectedTypeRef {
&self.type_ref
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FunctionReturnElementProjection {
type_ref: ProjectedTypeRef,
optional: bool,
}
impl FunctionReturnElementProjection {
#[must_use]
pub const fn new(type_ref: ProjectedTypeRef, optional: bool) -> Self {
Self { type_ref, optional }
}
#[must_use]
pub const fn type_ref(&self) -> &ProjectedTypeRef {
&self.type_ref
}
#[must_use]
pub const fn optional(&self) -> bool {
self.optional
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", content = "elements", rename_all = "snake_case")]
pub enum FunctionReturnProjection {
Scalar(FunctionReturnElementProjection),
Tuple(Vec<FunctionReturnElementProjection>),
Stream(Vec<FunctionReturnElementProjection>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FunctionProjection {
id: FunctionId,
target_name: TargetIdentifier,
parameters: Vec<FunctionParameterProjection>,
returns: FunctionReturnProjection,
#[serde(serialize_with = "serialize_map_values")]
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
}
impl FunctionProjection {
pub fn new(
id: FunctionId,
target_name: TargetIdentifier,
parameters: Vec<FunctionParameterProjection>,
returns: FunctionReturnProjection,
) -> Result<Self, Diagnostic> {
ensure_collection_limit(parameters.len(), "too_many_projected_function_parameters")?;
Ok(Self {
id,
target_name,
parameters,
returns,
annotations: BTreeMap::new(),
})
}
pub fn with_annotations(
mut self,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
) -> Result<Self, Diagnostic> {
if annotations.iter().any(|(key, value)| {
key != value.id() || key.subject() != &AnnotationSubjectId::Function(self.id.clone())
}) {
return Err(invalid_projection(
"invalid_projected_function_annotation",
"function annotations require the projected function subject",
));
}
ensure_collection_limit(annotations.len(), "too_many_projected_annotations")?;
self.annotations = annotations;
Ok(self)
}
#[must_use]
pub const fn id(&self) -> &FunctionId {
&self.id
}
#[must_use]
pub const fn target_name(&self) -> &TargetIdentifier {
&self.target_name
}
#[must_use]
pub fn parameters(&self) -> &[FunctionParameterProjection] {
&self.parameters
}
#[must_use]
pub const fn returns(&self) -> &FunctionReturnProjection {
&self.returns
}
#[must_use]
pub const fn annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.annotations
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PlayingProjection {
id: PlaysFactId,
role: RoleId,
target_name: Option<TargetIdentifier>,
multiplicity: ProjectedMultiplicity,
#[serde(serialize_with = "serialize_map_values")]
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
}
impl PlayingProjection {
pub fn new(
id: PlaysFactId,
role: RoleId,
multiplicity: ProjectedMultiplicity,
annotations: BTreeMap<AnnotationFactId, ProjectedAnnotation>,
) -> Result<Self, Diagnostic> {
if !multiplicity.collection_mode().is_unordered() {
return Err(invalid_projection(
"ordered_playing_projection",
"playing multiplicity must remain unordered",
));
}
if id.role() != &role
|| annotations.iter().any(|(key, value)| {
key != value.id() || key.subject() != &AnnotationSubjectId::Plays(id.clone())
})
{
return Err(invalid_projection(
"invalid_playing_projection_reference",
"playing metadata has a mismatched role or annotation subject",
));
}
ensure_collection_limit(annotations.len(), "too_many_projected_annotations")?;
Ok(Self {
id,
role,
target_name: None,
multiplicity,
annotations,
})
}
#[must_use]
pub fn with_target_name(mut self, target_name: TargetIdentifier) -> Self {
self.target_name = Some(target_name);
self
}
#[must_use]
pub const fn id(&self) -> &PlaysFactId {
&self.id
}
#[must_use]
pub const fn role(&self) -> &RoleId {
&self.role
}
#[must_use]
pub const fn target_name(&self) -> Option<&TargetIdentifier> {
self.target_name.as_ref()
}
#[must_use]
pub const fn multiplicity(&self) -> ProjectedMultiplicity {
self.multiplicity
}
#[must_use]
pub const fn annotations(&self) -> &BTreeMap<AnnotationFactId, ProjectedAnnotation> {
&self.annotations
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct EmissionPlan {
model_shells: Vec<TypeId>,
model_link_components: Vec<BTreeSet<TypeId>>,
structs: Vec<StructId>,
functions: Vec<FunctionId>,
}
impl EmissionPlan {
pub fn new(
model_shells: Vec<TypeId>,
model_link_components: Vec<BTreeSet<TypeId>>,
structs: Vec<StructId>,
functions: Vec<FunctionId>,
) -> Result<Self, Diagnostic> {
for length in [
model_shells.len(),
model_link_components.len(),
structs.len(),
functions.len(),
] {
ensure_collection_limit(length, "projection_emission_limit_exceeded")?;
}
Ok(Self {
model_shells,
model_link_components,
structs,
functions,
})
}
#[must_use]
pub fn model_shells(&self) -> &[TypeId] {
&self.model_shells
}
#[must_use]
pub fn model_link_components(&self) -> &[BTreeSet<TypeId>] {
&self.model_link_components
}
#[must_use]
pub fn structs(&self) -> &[StructId] {
&self.structs
}
#[must_use]
pub fn functions(&self) -> &[FunctionId] {
&self.functions
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct RuntimeProjection {
target: BindingTarget,
config: ProjectionConfig,
semantic_fingerprint: SemanticSchemaFingerprint,
projection_fingerprint: BindingProjectionFingerprint,
generator_handlers: Vec<ProjectionHandler>,
code_resources: Vec<CodeResourceDigest>,
#[serde(serialize_with = "serialize_map_values")]
models: BTreeMap<TypeId, ModelProjection>,
#[serde(serialize_with = "serialize_map_values")]
structs: BTreeMap<StructId, StructProjection>,
#[serde(serialize_with = "serialize_map_values")]
functions: BTreeMap<FunctionId, FunctionProjection>,
#[serde(serialize_with = "serialize_map_values")]
playing_facts: BTreeMap<PlaysFactId, PlayingProjection>,
emission: EmissionPlan,
}
#[derive(Serialize)]
struct RuntimeProjectionContentView<'a> {
#[serde(serialize_with = "serialize_map_values")]
models: &'a BTreeMap<TypeId, ModelProjection>,
#[serde(serialize_with = "serialize_map_values")]
structs: &'a BTreeMap<StructId, StructProjection>,
#[serde(serialize_with = "serialize_map_values")]
functions: &'a BTreeMap<FunctionId, FunctionProjection>,
#[serde(serialize_with = "serialize_map_values")]
playing_facts: &'a BTreeMap<PlaysFactId, PlayingProjection>,
emission: &'a EmissionPlan,
}
impl RuntimeProjection {
#[allow(clippy::too_many_arguments)]
pub fn try_new(
target: BindingTarget,
config: ProjectionConfig,
semantic_fingerprint: SemanticSchemaFingerprint,
handlers: &[ProjectionHandler],
resources: &[CodeResourceDigest],
models: BTreeMap<TypeId, ModelProjection>,
structs: BTreeMap<StructId, StructProjection>,
functions: BTreeMap<FunctionId, FunctionProjection>,
playing_facts: BTreeMap<PlaysFactId, PlayingProjection>,
emission: EmissionPlan,
) -> Result<Self, Diagnostic> {
if config.target() != target
|| models.iter().any(|(key, value)| key != value.id())
|| structs.iter().any(|(key, value)| key != value.id())
|| functions.iter().any(|(key, value)| key != value.id())
|| playing_facts.iter().any(|(key, value)| key != value.id())
{
return Err(invalid_projection(
"invalid_runtime_projection_map",
"runtime projection map keys or target configuration are inconsistent",
));
}
for length in [
models.len(),
structs.len(),
functions.len(),
playing_facts.len(),
] {
ensure_collection_limit(length, "runtime_projection_limit_exceeded")?;
}
if target == BindingTarget::C {
validate_c_create_limits(&models)?;
}
if matches!(target, BindingTarget::Rust | BindingTarget::C) {
let native_names_complete = models.values().all(|model| {
model.create().enabled() == model.create().target_name().is_some()
&& model.query_tokens().target_name().is_some()
&& model
.query_tokens()
.roles()
.values()
.all(|role| role.player_union_target_name().is_some())
&& matches!(model.id().kind(), TypeKind::Entity | TypeKind::Relation)
== model.reference_read().target_name().is_some()
}) && playing_facts
.values()
.all(|playing| playing.target_name().is_some());
if !native_names_complete {
let (code, message) = match target {
BindingTarget::Rust => (
"missing_rust_projection_identifier",
"Rust projection omits a required create, reference, query-token, player-union, or plays identifier",
),
BindingTarget::C => (
"missing_c_projection_identifier",
"C projection omits a required create, reference, query-token, player-union, or plays identifier",
),
BindingTarget::Python | BindingTarget::TypeScript => unreachable!(),
};
return Err(invalid_projection(code, message));
}
}
let model_ids = models.keys().cloned().collect::<BTreeSet<_>>();
if emission
.model_shells()
.iter()
.cloned()
.collect::<BTreeSet<_>>()
!= model_ids
|| emission.model_shells().len() != model_ids.len()
|| emission
.model_link_components()
.iter()
.flat_map(BTreeSet::iter)
.cloned()
.collect::<BTreeSet<_>>()
!= model_ids
|| emission
.model_link_components()
.iter()
.map(BTreeSet::len)
.sum::<usize>()
!= model_ids.len()
|| emission.structs() != structs.keys().cloned().collect::<Vec<_>>()
|| emission.functions() != functions.keys().cloned().collect::<Vec<_>>()
{
return Err(invalid_projection(
"invalid_projection_emission_plan",
"emission plan does not cover each projected value exactly once",
));
}
let all_model_refs_valid = models.values().all(|model| {
model.query_tokens().roles().values().all(|role| {
role.accepted_players()
.iter()
.all(|id| models.contains_key(id))
}) && model.create().roles().values().all(|role| {
role.players()
.iter()
.all(|value| models.contains_key(value.id()))
}) && model.complete_read().roles().values().all(|role| {
role.players()
.iter()
.all(|value| models.contains_key(value.id()))
})
});
if !all_model_refs_valid {
return Err(invalid_projection(
"invalid_projection_reference",
"projection references a model that is not present",
));
}
let declaring_owners_valid = models.values().all(|model| {
model.query_tokens().fields().values().all(|token| {
let declaring_owner = token.declaring_id().owner();
let mut curr = Some(model.id());
let mut found = false;
let mut visited = BTreeSet::new();
while let Some(curr_id) = curr {
if !visited.insert(curr_id) {
return false;
}
if curr_id == declaring_owner {
found = true;
break;
}
curr = models.get(curr_id).and_then(|m| m.declaration().parent());
}
found
})
});
if !declaring_owners_valid {
return Err(invalid_projection(
"invalid_projection_reference",
"field token declaring owner is not the effective owner or a valid ancestor",
));
}
let model_use_is_valid = |value: &ProjectedModelUse| {
models.get(value.id()).is_some_and(|model| {
value.form() != ProjectedModelForm::Reference
|| model.reference_read().target_name().is_some()
})
};
let read_model_use_is_valid = |value: &ProjectedModelUse| {
model_use_is_valid(value)
&& (value.form() != ProjectedModelForm::Complete
|| value.id().kind() != TypeKind::Relation)
};
let type_ref_is_valid = |value: &ProjectedTypeRef| match value {
ProjectedTypeRef::Scalar(_) => true,
ProjectedTypeRef::Model(value) => model_use_is_valid(value),
ProjectedTypeRef::Struct(id) => structs.contains_key(id),
};
let role_exists = |role: &RoleId| {
models.values().any(|model| {
model.id().kind() == TypeKind::Relation
&& model.id().label() == role.declaring_relation()
&& model.query_tokens().roles().contains_key(role)
})
};
let shell_positions = emission
.model_shells()
.iter()
.enumerate()
.map(|(index, id)| (id.clone(), index))
.collect::<BTreeMap<_, _>>();
let closed_models = models.values().all(|model| {
let declaration = model.declaration();
let parent_is_valid = declaration.parent().is_none_or(|parent| {
models.contains_key(parent) && shell_positions[parent] < shell_positions[model.id()]
});
let direct_sub_is_valid = match (declaration.parent(), declaration.direct_sub()) {
(None, None) => true,
(Some(parent), Some(sub)) => {
sub.id().subtype() == model.id() && sub.id().supertype() == parent
}
(None, Some(_)) | (Some(_), None) => false,
};
let direct_fields_are_valid = declaration
.direct_fields()
.iter()
.all(|id| model.query_tokens().fields().contains_key(id));
let direct_roles_are_valid = declaration.direct_roles().iter().all(|(id, role)| {
id == role.role()
&& model.query_tokens().roles().contains_key(id)
&& role.specializes().is_none_or(&role_exists)
});
let direct_plays_are_valid = declaration
.direct_plays()
.iter()
.all(|id| id.player() == model.id() && playing_facts.contains_key(id));
let fields_are_valid = model.query_tokens().fields().values().all(|field| {
models.keys().any(|id| {
id.kind() == TypeKind::Attribute && id.label() == field.id().attribute().label()
})
}) && model
.create()
.fields()
.iter()
.all(|field| type_ref_is_valid(field.value()))
&& model
.complete_read()
.fields()
.iter()
.all(|field| type_ref_is_valid(field.value()));
let roles_are_valid = model
.query_tokens()
.roles()
.values()
.all(|role| role.specializes().is_none_or(&role_exists))
&& model
.create()
.roles()
.values()
.all(|role| role.players().iter().all(&model_use_is_valid))
&& model
.complete_read()
.roles()
.values()
.all(|role| role.players().iter().all(&read_model_use_is_valid));
let role_upcasts_are_valid =
model
.complete_read()
.role_upcasts()
.iter()
.all(|(active, ancestors)| {
model.complete_read().roles().contains_key(active)
&& ancestors.iter().all(&role_exists)
});
let references_are_valid = model.reference_read().key_fields().iter().all(|id| {
model
.query_tokens()
.fields()
.get(id)
.is_some_and(FieldTokenProjection::is_key)
});
let value_subject = model.id().kind() != TypeKind::Attribute
|| model.declaration().value_annotations().keys().all(|id| {
id.subject()
== &AnnotationSubjectId::Value(ValueFactId::new(
AttributeId::new(model.id().label().as_str())
.expect("projected attribute label is valid"),
))
});
parent_is_valid
&& direct_sub_is_valid
&& direct_fields_are_valid
&& direct_roles_are_valid
&& direct_plays_are_valid
&& fields_are_valid
&& roles_are_valid
&& role_upcasts_are_valid
&& references_are_valid
&& value_subject
});
let closed_playing = playing_facts.values().all(|playing| {
models.contains_key(playing.id().player())
&& role_exists(playing.role())
&& (!matches!(
target,
BindingTarget::TypeScript | BindingTarget::Rust | BindingTarget::C
) || playing.target_name().is_some())
});
let closed_functions = functions.values().all(|function| {
function
.parameters()
.iter()
.all(|parameter| type_ref_is_valid(parameter.type_ref()))
&& match function.returns() {
FunctionReturnProjection::Scalar(element) => {
type_ref_is_valid(element.type_ref())
}
FunctionReturnProjection::Tuple(elements)
| FunctionReturnProjection::Stream(elements) => elements
.iter()
.all(|element| type_ref_is_valid(element.type_ref())),
}
});
if !closed_models || !closed_playing || !closed_functions {
return Err(invalid_projection(
"invalid_projection_reference",
"projection graph contains an unavailable type, field, role, specialization, reference, or function dependency",
));
}
let content = to_canonical_json(&RuntimeProjectionContentView {
models: &models,
structs: &structs,
functions: &functions,
playing_facts: &playing_facts,
emission: &emission,
})?;
let projection_fingerprint = BindingProjectionFingerprint::compute_with_projection(
target,
&semantic_fingerprint,
&config,
handlers,
resources,
&content,
)?;
let mut generator_handlers = handlers.to_vec();
generator_handlers.sort_by(|left, right| left.id().cmp(right.id()));
let mut code_resources = resources.to_vec();
code_resources.sort_by(|left, right| left.id().cmp(right.id()));
Ok(Self {
target,
config,
semantic_fingerprint,
projection_fingerprint,
generator_handlers,
code_resources,
models,
structs,
functions,
playing_facts,
emission,
})
}
#[must_use]
pub const fn target(&self) -> BindingTarget {
self.target
}
#[must_use]
pub const fn config(&self) -> &ProjectionConfig {
&self.config
}
#[must_use]
pub const fn semantic_fingerprint(&self) -> &SemanticSchemaFingerprint {
&self.semantic_fingerprint
}
#[must_use]
pub const fn projection_fingerprint(&self) -> &BindingProjectionFingerprint {
&self.projection_fingerprint
}
#[must_use]
pub fn projected_token_identity(
&self,
kind: ProjectedTokenKind,
ordinal: u32,
) -> Option<ProjectedTokenIdentity> {
let index = usize::try_from(ordinal).ok()?;
match kind {
ProjectedTokenKind::Model => self
.models
.keys()
.nth(index)
.cloned()
.map(ProjectedTokenIdentity::Model),
ProjectedTokenKind::Field => self
.models
.iter()
.flat_map(|(owner, model)| {
model.query_tokens().fields().keys().map(move |field| {
ProjectedTokenIdentity::Field {
owner: owner.clone(),
field: field.clone(),
}
})
})
.nth(index),
ProjectedTokenKind::Role => self
.models
.iter()
.flat_map(|(owner, model)| {
model.query_tokens().roles().keys().map(move |role| {
ProjectedTokenIdentity::Role {
owner: owner.clone(),
role: role.clone(),
}
})
})
.nth(index),
ProjectedTokenKind::Function => self
.functions
.keys()
.nth(index)
.cloned()
.map(ProjectedTokenIdentity::Function),
ProjectedTokenKind::Struct => self
.structs
.keys()
.nth(index)
.cloned()
.map(ProjectedTokenIdentity::Struct),
ProjectedTokenKind::Attribute => self
.models
.keys()
.filter(|model| model.kind() == TypeKind::Attribute)
.nth(index)
.and_then(|model| AttributeId::new(model.label().as_str()).ok())
.map(ProjectedTokenIdentity::Attribute),
}
}
#[must_use]
pub fn projected_token_ordinal(&self, identity: &ProjectedTokenIdentity) -> Option<u32> {
let index = match identity {
ProjectedTokenIdentity::Model(expected) => self
.models
.keys()
.position(|candidate| candidate == expected),
ProjectedTokenIdentity::Field {
owner: expected_owner,
field: expected_field,
} => self
.models
.iter()
.flat_map(|(owner, model)| {
model
.query_tokens()
.fields()
.keys()
.map(move |field| (owner, field))
})
.position(|(owner, field)| owner == expected_owner && field == expected_field),
ProjectedTokenIdentity::Role {
owner: expected_owner,
role: expected_role,
} => self
.models
.iter()
.flat_map(|(owner, model)| {
model
.query_tokens()
.roles()
.keys()
.map(move |role| (owner, role))
})
.position(|(owner, role)| owner == expected_owner && role == expected_role),
ProjectedTokenIdentity::Function(expected) => self
.functions
.keys()
.position(|candidate| candidate == expected),
ProjectedTokenIdentity::Struct(expected) => self
.structs
.keys()
.position(|candidate| candidate == expected),
ProjectedTokenIdentity::Attribute(expected) => self
.models
.keys()
.filter(|model| model.kind() == TypeKind::Attribute)
.position(|candidate| candidate.label() == expected.label()),
}?;
u32::try_from(index).ok()
}
#[must_use]
pub fn generator_handlers(&self) -> &[ProjectionHandler] {
&self.generator_handlers
}
#[must_use]
pub fn code_resources(&self) -> &[CodeResourceDigest] {
&self.code_resources
}
#[must_use]
pub const fn models(&self) -> &BTreeMap<TypeId, ModelProjection> {
&self.models
}
#[must_use]
pub const fn structs(&self) -> &BTreeMap<StructId, StructProjection> {
&self.structs
}
#[must_use]
pub const fn functions(&self) -> &BTreeMap<FunctionId, FunctionProjection> {
&self.functions
}
#[must_use]
pub const fn playing_facts(&self) -> &BTreeMap<PlaysFactId, PlayingProjection> {
&self.playing_facts
}
#[must_use]
pub const fn emission(&self) -> &EmissionPlan {
&self.emission
}
}
impl CodeResourceDigest {
pub(crate) fn from_wire(
id: impl Into<String>,
content_fingerprint: Fingerprint,
) -> Result<Self, Diagnostic> {
if content_fingerprint.domain().as_str() != CODE_RESOURCE_DOMAIN
|| content_fingerprint.canonicalization().as_str() != RAW_BYTES_CANONICALIZATION
|| content_fingerprint.semantic_profile().is_some()
{
return Err(Diagnostic::stable(
DiagnosticCategory::Integrity,
"invalid_code_resource_fingerprint",
"code resource fingerprint wire metadata is inconsistent",
));
}
Ok(Self {
id: CodeResourceId::new(id)?,
content_fingerprint,
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GeneratedManagerLookup<'a> {
field_name: &'a str,
lookup: &'a str,
}
impl<'a> GeneratedManagerLookup<'a> {
#[must_use]
pub const fn field_name(self) -> &'a str {
self.field_name
}
#[must_use]
pub const fn lookup(self) -> &'a str {
self.lookup
}
}
#[must_use]
pub fn resolve_generated_manager_lookup<'a>(
key: &'a str,
has_field: impl Fn(&str) -> bool,
) -> GeneratedManagerLookup<'a> {
let parsed = key.rsplit_once("__");
match parsed {
Some((field_name, lookup))
if matches!(
lookup,
"eq" | "exact"
| "ne"
| "gt"
| "gte"
| "lt"
| "lte"
| "contains"
| "startswith"
| "endswith"
| "regex"
| "like"
| "in"
| "isnull"
) && has_field(field_name) =>
{
GeneratedManagerLookup { field_name, lookup }
}
_ if has_field(key) => GeneratedManagerLookup {
field_name: key,
lookup: "eq",
},
Some((field_name, lookup)) => GeneratedManagerLookup { field_name, lookup },
None => GeneratedManagerLookup {
field_name: key,
lookup: "eq",
},
}
}