use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use type_bridge_contract::codec::to_canonical_json;
use type_bridge_contract::diagnostic::Diagnostic;
use type_bridge_contract::id::{TypeId, TypeKind};
use type_bridge_contract::projection::{
FunctionReturnElementProjection, FunctionReturnProjection, ModelProjection, ProjectedContainer,
ProjectedModelForm, ProjectedModelUse, ProjectedMultiplicity, ProjectedTypeRef,
RuntimeProjection, TYPESCRIPT_MODEL_RESERVED_NAMES as MODEL_RESERVED_NAMES,
};
use type_bridge_contract::schema::OwnsFactId;
use type_bridge_contract::value::ValueTypeTag;
use super::reserved::{
ORDERED_PUBLIC_RUNTIME_NAMES, PUBLIC_RUNTIME_NAMES, PUBLIC_SCHEMA_NAMES, is_typescript_keyword,
};
use crate::{
EmbeddedAuthority, GeneratedPackage, MIGRATION_HISTORY_BUNDLE_RESOURCE,
documentation_annotation, invalid, model_documentation, projection_uses_ordered_collections,
};
macro_rules! canonical_text {
($value:expr) => {
String::from_utf8(to_canonical_json($value)?)
.expect("canonical JSON emitted by the contract is UTF-8")
};
}
macro_rules! identity_literal {
($value:expr) => {{
let canonical = canonical_text!($value);
js_string(&canonical)?
}};
}
pub(super) fn render(
projection: &RuntimeProjection,
authority: &EmbeddedAuthority,
runtime: &[u8],
package_json: &[u8],
tsconfig_json: &[u8],
) -> Result<GeneratedPackage, Diagnostic> {
validate_projection(projection)?;
GeneratedPackage::try_new([
("package.json".to_owned(), package_json.to_vec()),
(
"src/authority.ts".to_owned(),
render_authority(authority)?.into_bytes(),
),
(
"src/functions.ts".to_owned(),
render_functions(projection)?.into_bytes(),
),
(
"src/index.ts".to_owned(),
render_index(projection)?.into_bytes(),
),
(
"src/models.ts".to_owned(),
render_models(projection)?.into_bytes(),
),
(
"src/node-fs.d.ts".to_owned(),
b"declare module \"node:fs\" {\n export function readFileSync(path: URL): Uint8Array;\n}\n"
.to_vec(),
),
("src/runtime.ts".to_owned(), runtime.to_vec()),
(
"src/schema.ts".to_owned(),
render_schema(projection)?.into_bytes(),
),
(
"src/structs.ts".to_owned(),
render_structs(projection)?.into_bytes(),
),
("tsconfig.json".to_owned(), tsconfig_json.to_vec()),
(MIGRATION_HISTORY_BUNDLE_RESOURCE.to_owned(), Vec::new()),
])
}
fn header() -> &'static str {
"// Generated by type-bridge from a verified RuntimeProjection. Do not edit.\n\n"
}
fn render_index(projection: &RuntimeProjection) -> Result<String, Diagnostic> {
let install = if projection_uses_ordered_collections(projection) {
"__installOrderedRuntimeProjectionPackage"
} else {
"__installRuntimeProjectionPackage"
};
let names = projection
.emission()
.model_shells()
.iter()
.map(|id| {
projection
.models()
.get(id)
.map(|model| model.target_name().as_str())
.ok_or_else(|| facet_error("emission plan references an absent model"))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(format!(
"{}import {{ readFileSync as _readFileSync }} from \"node:fs\";\nimport {{ MigrationCatalog, openMigrationCatalog as _openMigrationCatalog }} from \"@type-bridge/node\";\nimport {{ SCHEMA_AUTHORITY_JSON }} from \"./authority.js\";\nimport {{ {install} }} from \"./runtime.js\";\nimport {{ {} }} from \"./models.js\";\nimport {{ PROJECTION_FINGERPRINT_JSON, RUNTIME_PROJECTION_JSON, SEMANTIC_SCHEMA_FINGERPRINT_JSON }} from \"./schema.js\";\n\n{install}(\n RUNTIME_PROJECTION_JSON,\n SEMANTIC_SCHEMA_FINGERPRINT_JSON,\n PROJECTION_FINGERPRINT_JSON,\n [{}],\n SCHEMA_AUTHORITY_JSON,\n);\n\nexport {{ MigrationCatalog }};\nexport const MIGRATION_HISTORY_RESOURCE = \"typebridge/migration-history.json\" as const;\nexport function openMigrationCatalog(): MigrationCatalog {{\n const authority = new TextEncoder().encode(SCHEMA_AUTHORITY_JSON);\n const history = _readFileSync(new URL(`../${{MIGRATION_HISTORY_RESOURCE}}`, import.meta.url));\n return _openMigrationCatalog(authority, history);\n}}\n\nexport * from \"./runtime.js\";\nexport * from \"./models.js\";\nexport * from \"./structs.js\";\nexport * from \"./functions.js\";\nexport * from \"./schema.js\";\n",
header(),
names.join(", "),
names.join(", "),
))
}
fn render_authority(authority: &EmbeddedAuthority) -> Result<String, Diagnostic> {
Ok(format!(
"{}export const SCHEMA_AUTHORITY_JSON = {};\n",
header(),
js_string(&authority.canonical_envelope_json)?,
))
}
fn render_models(projection: &RuntimeProjection) -> Result<String, Diagnostic> {
let mut output = String::from(header());
let ordered = projection_uses_ordered_collections(projection);
let (define_model, model_token) = if ordered {
(
"defineOrderedModel as defineModel",
"type OrderedModelToken as ModelToken",
)
} else {
("defineModel", "type ModelToken")
};
let _ = write!(
output,
"import {{\n defineFieldToken,\n {define_model},\n defineRoleToken,\n type CompleteFacet,\n type FieldToken,\n {model_token},\n type ReferenceFacet,\n type RoleToken,\n}} from \"./runtime.js\";\nimport type * as Structs from \"./structs.js\";\n\n",
);
for id in projection.emission().model_shells() {
let model = projection
.models()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent model"))?;
render_model_shell(&mut output, projection, model)?;
}
for component in projection.emission().model_link_components() {
output.push_str("// Link one dependency component after every model shell exists.\n");
for id in component {
let model = projection
.models()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent model"))?;
render_model_link(&mut output, projection, model)?;
}
output.push('\n');
}
Ok(output)
}
fn render_model_shell(
output: &mut String,
projection: &RuntimeProjection,
model: &ModelProjection,
) -> Result<(), Diagnostic> {
let name = model.target_name().as_str();
let id = identity_literal!(model.id());
let documentation = model_documentation(model);
if let Some(documentation) = &documentation {
render_jsdoc(output, documentation);
}
if let Some(value_type) = model_value_type(model)? {
let value = scalar_type(value_type);
let _ = writeln!(
output,
"export interface {name} extends CompleteFacet<{id}> {{\n readonly value: {value};\n}}\n\nexport type {name}Create = {value};\n"
);
} else {
let _ = writeln!(
output,
"export interface {name} extends CompleteFacet<{id}> {{"
);
for field in model.complete_read().fields() {
let token = model
.query_tokens()
.fields()
.get(field.token())
.ok_or_else(|| facet_error("complete-read field has no query token"))?;
let value = projected_type(projection, field.value(), "", "Structs.")?;
let value = apply_multiplicity(value, field.multiplicity(), Position::Read);
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}: {value};",
token.target_name().as_str()
);
}
for (role, read) in model.complete_read().roles() {
let token = model
.query_tokens()
.roles()
.get(role)
.ok_or_else(|| facet_error("complete-read role has no query token"))?;
let value = projected_union(projection, read.players(), "")?;
let value = apply_multiplicity(value, read.multiplicity(), Position::Read);
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}: {value};",
token.target_name().as_str()
);
}
output.push_str("}\n\n");
let _ = writeln!(output, "export interface {name}Create {{");
for field in model.create().fields() {
let token = model
.query_tokens()
.fields()
.get(field.token())
.ok_or_else(|| facet_error("create field has no query token"))?;
let value = projected_type(projection, field.value(), "", "Structs.")?;
let value = apply_multiplicity(value, field.multiplicity(), Position::Create);
let optional = if field.multiplicity().required() {
""
} else {
"?"
};
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}{optional}: {value};",
token.target_name().as_str()
);
}
for (role, create) in model.create().roles() {
let token = model
.query_tokens()
.roles()
.get(role)
.ok_or_else(|| facet_error("create role has no query token"))?;
let value = projected_union(projection, create.players(), "")?;
let value = apply_multiplicity(value, create.multiplicity(), Position::Create);
let optional = if create.multiplicity().required() {
""
} else {
"?"
};
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}{optional}: {value};",
token.target_name().as_str()
);
}
output.push_str("}\n\n");
}
if let Some(reference_name) = model.reference_read().target_name() {
let _ = writeln!(output, "export interface {name}ReferenceInput {{");
for key in model.reference_read().key_fields() {
let token = model
.query_tokens()
.fields()
.get(key)
.ok_or_else(|| facet_error("reference key has no query token"))?;
let read = read_field(model, key)?;
let value = projected_type(projection, read.value(), "", "Structs.")?;
let value = apply_multiplicity(value, read.multiplicity(), Position::Read);
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}: {value};",
token.target_name().as_str()
);
}
let reference_name = reference_name.as_str();
output.push_str("}\n\n");
if let Some(documentation) = &documentation {
render_jsdoc(output, documentation);
}
let _ = writeln!(
output,
"export interface {reference_name} extends ReferenceFacet<{id}>, {name}ReferenceInput {{}}\n"
);
}
let _ = writeln!(output, "export interface {name}FieldTokens {{");
for (owns, token) in model.query_tokens().fields() {
let value = projected_type(projection, field_value(model, owns)?, "", "Structs.")?;
let attribute = identity_literal!(owns.attribute());
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}: FieldToken<{id}, {attribute}, {value}>;",
token.target_name().as_str()
);
}
output.push_str("}\n\n");
let _ = writeln!(output, "export interface {name}RoleTokens {{");
for token in model.query_tokens().roles().values() {
let role = identity_literal!(token.role());
let players = logical_player_union(projection, token.accepted_players(), "")?;
let subtype_roots = logical_player_union(
projection,
&compatible_subtype_roots(projection, token.accepted_players())?,
"",
)?;
if let Some(documentation) = documentation_annotation(token.annotations()) {
render_indented_jsdoc(output, " ", documentation);
}
let _ = writeln!(
output,
" readonly {}: RoleToken<{id}, {role}, {players}, {subtype_roots}>;",
token.target_name().as_str()
);
}
output.push_str("}\n\n");
let create_factory = if model.create().enabled() {
if model_value_type(model)?.is_some() {
format!("(value: {name}Create) => {name}")
} else {
format!("(input: {name}Create) => {name}")
}
} else {
"undefined".to_owned()
};
let reference_factory = match model.reference_read().target_name() {
Some(reference) => format!(
"(iid: string | null, keys: {name}ReferenceInput) => {}",
reference.as_str()
),
None => "undefined".to_owned(),
};
let _ = writeln!(
output,
"export let {name}: ModelToken<{id}, {name}, {create_factory}, {reference_factory}, {name}FieldTokens, {name}RoleTokens>;\n"
);
Ok(())
}
fn render_model_link(
output: &mut String,
projection: &RuntimeProjection,
model: &ModelProjection,
) -> Result<(), Diagnostic> {
let name = model.target_name().as_str();
let id = identity_literal!(model.id());
let create_factory = if model.create().enabled() {
if model_value_type(model)?.is_some() {
format!("(value: {name}Create) => {name}")
} else {
format!("(input: {name}Create) => {name}")
}
} else {
"undefined".to_owned()
};
let reference_factory = match model.reference_read().target_name() {
Some(reference) => format!(
"(iid: string | null, keys: {name}ReferenceInput) => {}",
reference.as_str()
),
None => "undefined".to_owned(),
};
let _ = writeln!(
output,
"{name} = defineModel<{id}, {name}, {create_factory}, {reference_factory}, {name}FieldTokens, {name}RoleTokens>({{"
);
let _ = writeln!(output, " typeKey: {id},");
let _ = writeln!(output, " id: {},", canonical_text!(model.id()));
let _ = writeln!(output, " name: {},", js_string(name)?);
let value_type = match model_value_type(model)? {
Some(value_type) => canonical_text!(&value_type),
None => "null".to_owned(),
};
let _ = writeln!(output, " valueType: {value_type},");
output.push_str(" fields: {\n");
for (owns, token) in model.query_tokens().fields() {
let value = projected_type(projection, field_value(model, owns)?, "", "Structs.")?;
let attribute = identity_literal!(owns.attribute());
let metadata = canonical_text!(token);
let _ = writeln!(
output,
" {}: defineFieldToken<{id}, {attribute}, {value}>({{",
token.target_name().as_str()
);
let _ = writeln!(output, " owner: {id},");
let _ = writeln!(output, " attribute: {attribute},");
let _ = writeln!(
output,
" name: {},",
js_string(token.target_name().as_str())?
);
let _ = writeln!(
output,
" multiplicity: {},",
canonical_text!(&token.multiplicity())
);
let _ = writeln!(output, " key: {},", token.is_key());
let _ = writeln!(output, " unique: {},", token.is_unique());
let _ = writeln!(output, " metadata: {metadata},\n }}),");
}
output.push_str(" },\n roles: {\n");
for token in model.query_tokens().roles().values() {
let role = identity_literal!(token.role());
let players = logical_player_union(projection, token.accepted_players(), "")?;
let subtype_roots = logical_player_union(
projection,
&compatible_subtype_roots(projection, token.accepted_players())?,
"",
)?;
let accepted = token
.accepted_players()
.iter()
.map(|player| Ok(identity_literal!(player)))
.collect::<Result<Vec<_>, Diagnostic>>()?
.join(", ");
let metadata = canonical_text!(token);
let _ = writeln!(
output,
" {}: defineRoleToken<{id}, {role}, {players}, {subtype_roots}>({{",
token.target_name().as_str()
);
let _ = writeln!(output, " owner: {id},");
let _ = writeln!(output, " role: {role},");
let _ = writeln!(
output,
" name: {},",
js_string(token.target_name().as_str())?
);
let _ = writeln!(output, " acceptedPlayers: [{accepted}],");
let _ = writeln!(
output,
" specializes: {},",
canonical_text!(&token.specializes())
);
let _ = writeln!(
output,
" multiplicity: {},",
canonical_text!(&token.multiplicity())
);
let _ = writeln!(output, " abstract: {},", token.is_abstract());
let _ = writeln!(output, " metadata: {metadata},\n }}),");
}
output.push_str(" },\n completeMembers: [\n");
for field in model.complete_read().fields() {
let token = model
.query_tokens()
.fields()
.get(field.token())
.ok_or_else(|| facet_error("complete-read field has no query token"))?;
let _ = writeln!(
output,
" {{ name: {}, multiplicity: {} }},",
js_string(token.target_name().as_str())?,
canonical_text!(&field.multiplicity())
);
}
for (role, read) in model.complete_read().roles() {
let token = model
.query_tokens()
.roles()
.get(role)
.ok_or_else(|| facet_error("complete-read role has no query token"))?;
let accepted = read
.players()
.iter()
.map(model_use_literal)
.collect::<Result<Vec<_>, _>>()?
.join(", ");
let _ = writeln!(
output,
" {{ name: {}, multiplicity: {}, accepted: [{accepted}] }},",
js_string(token.target_name().as_str())?,
canonical_text!(&read.multiplicity())
);
}
let _ = writeln!(
output,
" ],\n createEnabled: {},",
model.create().enabled()
);
output.push_str(" createMembers: [\n");
for field in model.create().fields() {
let token = model
.query_tokens()
.fields()
.get(field.token())
.ok_or_else(|| facet_error("create field has no query token"))?;
let _ = writeln!(
output,
" {{ name: {}, multiplicity: {} }},",
js_string(token.target_name().as_str())?,
canonical_text!(&field.multiplicity())
);
}
for (role, create) in model.create().roles() {
let token = model
.query_tokens()
.roles()
.get(role)
.ok_or_else(|| facet_error("create role has no query token"))?;
let accepted = create
.players()
.iter()
.map(model_use_literal)
.collect::<Result<Vec<_>, _>>()?
.join(", ");
let _ = writeln!(
output,
" {{ name: {}, multiplicity: {}, accepted: [{accepted}] }},",
js_string(token.target_name().as_str())?,
canonical_text!(&create.multiplicity())
);
}
output.push_str(" ],\n");
let reference_enabled = model.reference_read().target_name().is_some();
let _ = writeln!(output, " referenceEnabled: {reference_enabled},");
let keys = model
.reference_read()
.key_fields()
.iter()
.map(|key| {
model
.query_tokens()
.fields()
.get(key)
.ok_or_else(|| facet_error("reference key has no query token"))
.and_then(|token| js_string(token.target_name().as_str()))
})
.collect::<Result<Vec<_>, _>>()?
.join(", ");
let _ = writeln!(output, " referenceKeys: [{keys}],");
let _ = writeln!(output, " metadata: {},\n}});", canonical_text!(model));
Ok(())
}
fn render_structs(projection: &RuntimeProjection) -> Result<String, Diagnostic> {
let mut output = String::from(header());
output.push_str(
"import { defineStruct, type StructFactory, type StructValue } from \"./runtime.js\";\n\n",
);
for id in projection.emission().structs() {
let structure = projection
.structs()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent struct"))?;
let name = structure.target_name().as_str();
let id = TypeId::new(TypeKind::Struct, structure.id().label().as_str())?;
let id = identity_literal!(&id);
let _ = writeln!(
output,
"export interface {name} extends StructValue<{id}> {{"
);
for field in structure.fields() {
let value = scalar_type(field.value_type());
let value = if field.optional() {
format!("{value} | null")
} else {
value.to_owned()
};
let _ = writeln!(
output,
" readonly {}: {value};",
field.target_name().as_str()
);
}
let _ = writeln!(output, "}}\n\nexport interface {name}Input {{");
for field in structure.fields() {
let value = scalar_type(field.value_type());
let value = if field.optional() {
format!("{value} | null")
} else {
value.to_owned()
};
let optional = if field.optional() { "?" } else { "" };
let _ = writeln!(
output,
" readonly {}{optional}: {value};",
field.target_name().as_str()
);
}
let _ = writeln!(
output,
"}}\n\nexport const {name}: StructFactory<{id}, {name}, {name}Input> = defineStruct({{\n id: {id},\n fields: ["
);
for field in structure.fields() {
let _ = writeln!(
output,
" {{ name: {}, valueType: {}, optional: {} }},",
js_string(field.target_name().as_str())?,
canonical_text!(&field.value_type()),
field.optional()
);
}
let _ = writeln!(
output,
" ],\n metadata: {},\n}});\n",
canonical_text!(structure)
);
}
Ok(output)
}
fn render_functions(projection: &RuntimeProjection) -> Result<String, Diagnostic> {
let mut output = String::from(header());
output.push_str(
"import { defineFunctionToken, type BoundVar, type FunctionCall, type FunctionInput, type FunctionToken, type QueryMatchMode, QuerySession, RemoteQuerySession } from \"./runtime.js\";\nimport type * as Models from \"./models.js\";\nimport type * as Structs from \"./structs.js\";\n\n",
);
let mut emitted_inputs = BTreeSet::new();
let mut emitted_calls = BTreeSet::new();
for id in projection.emission().functions() {
let function = projection
.functions()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent function"))?;
let name = function.target_name().as_str();
let id = js_string(function.id().label().as_str())?;
let arguments = if function.parameters().is_empty() {
"readonly []".to_owned()
} else {
format!(
"readonly [{}]",
function
.parameters()
.iter()
.map(|parameter| {
Ok(format!(
"{}: {}",
parameter.target_name().as_str(),
projected_type(
projection,
parameter.type_ref(),
"Models.",
"Structs."
)?
))
})
.collect::<Result<Vec<_>, Diagnostic>>()?
.join(", ")
)
};
let returns = function_return(projection, function.returns())?;
let supported_return = match function.returns() {
FunctionReturnProjection::Scalar(element) if !element.optional() => {
if let ProjectedTypeRef::Scalar(domain) = element.type_ref() {
Some(*domain)
} else {
None
}
}
_ => None,
};
let supported = supported_return.is_some()
&& function.parameters().iter().all(|parameter| {
matches!(
parameter.type_ref(),
ProjectedTypeRef::Model(_) | ProjectedTypeRef::Scalar(_)
)
});
let token_name = if supported {
format!("__tb{}Token", upper_first(function.target_name().as_str()))
} else {
function.target_name().as_str().to_owned()
};
if let Some(documentation) = documentation_annotation(function.annotations()) {
render_jsdoc(&mut output, documentation);
}
let _ = writeln!(
output,
"{}const {token_name}: FunctionToken<{id}, {arguments}, {returns}> = defineFunctionToken({{\n id: {id},\n name: {},\n metadata: {},\n}});\n",
if supported { "" } else { "export " },
js_string(name)?,
canonical_text!(function)
);
let Some(return_domain) = supported_return.filter(|_| supported) else {
continue;
};
let call_alias = format!("{}Call", scalar_domain_name(return_domain));
if emitted_calls.insert(call_alias.clone()) {
let _ = writeln!(
output,
"export type {call_alias} = FunctionCall<{}>;",
scalar_type(return_domain)
);
}
for domain in
function
.parameters()
.iter()
.filter_map(|parameter| match parameter.type_ref() {
ProjectedTypeRef::Scalar(domain) => Some(*domain),
_ => None,
})
{
let alias = format!("{}Input", scalar_domain_name(domain));
if emitted_inputs.insert(alias.clone()) {
let constructor = format!("{}Input", scalar_domain_function_name(domain));
let scalar = scalar_type(domain);
let _ = writeln!(
output,
"export type {alias} = FunctionInput<{scalar}>;\nexport function {constructor}<Value extends {{ readonly value: {scalar} }}>(session: QuerySession | RemoteQuerySession, value: Value): {alias} {{\n return session.__functionInput(value);\n}}"
);
}
}
let mut generics = Vec::new();
let mut parameters = vec!["session: QuerySession | RemoteQuerySession".to_owned()];
let mut arguments_expr = Vec::new();
for (index, parameter) in function.parameters().iter().enumerate() {
let parameter_name = parameter.target_name().as_str();
match parameter.type_ref() {
ProjectedTypeRef::Model(model_use) => {
let model_name = projection
.models()
.get(model_use.id())
.ok_or_else(|| facet_error("function parameter references absent model"))?
.target_name()
.as_str();
generics.push(format!("Model{index} extends Models.{model_name}"));
generics.push(format!("Mode{index} extends QueryMatchMode"));
parameters.push(format!(
"{parameter_name}: BoundVar<Model{index}, Mode{index}>"
));
}
ProjectedTypeRef::Scalar(domain) => parameters.push(format!(
"{parameter_name}: {}Input | FunctionCall<{}>",
scalar_domain_name(*domain),
scalar_type(*domain)
)),
ProjectedTypeRef::Struct(_) => unreachable!("struct parameters were filtered"),
}
arguments_expr.push(parameter_name.to_owned());
}
let generic_clause = if generics.is_empty() {
String::new()
} else {
format!("<{}>", generics.join(", "))
};
let _ = writeln!(
output,
"export function {name}{generic_clause}({}): {call_alias} {{\n return session.__callFunction({token_name}, [{}]);\n}}\n",
parameters.join(", "),
arguments_expr.join(", "),
);
}
Ok(output)
}
fn scalar_domain_name(tag: ValueTypeTag) -> &'static str {
match tag {
ValueTypeTag::String => "String",
ValueTypeTag::Long => "Integer",
ValueTypeTag::Double => "Double",
ValueTypeTag::Boolean => "Boolean",
ValueTypeTag::Date => "Date",
ValueTypeTag::DateTime => "DateTime",
ValueTypeTag::DateTimeTz => "DateTimeTz",
ValueTypeTag::Decimal => "Decimal",
ValueTypeTag::Duration => "Duration",
}
}
fn scalar_domain_function_name(tag: ValueTypeTag) -> &'static str {
match tag {
ValueTypeTag::String => "string",
ValueTypeTag::Long => "integer",
ValueTypeTag::Double => "double",
ValueTypeTag::Boolean => "boolean",
ValueTypeTag::Date => "date",
ValueTypeTag::DateTime => "dateTime",
ValueTypeTag::DateTimeTz => "dateTimeTz",
ValueTypeTag::Decimal => "decimal",
ValueTypeTag::Duration => "duration",
}
}
fn upper_first(value: &str) -> String {
let mut characters = value.chars();
characters.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(characters).collect()
})
}
fn render_schema(projection: &RuntimeProjection) -> Result<String, Diagnostic> {
let mut output = String::from(header());
output.push_str("import { definePlaysToken } from \"./runtime.js\";\n\n");
for playing in projection.playing_facts().values() {
let name = playing
.target_name()
.ok_or_else(|| facet_error("TypeScript playing token has no target name"))?
.as_str();
let player = identity_literal!(playing.id().player());
let role = identity_literal!(playing.role());
if let Some(documentation) = documentation_annotation(playing.annotations()) {
render_jsdoc(&mut output, documentation);
}
let _ = writeln!(
output,
"export const {name} = definePlaysToken<{player}, {role}>({{\n player: {player},\n role: {role},\n name: {},\n multiplicity: {},\n metadata: {},\n}});\n",
js_string(name)?,
canonical_text!(&playing.multiplicity()),
canonical_text!(playing)
);
}
output.push_str("export const PLAYING_FACTS = Object.freeze([\n");
for playing in projection.playing_facts().values() {
let name = playing
.target_name()
.ok_or_else(|| facet_error("TypeScript playing token has no target name"))?
.as_str();
let _ = writeln!(output, " {name},");
}
output.push_str("]);\n\n");
let semantic = canonical_text!(projection.semantic_fingerprint());
let binding = canonical_text!(projection.projection_fingerprint());
let runtime = canonical_text!(projection);
let _ = writeln!(
output,
"export const SEMANTIC_SCHEMA_FINGERPRINT_JSON = {};\nexport const PROJECTION_FINGERPRINT_JSON = {};\nexport const RUNTIME_PROJECTION_JSON = {};\nexport const SEMANTIC_SCHEMA_FINGERPRINT = Object.freeze(JSON.parse(SEMANTIC_SCHEMA_FINGERPRINT_JSON));\nexport const PROJECTION_FINGERPRINT = Object.freeze(JSON.parse(PROJECTION_FINGERPRINT_JSON));",
js_string(&semantic)?,
js_string(&binding)?,
js_string(&runtime)?
);
Ok(output)
}
fn read_field<'a>(
model: &'a ModelProjection,
id: &OwnsFactId,
) -> Result<&'a type_bridge_contract::projection::ReadFieldProjection, Diagnostic> {
model
.complete_read()
.fields()
.iter()
.find(|field| field.token() == id)
.ok_or_else(|| facet_error("reference or query field has no complete-read value"))
}
fn field_value<'a>(
model: &'a ModelProjection,
id: &OwnsFactId,
) -> Result<&'a ProjectedTypeRef, Diagnostic> {
if let Some(read) = model
.complete_read()
.fields()
.iter()
.find(|field| field.token() == id)
{
return Ok(read.value());
}
model
.create()
.fields()
.iter()
.find(|field| field.token() == id)
.map(type_bridge_contract::projection::CreateFieldProjection::value)
.ok_or_else(|| facet_error("query field has no create or complete-read value"))
}
fn projected_type(
projection: &RuntimeProjection,
value: &ProjectedTypeRef,
model_prefix: &str,
struct_prefix: &str,
) -> Result<String, Diagnostic> {
match value {
ProjectedTypeRef::Scalar(tag) => Ok(scalar_type(*tag).to_owned()),
ProjectedTypeRef::Model(model) => {
let target = projection
.models()
.get(model.id())
.ok_or_else(|| facet_error("projected type references an absent model"))?;
let name = match model.form() {
ProjectedModelForm::Complete => target.target_name(),
ProjectedModelForm::Reference => {
target.reference_read().target_name().ok_or_else(|| {
facet_error("reference use targets a model without a reference facet")
})?
}
};
Ok(format!("{model_prefix}{}", name.as_str()))
}
ProjectedTypeRef::Struct(id) => projection
.structs()
.get(id)
.map(|structure| format!("{struct_prefix}{}", structure.target_name().as_str()))
.ok_or_else(|| facet_error("projected type references an absent struct")),
}
}
fn projected_union(
projection: &RuntimeProjection,
values: &BTreeSet<ProjectedModelUse>,
prefix: &str,
) -> Result<String, Diagnostic> {
if values.is_empty() {
return Ok("never".to_owned());
}
values
.iter()
.map(|value| {
projected_type(
projection,
&ProjectedTypeRef::Model(value.clone()),
prefix,
"Structs.",
)
})
.collect::<Result<Vec<_>, _>>()
.map(|values| values.join(" | "))
}
fn logical_player_union(
projection: &RuntimeProjection,
values: &BTreeSet<TypeId>,
prefix: &str,
) -> Result<String, Diagnostic> {
if values.is_empty() {
return Ok("never".to_owned());
}
values
.iter()
.map(|id| {
projection
.models()
.get(id)
.map(|model| format!("{prefix}{}", model.target_name().as_str()))
.ok_or_else(|| facet_error("role token references an absent player model"))
})
.collect::<Result<Vec<_>, _>>()
.map(|values| values.join(" | "))
}
fn compatible_subtype_roots(
projection: &RuntimeProjection,
accepted_players: &BTreeSet<TypeId>,
) -> Result<BTreeSet<TypeId>, Diagnostic> {
let mut roots = BTreeSet::new();
for player in accepted_players {
let projected = projection
.models()
.get(player)
.ok_or_else(|| facet_error("role token references an absent player model"))?;
roots.insert(player.clone());
roots.extend(projected.complete_read().nominal_upcasts().iter().cloned());
}
Ok(roots)
}
fn model_use_literal(value: &ProjectedModelUse) -> Result<String, Diagnostic> {
let type_key = identity_literal!(value.id());
let form = match value.form() {
ProjectedModelForm::Complete => "complete",
ProjectedModelForm::Reference => "reference",
};
Ok(format!("{{ typeKey: {type_key}, form: \"{form}\" }}"))
}
fn function_return(
projection: &RuntimeProjection,
returns: &FunctionReturnProjection,
) -> Result<String, Diagnostic> {
match returns {
FunctionReturnProjection::Scalar(value) => return_element(projection, value),
FunctionReturnProjection::Tuple(values) => Ok(format!(
"readonly [{}]",
values
.iter()
.map(|value| return_element(projection, value))
.collect::<Result<Vec<_>, _>>()?
.join(", ")
)),
FunctionReturnProjection::Stream(values) => {
let values = values
.iter()
.map(|value| return_element(projection, value))
.collect::<Result<Vec<_>, _>>()?;
let row = if values.len() == 1 {
values[0].clone()
} else {
format!("readonly [{}]", values.join(", "))
};
Ok(format!("AsyncIterable<{row}>"))
}
}
}
fn return_element(
projection: &RuntimeProjection,
element: &FunctionReturnElementProjection,
) -> Result<String, Diagnostic> {
let value = projected_type(projection, element.type_ref(), "Models.", "Structs.")?;
Ok(if element.optional() {
format!("{value} | null")
} else {
value
})
}
#[derive(Clone, Copy)]
enum Position {
Create,
Read,
}
fn apply_multiplicity(
value: String,
multiplicity: ProjectedMultiplicity,
_position: Position,
) -> String {
match (multiplicity.container(), multiplicity.required()) {
(ProjectedContainer::Scalar, true) => value,
(ProjectedContainer::Scalar, false) => format!("{value} | null"),
(ProjectedContainer::Sequence, _) => format!("readonly ({value})[]"),
}
}
fn scalar_type(tag: ValueTypeTag) -> &'static str {
match tag {
ValueTypeTag::String => "string",
ValueTypeTag::Long => "bigint",
ValueTypeTag::Double => "number",
ValueTypeTag::Boolean => "boolean",
ValueTypeTag::Date | ValueTypeTag::DateTime | ValueTypeTag::DateTimeTz => "Date",
ValueTypeTag::Decimal | ValueTypeTag::Duration => "string",
}
}
fn model_value_type(model: &ModelProjection) -> Result<Option<ValueTypeTag>, Diagnostic> {
match (model.id().kind(), model.declaration().value_type()) {
(TypeKind::Attribute, Some(value_type)) => Ok(Some(value_type)),
(TypeKind::Attribute, None) => Err(facet_error("attribute model has no scalar value type")),
(_, None) => Ok(None),
(_, Some(_)) => Err(facet_error("non-attribute model has a scalar value type")),
}
}
fn js_string(value: &str) -> Result<String, Diagnostic> {
Ok(String::from_utf8(to_canonical_json(&value)?)
.expect("canonical JSON emitted by the contract is UTF-8"))
}
fn render_jsdoc(output: &mut String, documentation: &str) {
render_indented_jsdoc(output, "", documentation);
}
fn render_indented_jsdoc(output: &mut String, indentation: &str, documentation: &str) {
let _ = writeln!(output, "{indentation}/**");
for line in documentation.split('\n') {
let _ = writeln!(output, "{indentation} * {}", safe_jsdoc_line(line));
}
let _ = writeln!(output, "{indentation} */");
}
fn safe_jsdoc_line(line: &str) -> String {
let mut escaped = String::new();
for character in line.chars() {
match character {
'\r' => escaped.push_str("\\r"),
character if character.is_control() && character != '\t' => {
let _ = write!(escaped, "\\u{{{:x}}}", u32::from(character));
}
character => escaped.push(character),
}
}
escaped.replace("*/", "*\\/")
}
fn validate_projection(projection: &RuntimeProjection) -> Result<(), Diagnostic> {
let mut public = BTreeMap::<String, String>::new();
for name in PUBLIC_RUNTIME_NAMES.iter().chain(PUBLIC_SCHEMA_NAMES) {
public.insert((*name).to_owned(), "generated runtime".to_owned());
}
if projection_uses_ordered_collections(projection) {
for name in ORDERED_PUBLIC_RUNTIME_NAMES {
public.insert((*name).to_owned(), "generated ordered runtime".to_owned());
}
}
for id in projection.emission().model_shells() {
let model = projection
.models()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent model"))?;
let name = model.target_name().as_str();
register_name(&mut public, name, "model")?;
register_name(&mut public, &format!("{name}Create"), "create facet")?;
register_name(
&mut public,
&format!("{name}FieldTokens"),
"field-token facet",
)?;
register_name(
&mut public,
&format!("{name}RoleTokens"),
"role-token facet",
)?;
if let Some(reference) = model.reference_read().target_name() {
register_name(&mut public, reference.as_str(), "reference facet")?;
register_name(
&mut public,
&format!("{name}ReferenceInput"),
"reference input",
)?;
}
validate_model_names(model)?;
}
for id in projection.emission().structs() {
let structure = projection
.structs()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent struct"))?;
let name = structure.target_name().as_str();
register_name(&mut public, name, "struct")?;
register_name(&mut public, &format!("{name}Input"), "struct input")?;
let mut fields = BTreeSet::new();
for field in structure.fields() {
validate_identifier(field.target_name().as_str())?;
if !fields.insert(field.target_name().as_str()) {
return Err(name_error("projected struct field names collide"));
}
}
}
for id in projection.emission().functions() {
let function = projection
.functions()
.get(id)
.ok_or_else(|| facet_error("emission plan references an absent function"))?;
register_name(&mut public, function.target_name().as_str(), "function")?;
let mut parameters = BTreeSet::new();
for parameter in function.parameters() {
validate_identifier(parameter.target_name().as_str())?;
if !parameters.insert(parameter.target_name().as_str()) {
return Err(name_error("projected function parameter names collide"));
}
}
}
for playing in projection.playing_facts().values() {
let name = playing
.target_name()
.ok_or_else(|| facet_error("TypeScript playing token has no target name"))?;
register_name(&mut public, name.as_str(), "playing token")?;
}
Ok(())
}
fn validate_model_names(model: &ModelProjection) -> Result<(), Diagnostic> {
let mut members = MODEL_RESERVED_NAMES
.iter()
.copied()
.collect::<BTreeSet<_>>();
for token in model.query_tokens().fields().values() {
validate_identifier(token.target_name().as_str())?;
if !members.insert(token.target_name().as_str()) {
return Err(name_error(
"projected model field collides with a reserved member",
));
}
}
for token in model.query_tokens().roles().values() {
validate_identifier(token.target_name().as_str())?;
if !members.insert(token.target_name().as_str()) {
return Err(name_error(
"projected model role collides with a reserved member",
));
}
}
Ok(())
}
fn validate_identifier(value: &str) -> Result<(), Diagnostic> {
if is_typescript_keyword(value) {
return Err(name_error(format!(
"TypeScript strict-mode keyword {value} cannot be emitted"
)));
}
Ok(())
}
fn register_name(
names: &mut BTreeMap<String, String>,
name: &str,
kind: &str,
) -> Result<(), Diagnostic> {
validate_identifier(name)?;
if let Some(previous) = names.insert(name.to_owned(), kind.to_owned()) {
return Err(name_error(format!(
"public name {name} collides between {previous} and {kind}"
)));
}
Ok(())
}
fn facet_error(message: &'static str) -> Diagnostic {
invalid("typescript_emitter_facet_mismatch", message)
}
fn name_error(message: impl Into<String>) -> Diagnostic {
invalid("typescript_emitter_name_collision", message)
}