use std::env; use std::fs; use std::path::Path; 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 = 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, 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, docinfo: &HashMap, 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 == "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, doc: String, } fn render_type( pair: pest::iterators::Pair, docinfo: TypeDocInfo, classes: &mut HashMap, ) -> 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::>(); 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, 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::>(); 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::>(); 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, } fn extract_docinfo( pair: pest::iterators::Pair, classes: &mut HashMap, ) -> 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)); } } Rule::EOI => {}, _ => { 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()) } fn main() { let src_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let types_dest_path = Path::new(&src_dir).join("src").join("tl_types.rs"); let methods_dest_path = Path::new(&src_dir).join("src").join("tl_methods.rs"); let src_path = Path::new(&src_dir).join("td_api.tl"); println!("cargo:rerun-if-changed={}",src_path.display()); let src = fs::read_to_string(src_path).expect("no td_api.tl file"); let (t, m) = generate(&src); fs::write(types_dest_path, t).expect("cannot write output file"); fs::write(methods_dest_path, m).expect("cannot write output file"); }