variable-codegen 0.1.3

TypeScript code generation for the Variable feature flag DSL
Documentation
use variable_core::ast::{Value, VarFile, VarType};

fn to_camel_case(s: &str) -> String {
    let mut result = String::new();
    let mut capitalize_next = false;
    for (i, c) in s.chars().enumerate() {
        if c == '_' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(c.to_uppercase().next().unwrap());
            capitalize_next = false;
        } else if i == 0 {
            result.push(c.to_lowercase().next().unwrap());
        } else {
            result.push(c);
        }
    }
    result
}

fn ts_type(var_type: &VarType) -> &'static str {
    match var_type {
        VarType::Boolean => "boolean",
        VarType::Number => "number",
        VarType::String => "string",
    }
}

fn ts_value(value: &Value) -> String {
    match value {
        Value::Boolean(b) => b.to_string(),
        Value::Number(n) => {
            if *n == (*n as i64) as f64 {
                format!("{}", *n as i64)
            } else {
                format!("{}", n)
            }
        }
        Value::String(s) => {
            let escaped = s
                .replace('\\', "\\\\")
                .replace('"', "\\\"")
                .replace('\n', "\\n")
                .replace('\r', "\\r")
                .replace('\t', "\\t");
            format!("\"{}\"", escaped)
        }
    }
}

pub fn generate_typescript(var_file: &VarFile) -> String {
    let mut output = String::new();
    output.push_str("// This file is generated by Variable. Do not edit.\n");
    output.push_str("import { VariableClient, type VariableIdMap } from \"@variable/runtime\";\n");

    for feature in &var_file.features {
        let interface_name = format!("{}Variables", feature.name);
        let defaults_name = format!("{}Defaults", to_camel_case(&feature.name));
        let variable_ids_name = format!("{}VariableIds", to_camel_case(&feature.name));
        let fn_name = format!("get{}Variables", feature.name);

        // Interface
        output.push_str(&format!("\nexport interface {} {{\n", interface_name));
        for var in &feature.variables {
            output.push_str(&format!("  {}: {};\n", var.name, ts_type(&var.var_type)));
        }
        output.push_str("}\n");

        // Defaults
        output.push_str(&format!(
            "\nconst {}: {} = {{\n",
            defaults_name, interface_name
        ));
        for var in &feature.variables {
            output.push_str(&format!("  {}: {},\n", var.name, ts_value(&var.default)));
        }
        output.push_str("};\n");

        // Variable IDs
        output.push_str(&format!(
            "\nconst {}: VariableIdMap<{}> = {{\n",
            variable_ids_name, interface_name
        ));
        for var in &feature.variables {
            output.push_str(&format!("  {}: {},\n", var.name, var.id));
        }
        output.push_str("};\n");

        // Getter function
        output.push_str(&format!(
            "\nexport function {}(client: VariableClient): {} {{\n",
            fn_name, interface_name
        ));
        output.push_str(&format!(
            "  return client.mergeFeatureValues({}, {}, {});\n",
            feature.id, defaults_name, variable_ids_name
        ));
        output.push_str("}\n");
    }

    output
}

#[cfg(test)]
mod tests {
    use super::*;
    use variable_core::parse_and_validate;

    fn generate(input: &str) -> String {
        let var_file = parse_and_validate(input).expect("parse failed");
        generate_typescript(&var_file)
    }

    #[test]
    fn single_boolean_variable() {
        let output = generate(
            r#"1: Feature Flags = {
    1: Variable enabled Boolean = true
}"#,
        );
        insta::assert_snapshot!(output);
    }

    #[test]
    fn single_number_variable() {
        let output = generate(
            r#"1: Feature Config = {
    1: Variable max_items Number = 50
}"#,
        );
        insta::assert_snapshot!(output);
    }

    #[test]
    fn single_string_variable() {
        let output = generate(
            r#"1: Feature Config = {
    1: Variable title String = "Hello"
}"#,
        );
        insta::assert_snapshot!(output);
    }

    #[test]
    fn multiple_variables() {
        let output = generate(
            r#"1: Feature Checkout = {
    1: Variable enabled Boolean = true
    2: Variable max_items Number = 50
    3: Variable header_text String = "Complete your purchase"
}"#,
        );
        insta::assert_snapshot!(output);
    }

    #[test]
    fn multiple_features() {
        let output = generate(
            r#"1: Feature Checkout = {
    1: Variable enabled Boolean = true
}

2: Feature Search = {
    1: Variable query String = "default"
}"#,
        );
        insta::assert_snapshot!(output);
    }

    #[test]
    fn string_with_special_characters() {
        let output = generate(
            r#"1: Feature Config = {
    1: Variable message String = "He said \"hello\"\nNew line"
}"#,
        );
        insta::assert_snapshot!(output);
    }

    #[test]
    fn full_example() {
        let output = generate(
            r#"1: Feature Checkout = {
    1: Variable enabled Boolean = true
    2: Variable max_items Number = 50
    3: Variable header_text String = "Complete your purchase"
}

2: Feature Search = {
    1: Variable enabled Boolean = false
    2: Variable max_results Number = 10
    3: Variable placeholder String = "Search..."
}"#,
        );
        insta::assert_snapshot!(output);
    }
}