use proc_macro::TokenStream;
use quote::quote;
use syn::{
Attribute, Data, DeriveInput, Field, Fields, GenericArgument, PathArguments, Type,
parse_macro_input,
};
#[derive(Clone, Copy)]
enum RenameRule {
Lowercase,
Uppercase,
PascalCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
KebabCase,
ScreamingKebabCase,
}
#[derive(Default)]
struct SerdeAttrs {
rename: Option<String>,
rename_all: Option<RenameRule>,
rename_all_fields: Option<RenameRule>,
skip: bool,
}
pub fn derive_type(item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
if let Some(error) = validate(&input) {
return error.into_compile_error().into();
}
let name = &input.ident;
let name_str = name.to_string();
let ts_def = ts_definition(&input);
quote! {
impl ::tyzen::TsType for #name {
fn ts_name() -> String {
#name_str.to_string()
}
}
::tyzen::inventory::submit! {
::tyzen::TypeMeta {
name: #name_str,
ts_def: #ts_def,
}
}
}
.into()
}
fn validate(input: &DeriveInput) -> Option<syn::Error> {
let mut error = None;
for field in all_fields(input) {
if has_tyzen_optional(&field.attrs) && option_inner_type(&field.ty).is_none() {
push_error(
&mut error,
syn::Error::new_spanned(
field,
"#[tyzen(optional)] can only be used on Option<T> fields",
),
);
}
}
error
}
fn all_fields(input: &DeriveInput) -> Vec<&Field> {
match &input.data {
Data::Struct(data) => data.fields.iter().collect(),
Data::Enum(data) => data
.variants
.iter()
.flat_map(|variant| variant.fields.iter())
.collect(),
_ => Vec::new(),
}
}
fn push_error(target: &mut Option<syn::Error>, error: syn::Error) {
if let Some(existing) = target {
existing.combine(error);
} else {
*target = Some(error);
}
}
fn ts_definition(input: &DeriveInput) -> proc_macro2::TokenStream {
let name = input.ident.to_string();
let serde = serde_attrs(&input.attrs);
match &input.data {
Data::Struct(data) => struct_definition(&name, &data.fields, serde.rename_all),
Data::Enum(data) => enum_definition(&name, &data.variants, &serde),
_ => quote! { || format!("export type {} = unknown", #name) },
}
}
fn struct_definition(
name: &str,
fields: &Fields,
rename_all: Option<RenameRule>,
) -> proc_macro2::TokenStream {
match fields {
Fields::Named(fields) => {
let fields = fields
.named
.iter()
.filter_map(|field| named_field_definition(field, rename_all))
.collect::<Vec<_>>();
quote! {
|| {
let fields = vec![#(#fields),*];
format!("export type {} = {{ {} }}", #name, fields.join(", "))
}
}
}
Fields::Unnamed(fields) => {
let types = fields
.unnamed
.iter()
.map(|field| {
let ty = &field.ty;
quote! { <#ty as ::tyzen::TsType>::ts_name() }
})
.collect::<Vec<_>>();
if types.len() == 1 {
quote! {
|| format!("export type {} = {}", #name, #(#types),*)
}
} else {
quote! {
|| {
let fields = vec![#(#types),*];
format!("export type {} = [{}]", #name, fields.join(", "))
}
}
}
}
Fields::Unit => quote! {
|| format!("export type {} = null", #name)
},
}
}
fn enum_definition(
name: &str,
variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
serde: &SerdeAttrs,
) -> proc_macro2::TokenStream {
let variants = variants
.iter()
.filter_map(|variant| enum_variant_definition(variant, serde))
.collect::<Vec<_>>();
quote! {
|| {
let variants = vec![#(#variants),*];
format!("export type {} = {}", #name, variants.join(" | "))
}
}
}
fn enum_variant_definition(
variant: &syn::Variant,
container_serde: &SerdeAttrs,
) -> Option<proc_macro2::TokenStream> {
let variant_serde = serde_attrs(&variant.attrs);
if variant_serde.skip {
return None;
}
let variant_name = ts_name(
&variant.ident.to_string(),
variant_serde.rename,
container_serde.rename_all,
);
let field_rename_all = variant_serde
.rename_all
.or(container_serde.rename_all_fields);
Some(match &variant.fields {
Fields::Unit => quote! {
format!("\"{}\"", #variant_name)
},
Fields::Unnamed(fields) => {
let values = fields
.unnamed
.iter()
.map(|field| {
let ty = &field.ty;
quote! { <#ty as ::tyzen::TsType>::ts_name() }
})
.collect::<Vec<_>>();
if values.len() == 1 {
quote! {
format!("{{ tag: \"{}\", value: {} }}", #variant_name, #(#values),*)
}
} else {
quote! {
{
let values = vec![#(#values),*];
format!("{{ tag: \"{}\", value: [{}] }}", #variant_name, values.join(", "))
}
}
}
}
Fields::Named(fields) => {
let fields = fields
.named
.iter()
.filter_map(|field| named_field_definition(field, field_rename_all))
.collect::<Vec<_>>();
quote! {
format!("{{ tag: \"{}\", {} }}", #variant_name, vec![#(#fields),*].join(", "))
}
}
})
}
fn named_field_definition(
field: &Field,
rename_all: Option<RenameRule>,
) -> Option<proc_macro2::TokenStream> {
let serde = serde_attrs(&field.attrs);
if serde.skip {
return None;
}
let field_name = field.ident.as_ref().unwrap().to_string();
let field_name = ts_name(&field_name, serde.rename, rename_all);
let optional = has_tyzen_optional(&field.attrs);
let label = if optional {
format!("{field_name}?")
} else {
field_name
};
let ty = if optional {
option_inner_type(&field.ty).unwrap()
} else {
&field.ty
};
Some(quote! {
format!("{}: {}", #label, <#ty as ::tyzen::TsType>::ts_name())
})
}
fn ts_name(name: &str, rename: Option<String>, rename_all: Option<RenameRule>) -> String {
if let Some(rename) = rename {
rename
} else if let Some(rule) = rename_all {
apply_rename_rule(name, rule)
} else {
name.to_string()
}
}
fn serde_attrs(attrs: &[Attribute]) -> SerdeAttrs {
let mut serde = SerdeAttrs::default();
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip")
|| meta.path.is_ident("skip_serializing")
|| meta.path.is_ident("skip_deserializing")
{
serde.skip = true;
return Ok(());
}
if meta.path.is_ident("rename") {
let value = meta.value()?.parse::<syn::LitStr>()?;
serde.rename = Some(value.value());
return Ok(());
}
if meta.path.is_ident("rename_all") {
let value = meta.value()?.parse::<syn::LitStr>()?;
serde.rename_all = rename_rule(&value.value());
return Ok(());
}
if meta.path.is_ident("rename_all_fields") {
let value = meta.value()?.parse::<syn::LitStr>()?;
serde.rename_all_fields = rename_rule(&value.value());
return Ok(());
}
Ok(())
});
}
serde
}
fn rename_rule(rule: &str) -> Option<RenameRule> {
Some(match rule {
"lowercase" => RenameRule::Lowercase,
"UPPERCASE" => RenameRule::Uppercase,
"PascalCase" => RenameRule::PascalCase,
"camelCase" => RenameRule::CamelCase,
"snake_case" => RenameRule::SnakeCase,
"SCREAMING_SNAKE_CASE" => RenameRule::ScreamingSnakeCase,
"kebab-case" => RenameRule::KebabCase,
"SCREAMING-KEBAB-CASE" => RenameRule::ScreamingKebabCase,
_ => return None,
})
}
fn apply_rename_rule(name: &str, rule: RenameRule) -> String {
let words = words(name);
match rule {
RenameRule::Lowercase => words.join("").to_lowercase(),
RenameRule::Uppercase => words.join("").to_uppercase(),
RenameRule::PascalCase => words.iter().map(|word| capitalize(word)).collect(),
RenameRule::CamelCase => {
let mut out = String::new();
for (index, word) in words.iter().enumerate() {
if index == 0 {
out.push_str(&word.to_lowercase());
} else {
out.push_str(&capitalize(word));
}
}
out
}
RenameRule::SnakeCase => words.join("_").to_lowercase(),
RenameRule::ScreamingSnakeCase => words.join("_").to_uppercase(),
RenameRule::KebabCase => words.join("-").to_lowercase(),
RenameRule::ScreamingKebabCase => words.join("-").to_uppercase(),
}
}
fn words(name: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
for ch in name.chars() {
if ch == '_' || ch == '-' {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
if ch.is_uppercase() && !current.is_empty() {
words.push(std::mem::take(&mut current));
}
current.push(ch);
}
if !current.is_empty() {
words.push(current);
}
words
}
fn capitalize(word: &str) -> String {
let mut chars = word.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
None => String::new(),
}
}
fn has_tyzen_optional(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
attr.path().is_ident("tyzen")
&& attr
.parse_args::<syn::Ident>()
.map(|ident| ident == "optional")
.unwrap_or(false)
})
}
fn option_inner_type(ty: &Type) -> Option<&Type> {
let Type::Path(type_path) = ty else {
return None;
};
let segment = type_path.path.segments.last()?;
if segment.ident != "Option" {
return None;
}
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return None;
};
args.args.iter().find_map(|arg| match arg {
GenericArgument::Type(inner) => Some(inner),
_ => None,
})
}