typescript_definitions_derive/
lib.rs

1// Copyright 2019 Ian Castleden
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Exports serde-serializable structs and enums to Typescript definitions.
10//!
11//! Please see documentation at [crates.io](https://crates.io/crates/typescript-definitions)
12
13extern crate proc_macro;
14#[macro_use]
15extern crate cfg_if;
16use proc_macro2::Ident;
17use quote::quote;
18use serde_derive_internals::{ast, Ctxt, Derive};
19// use std::str::FromStr;
20use 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
38// too many TokenStreams around! give it a different name
39type QuoteT = proc_macro2::TokenStream;
40
41//type QuoteMaker = quotet::QuoteT<'static>;
42
43type 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
66/// derive proc_macro to expose Typescript definitions to `wasm-bindgen`.
67///
68/// Please see documentation at [crates.io](https://crates.io/crates/typescript-definitions).
69///
70cfg_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
92/// derive proc_macro to expose Typescript definitions as a static function.
93///
94/// Please see documentation at [crates.io](https://crates.io/crates/typescript-definitions).
95///
96cfg_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    // just to allow testing... only `--features=test` seems to work
138    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; // self.ts_generics(true);
229                    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    /// type name suitable for typescript i.e. *no* 'a lifetimes
293    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                // we ignore trait bounds for typescript
303                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            // erase serde context
346            (
347                typescript,
348                ParseContext {
349                    ctxt: None,
350                    ..pctxt
351                },
352            )
353        };
354
355        // consumes context panics with errors
356        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    // lifetime params are represented by None since we are only going
368    // to translate them to '_
369
370    // impl#generics TypeScriptTrait for A<... lifetimes to '_ and T without bounds>
371
372    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, // skip lifetimes for bounds
384                    })
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, // e.g. undefined
407        syn::ReturnType::Type(_, tp) => Some(*tp.clone()),
408    }
409}
410
411// represents a typescript type T<A,B>
412struct TSType {
413    ident: syn::Ident,
414    args: Vec<syn::Type>,
415    path: Vec<syn::Ident>,          // full path
416    return_type: Option<syn::Type>, // only if function
417}
418impl TSType {
419    fn path(&self) -> Vec<String> {
420        self.path.iter().map(|i| i.to_string()).collect() // hold the memory
421    }
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                // closures Fn(A,B) -> C
435                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            // ignore lifetimes
455            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, // lifetimes, expr, constraints A : B ... skip!
463                })
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>, // global parse context
480    pub field: &'a ast::Field<'a>,  // field being parsed
481    pub attrs: Attrs,               // field attributes
482}
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>, // serde parse context for error reporting
497    arg_name: QuoteT,       // top level "name" of argument for verifier
498    global_attrs: Attrs,    // global #[ts(...)] attributes
499    gen_guard: bool,        // generate type guard for this struct/enum
500    ident: syn::Ident,      // name of enum struct
501    ts_generics: Vec<Option<(Ident, Bounds)>>, // None means a lifetime parameter
502    rust_generics: syn::Generics, // original rust generics
503    extra: RefCell<Vec<QuoteT>>, // for generic verifier hack!
504}
505
506impl<'a> ParseContext<'a> {
507    // Some helpers
508
509    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 user has provided a type ... use that
520        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(); // use serde name instead of field.member
546        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()); // .any(|f| f);
569        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}