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!(" {}: {};\n", var.name, ts_type(&var.var_type)));
65 }
66 output.push_str("}\n");
67
68 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 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}