variable-codegen 0.1.0

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 } 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 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");

        // Getter function
        output.push_str(&format!(
            "\nexport function {}(client: VariableClient): {} {{\n",
            fn_name, interface_name
        ));
        output.push_str(&format!(
            "  const overrides = client.getFeatureValues(\"{}\");\n",
            feature.name
        ));
        output.push_str("  return {\n");
        output.push_str(&format!("    ...{},\n", defaults_name));
        output.push_str("    ...overrides,\n");
        output.push_str(&format!("  }} as {};\n", interface_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#"Feature Flags {
    Variable enabled Boolean = true
}"#);
        insta::assert_snapshot!(output);
    }

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

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

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

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

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

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

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

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