env_loader_convert 0.2.1

Macro for value conversion for env-loader crate. Syntax sugar.
Documentation
use proc_macro::TokenStream;
use quote::quote;
use std::str::FromStr;
mod validators;
use validators::{validate_bool_constraints, validate_num_constraints, Constraint};
struct KeyValue {
    key: String,
    ty: String,
    constraints: Option<Vec<Constraint>>,
}

impl FromStr for KeyValue {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s.split("=>").collect();

        let kv_parts: Vec<&str> = parts[0].split(':').collect();

        if kv_parts.len() != 2 {
            return Err(());
        }
        let key = kv_parts[0].trim().to_string();
        let ty = kv_parts[1].trim().to_string();

        if parts.len() != 2 {
            return Ok(KeyValue {
                key,
                ty,
                constraints: None,
            });
        }

        let splitted_constraints: Vec<String> = parts[1]
            .trim()
            .split_ascii_whitespace()
            .map(|cons| cons.trim().to_string())
            .collect();

        let mut constraints = vec![];

        for constraint in splitted_constraints {
            if constraint.starts_with("min(") && constraint.ends_with(')') {
                let value = constraint[4..constraint.len() - 1].trim().parse::<i64>();
                if value.is_err() {
                    panic!("Wrong value for constraint Min provided");
                }
                constraints.push(Constraint::Min(value.unwrap()));
                continue;
            } else if constraint.starts_with("max(") && constraint.ends_with(')') {
                let value = constraint[4..constraint.len() - 1].trim().parse::<i64>();
                if value.is_err() {
                    panic!("Wrong value for constraint Max provided");
                }
                constraints.push(Constraint::Max(value.unwrap()));
                continue;
            } else if constraint.to_ascii_lowercase() == "notempty" {
                constraints.push(Constraint::NotEmpty);
                continue;
            } else if constraint.to_ascii_lowercase() == "optional" {
                constraints.push(Constraint::Optional);
                continue;
            } else if constraint.starts_with("len(") && constraint.ends_with(')') {
                let value = constraint[4..constraint.len() - 1].trim().parse::<usize>();
                if value.is_err() {
                    panic!("Wrong value for constraint Len provided");
                }
                constraints.push(Constraint::Len(value.unwrap()));
                continue;
            } else {
                panic!("Wrong constraint syntax {key}: {constraint}.");
            }
        }

        Ok(KeyValue {
            key,
            ty,
            constraints: Some(constraints),
        })
    }
}

fn generate_mask(constraints: &Option<Vec<Constraint>>) -> String {
    let constraints = constraints.as_ref().unwrap();
    let mut mask = [
        "".to_string(),
        "".to_string(),
        "".to_string(),
        "".to_string(),
        "".to_string(),
        "".to_string(),
    ];
    for cons in constraints {
        match cons {
            Constraint::Max(val) => mask[0].push_str(&format!("{val}")),
            Constraint::Min(val) => mask[1].push_str(&format!("{val}")),
            Constraint::Optional => mask[2].push('1'),
            Constraint::NotEmpty => mask[3].push('1'),

            Constraint::Len(len) => mask[4].push_str(&format!("{len}")),
        }
    }

    mask.join(",")
}

#[proc_macro]
pub fn convert(input: TokenStream) -> TokenStream {
    let input = input.to_string();
    let input = input.trim();
    let input = input.strip_prefix('{').unwrap_or(input);
    let input = input.strip_suffix('}').unwrap_or(input);
    let input = input.trim();

    let key_values: Vec<KeyValue> = input
        .split(',')
        .map(|s| s.trim().parse())
        .collect::<Result<Vec<KeyValue>, _>>()
        .unwrap_or_else(|_| panic!("Invalid input: {:?}", input));

    let result = key_values.iter().map(
        |KeyValue {
             key,
             ty,
             constraints,
         }| {
            let key_str = key.as_str();
            let typing = match ty.to_ascii_lowercase().as_str() {
                "int" | "integer" | "i32" => {
                    if constraints.is_some() {
                        validate_num_constraints(key, constraints.as_ref().unwrap());
                    }

                    quote! { Value::Int }
                }
                "str" | "string" => {
                    quote! { Value::Str }
                }
                "long" | "i64" => {
                    if constraints.is_some() {
                        validate_num_constraints(key, constraints.as_ref().unwrap());
                    }
                    quote! {Value::Long }
                }
                "bool" | "boolean" => {
                    if constraints.is_some() {
                        validate_bool_constraints(key, constraints.as_ref().unwrap());
                    }
                    quote! { Value::Bool }
                }
                _ => panic!("Unsupported type: {:?}", ty),
            };

            if constraints.is_none() {
                quote! { (#key_str, #typing, "".to_string() ) }
            } else {
                let mask = generate_mask(constraints);
                quote! { (#key_str, #typing, #mask.to_string() ) }
            }
        },
    );

    let output = quote! { [ #( #result ),* ] };

    output.into()
}