dotenv_config_ext 0.1.3

parse `env` to config struct for Rust
Documentation
/* 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<Fd>,
    contains: fn(haystack: &[&str], needle: &str) -> bool,
    uppersnake: fn(s: &str) -> String,
    parse_bool: fn(s: &str) -> bool,
}

#[derive(Debug, Default)]
struct Fd {
    name: String,
    typ: String,
    optional: bool,
    attr_name: String,
    attr_default: String,
    attr_ext: String,
    attr_ext_post_with: String,
}

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_ext: String = String::from("");
        let mut attr_ext_post_with: String = String::from("");
        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();
                    match ident {
                        TokenTree::Group(g) => {
                            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;
                                    }
                                    "ext" => {
                                        attr_ext = item.1;
                                    }
                                    "ext_post_with" => {
                                        attr_ext_post_with = item.1;
                                    }
                                    _ => {}
                                }
                            }
                        }
                        _ => {}
                    }
                    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::<Vec<_>>();

        // 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_ext,
                    attr_ext_post_with
                }
            }
            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),
            parse_bool: |s| to_bool(s),
        }
    }

    /// render template to code Token
    pub fn render(input: TokenStream) -> Result<String> {
        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::<VecDeque<_>>();
    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<Fd> {
    let input = input.into_iter().collect::<Vec<_>>();
    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::<Vec<_>>()
        })
        .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::<Vec<_>>();
    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::<Vec<_>>()
        })
        .filter(|tokens| tokens.len() == 2)
        .map(|tokens| {
            (
                tokens[0]
                    .last()
                    .unwrap()
                    .to_string()
                    .trim_matches(|c: char| c == '"' || c == '\'')
                    .to_string(),
                tokens[1]
                    .last()
                    .unwrap()
                    .to_string()
                    .trim_matches(|c: char| c == '"' || c == '\'')
                    .to_string(),
            )
        })
        .collect()
}

fn to_bool(s: impl Into<String>) -> bool {
    match s.into().parse::<bool>() {
        Ok(b) => b,
        _ => false,
    }
}