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 } 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 fn_name = format!("get{}Variables", feature.name);
60
61        // Interface
62        output.push_str(&format!("\nexport interface {} {{\n", interface_name));
63        for var in &feature.variables {
64            output.push_str(&format!("  {}: {};\n", var.name, ts_type(&var.var_type)));
65        }
66        output.push_str("}\n");
67
68        // Defaults
69        output.push_str(&format!(
70            "\nconst {}: {} = {{\n",
71            defaults_name, interface_name
72        ));
73        for var in &feature.variables {
74            output.push_str(&format!("  {}: {},\n", var.name, ts_value(&var.default)));
75        }
76        output.push_str("};\n");
77
78        // Getter function
79        output.push_str(&format!(
80            "\nexport function {}(client: VariableClient): {} {{\n",
81            fn_name, interface_name
82        ));
83        output.push_str(&format!(
84            "  const overrides = client.getFeatureValues(\"{}\");\n",
85            feature.name
86        ));
87        output.push_str("  return {\n");
88        output.push_str(&format!("    ...{},\n", defaults_name));
89        output.push_str("    ...overrides,\n");
90        output.push_str(&format!("  }} as {};\n", interface_name));
91        output.push_str("}\n");
92    }
93
94    output
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use variable_core::parse_and_validate;
101
102    fn generate(input: &str) -> String {
103        let var_file = parse_and_validate(input).expect("parse failed");
104        generate_typescript(&var_file)
105    }
106
107    #[test]
108    fn single_boolean_variable() {
109        let output = generate(
110            r#"1: Feature Flags = {
111    1: Variable enabled Boolean = true
112}"#,
113        );
114        insta::assert_snapshot!(output);
115    }
116
117    #[test]
118    fn single_number_variable() {
119        let output = generate(
120            r#"1: Feature Config = {
121    1: Variable max_items Number = 50
122}"#,
123        );
124        insta::assert_snapshot!(output);
125    }
126
127    #[test]
128    fn single_string_variable() {
129        let output = generate(
130            r#"1: Feature Config = {
131    1: Variable title String = "Hello"
132}"#,
133        );
134        insta::assert_snapshot!(output);
135    }
136
137    #[test]
138    fn multiple_variables() {
139        let output = generate(
140            r#"1: Feature Checkout = {
141    1: Variable enabled Boolean = true
142    2: Variable max_items Number = 50
143    3: Variable header_text String = "Complete your purchase"
144}"#,
145        );
146        insta::assert_snapshot!(output);
147    }
148
149    #[test]
150    fn multiple_features() {
151        let output = generate(
152            r#"1: Feature Checkout = {
153    1: Variable enabled Boolean = true
154}
155
1562: Feature Search = {
157    1: Variable query String = "default"
158}"#,
159        );
160        insta::assert_snapshot!(output);
161    }
162
163    #[test]
164    fn string_with_special_characters() {
165        let output = generate(
166            r#"1: Feature Config = {
167    1: Variable message String = "He said \"hello\"\nNew line"
168}"#,
169        );
170        insta::assert_snapshot!(output);
171    }
172
173    #[test]
174    fn full_example() {
175        let output = generate(
176            r#"1: Feature Checkout = {
177    1: Variable enabled Boolean = true
178    2: Variable max_items Number = 50
179    3: Variable header_text String = "Complete your purchase"
180}
181
1822: Feature Search = {
183    1: Variable enabled Boolean = false
184    2: Variable max_results Number = 10
185    3: Variable placeholder String = "Search..."
186}"#,
187        );
188        insta::assert_snapshot!(output);
189    }
190}