tl-codegen 0.1.0

Generate rust types from a tl file
Documentation
extern crate pest;
#[macro_use]
extern crate pest_derive;
#[macro_use]
extern crate quote;

use pest::Parser;
use std::collections::HashMap;

#[derive(Parser)]
#[grammar = "tl.pest"]
struct TlParser;
const _GRAMMAR: &str = include_str!("tl.pest");

fn capitalize(s: &str) -> String {
    let mut v: Vec<char> = s.chars().collect();
    v[0] = v[0].to_uppercase().nth(0).unwrap();
    v.into_iter().collect()
}

fn convert_type(t: &str) -> String {
    match t {
        "double" => "f64".to_owned(),
        "string" => "String".to_owned(),
        "int32" => "i32".to_owned(),
        "int53" => "i64".to_owned(),
        "int64" => "i64".to_owned(),
        "Bool" => "bool".to_owned(),
        "bytes" => "String".to_owned(),
        _ => capitalize(t),
    }.to_owned()
}

fn convert_typeid(pair: pest::iterators::Pair<Rule>, res: &mut String) {
    match pair.as_rule() {
        Rule::vector => {
            let inner = pair
                .into_inner()
                .next()
                .unwrap()
                .into_inner()
                .next()
                .unwrap();
            res.push_str("Vec<");
            convert_typeid(inner, res);
            res.push_str(">");
        }
        Rule::ident => {
            let ident = pair.as_str();
            let ident = convert_type(ident);
            res.push_str(&ident);
        }
        _ => unreachable!(),
    };
}

fn render_param(
    pair: pest::iterators::Pair<Rule>,
    docinfo: &HashMap<String, ParamDocInfo>,
    parent_class: &str,
) -> quote::Tokens {
    let mut pairs = pair.into_inner();
    let mut name = pairs.next().unwrap().as_str().to_owned();
    let docinfo = docinfo.get(&name).unwrap();
    let typeid = pairs.next().unwrap();
    let typeid = typeid.into_inner().next().unwrap();
    let mut typeid_str = String::new();
    convert_typeid(typeid, &mut typeid_str);
    if typeid_str == parent_class {
        typeid_str = format!("Box<{}>", typeid_str);
    }
    let typeid = typeid_str;
    let mut pre = if name == "type" {
        name.push('_');
        quote! {
            #[serde(rename="type")]
        }
    } else {
        quote::Tokens::new()
    };
    let default_false = if typeid == "bool" {
        quote!{
            #[serde(default)]
        }
    } else {
        quote::Tokens::new()
    };
    let serialize_number = if typeid == "i32" || typeid == "i64" {
        quote!{
            #[serde(deserialize_with="::serde_aux::field_attributes::deserialize_number_from_string")]
        }
    } else {
        quote::Tokens::new()
    };
    let typeid = if docinfo.optional {
        quote::Ident::new(format!("Option<{}>", typeid))
    } else {
        quote::Ident::new(typeid)
    };
    let name = quote::Ident::new(name);
    let doc = docinfo.doc.replace("//-", " ");
    pre.append(quote! {
        #[doc = #doc]
        #serialize_number
        #default_false
        pub #name:#typeid
    });
    pre
}

#[derive(Debug)]
struct Class {
    name: String,
    types: Vec<String>,
    doc: String,
}

fn render_type(
    pair: pest::iterators::Pair<Rule>,
    docinfo: TypeDocInfo,
    classes: &mut HashMap<String, Class>,
) -> quote::Tokens {
    let mut pairs = pair.into_inner();
    let name = pairs.next().unwrap().as_str();
    let name_capitalized = quote::Ident::new(capitalize(name));
    let params = pairs.next().unwrap();
    let classname = capitalize(pairs.next().unwrap().as_str());
    let params = params
        .into_inner()
        .map(|p| render_param(p, &docinfo.params, &classname))
        .collect::<Vec<_>>();
    let class = classes.entry(classname.clone()).or_insert_with(|| Class {
        name: classname,
        types: Vec::new(),
        doc: String::new(),
    });
    class.types.push(capitalize(name));

    let doc = docinfo.doc.replace("//-", " ");
    quote! {
        #[derive(Serialize, Deserialize, Debug, Clone)]
        #[doc = #doc]
        pub struct #name_capitalized {
            #(#params),*
        }
    }
}

