typescript-definitions-derive 0.1.10

serde support for exporting Typescript definitions
Documentation
// Copyright 2019 Ian Castleden
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use super::{ast, ident_from_str, Ctxt};
use quote::quote;

use proc_macro2::TokenStream;
use syn::{Attribute, Ident, Lit, Meta, /* MetaList,*/ MetaNameValue, NestedMeta};

#[derive(Debug)]
pub struct Attrs {
    pub comments: Vec<String>,
    pub guard: bool,
    pub only_first: bool,
    pub ts_type: Option<String>,
    pub ts_guard: Option<String>,
    pub ts_as: Option<syn::Type>,
}

#[inline]
fn path_to_str(path: &syn::Path) -> String {
    quote!(#path).to_string()
}

#[allow(unused)]
pub fn turbofish_check(v: &str) -> Result<TokenStream, String> {
    match v.parse::<proc_macro2::TokenStream>() {
        // just get LexError as error message... so make our own.
        Err(_) => Err(format!("Can't lex turbofish \"{}\"", v)),
        Ok(tokens) => match syn::parse2::<syn::DeriveInput>(quote!( struct S{ a:v#tokens} )) {
            Err(_) => Err(format!("Can't parse turbofish \"{}\"", v)),
            Ok(_) => Ok(tokens),
        },
    }
}
impl Attrs {
    pub fn new() -> Attrs {
        Attrs {
            comments: vec![],
            // turbofish: None,
            guard: true,
            only_first: false,
            ts_type: None,
            ts_guard: None,
            ts_as : None
            // isa: HashMap::new(),
        }
    }
    pub fn push_doc_comment(&mut self, attrs: &[Attribute]) {
        let doc_comments = attrs
            .iter()
            .filter_map(|attr| {
                if path_to_str(&attr.path) == "doc" {
                    attr.parse_meta().ok()
                } else {
                    None
                }
            })
            .filter_map(|attr| {
                use Lit::*;
                use Meta::*;
                if let NameValue(MetaNameValue {
                    ident, lit: Str(s), ..
                }) = attr
                {
                    if ident != "doc" {
                        return None;
                    }
                    let value = s.value();
                    let text = value
                        .trim_start_matches("//!")
                        .trim_start_matches("///")
                        .trim_start_matches("/*!")
                        .trim_start_matches("/**")
                        .trim_end_matches("*/")
                        .trim();
                    if text.is_empty() {
                        None
                    } else {
                        Some(text.to_string())
                    }
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        if doc_comments.is_empty() {
            return;
        }

        let merged_lines = doc_comments
            .iter()
            .map(|s| format!("// {}", s))
            .collect::<Vec<_>>()
            .join("\n");

        self.comments.push(merged_lines);
    }

    pub fn to_comment_str(&self) -> String {
        if self.comments.is_empty() {
            String::default()
        } else {
            self.comments.join("\n") + "\n" // <-- need better way!
        }
    }
    fn err_msg<'a>(&self, msg: String, ctxt: Option<&'a Ctxt>) {
        if let Some(ctxt) = ctxt {
            ctxt.error(msg);
        } else {
            panic!(msg)
        };
    }
    pub fn find_typescript<'a>(
        attrs: &'a [Attribute],
        ctxt: Option<&'a Ctxt>,
    ) -> impl Iterator<Item = Meta> + 'a {
        use syn::Meta::*;
        use NestedMeta::*;

        fn err(msg: String, ctxt: Option<&'_ Ctxt>) {
            if let Some(ctxt) = ctxt {
                ctxt.error(format!("invalid typescript syntax: {}", msg));
            } else {
                panic!("invalid typescript syntax: {}", msg)
            };
        }

        attrs
            .iter()
            .filter_map(move |attr| match path_to_str(&attr.path).as_ref() {
                "ts" => match attr.parse_meta() {
                    Ok(v) => Some(v),
                    Err(msg) => {
                        err(msg.to_string(), ctxt);
                        None
                    }
                },
                _ => None,
            })
            .filter_map(move |m| match m {
                List(l) => Some(l.nested),
                ref tokens => {
                    err(quote!(#tokens).to_string(), ctxt);
                    None
                }
            })
            .flatten()
            .filter_map(move |m| match m {
                Meta(m) => Some(m),
                ref tokens => {
                    err(quote!(#tokens).to_string(), ctxt);
                    None
                }
            })
    }
    pub fn push_attrs(&mut self, struct_ident: &Ident, attrs: &[Attribute], ctxt: Option<&Ctxt>) {
        use syn::Meta::*;
        use Lit::*;
        // use NestedMeta::*;

        for attr in Self::find_typescript(&attrs, ctxt) {
            match attr {
                NameValue(MetaNameValue {
                    ref ident,
                    lit: Bool(ref value),
                    ..
                }) if ident == "guard" => {
                    self.guard = value.value;
                }
                NameValue(MetaNameValue {
                    ref ident,
                    lit: Str(ref value),
                    ..
                }) if ident == "guard" => {
                    self.guard = match value.value().parse() {
                        Ok(v) => v,
                        Err(..) => {
                            self.err_msg(
                                format!(
                                    "{}: guard must be true or false not \"{}\"",
                                    struct_ident,
                                    quote!(#value)
                                ),
                                ctxt,
                            );
                            false
                        }
                    }
                }
                Word(ref w) if w == "guard" => self.guard = true,
                // List(MetaList {
                //     ref ident,
                //     ref nested,
                //     ..
                // }) if ident == "isa" => {
                //     for method in nested {
                //         match *method {
                //             Meta(NameValue(MetaNameValue {
                //                 ref ident,
                //                 lit: Str(ref v),
                //                 ..
                //             })) => match v.value().parse::<TokenStream>() {
                //                 Ok(t) => {
                //                     self.isa.insert(ident.to_string(), quote!(#t));
                //                 }
                //                 Err(_) => self.err_msg(format!("Can't parse {}", quote!(#v)), ctxt),
                //             },
                //             ref mi @ _ => panic!("unsupported raw entry: {}", quote!(#mi)),
                //         }
                //     }
                // }
                // NameValue(MetaNameValue {
                //     ref ident,
                //     lit: Str(ref value),
                //     ..
                // }) if ident == "turbofish" => {
                //     let v = value.value();
                //     match turbofish_check(&v) {
                //         Err(msg) => self.err_msg(msg, ctxt),
                //         Ok(tokens) => self.turbofish = Some(tokens),
                //     }
                // }
                ref i @ NameValue(..) | ref i @ List(..) | ref i @ Word(..) => {
                    self.err_msg(format!("unsupported option: {}", quote!(#i)), ctxt);
                }
            }
        }
    }
    pub fn push_field_attrs(
        &mut self,
        struct_ident: &Ident,
        attrs: &[Attribute],
        ctxt: Option<&Ctxt>,
    ) {
        use syn::Meta::*;
        use Lit::*;
        // use NestedMeta::*;

        for attr in Self::find_typescript(&attrs, ctxt) {
            match attr {
                NameValue(MetaNameValue {
                    ref ident,
                    lit: Str(ref value),
                    ..
                }) if ident == "ts_type" => {
                    let v = value.value();

                    self.ts_type = Some(v);
                }
                NameValue(MetaNameValue {
                    ref ident,
                    lit: Str(ref value),
                    ..
                }) if ident == "ts_guard" => {
                    let v = value.value();
                    self.ts_guard = Some(v);
                }
                NameValue(MetaNameValue {
                    ref ident,
                    lit: Str(ref value),
                    ..
                }) if ident == "ts_as" => {
                    let v = value.value();
                    match syn::parse_str::<syn::Type>(&v) {
                        Ok(t) => self.ts_as = Some(t),
                        Err(..) => {
                            self.err_msg(
                                format!("ts_as: \"{}\" is not a valid rust type", v),
                                ctxt,
                            );
                        }
                    }
                    //
                }
                NameValue(MetaNameValue {
                    ref ident,
                    lit: Str(ref value),
                    ..
                }) if ident == "array_check" => {
                    self.only_first = match value.value().as_ref() {
                        "first" => true,
                        "all" => false,
                        _ => {
                            self.err_msg(
                                format!(
                                    r#"{}: array_check value must be "first" or "all" not "{}""#,
                                    struct_ident,
                                    quote!(#value)
                                ),
                                ctxt,
                            );
                            false
                        }
                    }
                }
                Word(ref w) if w == "array_check" => self.only_first = true,

                ref i @ NameValue(..) | ref i @ List(..) | ref i @ Word(..) => {
                    self.err_msg(format!("unsupported option: {}", quote!(#i)), ctxt);
                }
            }
        }
    }
    pub fn from_field(field: &ast::Field, ctxt: Option<&Ctxt>) -> Attrs {
        let mut res = Self::new();
        if let Some(ref ident) = field.original.ident {
            res.push_field_attrs(ident, &field.original.attrs, ctxt);
        } else {
            let id = ident_from_str("unnamed");
            res.push_field_attrs(&id, &field.original.attrs, ctxt);
        }
        res
    }
}