use crate::{
options::{FieldWasm, WasmOptions},
ts_type::{TsType, TsTypeElement, TypeLiteralTsType},
};
use bomboni_core::{string::str_to_case, syn::type_is_phantom};
use convert_case::{Case, Casing};
use serde_derive_internals::{
ast,
attr::{RenameRule, TagType},
};
use std::fmt::Write;
use std::{
collections::BTreeSet,
fmt::{self, Display, Formatter},
};
#[derive(Debug)]
pub enum TsDecl {
TypeAlias(TypeAliasTsDecl),
Interface(InterfaceTsDecl),
Enum(EnumTsDecl),
}
#[derive(Debug)]
pub struct TypeAliasTsDecl {
pub name: String,
pub type_params: Vec<String>,
pub alias_type: TsType,
}
#[derive(Debug)]
pub struct InterfaceTsDecl {
pub name: String,
pub type_params: Vec<String>,
pub extends: Vec<TsType>,
pub body: Vec<TsTypeElement>,
}
#[derive(Debug)]
pub struct EnumTsDecl {
pub name: String,
pub type_params: Vec<String>,
pub members: Vec<TypeAliasTsDecl>,
pub external_tag: bool,
pub enum_value: bool,
}
pub struct TsDeclParser<'a> {
options: &'a WasmOptions<'a>,
}
impl From<TypeAliasTsDecl> for TsDecl {
fn from(decl: TypeAliasTsDecl) -> Self {
Self::TypeAlias(decl)
}
}
impl Display for TypeAliasTsDecl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"export type {}{} = {};",
self.name,
if self.type_params.is_empty() {
String::new()
} else {
format!("<{}>", self.type_params.join(", "))
},
self.alias_type
)
}
}
impl From<InterfaceTsDecl> for TsDecl {
fn from(decl: InterfaceTsDecl) -> Self {
Self::Interface(decl)
}
}
impl Display for InterfaceTsDecl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "export interface {}", self.name)?;
if !self.type_params.is_empty() {
write!(f, "<{}>", self.type_params.join(", "))?;
}
if !self.extends.is_empty() {
write!(
f,
" extends {}",
self.extends
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)?;
}
if self.body.is_empty() {
write!(f, " {{}}")
} else {
write!(
f,
" {{{}\n}}",
self.body.iter().fold(String::new(), |mut s, element| {
let _ = write!(s, "\n {element};");
s
})
)
}
}
}
impl From<EnumTsDecl> for TsDecl {
fn from(decl: EnumTsDecl) -> Self {
Self::Enum(decl)
}
}
impl Display for EnumTsDecl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.enum_value {
assert!(
self.type_params.is_empty(),
"enum with type params not supported"
);
write!(f, "export enum {} {{", self.name)?;
for member in &self.members {
write!(
f,
"\n {} = {},",
str_to_case(&member.name, Case::Constant),
&member.alias_type
)?;
}
write!(f, "\n}}")
} else {
write!(
f,
"export type {}{} = {};",
self.name,
if self.type_params.is_empty() {
String::new()
} else {
format!("<{}>", self.type_params.join(", "))
},
TsType::Union(
self.members
.iter()
.enumerate()
.map(|(i, member)| {
let mut member_type = member.alias_type.clone();
if self.external_tag {
if let TsType::TypeLiteral(member_type) = &mut member_type {
member_type.members.extend(
self.members.iter().enumerate().filter_map(
|(j, other_member)| {
if j == i {
None
} else {
Some(TsTypeElement {
key: other_member.name.clone(),
alias_type: TsType::nullish(),
optional: true,
})
}
},
),
);
}
}
member_type
})
.collect(),
)
)
}
}
}
impl TsDecl {
pub fn name(&self) -> &str {
match self {
Self::TypeAlias(decl) => &decl.name,
Self::Interface(decl) => &decl.name,
Self::Enum(decl) => &decl.name,
}
}
}
impl Display for TsDecl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::TypeAlias(decl) => decl.fmt(f),
Self::Interface(decl) => decl.fmt(f),
Self::Enum(decl) => decl.fmt(f),
}
}
}
#[derive(Debug)]
enum ParsedFields {
Named(Vec<TsTypeElement>, Vec<TsType>),
Unnamed(Vec<TsType>),
Transparent(TsType),
}
impl<'a> TsDeclParser<'a> {
pub const fn new(options: &'a WasmOptions<'a>) -> Self {
Self { options }
}
pub fn parse(&self) -> TsDecl {
if let Some(override_type) = self.options.override_type.as_ref() {
return TypeAliasTsDecl {
name: self.options.name().into(),
type_params: Vec::new(),
alias_type: TsType::Override(override_type.clone()),
}
.into();
}
match &self.options.serde_data() {
ast::Data::Struct(style, fields) => self.parse_struct(*style, fields),
ast::Data::Enum(variants) => self.parse_enum(variants).into(),
}
}
fn parse_struct(&self, style: ast::Style, fields: &[ast::Field]) -> TsDecl {
match (
self.options.serde_attrs().tag(),
self.parse_fields(style, fields, &self.options.fields),
) {
(TagType::Internal { tag, .. }, ParsedFields::Named(members, extends)) => {
let name = self.options.name();
let tag_field = TsTypeElement {
key: tag.clone(),
alias_type: TsType::Literal(name.into()),
optional: false,
};
let mut vec = Vec::with_capacity(members.len() + 1);
vec.push(tag_field);
vec.extend(members);
self.make_named_decl(vec, extends)
}
(_, ParsedFields::Named(members, extends)) => self.make_named_decl(members, extends),
(_, fields) => self.make_type_alias(fields.into()).into(),
}
}
fn parse_enum(&self, variants: &[ast::Variant]) -> EnumTsDecl {
let tag_type = self.options.serde_attrs().tag();
let members: Vec<TypeAliasTsDecl> = variants
.iter()
.filter_map(|variant| {
if variant.attrs.skip_serializing() || variant.attrs.skip_deserializing() {
None
} else {
Some(self.parse_variant(variant, tag_type))
}
})
.collect();
EnumTsDecl {
name: self.options.name().into(),
type_params: self.make_ref_type_params(
members
.iter()
.flat_map(|member| member.alias_type.get_reference_names())
.collect(),
),
members,
external_tag: matches!(tag_type, TagType::External),
enum_value: self.options.enum_value,
}
}
fn parse_fields(
&self,
style: ast::Style,
fields: &[ast::Field],
wasm_fields: &'a [FieldWasm],
) -> ParsedFields {
match style {
ast::Style::Newtype => {
return ParsedFields::Transparent(self.parse_field(&fields[0], wasm_fields).1);
}
ast::Style::Unit => return ParsedFields::Transparent(TsType::nullish()),
_ => {}
}
let fields: Vec<_> = fields
.iter()
.filter(|field| {
!field.attrs.skip_serializing()
&& !field.attrs.skip_deserializing()
&& !type_is_phantom(field.ty)
})
.collect();
if fields.len() == 1 && self.options.serde_attrs().transparent() {
return ParsedFields::Transparent(self.parse_field(fields[0], wasm_fields).1);
}
match style {
ast::Style::Struct => {
let (flatten_fields, members): (Vec<_>, Vec<_>) =
fields.into_iter().partition(|field| field.attrs.flatten());
let members = members
.into_iter()
.map(|field| {
let (key, field_type, wasm_field) = self.parse_field(field, wasm_fields);
let optional = wasm_field.optional;
let alias_type = if optional {
if let TsType::Option(t) = field_type {
*t
} else {
field_type
}
} else {
field_type
};
TsTypeElement {
key,
alias_type,
optional: optional
|| !(self.options.serde_attrs().default().is_none()
&& field.attrs.default().is_none()),
}
})
.collect();
let flatten_fields = flatten_fields
.into_iter()
.map(|field| self.parse_field(field, wasm_fields).1)
.collect();
ParsedFields::Named(members, flatten_fields)
}
ast::Style::Tuple => ParsedFields::Unnamed(
fields
.into_iter()
.map(|field| self.parse_field(field, wasm_fields).1)
.collect(),
),
_ => unreachable!(),
}
}
fn parse_field(
&self,
field: &ast::Field,
wasm_fields: &'a [FieldWasm],
) -> (String, TsType, &'a FieldWasm) {
let wasm_field = wasm_fields
.iter()
.find(|f| f.member == field.member)
.unwrap();
let name = wasm_field.rename.clone().unwrap_or_else(|| {
let mut name = field.attrs.name().serialize_name().to_string();
if let Some(rename_all) = self.options.rename_all {
name = self.apply_rename(&name, rename_all);
}
name
});
if let Some(override_type) = wasm_field.override_type.clone() {
return (name, TsType::Override(override_type), wasm_field);
}
let mut field_type = TsType::from_type(field.ty);
if wasm_field.always_some.unwrap_or_default()
&& let TsType::Option(some_type) = field_type
{
field_type = *some_type;
}
if wasm_field
.rename_wrapper
.or(self.options.rename_wrapper)
.unwrap_or_default()
{
field_type = field_type.rename_protobuf_wrapper();
}
field_type = field_type.change_reference(
if wasm_field.reference_change.name.is_some()
|| !wasm_field.reference_change.types.is_empty()
{
&wasm_field.reference_change
} else {
&self.options.reference_change
},
);
(name, field_type, wasm_field)
}
fn parse_variant(&self, variant: &ast::Variant, tag_type: &TagType) -> TypeAliasTsDecl {
let wasm_variant = self
.options
.variants
.iter()
.find(|v| v.ident == variant.ident)
.unwrap();
let name = wasm_variant.rename.clone().unwrap_or_else(|| {
let mut name = variant.attrs.name().serialize_name().to_string();
if let Some(rename_all) = self.options.rename_all {
name = self.apply_rename(&name, rename_all);
}
name
});
if let Some(override_type) = wasm_variant.override_type.clone() {
return self.make_type_alias(TsType::Override(override_type));
}
let variant_type =
TsType::from(self.parse_fields(variant.style, &variant.fields, &wasm_variant.fields))
.with_tag_type(tag_type, &name, variant.style);
let mut alias_type = self.make_type_alias(variant_type);
alias_type.name = name;
alias_type
}
fn apply_rename(&self, s: &str, rename_rule: RenameRule) -> String {
if self.options.rename_boundary.is_empty() {
match rename_rule {
RenameRule::None => s.to_owned(),
RenameRule::LowerCase => str_to_case(s, Case::Lower),
RenameRule::UpperCase => str_to_case(s, Case::Upper),
RenameRule::PascalCase => str_to_case(s, Case::Pascal),
RenameRule::CamelCase => str_to_case(s, Case::Camel),
RenameRule::SnakeCase => str_to_case(s, Case::Snake),
RenameRule::ScreamingSnakeCase => str_to_case(s, Case::Constant),
RenameRule::KebabCase => str_to_case(s, Case::Kebab),
RenameRule::ScreamingKebabCase => str_to_case(s, Case::Cobol),
}
} else {
match rename_rule {
RenameRule::None => s.to_owned(),
RenameRule::LowerCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Lower),
RenameRule::UpperCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Upper),
RenameRule::PascalCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Pascal),
RenameRule::CamelCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Camel),
RenameRule::SnakeCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Snake),
RenameRule::ScreamingSnakeCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Constant),
RenameRule::KebabCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Kebab),
RenameRule::ScreamingKebabCase => s
.with_boundaries(&self.options.rename_boundary)
.to_case(Case::Cobol),
}
}
}
fn make_named_decl(&self, members: Vec<TsTypeElement>, extends: Vec<TsType>) -> TsDecl {
if extends.iter().all(TsType::is_reference) {
InterfaceTsDecl {
name: self.options.name().into(),
type_params: self.make_ref_type_params(
members
.iter()
.map(|member| member.alias_type.get_reference_names())
.chain(extends.iter().map(TsType::get_reference_names))
.flatten()
.collect(),
),
extends,
body: members,
}
.into()
} else {
self.make_type_alias(
TsType::from(TypeLiteralTsType { members }).intersection(TsType::Intersection(
extends
.into_iter()
.map(|ty| match ty {
TsType::Option(ty) => TsType::Partial(Box::new(*ty)),
_ => ty,
})
.collect(),
)),
)
.into()
}
}
fn make_type_alias(&self, alias_type: TsType) -> TypeAliasTsDecl {
TypeAliasTsDecl {
name: self.options.name().into(),
type_params: self.make_ref_type_params(alias_type.get_reference_names()),
alias_type,
}
}
fn make_ref_type_params(&self, type_ref_names: BTreeSet<String>) -> Vec<String> {
self.options
.generics()
.type_params()
.map(|p| p.ident.to_string())
.filter(|t| type_ref_names.contains(t))
.collect()
}
}
impl From<ParsedFields> for TsType {
fn from(fields: ParsedFields) -> Self {
match fields {
ParsedFields::Named(members, extends) => {
let ty = Self::from(TypeLiteralTsType { members });
if extends.is_empty() {
ty
} else {
ty.intersection(Self::Intersection(extends))
}
}
ParsedFields::Unnamed(elements) => Self::Tuple(elements),
ParsedFields::Transparent(ty) => ty,
}
}
}