Skip to main content

variable_codegen/
typescript.rs

1use variable_core::ast::{Value, VarFile, VarType};
2
3fn to_camel_case(s: &str) -> String {
4    let mut result = String::new();
5    let mut capitalize_next = false;
6    for (i, c) in s.chars().enumerate() {
7        if c == '_' {
8            capitalize_next = true;
9        } else if capitalize_next {
10            result.push(c.to_uppercase().next().unwrap());
11            capitalize_next = false;
12        } else if i == 0 {
13            result.push(c.to_lowercase().next().unwrap());
14        } else {
15            result.push(c);
16        }
17    }
18    result
19}
20
21fn ts_type(var_type: &VarType) -> &'static str {
22    match var_type {
23        VarType::Boolean => "boolean",
24        VarType::Number => "number",
25        VarType::String => "string",
26    }
27}
28
29fn ts_value(value: &Value) -> String {
30    match value {
31        Value::Boolean(b) => b.to_string(),
32        Value::Number(n) => {
33            if *n == (*n as i64) as f64 {
34                format!("{}", *n as i64)
35            } else {
36                format!("{}", n)
37            }
38        }
39        Value::String(s) => {
40            let escaped = s
41                .replace('\\', "\\\\")
42                .replace('"', "\\\"")
43                .replace('\n', "\\n")
44                .replace('\r', "\\r")
45                .replace('\t', "\\t");
46            format!("\"{}\"", escaped)
47        }
48    }
49}
50
51pub fn generate_typescript(var_file: &VarFile) -> String {
52    let mut output = String::new();
53    output.push_str("// This file is generated by Variable. Do not edit.\n");
54    output.push_str("import { VariableClient, type VariableIdMap } from \"@variable/runtime\";\n");
55
56    for feature in &var_file.features {
57        let interface_name = format!("{}Variables", feature.name);
58        let defaults_name = format!("{}Defaults", to_camel_case(&feature.name));
59        let variable_ids_name = format!("{}VariableIds", to_camel_case(&feature.name));
60        let fn_name = format!("get{}Variables", feature.name);
61
62        // Interface
63        output.push_str(&format!("\nexport interface {} {{\n", interface_name));
64        for var in &feature.variables {
65            output.push_str(&format!("  {}: {};\n", var.name, ts_type(&var.var_type)));
66        }
67        output.push_str("}\n");
68
69        // Defaults
70        output.push_str(&format!(
71            "\nconst {}: {} = {{\n",
72            defaults_name, interface_name
73        ));
74        for var in &feature.variables {
75            output.push_str(&format!("  {}: {},\n", var.name, ts_value(&var.default)));
76        }
77        output.push_str("};\n");
78
79        // Variable IDs
80        output.push_str(&format!(
81            "\nconst {}: VariableIdMap<{}> = {{\n",
82            variable_ids_name, interface_name
83        ));
84        for var in &feature.variables {
85            output.push_str(&format!("  {}: {},\n", var.name, var.id));
86        }
87        output.push_str("};\n");
88
89        // Getter function
90        output.push_str(&format!(
91            "\nexport function {}(client: VariableClient): {} {{\n",
92            fn_name, interface_name
93        ));
94        output.push_str(&format!(
95            "  return client.mergeFeatureValues({}, {}, {});\n",
96            feature.id, defaults_name, variable_ids_name
97        ));
98        output.push_str("}\n");
99    }
100
101    output
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use variable_core::parse_and_validate;
108
109    fn generate(input: &str) -> String {
110        let var_file = parse_and_validate(input).expect("parse failed");
111        generate_typescript(&var_file)
112    }
113
114    #[test]
115    fn single_boolean_variable() {
116        let output = generate(
117            r#"1: Feature Flags = {
118    1: Variable enabled Boolean = true
119}"#,
120        );
121        insta::assert_snapshot!(output);
122    }
123
124    #[test]
125    fn single_number_variable() {
126        let output = generate(
127            r#"1: Feature Config = {
128    1: Variable max_items Number = 50
129}"#,
130        );
131        insta::assert_snapshot!(output);
132    }
133
134    #[test]
135    fn single_string_variable() {
136        let output = generate(
137            r#"1: Feature Config = {
138    1: Variable title String = "Hello"
139}"#,
140        );
141        insta::assert_snapshot!(output);
142    }
143
144    #[test]
145    fn multiple_variables() {
146        let output = generate(
147            r#"1: Feature Checkout = {
148    1: Variable enabled Boolean = true
149    2: Variable max_items Number = 50
150    3: Variable header_text String = "Complete your purchase"
151}"#,
152        );
153        insta::assert_snapshot!(output);
154    }
155
156    #[test]
157    fn multiple_features() {
158        let output = generate(
159            r#"1: Feature Checkout = {
160    1: Variable enabled Boolean = true
161}
162
1632: Feature Search = {
164    1: Variable query String = "default"
165}"#,
166        );
167        insta::assert_snapshot!(output);
168    }
169
170    #[test]
171    fn string_with_special_characters() {
172        let output = generate(
173            r#"1: Feature Config = {
174    1: Variable message String = "He said \"hello\"\nNew line"
175}"#,
176        );
177        insta::assert_snapshot!(output);
178    }
179
180    #[test]
181    fn full_example() {
182        let output = generate(
183            r#"1: Feature Checkout = {
184    1: Variable enabled Boolean = true
185    2: Variable max_items Number = 50
186    3: Variable header_text String = "Complete your purchase"
187}
188
1892: Feature Search = {
190    1: Variable enabled Boolean = false
191    2: Variable max_results Number = 10
192    3: Variable placeholder String = "Search..."
193}"#,
194        );
195        insta::assert_snapshot!(output);
196    }
197}