typescript_definitions_derive/
lib.rs1extern crate proc_macro;
14#[macro_use]
15extern crate cfg_if;
16use proc_macro2::Ident;
17use quote::quote;
18use serde_derive_internals::{ast, Ctxt, Derive};
19use std::cell::RefCell;
21use syn::DeriveInput;
22
23mod attrs;
24mod derive_enum;
25mod derive_struct;
26mod guards;
27mod patch;
28mod tests;
29mod tots;
30mod typescript;
31mod utils;
32
33use attrs::Attrs;
34use utils::*;
35
36use patch::patch;
37
38type QuoteT = proc_macro2::TokenStream;
40
41type Bounds = Vec<TSType>;
44
45struct QuoteMaker {
46 pub body: QuoteT,
47 pub verify: Option<QuoteT>,
48 pub is_enum: bool,
49}
50#[allow(unused)]
51fn is_wasm32() -> bool {
52 use std::env;
53 match env::var("WASM32") {
54 Ok(ref v) => return v == "1",
55 _ => {}
56 }
57 let mut t = env::args().skip_while(|t| t != "--target").skip(1);
58 if let Some(target) = t.next() {
59 if target.contains("wasm32") {
60 return true;
61 }
62 };
63 false
64}
65
66cfg_if! {
71 if #[cfg(any(debug_assertions, feature = "export-typescript"))] {
72
73 #[proc_macro_derive(TypescriptDefinition, attributes(ts))]
74 pub fn derive_typescript_definition(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
75
76 if !(is_wasm32() || cfg!(feature="test")) {
77 return proc_macro::TokenStream::new();
78 }
79
80 let input = QuoteT::from(input);
81 do_derive_typescript_definition(input).into()
82 }
83 } else {
84
85 #[proc_macro_derive(TypescriptDefinition, attributes(ts))]
86 pub fn derive_typescript_definition(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
87 proc_macro::TokenStream::new()
88 }
89 }
90}
91
92cfg_if! {
97 if #[cfg(any(debug_assertions, feature = "export-typescript"))] {
98
99 #[proc_macro_derive(TypeScriptify, attributes(ts))]
100 pub fn derive_type_script_ify(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
101 let input = QuoteT::from(input);
102 do_derive_type_script_ify(input).into()
103
104 }
105 } else {
106
107 #[proc_macro_derive(TypeScriptify, attributes(ts))]
108 pub fn derive_type_script_ify(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
109 proc_macro::TokenStream::new()
110 }
111 }
112}
113
114#[allow(unused)]
115fn do_derive_typescript_definition(input: QuoteT) -> QuoteT {
116 let verify = cfg!(feature = "type-guards");
117 let parsed = Typescriptify::parse(verify, input);
118 let export_string = parsed.wasm_string();
119 let name = parsed.ctxt.ident.to_string().to_uppercase();
120
121 let export_ident = ident_from_str(&format!("TS_EXPORT_{}", name));
122
123 let mut q = quote! {
124
125 #[wasm_bindgen(typescript_custom_section)]
126 pub const #export_ident : &'static str = #export_string;
127 };
128
129 if let Some(ref verify) = parsed.wasm_verify() {
130 let export_ident = ident_from_str(&format!("TS_EXPORT_VERIFY_{}", name));
131 q.extend(quote!(
132 #[wasm_bindgen(typescript_custom_section)]
133 pub const #export_ident : &'static str = #verify;
134 ))
135 }
136
137 if cfg!(any(test, feature = "test")) {
139 let typescript_ident =
140 ident_from_str(&format!("{}___typescript_definition", &parsed.ctxt.ident));
141
142 q.extend(quote!(
143 fn #typescript_ident ( ) -> &'static str {
144 #export_string
145 }
146
147 ));
148 }
149 if let Some("1") = option_env!("TFY_SHOW_CODE") {
150 eprintln!("{}", patch(&q.to_string()));
151 }
152
153 q
154}
155
156#[allow(unused)]
157fn do_derive_type_script_ify(input: QuoteT) -> QuoteT {
158 let verify = cfg!(feature = "type-guards");
159
160 let parsed = Typescriptify::parse(verify, input);
161 let export_string = parsed.wasm_string();
162 let ident = &parsed.ctxt.ident;
163
164 let (impl_generics, ty_generics, where_clause) = parsed.ctxt.rust_generics.split_for_impl();
165
166 let type_script_guard = if cfg!(feature = "type-guards") {
167 let verifier = match parsed.wasm_verify() {
168 Some(ref txt) => quote!(Some(::std::borrow::Cow::Borrowed(#txt))),
169 None => quote!(None),
170 };
171 quote!(
172 fn type_script_guard() -> Option<::std::borrow::Cow<'static,str>> {
173 #verifier
174 }
175 )
176 } else {
177 quote!()
178 };
179 let ret = quote! {
180
181 impl #impl_generics ::typescript_definitions::TypeScriptifyTrait for #ident #ty_generics #where_clause {
182 fn type_script_ify() -> ::std::borrow::Cow<'static,str> {
183 ::std::borrow::Cow::Borrowed(#export_string)
184 }
185 #type_script_guard
186 }
187
188 };
189 if let Some("1") = option_env!("TFY_SHOW_CODE") {
190 eprintln!("{}", patch(&ret.to_string()));
191 }
192
193 ret
194}
195struct Typescriptify {
196 ctxt: ParseContext<'static>,
197 body: QuoteMaker,
198}
199impl Typescriptify {
200 fn wasm_string(&self) -> String {
201 if self.body.is_enum {
202 format!(
203 "{}export enum {} {};",
204 self.ctxt.global_attrs.to_comment_str(),
205 self.ts_ident_str(),
206 self.ts_body_str()
207 )
208 } else {
209 format!(
210 "{}export type {} = {};",
211 self.ctxt.global_attrs.to_comment_str(),
212 self.ts_ident_str(),
213 self.ts_body_str()
214 )
215 }
216 }
217 fn wasm_verify(&self) -> Option<String> {
218 match self.body.verify {
219 None => None,
220 Some(ref body) => {
221 let mut s = {
222 let ident = &self.ctxt.ident;
223 let obj = &self.ctxt.arg_name;
224 let body = body.to_string();
225 let body = patch(&body);
226
227 let generics = self.ts_generics(false);
228 let generics_wb = &generics; let is_generic = !self.ctxt.ts_generics.is_empty();
230 let name = guard_name(&ident);
231 if is_generic {
232 format!(
233 "export const {name} = {generics_wb}({obj}: any, typename: string): \
234 {obj} is {ident}{generics} => {body}",
235 name = name,
236 obj = obj,
237 body = body,
238 generics = generics,
239 generics_wb = generics_wb,
240 ident = ident
241 )
242 } else {
243 format!(
244 "export const {name} = {generics_wb}({obj}: any): \
245 {obj} is {ident}{generics} => {body}",
246 name = name,
247 obj = obj,
248 body = body,
249 generics = generics,
250 generics_wb = generics_wb,
251 ident = ident
252 )
253 }
254 };
255 for txt in self.extra_verify() {
256 s.push('\n');
257 s.push_str(&txt);
258 }
259 Some(s)
260 }
261 }
262 }
263 fn extra_verify(&self) -> Vec<String> {
264 let v = self.ctxt.extra.borrow();
265 v.iter()
266 .map(|extra| {
267 let e = extra.to_string();
268
269 let extra = patch(&e);
270 "// generic test \n".to_string() + &extra
271 })
272 .collect()
273 }
274
275 fn ts_ident_str(&self) -> String {
276 let ts_ident = self.ts_ident().to_string();
277 patch(&ts_ident).into()
278 }
279 fn ts_body_str(&self) -> String {
280 let ts = self.body.body.to_string();
281 let ts = patch(&ts);
282 ts.into()
283 }
284 fn ts_generics(&self, with_bound: bool) -> QuoteT {
285 let args_wo_lt: Vec<_> = self.ts_generic_args_wo_lifetimes(with_bound).collect();
286 if args_wo_lt.is_empty() {
287 quote!()
288 } else {
289 quote!(<#(#args_wo_lt),*>)
290 }
291 }
292 fn ts_ident(&self) -> QuoteT {
294 let ident = &self.ctxt.ident;
295 let generics = self.ts_generics(false);
296 quote!(#ident#generics)
297 }
298
299 fn ts_generic_args_wo_lifetimes(&self, with_bounds: bool) -> impl Iterator<Item = QuoteT> + '_ {
300 self.ctxt.ts_generics.iter().filter_map(move |g| match g {
301 Some((ref ident, ref bounds)) => {
302 if bounds.is_empty() || !with_bounds {
304 Some(quote! (#ident))
305 } else {
306 let bounds = bounds.iter().map(|ts| &ts.ident);
307 Some(quote! { #ident extends #(#bounds)&* })
308 }
309 }
310
311 None => None,
312 })
313 }
314
315 fn parse(gen_verifier: bool, input: QuoteT) -> Self {
316 let input: DeriveInput = syn::parse2(input).unwrap();
317
318 let cx = Ctxt::new();
319 let mut attrs = attrs::Attrs::new();
320 attrs.push_doc_comment(&input.attrs);
321 attrs.push_attrs(&input.ident, &input.attrs, Some(&cx));
322
323 let container = ast::Container::from_ast(&cx, &input, Derive::Serialize);
324 let ts_generics = ts_generics(container.generics);
325 let gv = gen_verifier && attrs.guard;
326
327 let (typescript, ctxt) = {
328 let pctxt = ParseContext {
329 ctxt: Some(&cx),
330 arg_name: quote!(obj),
331 global_attrs: attrs,
332 gen_guard: gv,
333 ident: container.ident.clone(),
334 ts_generics,
335 rust_generics: container.generics.clone(),
336 extra: RefCell::new(vec![]),
337 };
338
339 let typescript = match container.data {
340 ast::Data::Enum(ref variants) => pctxt.derive_enum(variants, &container),
341 ast::Data::Struct(style, ref fields) => {
342 pctxt.derive_struct(style, fields, &container)
343 }
344 };
345 (
347 typescript,
348 ParseContext {
349 ctxt: None,
350 ..pctxt
351 },
352 )
353 };
354
355 if let Err(m) = cx.check() {
357 panic!(m);
358 }
359 Self {
360 ctxt,
361 body: typescript,
362 }
363 }
364}
365
366fn ts_generics(g: &syn::Generics) -> Vec<Option<(Ident, Bounds)>> {
367 use syn::{GenericParam, TypeParamBound};
373 g.params
374 .iter()
375 .map(|p| match p {
376 GenericParam::Lifetime(..) => None,
377 GenericParam::Type(ref ty) => {
378 let bounds = ty
379 .bounds
380 .iter()
381 .filter_map(|b| match b {
382 TypeParamBound::Trait(t) => Some(&t.path),
383 _ => None, })
385 .map(last_path_element)
386 .filter_map(|b| b)
387 .collect::<Vec<_>>();
388
389 Some((ty.ident.clone(), bounds))
390 }
391 GenericParam::Const(ref param) => {
392 let ty = TSType {
393 ident: param.ident.clone(),
394 path: vec![],
395 args: vec![param.ty.clone()],
396 return_type: None,
397 };
398 Some((param.ident.clone(), vec![ty]))
399 }
400 })
401 .collect()
402}
403
404fn return_type(rt: &syn::ReturnType) -> Option<syn::Type> {
405 match rt {
406 syn::ReturnType::Default => None, syn::ReturnType::Type(_, tp) => Some(*tp.clone()),
408 }
409}
410
411struct TSType {
413 ident: syn::Ident,
414 args: Vec<syn::Type>,
415 path: Vec<syn::Ident>, return_type: Option<syn::Type>, }
418impl TSType {
419 fn path(&self) -> Vec<String> {
420 self.path.iter().map(|i| i.to_string()).collect() }
422}
423fn last_path_element(path: &syn::Path) -> Option<TSType> {
424 let fullpath = path
425 .segments
426 .iter()
427 .map(|s| s.ident.clone())
428 .collect::<Vec<_>>();
429 match path.segments.last().map(|p| p.into_value()) {
430 Some(t) => {
431 let ident = t.ident.clone();
432 let args = match &t.arguments {
433 syn::PathArguments::AngleBracketed(ref path) => &path.args,
434 syn::PathArguments::Parenthesized(ref path) => {
436 let args: Vec<_> = path.inputs.iter().cloned().collect();
437 let ret = return_type(&path.output);
438 return Some(TSType {
439 ident,
440 args,
441 path: fullpath,
442 return_type: ret,
443 });
444 }
445 syn::PathArguments::None => {
446 return Some(TSType {
447 ident,
448 args: vec![],
449 path: fullpath,
450 return_type: None,
451 });
452 }
453 };
454 let args = args
456 .iter()
457 .filter_map(|p| match p {
458 syn::GenericArgument::Type(t) => Some(t),
459 syn::GenericArgument::Binding(t) => Some(&t.ty),
460 syn::GenericArgument::Constraint(..) => None,
461 syn::GenericArgument::Const(..) => None,
462 _ => None, })
464 .cloned()
465 .collect::<Vec<_>>();
466
467 Some(TSType {
468 ident,
469 path: fullpath,
470 args,
471 return_type: None,
472 })
473 }
474 None => None,
475 }
476}
477
478pub(crate) struct FieldContext<'a> {
479 pub ctxt: &'a ParseContext<'a>, pub field: &'a ast::Field<'a>, pub attrs: Attrs, }
483
484impl<'a> FieldContext<'a> {
485 pub fn get_path(&self, ty: &syn::Type) -> Option<TSType> {
486 use syn::Type::Path;
487 use syn::TypePath;
488 match ty {
489 Path(TypePath { path, .. }) => last_path_element(&path),
490 _ => None,
491 }
492 }
493}
494
495pub(crate) struct ParseContext<'a> {
496 ctxt: Option<&'a Ctxt>, arg_name: QuoteT, global_attrs: Attrs, gen_guard: bool, ident: syn::Ident, ts_generics: Vec<Option<(Ident, Bounds)>>, rust_generics: syn::Generics, extra: RefCell<Vec<QuoteT>>, }
505
506impl<'a> ParseContext<'a> {
507 fn err_msg(&self, msg: &str) {
510 if let Some(ctxt) = self.ctxt {
511 ctxt.error(msg);
512 } else {
513 panic!(msg.to_string())
514 }
515 }
516
517 fn field_to_ts(&self, field: &ast::Field<'a>) -> QuoteT {
518 let attrs = Attrs::from_field(field, self.ctxt);
519 if attrs.ts_type.is_some() {
521 use std::str::FromStr;
522 let s = attrs.ts_type.unwrap();
523 return match QuoteT::from_str(&s) {
524 Ok(tokens) => tokens,
525 Err(..) => {
526 self.err_msg(&format!("{}: can't parse type {}", self.ident, s));
527 quote!()
528 }
529 };
530 }
531
532 let fc = FieldContext {
533 attrs,
534 ctxt: &self,
535 field,
536 };
537 if let Some(ref ty) = fc.attrs.ts_as {
538 fc.type_to_ts(ty)
539 } else {
540 fc.type_to_ts(&field.ty)
541 }
542 }
543
544 fn derive_field(&self, field: &ast::Field<'a>) -> QuoteT {
545 let field_name = field.attrs.name().serialize_name(); let field_name = ident_from_str(&field_name);
547
548 let ty = self.field_to_ts(&field);
549
550 quote! {
551 #field_name: #ty
552 }
553 }
554 fn derive_fields(
555 &'a self,
556 fields: &'a [&'a ast::Field<'a>],
557 ) -> impl Iterator<Item = QuoteT> + 'a {
558 fields.iter().map(move |f| self.derive_field(f))
559 }
560 fn derive_field_tuple(
561 &'a self,
562 fields: &'a [&'a ast::Field<'a>],
563 ) -> impl Iterator<Item = QuoteT> + 'a {
564 fields.iter().map(move |f| self.field_to_ts(f))
565 }
566
567 fn check_flatten(&self, fields: &[&'a ast::Field<'a>], ast_container: &ast::Container) -> bool {
568 let has_flatten = fields.iter().any(|f| f.attrs.flatten()); if has_flatten {
570 self.err_msg(&format!(
571 "{}: #[serde(flatten)] does not work for typescript-definitions.",
572 ast_container.ident
573 ));
574 };
575 has_flatten
576 }
577}