use std::collections::HashSet;
use facet_core::{ScalarType, Shape};
use vox_types::{
EnumInfo, ServiceDescriptor, ShapeKind, StructInfo, VariantKind, classify_shape,
classify_variant, is_bytes,
};
fn transparent_named_alias(shape: &'static Shape) -> Option<(&'static str, &'static Shape)> {
if !shape.is_transparent() {
return None;
}
let name = extract_type_name(shape.type_identifier)?;
let inner = shape.inner?;
Some((name, inner))
}
fn extract_type_name(type_identifier: &'static str) -> Option<&'static str> {
if type_identifier.is_empty()
|| type_identifier.starts_with('(')
|| type_identifier.starts_with('[')
{
return None;
}
Some(type_identifier)
}
pub fn ts_field_access(expr: &str, field_name: &str) -> String {
if field_name
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
{
format!("{expr}[{field_name}]")
} else {
format!("{expr}.{field_name}")
}
}
pub fn collect_named_types(service: &ServiceDescriptor) -> Vec<(String, &'static Shape)> {
let mut seen = HashSet::new();
let mut types = Vec::new();
fn visit(
shape: &'static Shape,
seen: &mut HashSet<String>,
types: &mut Vec<(String, &'static Shape)>,
) {
if let Some((name, inner)) = transparent_named_alias(shape) {
if !seen.contains(name) {
seen.insert(name.to_string());
visit(inner, seen, types);
types.push((name.to_string(), shape));
}
return;
}
match classify_shape(shape) {
ShapeKind::Struct(StructInfo {
name: Some(name),
fields,
..
}) if seen.insert(name.to_string()) => {
for field in fields {
visit(field.shape(), seen, types);
}
types.push((name.to_string(), shape));
}
ShapeKind::Enum(EnumInfo {
name: Some(name),
variants,
}) if seen.insert(name.to_string()) => {
for variant in variants {
match classify_variant(variant) {
VariantKind::Newtype { inner } => visit(inner, seen, types),
VariantKind::Struct { fields } | VariantKind::Tuple { fields } => {
for field in fields {
visit(field.shape(), seen, types);
}
}
VariantKind::Unit => {}
}
}
types.push((name.to_string(), shape));
}
ShapeKind::List { element } => visit(element, seen, types),
ShapeKind::Option { inner } => visit(inner, seen, types),
ShapeKind::Array { element, .. } => visit(element, seen, types),
ShapeKind::Map { key, value } => {
visit(key, seen, types);
visit(value, seen, types);
}
ShapeKind::Set { element } => visit(element, seen, types),
ShapeKind::Tuple { elements } => {
for param in elements {
visit(param.shape, seen, types);
}
}
ShapeKind::Tx { inner } | ShapeKind::Rx { inner } => visit(inner, seen, types),
ShapeKind::Pointer { pointee } => visit(pointee, seen, types),
ShapeKind::Result { ok, err } => {
visit(ok, seen, types);
visit(err, seen, types);
}
_ => {}
}
}
for method in service.methods {
for arg in method.args {
visit(arg.shape, &mut seen, &mut types);
}
visit(method.return_shape, &mut seen, &mut types);
}
types
}
pub fn generate_named_types(named_types: &[(String, &'static Shape)]) -> String {
let mut out = String::new();
if named_types.is_empty() {
return out;
}
out.push_str("// Named type definitions\n");
for (name, shape) in named_types {
if let Some((_, inner)) = transparent_named_alias(shape) {
out.push_str(&format!(
"export type {name} = {};\n\n",
ts_type_base_named(inner)
));
continue;
}
match classify_shape(shape) {
ShapeKind::Struct(StructInfo { fields, .. }) => {
out.push_str(&format!("export interface {} {{\n", name));
for field in fields {
out.push_str(&format!(
" {}: {};\n",
field.name,
ts_type_base_named(field.shape())
));
}
out.push_str("}\n\n");
}
ShapeKind::Enum(EnumInfo { variants, .. }) => {
out.push_str(&format!("export type {} =\n", name));
for (i, variant) in variants.iter().enumerate() {
let variant_type = match classify_variant(variant) {
VariantKind::Unit => format!("{{ tag: '{}' }}", variant.name),
VariantKind::Newtype { inner } => {
format!(
"{{ tag: '{}'; value: {} }}",
variant.name,
ts_type_base_named(inner)
)
}
VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
let field_strs = fields
.iter()
.map(|f| format!("{}: {}", f.name, ts_type_base_named(f.shape())))
.collect::<Vec<_>>()
.join("; ");
format!("{{ tag: '{}'; {} }}", variant.name, field_strs)
}
};
let sep = if i < variants.len() - 1 { "" } else { ";" };
out.push_str(&format!(" | {}{}\n", variant_type, sep));
}
out.push('\n');
}
_ => {}
}
}
out
}
pub fn ts_type_base_named(shape: &'static Shape) -> String {
if let Some((name, _)) = transparent_named_alias(shape) {
return name.to_string();
}
match classify_shape(shape) {
ShapeKind::Struct(StructInfo {
name: Some(name), ..
}) => name.to_string(),
ShapeKind::Enum(EnumInfo {
name: Some(name), ..
}) => name.to_string(),
ShapeKind::List { element } => {
if is_bytes(shape) {
return "Uint8Array".into();
}
if matches!(
classify_shape(element),
ShapeKind::Enum(EnumInfo { name: None, .. })
) {
format!("({})[]", ts_type_base_named(element))
} else {
format!("{}[]", ts_type_base_named(element))
}
}
ShapeKind::Option { inner } => format!("{} | null", ts_type_base_named(inner)),
ShapeKind::Array { element, len } => format!("[{}; {}]", ts_type_base_named(element), len),
ShapeKind::Map { key, value } => {
format!(
"Map<{}, {}>",
ts_type_base_named(key),
ts_type_base_named(value)
)
}
ShapeKind::Set { element } => format!("Set<{}>", ts_type_base_named(element)),
ShapeKind::Tuple { elements } => {
let inner = elements
.iter()
.map(|p| ts_type_base_named(p.shape))
.collect::<Vec<_>>()
.join(", ");
format!("[{inner}]")
}
ShapeKind::Tx { inner } => format!("Tx<{}>", ts_type_base_named(inner)),
ShapeKind::Rx { inner } => format!("Rx<{}>", ts_type_base_named(inner)),
ShapeKind::Struct(StructInfo {
name: None, fields, ..
}) => {
let inner = fields
.iter()
.map(|f| format!("{}: {}", f.name, ts_type_base_named(f.shape())))
.collect::<Vec<_>>()
.join("; ");
format!("{{ {inner} }}")
}
ShapeKind::Enum(EnumInfo {
name: None,
variants,
}) => variants
.iter()
.map(|v| match classify_variant(v) {
VariantKind::Unit => format!("{{ tag: '{}' }}", v.name),
VariantKind::Newtype { inner } => {
format!(
"{{ tag: '{}'; value: {} }}",
v.name,
ts_type_base_named(inner)
)
}
VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
let field_strs = fields
.iter()
.map(|f| format!("{}: {}", f.name, ts_type_base_named(f.shape())))
.collect::<Vec<_>>()
.join("; ");
format!("{{ tag: '{}'; {} }}", v.name, field_strs)
}
})
.collect::<Vec<_>>()
.join(" | "),
ShapeKind::Scalar(scalar) => ts_scalar_type(scalar),
ShapeKind::Slice { element } => format!("{}[]", ts_type_base_named(element)),
ShapeKind::Pointer { pointee } => ts_type_base_named(pointee),
ShapeKind::Result { ok, err } => {
format!(
"{{ ok: true; value: {} }} | {{ ok: false; error: {} }}",
ts_type_base_named(ok),
ts_type_base_named(err)
)
}
ShapeKind::TupleStruct { fields } => {
let inner = fields
.iter()
.map(|f| ts_type_base_named(f.shape()))
.collect::<Vec<_>>()
.join(", ");
format!("[{inner}]")
}
ShapeKind::Opaque => "unknown".into(),
}
}
pub fn ts_scalar_type(scalar: ScalarType) -> String {
match scalar {
ScalarType::Bool => "boolean".into(),
ScalarType::U8
| ScalarType::U16
| ScalarType::U32
| ScalarType::I8
| ScalarType::I16
| ScalarType::I32
| ScalarType::F32
| ScalarType::F64 => "number".into(),
ScalarType::U64
| ScalarType::U128
| ScalarType::I64
| ScalarType::I128
| ScalarType::USize
| ScalarType::ISize => "bigint".into(),
ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
"string".into()
}
ScalarType::Unit => "void".into(),
_ => "unknown".into(),
}
}
pub fn ts_type_client_arg(shape: &'static Shape) -> String {
match classify_shape(shape) {
ShapeKind::Tx { inner } => format!("Tx<{}>", ts_type_client_arg(inner)),
ShapeKind::Rx { inner } => format!("Rx<{}>", ts_type_client_arg(inner)),
_ => ts_type_base_named(shape),
}
}
pub fn ts_type_client_return(shape: &'static Shape) -> String {
crate::targets::swift::types::assert_no_channels_in_return_shape(shape);
ts_type_base_named(shape)
}
pub fn ts_type_server_arg(shape: &'static Shape) -> String {
match classify_shape(shape) {
ShapeKind::Tx { inner } => format!("Tx<{}>", ts_type_server_arg(inner)),
ShapeKind::Rx { inner } => format!("Rx<{}>", ts_type_server_arg(inner)),
_ => ts_type_base_named(shape),
}
}
pub fn ts_type_server_return(shape: &'static Shape) -> String {
crate::targets::swift::types::assert_no_channels_in_return_shape(shape);
ts_type_base_named(shape)
}
pub fn ts_type(shape: &'static Shape) -> String {
ts_type_base_named(shape)
}
pub fn is_fully_supported(shape: &'static Shape) -> bool {
match classify_shape(shape) {
ShapeKind::Tx { inner } | ShapeKind::Rx { inner } => is_fully_supported(inner),
ShapeKind::List { element }
| ShapeKind::Option { inner: element }
| ShapeKind::Set { element }
| ShapeKind::Array { element, .. }
| ShapeKind::Slice { element } => is_fully_supported(element),
ShapeKind::Map { key, value } => is_fully_supported(key) && is_fully_supported(value),
ShapeKind::Tuple { elements } => elements.iter().all(|p| is_fully_supported(p.shape)),
ShapeKind::TupleStruct { fields } => fields.iter().all(|f| is_fully_supported(f.shape())),
ShapeKind::Struct(StructInfo { fields, .. }) => {
fields.iter().all(|f| is_fully_supported(f.shape()))
}
ShapeKind::Enum(EnumInfo { variants, .. }) => {
variants.iter().all(|v| match classify_variant(v) {
VariantKind::Unit => true,
VariantKind::Newtype { inner } => is_fully_supported(inner),
VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
fields.iter().all(|f| is_fully_supported(f.shape()))
}
})
}
ShapeKind::Pointer { pointee } => is_fully_supported(pointee),
ShapeKind::Scalar(_) => true,
ShapeKind::Result { ok, err } => is_fully_supported(ok) && is_fully_supported(err),
ShapeKind::Opaque => false,
}
}