variable_codegen/
typescript.rs1use crate::template::{
2 self, FeatureContext, FileContext, StructContext, StructFieldContext, VariableContext,
3};
4use variable_core::ast::{Value, VarFile, VarType};
5
6const TEMPLATE: &str = include_str!("templates/typescript.ts.j2");
7
8fn to_camel_case(s: &str) -> String {
9 let mut result = String::new();
10 let mut capitalize_next = false;
11 for (i, c) in s.chars().enumerate() {
12 if c == '_' {
13 capitalize_next = true;
14 } else if capitalize_next {
15 result.push(c.to_uppercase().next().unwrap());
16 capitalize_next = false;
17 } else if i == 0 {
18 result.push(c.to_lowercase().next().unwrap());
19 } else {
20 result.push(c);
21 }
22 }
23 result
24}
25
26fn ts_type(var_type: &VarType) -> String {
27 match var_type {
28 VarType::Boolean => "boolean".to_string(),
29 VarType::Integer => "number".to_string(),
30 VarType::Float => "number".to_string(),
31 VarType::String => "string".to_string(),
32 VarType::Struct(name) => name.clone(),
33 }
34}
35
36fn ts_value(value: &Value, var_file: &VarFile) -> String {
37 match value {
38 Value::Boolean(b) => b.to_string(),
39 Value::Integer(n) => format!("{}", n),
40 Value::Float(n) => {
41 if *n == (*n as i64) as f64 {
42 format!("{}", *n as i64)
43 } else {
44 format!("{}", n)
45 }
46 }
47 Value::String(s) => {
48 let escaped = s
49 .replace('\\', "\\\\")
50 .replace('"', "\\\"")
51 .replace('\n', "\\n")
52 .replace('\r', "\\r")
53 .replace('\t', "\\t");
54 format!("\"{}\"", escaped)
55 }
56 Value::Struct {
57 struct_name,
58 fields,
59 } => {
60 let struct_def = var_file.structs.iter().find(|s| &s.name == struct_name);
62 let mut all_fields = Vec::new();
63 if let Some(def) = struct_def {
64 for field in &def.fields {
65 let val = fields.get(&field.name).unwrap_or(&field.default);
66 all_fields.push(format!(" {}: {}", field.name, ts_value(val, var_file)));
67 }
68 } else {
69 for (name, val) in fields {
71 all_fields.push(format!(" {}: {}", name, ts_value(val, var_file)));
72 }
73 }
74 if all_fields.is_empty() {
75 "{}".to_string()
76 } else {
77 format!("{{\n{},\n }}", all_fields.join(",\n"))
78 }
79 }
80 }
81}
82
83fn build_context(var_file: &VarFile) -> FileContext {
84 let structs = var_file
85 .structs
86 .iter()
87 .map(|struct_def| StructContext {
88 name: struct_def.name.clone(),
89 fields: struct_def
90 .fields
91 .iter()
92 .map(|field| StructFieldContext {
93 name: field.name.clone(),
94 type_name: ts_type(&field.field_type),
95 })
96 .collect(),
97 })
98 .collect();
99
100 let features = var_file
101 .features
102 .iter()
103 .map(|feature| {
104 let camel_name = to_camel_case(&feature.name);
105 FeatureContext {
106 name: feature.name.clone(),
107 id: feature.id,
108 interface_name: format!("{}Variables", feature.name),
109 defaults_name: format!("{}Defaults", camel_name),
110 variable_ids_name: format!("{}VariableIds", camel_name),
111 fn_name: format!("get{}Variables", feature.name),
112 variables: feature
113 .variables
114 .iter()
115 .map(|var| VariableContext {
116 name: var.name.clone(),
117 type_name: ts_type(&var.var_type),
118 default_value: ts_value(&var.default, var_file),
119 id: var.id,
120 })
121 .collect(),
122 }
123 })
124 .collect();
125
126 FileContext { structs, features }
127}
128
129pub fn generate_typescript(var_file: &VarFile) -> String {
130 let context = build_context(var_file);
131 template::render(TEMPLATE, &context)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use variable_core::parse_and_validate;
138
139 fn generate(input: &str) -> String {
140 let var_file = parse_and_validate(input).expect("parse failed");
141 generate_typescript(&var_file)
142 }
143
144 #[test]
145 fn single_boolean_variable() {
146 let output = generate(
147 r#"1: Feature Flags = {
148 1: enabled Boolean = true
149}"#,
150 );
151 insta::assert_snapshot!(output);
152 }
153
154 #[test]
155 fn single_integer_variable() {
156 let output = generate(
157 r#"1: Feature Config = {
158 1: max_items Integer = 50
159}"#,
160 );
161 insta::assert_snapshot!(output);
162 }
163
164 #[test]
165 fn single_float_variable() {
166 let output = generate(
167 r#"1: Feature Config = {
168 1: ratio Float = 3.14
169}"#,
170 );
171 insta::assert_snapshot!(output);
172 }
173
174 #[test]
175 fn single_string_variable() {
176 let output = generate(
177 r#"1: Feature Config = {
178 1: title String = "Hello"
179}"#,
180 );
181 insta::assert_snapshot!(output);
182 }
183
184 #[test]
185 fn multiple_variables() {
186 let output = generate(
187 r#"1: Feature Checkout = {
188 1: enabled Boolean = true
189 2: max_items Integer = 50
190 3: header_text String = "Complete your purchase"
191}"#,
192 );
193 insta::assert_snapshot!(output);
194 }
195
196 #[test]
197 fn multiple_features() {
198 let output = generate(
199 r#"1: Feature Checkout = {
200 1: enabled Boolean = true
201}
202
2032: Feature Search = {
204 1: query String = "default"
205}"#,
206 );
207 insta::assert_snapshot!(output);
208 }
209
210 #[test]
211 fn string_with_special_characters() {
212 let output = generate(
213 r#"1: Feature Config = {
214 1: message String = "He said \"hello\"\nNew line"
215}"#,
216 );
217 insta::assert_snapshot!(output);
218 }
219
220 #[test]
221 fn full_example() {
222 let output = generate(
223 r#"1: Feature Checkout = {
224 1: enabled Boolean = true
225 2: max_items Integer = 50
226 3: header_text String = "Complete your purchase"
227 4: discount_rate Float = 0.15
228}
229
2302: Feature Search = {
231 1: enabled Boolean = false
232 2: max_results Integer = 10
233 3: placeholder String = "Search..."
234 4: boost_factor Float = 1.5
235}"#,
236 );
237 insta::assert_snapshot!(output);
238 }
239
240 #[test]
241 fn feature_with_struct_type() {
242 let output = generate(
243 r#"1: Struct Theme = {
244 1: dark_mode Boolean = false
245 2: font_size Integer = 14
246}
247
2481: Feature Dashboard = {
249 1: enabled Boolean = true
250 2: theme Theme = Theme {}
251}"#,
252 );
253 insta::assert_snapshot!(output);
254 }
255
256 #[test]
257 fn feature_with_struct_overrides() {
258 let output = generate(
259 r#"1: Struct Config = {
260 1: retries Integer = 3
261 2: verbose Boolean = false
262}
263
2641: Feature App = {
265 1: config Config = Config { retries = 5 }
266}"#,
267 );
268 insta::assert_snapshot!(output);
269 }
270}