variable_codegen/
typescript.rs1use 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 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 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 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}