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