fn render_method(pair: pest::iterators::Pair<Rule>, docinfo: TypeDocInfo) -> quote::Tokens {
    let mut pairs = pair.into_inner();
    let name = pairs.next().unwrap().as_str();
    let name_capitalized = capitalize(name);
    let params = pairs.next().unwrap();
    let params = params
        .into_inner()
        .map(|p| render_param(p, &docinfo.params, ""))
        .collect::<Vec<_>>();
    let name_ident = quote::Ident::new(name_capitalized);
    let rettype = quote::Ident::new(convert_type(pairs.next().unwrap().as_str()));

    let doc = docinfo.doc.replace("//-", " ");
    quote! {
        #[derive(Serialize, Deserialize, Debug, Clone)]
        #[doc = #doc]
        pub struct #name_ident {
            #(#params),*
        }
        impl Method for #name_ident {
            const TYPE: &'static str = #name;
            type Response = #rettype;
        }
    }
}

fn render_class(class: Class) -> quote::Tokens {
    let name = quote::Ident::new(class.name);
    let types = class
        .types
        .into_iter()
        .map(|t| quote::Ident::new(t))
        .collect::<Vec<_>>();
    if types.len() <= 1 {
        return quote::Tokens::new();
    }
    let types2 = types.clone();
    let doc = class.doc.replace("//-", " ");
    quote! {
        #[derive(Serialize, Deserialize, Debug, Clone)]
        #[serde(rename_all="camelCase")]
        #[serde(tag="@type")]
        #[doc = #doc]
        pub enum #name {
            #(#types(#types2)),*
        }
    }
}

#[derive(Debug)]
struct ParamDocInfo {
    optional: bool,
    doc: String,
}
#[derive(Debug)]
struct TypeDocInfo {
    doc: String,
    params: HashMap<String, ParamDocInfo>,
}

fn extract_docinfo(
    pair: pest::iterators::Pair<Rule>,
    classes: &mut HashMap<String, Class>,
) -> TypeDocInfo {
    let mut params = HashMap::new();
    let mut doc = String::new();
    for p in pair.into_inner() {
        let mut pairs = p.into_inner();
        let name = pairs.next().unwrap().as_str();
        let descr = pairs.next().unwrap().as_str();
        if name == "description" {
            doc = descr.to_owned();
        } else if name == "class" {
            classes.insert(
                name.to_owned(),
                Class {
                    name: name.to_owned(),
                    types: Vec::new(),
                    doc: descr.to_owned(),
                },
            );
        } else {
            let optional = descr.contains("may be null")
                || descr.contains("only available to bots")
                || descr.contains("bots only")
                || descr.contains("or null");
            let n = if name == "param_description" {
                "description"
            } else {
                name
            };
            params.insert(
                n.to_owned(),
                ParamDocInfo {
                    optional,
                    doc: descr.to_owned(),
                },
            );
        }
    }
    TypeDocInfo { doc, params }
}

pub fn generate(src: &str) -> (String, String) {
    let pairs = TlParser::parse(Rule::tl, &src).unwrap_or_else(|e| panic!("{}", e));

    let mut functions = false;
    let mut classes = HashMap::new();
    let mut type_tokens = quote::Tokens::new();
    let mut method_tokens = quote::Tokens::new();
    for pair in pairs {
        match pair.as_rule() {
            Rule::section => {
                functions = true;
            }
            Rule::definition => {
                let mut pairs = pair.into_inner();
                let docstring = pairs.next().unwrap();
                let typedef = pairs.next().unwrap();
                let docinfo = extract_docinfo(docstring, &mut classes);
                if functions {
                    method_tokens.append(render_method(typedef, docinfo));
                } else {
                    type_tokens.append(render_type(typedef, docinfo, &mut classes));
                }
            }
            _ => {
                unreachable!();
            }
        }
    }
    for (_, class) in classes.into_iter() {
        type_tokens.append(render_class(class));
    }
    (type_tokens.as_str().to_owned(), method_tokens.as_str().to_owned())
}