use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Data, DataEnum, DeriveInput, Fields, Generics, Ident, PathArguments, Type, TypePath,
parse_macro_input,
};
pub fn derive_ts_type_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident.to_string();
let generic_params = collect_generic_params(&input.generics);
let generic_decl = format_generic_decl(&generic_params);
match &input.data {
Data::Struct(s) => derive_struct(&name, &generic_decl, &s.fields, &input.ident),
Data::Enum(e) => derive_enum(&name, &generic_decl, e, &input.ident),
Data::Union(_) => syn::Error::new_spanned(
&input.ident,
"TsType can only be derived for structs and enums",
)
.to_compile_error()
.into(),
}
}
pub fn rust_type_to_ts_string(ty: &Type) -> String {
rust_type_to_ts(ty)
}
pub fn rust_type_to_ts_from_path(tp: &TypePath) -> String {
rust_type_to_ts(&Type::Path(tp.clone()))
}
fn rust_type_to_ts(ty: &Type) -> String {
match ty {
Type::Path(TypePath { path, .. }) => {
let Some(seg) = path.segments.last() else {
return "any".to_string();
};
let ident = seg.ident.to_string();
match ident.as_str() {
"String" | "str" => "string".to_string(),
"bool" => "boolean".to_string(),
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
| "i128" | "isize" | "f32" | "f64" => "number".to_string(),
"Vec" => {
let inner = extract_first_generic_arg(&seg.arguments);
format!("{inner}[]")
}
"Option" => {
let inner = extract_first_generic_arg(&seg.arguments);
format!("{inner} | null")
}
"HashMap" | "BTreeMap" => {
let (key, val) = extract_two_generic_args(&seg.arguments);
format!("Record<{key}, {val}>")
}
"Result" => {
let (ok, err) = extract_two_generic_args(&seg.arguments);
format!("{{ Ok: {ok} }} | {{ Err: {err} }}")
}
"ActorRef" => {
let inner = extract_first_generic_arg(&seg.arguments);
format!("{inner}Ref")
}
other => other.to_string(),
}
}
Type::Reference(r) => rust_type_to_ts(&r.elem),
Type::Tuple(tuple) => {
if tuple.elems.is_empty() {
"void".to_string()
} else {
let elems: Vec<String> = tuple.elems.iter().map(rust_type_to_ts).collect();
format!("[{}]", elems.join(", "))
}
}
_ => "any".to_string(),
}
}
fn extract_first_generic_arg(args: &PathArguments) -> String {
let syn::PathArguments::AngleBracketed(ab) = args else {
return "any".to_string();
};
let Some(syn::GenericArgument::Type(ty)) = ab.args.first() else {
return "any".to_string();
};
rust_type_to_ts(ty)
}
fn extract_two_generic_args(args: &PathArguments) -> (String, String) {
let syn::PathArguments::AngleBracketed(ab) = args else {
return ("string".to_string(), "any".to_string());
};
let mut iter = ab.args.iter();
let key = iter
.next()
.and_then(|a| match a {
syn::GenericArgument::Type(ty) => Some(rust_type_to_ts(ty)),
_ => None,
})
.unwrap_or_else(|| "string".to_string());
let val = iter
.next()
.and_then(|a| match a {
syn::GenericArgument::Type(ty) => Some(rust_type_to_ts(ty)),
_ => None,
})
.unwrap_or_else(|| "any".to_string());
(key, val)
}
fn collect_generic_params(generics: &Generics) -> Vec<String> {
generics
.type_params()
.map(|tp| tp.ident.to_string())
.collect()
}
fn format_generic_decl(params: &[String]) -> String {
if params.is_empty() {
String::new()
} else {
format!("<{}>", params.join(", "))
}
}
fn derive_struct(
name: &str,
generic_decl: &str,
fields: &Fields,
ident: &Ident,
) -> proc_macro::TokenStream {
let ts_fields = match fields {
Fields::Named(named) => named
.named
.iter()
.map(|f| {
let field_name = f.ident.as_ref().unwrap().to_string();
let ts_ty = rust_type_to_ts(&f.ty);
format!(" {field_name}: {ts_ty};")
})
.collect::<Vec<_>>()
.join("\n"),
Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
let ts_ty = rust_type_to_ts(&unnamed.unnamed.first().unwrap().ty);
let ts_section = format!("export type {name}{generic_decl} = {ts_ty};");
return generate_custom_section(&ts_section).into();
}
Fields::Unit => {
let ts_section = format!("export type {name}{generic_decl} = null;");
return generate_custom_section(&ts_section).into();
}
Fields::Unnamed(_) => {
return syn::Error::new_spanned(
ident,
"TsType: tuple structs with multiple fields are not supported",
)
.to_compile_error()
.into();
}
};
let ts_section = format!("export interface {name}{generic_decl} {{\n{ts_fields}\n}}");
generate_custom_section(&ts_section).into()
}
fn derive_enum(
name: &str,
generic_decl: &str,
data: &DataEnum,
_ident: &Ident,
) -> proc_macro::TokenStream {
let variants: Vec<String> = data
.variants
.iter()
.map(|v| {
let variant_name = v.ident.to_string();
match &v.fields {
Fields::Unit => format!("\"{variant_name}\""),
Fields::Named(named) => {
let fields: Vec<String> = named
.named
.iter()
.map(|f| {
let field_name = f.ident.as_ref().unwrap().to_string();
let ts_ty = rust_type_to_ts(&f.ty);
format!("{field_name}: {ts_ty}")
})
.collect();
format!("{{ {variant_name}: {{ {} }} }}", fields.join("; "))
}
Fields::Unnamed(unnamed) => {
if unnamed.unnamed.len() == 1 {
let ts_ty = rust_type_to_ts(&unnamed.unnamed.first().unwrap().ty);
format!("{{ {variant_name}: {ts_ty} }}")
} else {
let elems: Vec<String> = unnamed
.unnamed
.iter()
.map(|f| rust_type_to_ts(&f.ty))
.collect();
format!("{{ {variant_name}: [{}] }}", elems.join(", "))
}
}
}
})
.collect();
let union = if variants.is_empty() {
"never".to_string()
} else {
variants.join(" | ")
};
let ts_section = format!("export type {name}{generic_decl} = {union};");
generate_custom_section(&ts_section).into()
}
fn generate_custom_section(ts_section: &str) -> TokenStream {
quote! {
#[cfg(all(feature = "ts", target_arch = "wasm32"))] #[allow(non_upper_case_globals)] const _
: () = {
#[::theta_ts::prelude::wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] const
TS_TYPE : &'static str = # ts_section; };
}
}