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