/* Copyright 2022 Zinc Labs Inc. and Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use anyhow::Result; use askama::Template; use convert_case::{Case, Casing}; use proc_macro::{Ident, TokenStream, TokenTree}; use std::collections::VecDeque; #[derive(Template)] #[template(path = "builder.j2", escape = "none")] pub struct BuilderContext { name: String, fields: Vec, contains: fn(haystack: &[&str], needle: &str) -> bool, uppersnake: fn(s: &str) -> String, } #[derive(Debug, Default)] struct Fd { name: String, typ: String, optional: bool, attr_name: String, attr_default: String, attr_help: String, // new field for storing documentation comments attr_parse: bool, // whether use FromStr trait to parse into a field } impl Fd { pub fn new(name: &[TokenTree], typ: &[TokenTree]) -> Self { // collect Ident("Option"), Punct('<'), Ident("String"), Punct('>') into a String vec // like: vec!["Option", "<", "String", ">"] // find env_config Group let mut attr_name: String = String::from(""); let mut attr_default: String = String::from(""); let mut attr_help: String = String::from(""); let mut attr_parse: bool = false; for item in name { if let TokenTree::Group(g) = item { let mut g = g.stream().into_iter(); let ident = g.next().unwrap(); if ident.to_string() == "env_config" { let ident = g.next().unwrap(); if let TokenTree::Group(g) = ident { let attrs = get_struct_attribute(g.stream()); for item in attrs { match item.0.as_str() { "name" => { attr_name = item.1; } "default" => { attr_default = item.1; } "help" => { attr_help = item.1; } "parse" => { if item.1.is_empty() { attr_parse = true; } else { attr_parse = item.1.parse().unwrap(); } } _ => {} } } } break; } } } let typ = typ .iter() .map(|v| match v { TokenTree::Ident(n) => n.to_string(), TokenTree::Punct(p) => p.as_char().to_string(), e => panic!("Expect ident, but got {:?}", e), }) .collect::>(); // it's name of field that last TokenTree before Punct(':') // eg: executable: String, // warn: there not use name[0], because it maybe `pub executable: String` match name.last() { Some(TokenTree::Ident(name)) => { // if typ first is Option, then from second take last let (typ, optional) = if typ[0].as_str() == "Option" { (&typ[2..typ.len() - 1], true) } else { (&typ[..], false) }; Self { name: name.to_string(), typ: typ.join(""), optional, attr_name, attr_default, attr_help, attr_parse, } } e => panic!("Expect ident, but got {:?}", e), } } } impl BuilderContext { /// build BuilderContext from TokenStream fn new(input: TokenStream) -> Self { let (name, input) = split(input); let fields = get_struct_fields(input); Self { name: name.to_string(), fields, contains: |haystack, needle| haystack.contains(&needle), uppersnake: |s| s.to_case(Case::UpperSnake), } } /// render template to code Token pub fn render(input: TokenStream) -> Result { let template = Self::new(input); Ok(template.render()?) } } /// split TokenStream to struct name, fields fn split(input: TokenStream) -> (Ident, TokenStream) { let mut input = input.into_iter().collect::>(); while let Some(item) = input.pop_front() { if let TokenTree::Ident(v) = item { if v.to_string() == "struct" { break; } } } // struct name should behind struct let ident; if let Some(TokenTree::Ident(v)) = input.pop_front() { ident = v; } else { panic!("Didn't find struct name"); } // find first Group let mut group = None; for item in input { if let TokenTree::Group(g) = item { group = Some(g); break; } } (ident, group.expect("Didn't find field group").stream()) } /// find all Fd from TokenStream fn get_struct_fields(input: TokenStream) -> Vec { let input = input.into_iter().collect::>(); input .split(|v| match v { TokenTree::Punct(p) => p.as_char() == ',', _ => false, }) .map(|tokens| { tokens .split(|v| match v { TokenTree::Punct(p) => p.as_char() == ':', _ => false, }) .collect::>() }) .filter(|tokens| tokens.len() == 2) .map(|tokens| Fd::new(tokens[0], tokens[1])) .collect() } /// find all attribute from TokenStream fn get_struct_attribute(input: TokenStream) -> Vec<(String, String)> { let input = input.into_iter().collect::>(); input .split(|v| match v { TokenTree::Punct(p) => p.as_char() == ',', _ => false, }) .map(|tokens| { tokens .split(|v| match v { TokenTree::Punct(p) => p.as_char() == '=', _ => false, }) .collect::>() }) .map(|tokens| { let token0 = tokens[0] .last() .unwrap() .to_string() .trim_matches(|c: char| c == '"' || c == '\'') .to_string(); let token1 = if tokens.len() > 1 { tokens[1] .last() .unwrap() .to_string() .trim_matches(|c: char| c == '"' || c == '\'') .to_string() } else { String::from("") }; (token0, token1) }) .collect() }