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, 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 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 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 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 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}