extern crate proc_macro;
use proc_macro2::Ident;
use quote::quote;
use serde_derive_internals::{ast, Ctxt, Derive};
use syn::DeriveInput;
mod derive_enum;
mod derive_struct;
mod patch;
mod quotet;
mod utils;
use std::cell::Cell;
use utils::*;
use patch::patch;
type QuoteT = proc_macro2::TokenStream;
type QuoteMaker = quotet::QuoteT<'static>;
type Bounds = Vec<TSType>;
#[proc_macro_derive(TypescriptDefinition)]
pub fn derive_typescript_definition(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
if cfg!(any(debug_assertions, feature = "export-typescript")) {
let input = QuoteT::from(input);
do_derive_typescript_definition(input).into()
} else {
proc_macro::TokenStream::new()
}
}
#[proc_macro_derive(TypeScriptify)]
pub fn derive_type_script_ify(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
if cfg!(any(debug_assertions, feature = "export-typescript")) {
let input = QuoteT::from(input);
do_derive_type_script_ify(input).into()
} else {
proc_macro::TokenStream::new()
}
}
fn do_derive_typescript_definition(input: QuoteT) -> QuoteT {
let parsed = Typescriptify::parse(false, input);
let export_string = parsed.wasm_string();
let export_ident = ident_from_str(&format!(
"TS_EXPORT_{}",
parsed.ident.to_string().to_uppercase()
));
let mut q = quote! {
#[wasm_bindgen(typescript_custom_section)]
pub const #export_ident : &'static str = #export_string;
};
if cfg!(any(test, feature = "test")) {
let typescript_ident =
ident_from_str(&format!("{}___typescript_definition", &parsed.ident));
q.extend(quote!(
fn #typescript_ident ( ) -> &'static str {
#export_string
}
));
}
q
}
fn do_derive_type_script_ify(input: QuoteT) -> QuoteT {
let parsed = Typescriptify::parse(true, input);
let ts_ident = parsed.ts_ident_str();
let fmt = if parsed.ctxt.is_enum.get() {
"export enum {} {};"
} else {
"export type {} = {};"
};
let body = match &parsed.body {
quotet::QuoteT::Builder(b) => {
let b = b.build();
quote!( let f = #b; format!(#fmt, #ts_ident, f) )
}
_ => {
let b = parsed.body.to_string();
let b = patch(&b);
quote!(format!(#fmt, #ts_ident, #b))
}
};
let ident = &parsed.ident;
let ret = if parsed.ts_generics.is_empty() {
quote! {
impl ::typescript_definitions::TypeScriptifyTrait for #ident {
fn type_script_ify() -> String {
#body
}
}
}
} else {
let generics = parsed.generic_args_with_lifetimes();
let rustg = &parsed.rust_generics;
quote! {
impl#rustg ::typescript_definitions::TypeScriptifyTrait for #ident<#(#generics),*> {
fn type_script_ify() -> String {
#body
}
}
}
};
if let Some("1") = option_env!("TFY_SHOW_CODE") {
eprintln!("{}", patch(&ret.to_string()));
}
ret
}
struct Typescriptify {
ctxt: ParseContext<'static>,
ident: syn::Ident,
ts_generics: Vec<Option<(Ident, Bounds)>>,
body: QuoteMaker,
rust_generics: syn::Generics,
}
impl Typescriptify {
fn wasm_string(&self) -> String {
if self.ctxt.is_enum.get() {
format!(
"export enum {} {};",
self.ts_ident_str(),
self.ts_body_str()
)
} else {
format!(
"export type {} = {};",
self.ts_ident_str(),
self.ts_body_str()
)
}
}
fn ts_ident_str(&self) -> String {
let ts_ident = self.ts_ident().to_string();
patch(&ts_ident).into()
}
fn ts_body_str(&self) -> String {
let ts = self.body.to_string();
patch(&ts).into()
}
fn ts_ident(&self) -> QuoteT {
let ident = &self.ident;
let args_wo_lt: Vec<_> = self.ts_generic_args_wo_lifetimes(false).collect();
if args_wo_lt.is_empty() {
quote!(#ident)
} else {
quote!(#ident<#(#args_wo_lt),*>)
}
}
fn ts_generic_args_wo_lifetimes(&self, with_bounds: bool) -> impl Iterator<Item = QuoteT> + '_ {
self.ts_generics.iter().filter_map(move |g| match g {
Some((ref ident, ref bounds)) => {
if bounds.is_empty() || !with_bounds {
Some(quote! (#ident))
} else {
let bounds = bounds.iter().map(|ts| &ts.ident);
Some(quote! { #ident extends #(#bounds)&* })
}
}
_ => None,
})
}
fn generic_args_with_lifetimes(&self) -> impl Iterator<Item = QuoteT> + '_ {
self.ts_generics.iter().map(|g| match g {
Some((ref i, ref _bounds)) => quote!(#i),
None => quote!('_),
})
}
#[allow(unused)]
fn map(&self) -> QuoteT {
match &self.body {
quotet::QuoteT::Builder(b) => match b.map() {
Some(t) => t,
_ => quote!(None),
},
_ => quote!(None),
}
}
fn parse(is_type_script_ify: bool, input: QuoteT) -> Self {
let input: DeriveInput = syn::parse2(input).unwrap();
let cx = Ctxt::new();
let container = ast::Container::from_ast(&cx, &input, Derive::Serialize);
let (typescript, ctxt) = {
let pctxt = ParseContext::new(is_type_script_ify, &cx);
let typescript = match container.data {
ast::Data::Enum(ref variants) => pctxt.derive_enum(variants, &container),
ast::Data::Struct(style, ref fields) => {
pctxt.derive_struct(style, fields, &container)
}
};
(
typescript,
ParseContext {
ctxt: None,
..pctxt
},
)
};
let ts_generics = ts_generics(container.generics);
cx.check().unwrap();
Self {
ctxt,
ident: container.ident,
ts_generics,
body: typescript,
rust_generics: container.generics.clone(),
}
}
}
fn ts_generics(g: &syn::Generics) -> Vec<Option<(Ident, Bounds)>> {
use syn::{ConstParam, GenericParam, LifetimeDef, TypeParam, TypeParamBound};
g.params
.iter()
.map(|p| match p {
GenericParam::Lifetime(LifetimeDef { .. }) => None,
GenericParam::Type(TypeParam { ident, bounds, ..}) => {
let bounds = bounds.iter()
.filter_map(|b| match b {
TypeParamBound::Trait(t) => Some(&t.path),
_ => None
})
.map(last_path_element)
.filter_map(|b| b)
.collect::<Vec<_>>();
Some((ident.clone(), bounds))
},
GenericParam::Const(ConstParam { ident, ..}) => Some((ident.clone(), vec![])),
})
.collect()
}
fn return_type(rt: &syn::ReturnType) -> Option<syn::Type> {
match rt {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, tp) => Some(*tp.clone()),
}
}
struct TSType {
ident: syn::Ident,
args: Vec<syn::Type>,
return_type: Option<syn::Type>,
}
fn last_path_element(path: &syn::Path) -> Option<TSType> {
match path.segments.last().map(|p| p.into_value()) {
Some(t) => {
let ident = t.ident.clone();
let args = match &t.arguments {
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args,
..
}) => args,
syn::PathArguments::Parenthesized(syn::ParenthesizedGenericArguments {
output,
inputs,
..
}) => {
let args: Vec<_> = inputs.iter().cloned().collect();
let ret = return_type(output);
return Some(TSType {
ident,
args,
return_type: ret,
});
}
syn::PathArguments::None => {
return Some(TSType {
ident,
args: vec![],
return_type: None,
});
}
};
let args = args
.iter()
.filter_map(|p| match p {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
})
.cloned()
.collect::<Vec<_>>();
Some(TSType {
ident,
args,
return_type: None,
})
}
None => None,
}
}
struct ParseContext<'a> {
ctxt: Option<&'a Ctxt>,
is_enum: Cell<bool>,
#[allow(unused)]
is_type_script_ify: bool,
}
impl<'a> ParseContext<'a> {
fn new(is_type_script_ify: bool, ctxt: &'a Ctxt) -> ParseContext<'a> {
ParseContext {
is_enum: Cell::new(false),
ctxt: Some(ctxt),
is_type_script_ify,
}
}
fn generic_to_ts(&self, ts: TSType, field: &'a ast::Field<'a>) -> QuoteT {
let to_ts = |ty: &syn::Type| self.type_to_ts(ty, field);
match ts.ident.to_string().as_ref() {
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
| "i128" | "isize" | "f64" | "f32" => quote! { number },
"String" | "str" => quote! { string },
"bool" => quote! { boolean },
"Box" | "Cow" | "Rc" | "Arc" if ts.args.len() == 1 => to_ts(&ts.args[0]),
"Vec" | "VecDeque" | "LinkedList" if ts.args.len() == 1 => {
self.type_to_array(&ts.args[0], field)
}
"HashMap" | "BTreeMap" if ts.args.len() == 2 => {
let k = to_ts(&ts.args[0]);
let v = to_ts(&ts.args[1]);
quote!( { [key: #k]:#v } )
}
"HashSet" | "BTreeSet" if ts.args.len() == 1 => {
let k = to_ts(&ts.args[0]);
quote! ( #k[] )
}
"Option" if ts.args.len() == 1 => {
let k = to_ts(&ts.args[0]);
quote!( #k | null )
}
"Result" if ts.args.len() == 2 => {
let k = to_ts(&ts.args[0]);
let v = to_ts(&ts.args[1]);
let vertical_bar = ident_from_str(patch::PATCH);
quote!( { Ok : #k } #vertical_bar { Err : #v } )
}
"Fn" | "FnOnce" | "FnMut" => {
let args = self.derive_syn_types(&ts.args, field);
if let Some(ref rt) = ts.return_type {
let rt = to_ts(rt);
quote! { (#(#args),*) => #rt }
} else {
quote! { (#(#args),*) => undefined }
}
}
_ => {
let ident = ts.ident;
if !ts.args.is_empty() {
let args = self.derive_syn_types(&ts.args, field);
quote! { #ident<#(#args),*> }
} else {
quote! {#ident}
}
}
}
}
fn get_path(&self, ty: &syn::Type) -> Option<TSType> {
use syn::Type::Path;
use syn::TypePath;
match ty {
Path(TypePath { path, .. }) => last_path_element(&path),
_ => None,
}
}
fn type_to_array(&self, elem: &syn::Type, field: &'a ast::Field<'a>) -> QuoteT {
if let Some(ty) = self.get_path(elem) {
if ty.ident == "u8" && is_bytes(field) {
return quote!(string);
};
};
let tp = self.type_to_ts(elem, field);
quote! { #tp[] }
}
fn type_to_ts(&self, ty: &syn::Type, field: &'a ast::Field<'a>) -> QuoteT {
use syn::Type::*;
use syn::{
BareFnArgName, TypeArray, TypeBareFn, TypeGroup, TypeImplTrait, TypeParamBound,
TypeParen, TypePath, TypePtr, TypeReference, TypeSlice, TypeTraitObject, TypeTuple,
};
match ty {
Slice(TypeSlice { elem, .. })
| Array(TypeArray { elem, .. })
| Ptr(TypePtr { elem, .. }) => self.type_to_array(elem, field),
Reference(TypeReference { elem, .. }) => self.type_to_ts(elem, field),
BareFn(TypeBareFn { output, inputs, .. }) => {
let mut args: Vec<Ident> = Vec::with_capacity(inputs.len());
let mut typs: Vec<&syn::Type> = Vec::with_capacity(inputs.len());
for (idx, t) in inputs.iter().enumerate() {
let i = match t.name {
Some((ref n, _)) => match n {
BareFnArgName::Named(m) => m.clone(),
_ => ident_from_str("_"),
},
_ => ident_from_str(&format!("_dummy{}", idx)),
};
args.push(i);
typs.push(&t.ty);
}
let typs = self.derive_syn_types_ptr(&typs, field);
if let Some(ref rt) = return_type(&output) {
let rt = self.type_to_ts(rt, field);
quote! { ( #(#args: #typs),* ) => #rt }
} else {
quote! { ( #(#args: #typs),* ) => undefined}
}
}
Never(..) => quote! { never },
Tuple(TypeTuple { elems, .. }) => {
let elems = elems.iter().map(|t| self.type_to_ts(t, field));
quote!([ #(#elems),* ])
}
Path(TypePath { path, .. }) => match last_path_element(&path) {
Some(ts) => self.generic_to_ts(ts, field),
_ => quote! { any },
},
TraitObject(TypeTraitObject { bounds, .. })
| ImplTrait(TypeImplTrait { bounds, .. }) => {
let elems = bounds
.iter()
.filter_map(|t| match t {
TypeParamBound::Trait(t) => last_path_element(&t.path),
_ => None,
})
.map(|t| self.generic_to_ts(t, field));
quote!(#(#elems)&*)
}
Paren(TypeParen { elem, .. }) | Group(TypeGroup { elem, .. }) => {
let tp = self.type_to_ts(elem, field);
quote! { ( #tp ) }
}
Infer(..) | Macro(..) | Verbatim(..) => quote! { any },
}
}
fn field_to_ts(&self, field: &ast::Field<'a>) -> QuoteT {
self.type_to_ts(&field.ty, field)
}
fn derive_field(&self, field: &ast::Field<'a>) -> QuoteT {
let field_name = field.attrs.name().serialize_name();
let field_name = ident_from_str(&field_name);
let ty = self.field_to_ts(&field);
quote! {
#field_name: #ty
}
}
fn derive_fields(
&'a self,
fields: &'a [&'a ast::Field<'a>],
) -> impl Iterator<Item = QuoteT> + 'a {
fields.iter().map(move |f| self.derive_field(f))
}
fn derive_field_types(
&'a self,
fields: &'a [&'a ast::Field<'a>],
) -> impl Iterator<Item = QuoteT> + 'a {
fields.iter().map(move |f| self.field_to_ts(f))
}
fn derive_syn_types_ptr(
&'a self,
types: &'a [&'a syn::Type],
field: &'a ast::Field<'a>,
) -> impl Iterator<Item = QuoteT> + 'a {
types.iter().map(move |ty| self.type_to_ts(ty, field))
}
fn derive_syn_types(
&'a self,
types: &'a [syn::Type],
field: &'a ast::Field<'a>,
) -> impl Iterator<Item = QuoteT> + 'a {
types.iter().map(move |ty| self.type_to_ts(ty, field))
}
fn check_flatten(&self, fields: &[&'a ast::Field<'a>], ast_container: &ast::Container) -> bool {
let has_flatten = fields.iter().any(|f| f.attrs.flatten());
if has_flatten {
if let Some(ref ct) = self.ctxt {
ct.error(format!(
"{}: #[serde(flatten)] does not work for typescript-definitions currently",
ast_container.ident.to_string()
));
}
};
has_flatten
}
}
#[cfg(test)]
mod macro_test {
use super::quote;
use super::Typescriptify;
use insta::assert_debug_snapshot_matches;
#[test]
fn tag_clash_in_enum() {
let tokens = quote!(
#[derive(Serialize)]
#[serde(tag = "kind")]
enum A {
Unit,
B { kind: i32, b: String },
}
);
let result = std::panic::catch_unwind(move || Typescriptify::parse(true, tokens));
match result {
Ok(_x) => assert!(false, "expecting panic!"),
Err(ref msg) => assert_debug_snapshot_matches!( msg.downcast_ref::<String>().unwrap(),
@r###""called `Result::unwrap()` on an `Err` value: \"2 errors:\\n\\t# variant field name `kind` conflicts with internal tag\\n\\t# clash with field in \\\"A::B\\\". Maybe use a #[serde(content=\\\"...\\\")] attribute.\"""###
),
}
}
#[test]
fn flatten_is_fail() {
let tokens = quote!(
#[derive(Serialize)]
struct SSS {
a: i32,
b: f64,
#[serde(flatten)]
c: DDD,
}
);
let result = std::panic::catch_unwind(move || Typescriptify::parse(true, tokens));
match result {
Ok(_x) => assert!(false, "expecting panic!"),
Err(ref msg) => assert_debug_snapshot_matches!( msg.downcast_ref::<String>().unwrap(),
@r###""called `Result::unwrap()` on an `Err` value: \"SSS: #[serde(flatten)] does not work for typescript-definitions currently\"""###
),
}
}
}