use crate::adapter::cst::MaterialiseError;
use crate::adapter::interface::{self, Attribute, BindingError, Operation, Service};
use crate::adapter::typescript::ts_member_type;
use crate::apply::Applied;
use zerodds_ast::{Ast, Bound, CharWidth, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::Id;
use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
use zerodds_lang_cst::typescript as ts;
pub const RUNTIME_MODULE: &str = "@zerodds/wire";
pub const RESULT_PROPERTY: &str = "result";
const MAX_ALIAS_DEPTH: usize = 64;
pub fn ts_interface_items(ast: &Ast, applied: &Applied) -> Result<Vec<ts::Decl>, BindingError> {
let mut pending: Vec<(Vec<String>, ts::Decl)> = Vec::new();
for service in interface::services(ast)? {
let chain: Vec<String> = interface::module_chain(ast, service.module)?
.into_iter()
.map(|(_, name)| ts::escape_keyword(&name))
.collect();
for decl in decls_of(ast, applied, &service)? {
pending.push((chain.clone(), decl));
}
}
Ok(nest(&pending, 0))
}
fn nest(items: &[(Vec<String>, ts::Decl)], depth: usize) -> Vec<ts::Decl> {
let mut out = Vec::new();
let mut index = 0;
while index < items.len() {
let Some((chain, decl)) = items.get(index) else {
break;
};
let Some(name) = chain.get(depth) else {
out.push(decl.clone());
index += 1;
continue;
};
let mut end = index;
while items
.get(end)
.is_some_and(|(other, _)| other.get(depth) == Some(name))
{
end += 1;
}
let Some(run) = items.get(index..end) else {
break;
};
out.push(ts::Decl::Namespace(ts::Namespace {
doc: Doc::line("An IDL module, as the namespace its declarations nest in."),
exported: true,
declare: false,
name: Ident::new(name),
decls: nest(run, depth + 1),
}));
index = end;
}
out
}
fn decls_of(
ast: &Ast,
applied: &Applied,
service: &Service,
) -> Result<Vec<ts::Decl>, BindingError> {
let name = ts::escape_keyword(&service.name);
let members = signatures(ast, applied, service)?;
Ok(vec![
ts::Decl::Interface(ts::Interface {
doc: Doc::line("The client half of the service. DDS-TS 1.0 §11.1."),
exported: true,
name: Ident::new(&format!("{name}Client")),
generics: Vec::new(),
extends: service
.bases
.iter()
.map(|base| named(&format!("{}Client", ts::escape_keyword(&base.name))))
.collect(),
members: members.clone(),
}),
ts::Decl::Interface(ts::Interface {
doc: Doc::line(
"The handler half of the service: what a service-side implementer satisfies. \
DDS-TS 1.0 §11.1.",
),
exported: true,
name: Ident::new(&format!("{name}Handler")),
generics: Vec::new(),
extends: service
.bases
.iter()
.map(|base| named(&format!("{}Handler", ts::escape_keyword(&base.name))))
.collect(),
members,
}),
descriptor(ast, service, &name)?,
])
}
fn signatures(
ast: &Ast,
applied: &Applied,
service: &Service,
) -> Result<Vec<ts::Signature>, BindingError> {
let mut out = Vec::new();
for operation in &service.operations {
out.push(ts::Signature::Method(operation_signature(
ast, applied, service, operation,
)?));
}
for attribute in &service.attributes {
out.extend(attribute_signatures(ast, applied, service, attribute)?);
}
Ok(out)
}
fn operation_signature(
ast: &Ast,
applied: &Applied,
service: &Service,
operation: &Operation,
) -> Result<ts::MethodSignature, BindingError> {
let mut params = Vec::new();
for param in operation.inputs() {
params.push(ts::Param {
name: Ident::new(&ts::escape_keyword(¶m.name)),
optional: false,
rest: false,
type_: Some(ts_member_type(
ast,
applied,
param.type_,
&at(service, operation, ¶m.name),
)?),
default: None,
});
}
let resolved = if operation.has_outputs() {
let mut fields = Vec::new();
if let Some(target) = operation.returns {
fields.push(property(
RESULT_PROPERTY,
ts_member_type(ast, applied, target, &at(service, operation, "return"))?,
));
}
for param in operation.outputs() {
fields.push(property(
&ts::escape_member(¶m.name),
ts_member_type(
ast,
applied,
param.type_,
&at(service, operation, ¶m.name),
)?,
));
}
ts::Type::Object(fields)
} else if let Some(target) = operation.returns {
ts_member_type(ast, applied, target, &at(service, operation, "return"))?
} else {
ts::Type::Keyword(ts::Keyword::Void)
};
Ok(ts::MethodSignature {
doc: Doc::none(),
name: ts::PropertyName::Ident(Ident::new(&ts::escape_member(&operation.name))),
optional: false,
generics: Vec::new(),
params,
returns: promise_of(resolved),
})
}
fn attribute_signatures(
ast: &Ast,
applied: &Applied,
service: &Service,
attribute: &Attribute,
) -> Result<Vec<ts::Signature>, BindingError> {
let where_ = format!("{}::{}", service.name, attribute.name);
let type_ = ts_member_type(ast, applied, attribute.type_, &where_)?;
let mut out = vec![ts::Signature::Method(ts::MethodSignature {
doc: Doc::line("An attribute's read half. DDS-TS 1.0 §11.3."),
name: ts::PropertyName::Ident(Ident::new(&format!(
"get_{}",
ts::escape_member(&attribute.name)
))),
optional: false,
generics: Vec::new(),
params: Vec::new(),
returns: promise_of(type_.clone()),
})];
if !attribute.readonly {
out.push(ts::Signature::Method(ts::MethodSignature {
doc: Doc::line("An attribute's write half. DDS-TS 1.0 §11.3."),
name: ts::PropertyName::Ident(Ident::new(&format!(
"set_{}",
ts::escape_member(&attribute.name)
))),
optional: false,
generics: Vec::new(),
params: vec![ts::Param {
name: Ident::new("value"),
optional: false,
rest: false,
type_: Some(type_),
default: None,
}],
returns: promise_of(ts::Type::Keyword(ts::Keyword::Void)),
}));
}
Ok(out)
}
fn descriptor(ast: &Ast, service: &Service, name: &str) -> Result<ts::Decl, BindingError> {
let mut operations = Vec::new();
for operation in &service.operations {
operations.push(operation_descriptor(ast, service, operation)?);
}
let mut attributes = Vec::new();
for attribute in &service.attributes {
attributes.push(attribute_descriptor(ast, service, attribute)?);
}
let inherits = service
.bases
.iter()
.map(|base| {
ts::Expr::Path(Path::single(&format!(
"{}Service",
ts::escape_keyword(&base.name)
)))
})
.collect();
Ok(ts::Decl::Variable(ts::Variable {
doc: Doc::line("The service's operation metadata. DDS-TS 1.0 §11.5."),
exported: true,
declare: false,
kind: ts::VarKind::Const,
name: Ident::new(&format!("{name}Service")),
type_: Some(ts::Type::Named {
path: Path::single("ServiceDescriptor"),
args: vec![
named(&format!("{name}Client")),
named(&format!("{name}Handler")),
],
}),
value: Some(ts::Expr::Object(vec![
(
ts::PropertyName::ident("name"),
string(&qualified_name(ast, service)?),
),
(
ts::PropertyName::ident("inherits"),
ts::Expr::Array(inherits),
),
(
ts::PropertyName::ident("operations"),
ts::Expr::Array(operations),
),
(
ts::PropertyName::ident("attributes"),
ts::Expr::Array(attributes),
),
])),
}))
}
fn operation_descriptor(
ast: &Ast,
service: &Service,
operation: &Operation,
) -> Result<ts::Expr, BindingError> {
let mut params = Vec::new();
for param in &operation.params {
params.push(ts::Expr::Object(vec![
(ts::PropertyName::ident("name"), string(¶m.name)),
(
ts::PropertyName::ident("mode"),
string(param.direction.as_str()),
),
(
ts::PropertyName::ident("type"),
type_ref(ast, param.type_, &at(service, operation, ¶m.name))?,
),
]));
}
let returns = match operation.returns {
Some(target) => type_ref(ast, target, &at(service, operation, "return"))?,
None => ts::Expr::Object(vec![(ts::PropertyName::ident("kind"), string("void"))]),
};
Ok(ts::Expr::Object(vec![
(ts::PropertyName::ident("name"), string(&operation.name)),
(
ts::PropertyName::ident("oneway"),
ts::Expr::Literal(Literal::Bool(false)),
),
(ts::PropertyName::ident("returnType"), returns),
(
ts::PropertyName::ident("parameters"),
ts::Expr::Array(params),
),
(ts::PropertyName::ident("raises"), raises(&operation.raises)),
]))
}
fn attribute_descriptor(
ast: &Ast,
service: &Service,
attribute: &Attribute,
) -> Result<ts::Expr, BindingError> {
let where_ = format!("{}::{}", service.name, attribute.name);
Ok(ts::Expr::Object(vec![
(ts::PropertyName::ident("name"), string(&attribute.name)),
(
ts::PropertyName::ident("readonly"),
ts::Expr::Literal(Literal::Bool(attribute.readonly)),
),
(
ts::PropertyName::ident("type"),
type_ref(ast, attribute.type_, &where_)?,
),
(
ts::PropertyName::ident("getRaises"),
raises(&attribute.get_raises),
),
(
ts::PropertyName::ident("setRaises"),
raises(&attribute.set_raises),
),
]))
}
fn raises(list: &[interface::Raise]) -> ts::Expr {
ts::Expr::Array(
list.iter()
.map(|raise| {
ts::Expr::Path(Path::single(&format!(
"{}Type",
ts::escape_keyword(&raise.name)
)))
})
.collect(),
)
}
fn type_ref(ast: &Ast, id: Id<TypeNode>, at: &str) -> Result<ts::Expr, BindingError> {
type_ref_at(ast, id, at, 0)
}
fn type_ref_at(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<ts::Expr, BindingError> {
if depth > MAX_ALIAS_DEPTH {
return Err(BindingError::Materialise(MaterialiseError::Unevaluated {
construct: format!("type reference — it nests past {MAX_ALIAS_DEPTH}"),
at: at.to_string(),
}));
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
let expr = match &node.kind {
TypeKind::Primitive(kind) => ts::Expr::Object(vec![
(ts::PropertyName::ident("kind"), string("primitive")),
(
ts::PropertyName::ident("name"),
string(primitive_name(*kind)),
),
]),
TypeKind::String { width, bound } => {
let mut fields = vec![(ts::PropertyName::ident("kind"), string("string"))];
if let Some(value) = bound_value(ast, *bound, at)? {
fields.push((ts::PropertyName::ident("bound"), number(value)));
}
fields.push((
ts::PropertyName::ident("wide"),
ts::Expr::Literal(Literal::Bool(matches!(width, CharWidth::WChar))),
));
ts::Expr::Object(fields)
}
TypeKind::Sequence { element, bound } => {
let mut fields = vec![(ts::PropertyName::ident("kind"), string("sequence"))];
fields.push((
ts::PropertyName::ident("element"),
element_ref(ast, element.resolved(), at, depth)?,
));
if let Some(value) = bound_value(ast, *bound, at)? {
fields.push((ts::PropertyName::ident("bound"), number(value)));
}
ts::Expr::Object(fields)
}
TypeKind::Array { element, dims } => {
let mut lengths = Vec::new();
for dim in dims {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
})?;
let length = u64::try_from(length).map_err(|_| MaterialiseError::Unevaluated {
construct: format!("array length — it evaluates to {length}"),
at: at.to_string(),
})?;
lengths.push(number(length));
}
ts::Expr::Object(vec![
(ts::PropertyName::ident("kind"), string("array")),
(
ts::PropertyName::ident("element"),
element_ref(ast, element.resolved(), at, depth)?,
),
(ts::PropertyName::ident("lengths"), ts::Expr::Array(lengths)),
])
}
TypeKind::Map { key, value, bound } => {
let mut fields = vec![
(ts::PropertyName::ident("kind"), string("map")),
(
ts::PropertyName::ident("key"),
element_ref(ast, key.resolved(), at, depth)?,
),
(
ts::PropertyName::ident("value"),
element_ref(ast, value.resolved(), at, depth)?,
),
];
if let Some(value) = bound_value(ast, *bound, at)? {
fields.push((ts::PropertyName::ident("bound"), number(value)));
}
ts::Expr::Object(fields)
}
TypeKind::Any => ts::Expr::Object(vec![(ts::PropertyName::ident("kind"), string("any"))]),
TypeKind::Alias { aliased } if node.name.is_empty() => {
element_ref(ast, aliased.resolved(), at, depth)?
}
TypeKind::Alias { .. }
| TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => ts::Expr::Object(vec![
(ts::PropertyName::ident("kind"), string("ref")),
(
ts::PropertyName::ident("name"),
string(&qualified_type_name(ast, id)?),
),
]),
TypeKind::Fixed { .. }
| TypeKind::Native
| TypeKind::ValueBox { .. }
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(BindingError::Materialise(
MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "TypeScript",
at: at.to_string(),
},
));
}
};
Ok(expr)
}
fn bound_value(ast: &Ast, bound: Bound, at: &str) -> Result<Option<u64>, BindingError> {
let Some(expr) = bound.expr() else {
return Ok(None);
};
let value = ast
.eval_integral(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "collection bound".to_string(),
at: at.to_string(),
})?;
let value = u64::try_from(value).map_err(|_| MaterialiseError::Unevaluated {
construct: format!("collection bound — it evaluates to {value}"),
at: at.to_string(),
})?;
Ok(Some(value))
}
fn element_ref(
ast: &Ast,
target: Option<Id<TypeNode>>,
at: &str,
depth: usize,
) -> Result<ts::Expr, BindingError> {
let target = target.ok_or(MaterialiseError::Unevaluated {
construct: "element type".to_string(),
at: at.to_string(),
})?;
type_ref_at(ast, target, at, depth + 1)
}
fn primitive_name(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Boolean => "boolean",
PrimitiveKind::Octet | PrimitiveKind::UInt8 => "octet",
PrimitiveKind::Char => "char",
PrimitiveKind::WChar => "wchar",
PrimitiveKind::Int16 => "int16",
PrimitiveKind::UInt16 => "uint16",
PrimitiveKind::Int32 => "int32",
PrimitiveKind::UInt32 => "uint32",
PrimitiveKind::Int64 => "int64",
PrimitiveKind::UInt64 => "uint64",
PrimitiveKind::Float32 => "float",
PrimitiveKind::Float64 => "double",
PrimitiveKind::Float128 => "longDouble",
PrimitiveKind::Int8 => "int8",
}
}
fn qualified_name(ast: &Ast, service: &Service) -> Result<String, BindingError> {
Ok(scoped(
&interface::module_chain(ast, service.module)?,
&service.name,
))
}
fn qualified_type_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, BindingError> {
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: "a type reference".to_string(),
})?;
Ok(scoped(
&interface::module_chain(ast, node.module)?,
&node.name,
))
}
fn scoped(chain: &[(Id<ModuleNode>, String)], name: &str) -> String {
let mut segments: Vec<&str> = chain.iter().map(|(_, name)| name.as_str()).collect();
segments.push(name);
segments.join("::")
}
fn promise_of(payload: ts::Type) -> ts::Type {
ts::Type::Named {
path: Path::single("Promise"),
args: vec![payload],
}
}
fn named(name: &str) -> ts::Type {
ts::Type::named(Path::single(name))
}
fn property(name: &str, type_: ts::Type) -> ts::Signature {
ts::Signature::Property(ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::Ident(Ident::new(name)),
optional: false,
type_,
})
}
fn string(text: &str) -> ts::Expr {
ts::Expr::Literal(Literal::Str(text.to_string()))
}
fn number(value: u64) -> ts::Expr {
ts::Expr::Literal(Literal::Int(value.to_string()))
}
fn at(service: &Service, operation: &Operation, what: &str) -> String {
format!("{}::{}({what})", service.name, operation.name)
}