1use serde_json::{Map, Value};
25
26use crate::data::{resolve_path, resolve_path_string};
27use crate::spec::Spec;
28
29const EXPR_DATA_KEY: &str = "$data";
30const EXPR_TEMPLATE_KEY: &str = "$template";
31
32pub fn resolve_expressions(spec: &mut Spec) {
36 let data = spec.data.clone();
37 for el in spec.elements.values_mut() {
38 resolve_value(&mut el.props, &data);
39 }
40}
41
42fn resolve_value(val: &mut Value, data: &Value) {
43 match val {
44 Value::Object(map) => {
45 if let Some(path) = is_data_expr(map) {
46 let path = path.to_owned();
47 *val = resolve_path(data, &path).cloned().unwrap_or(Value::Null);
48 } else if let Some(tmpl) = is_template_expr(map) {
50 let tmpl = tmpl.to_owned();
51 *val = Value::String(substitute_template(&tmpl, data));
52 } else {
54 for v in map.values_mut() {
55 resolve_value(v, data);
56 }
57 }
58 }
59 Value::Array(arr) => {
60 for v in arr.iter_mut() {
61 resolve_value(v, data);
62 }
63 }
64 _ => {}
65 }
66}
67
68fn is_data_expr(obj: &Map<String, Value>) -> Option<&str> {
69 if obj.len() == 1 {
70 if let Some(Value::String(path)) = obj.get(EXPR_DATA_KEY) {
71 return Some(path.as_str());
72 }
73 }
74 None
75}
76
77fn is_template_expr(obj: &Map<String, Value>) -> Option<&str> {
78 if obj.len() == 1 {
79 if let Some(Value::String(tmpl)) = obj.get(EXPR_TEMPLATE_KEY) {
80 return Some(tmpl.as_str());
81 }
82 }
83 None
84}
85
86fn substitute_template(template: &str, data: &Value) -> String {
87 let mut out = String::with_capacity(template.len());
88 let mut chars = template.chars().peekable();
89
90 while let Some(ch) = chars.next() {
91 match ch {
92 '\\' => match chars.peek() {
93 Some('{') => {
94 out.push('{');
95 chars.next();
96 }
97 Some('}') => {
98 out.push('}');
99 chars.next();
100 }
101 Some('\\') => {
102 out.push('\\');
103 chars.next();
104 }
105 _ => out.push('\\'),
106 },
107 '{' => {
108 let mut path = String::new();
109 let mut closed = false;
110 for inner in chars.by_ref() {
111 if inner == '}' {
112 closed = true;
113 break;
114 }
115 path.push(inner);
116 }
117 if closed {
118 let trimmed = path.trim();
119 let resolved = resolve_path_string(data, trimmed).unwrap_or_default();
120 out.push_str(&resolved);
121 } else {
122 out.push('{');
123 out.push_str(&path);
124 }
125 }
126 _ => out.push(ch),
127 }
128 }
129 out
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::spec::{Element, Spec};
136 use crate::visibility::{Visibility, VisibilityCondition, VisibilityOperator};
137 use serde_json::json;
138
139 fn run(data: Value, props: Value) -> Value {
142 let mut spec = Spec::builder()
143 .data(data)
144 .element("root", Element::new("Text").prop("x", props))
145 .build()
146 .unwrap();
147 resolve_expressions(&mut spec);
148 spec.elements
149 .get("root")
150 .unwrap()
151 .props
152 .get("x")
153 .cloned()
154 .unwrap_or(Value::Null)
155 }
156
157 #[test]
159 fn data_simple_path() {
160 let out = run(json!({ "name": "Alice" }), json!({ "$data": "/name" }));
161 assert_eq!(out, json!("Alice"));
162 }
163
164 #[test]
165 fn data_nested_path() {
166 let out = run(
167 json!({ "user": { "name": "Bob" } }),
168 json!({ "$data": "/user/name" }),
169 );
170 assert_eq!(out, json!("Bob"));
171 }
172
173 #[test]
174 fn data_array_index() {
175 let out = run(
176 json!({ "items": ["x", "y"] }),
177 json!({ "$data": "/items/0" }),
178 );
179 assert_eq!(out, json!("x"));
180 }
181
182 #[test]
183 fn data_preserves_number() {
184 let out = run(json!({ "count": 42 }), json!({ "$data": "/count" }));
185 assert_eq!(out, json!(42));
186 assert!(out.is_number());
187 }
188
189 #[test]
190 fn data_preserves_bool() {
191 let out = run(json!({ "active": true }), json!({ "$data": "/active" }));
192 assert_eq!(out, json!(true));
193 assert!(out.is_boolean());
194 }
195
196 #[test]
197 fn data_preserves_object() {
198 let payload = json!({ "user": { "name": "Carol", "age": 30 } });
199 let out = run(payload.clone(), json!({ "$data": "/user" }));
200 assert_eq!(out, json!({ "name": "Carol", "age": 30 }));
201 assert!(out.is_object());
202 }
203
204 #[test]
205 fn data_preserves_array() {
206 let payload = json!({ "items": [1, 2, 3] });
207 let out = run(payload, json!({ "$data": "/items" }));
208 assert_eq!(out, json!([1, 2, 3]));
209 assert!(out.is_array());
210 }
211
212 #[test]
213 fn data_missing_path() {
214 let out = run(json!({ "name": "Alice" }), json!({ "$data": "/missing" }));
215 assert_eq!(out, Value::Null);
216 }
217
218 #[test]
220 fn data_non_string_value() {
221 let input = json!({ "$data": 42 });
222 let out = run(json!({}), input.clone());
223 assert_eq!(out, input);
224 }
225
226 #[test]
227 fn data_sibling_keys() {
228 let input = json!({ "$data": "/x", "class": "y" });
229 let out = run(json!({ "x": "resolved" }), input.clone());
230 assert_eq!(out, input);
231 }
232
233 #[test]
234 fn data_null_value() {
235 let input = json!({ "$data": null });
236 let out = run(json!({}), input.clone());
237 assert_eq!(out, input);
238 }
239
240 #[test]
242 fn template_single_placeholder() {
243 let out = run(
244 json!({ "name": "Alice" }),
245 json!({ "$template": "Hi, {/name}!" }),
246 );
247 assert_eq!(out, json!("Hi, Alice!"));
248 }
249
250 #[test]
251 fn template_multiple_placeholders() {
252 let out = run(
253 json!({ "a": "v1", "b": "v2" }),
254 json!({ "$template": "{/a} and {/b}" }),
255 );
256 assert_eq!(out, json!("v1 and v2"));
257 }
258
259 #[test]
260 fn template_no_placeholder() {
261 let out = run(json!({}), json!({ "$template": "static text" }));
262 assert_eq!(out, json!("static text"));
263 }
264
265 #[test]
266 fn template_missing_placeholder() {
267 let out = run(json!({}), json!({ "$template": "before {/missing} after" }));
268 assert_eq!(out, json!("before after"));
269 }
270
271 #[test]
272 fn template_whitespace_trimmed() {
273 let out = run(
274 json!({ "name": "Alice" }),
275 json!({ "$template": "{ /name }" }),
276 );
277 assert_eq!(out, json!("Alice"));
278 }
279
280 #[test]
282 fn template_escaped_open_brace() {
283 let out = run(json!({}), json!({ "$template": "\\{not a placeholder}" }));
284 assert_eq!(out, json!("{not a placeholder}"));
285 }
286
287 #[test]
288 fn template_escaped_close_brace() {
289 let out = run(json!({}), json!({ "$template": "text\\}" }));
290 assert_eq!(out, json!("text}"));
291 }
292
293 #[test]
294 fn template_escaped_backslash() {
295 let out = run(json!({}), json!({ "$template": "a\\\\b" }));
296 assert_eq!(out, json!("a\\b"));
297 }
298
299 #[test]
300 fn template_unclosed_brace() {
301 let out = run(json!({}), json!({ "$template": "{/missing_close" }));
302 assert_eq!(out, json!("{/missing_close"));
303 }
304
305 #[test]
307 fn template_non_string_value() {
308 let input = json!({ "$template": 42 });
309 let out = run(json!({}), input.clone());
310 assert_eq!(out, input);
311 }
312
313 #[test]
315 fn nested_in_array() {
316 let out = run(
317 json!({ "x": "resolved" }),
318 json!([{ "key": "lit" }, { "$data": "/x" }]),
319 );
320 assert_eq!(out, json!([{ "key": "lit" }, "resolved"]));
321 }
322
323 #[test]
324 fn nested_in_object_values() {
325 let out = run(
326 json!({ "x": "resolved" }),
327 json!({ "inner": { "$data": "/x" } }),
328 );
329 assert_eq!(out, json!({ "inner": "resolved" }));
330 }
331
332 #[test]
334 fn does_not_touch_spec_data() {
335 let data = json!({ "marker": { "$data": "/should_not_resolve" }, "target": "v" });
336 let mut spec = Spec::builder()
337 .data(data.clone())
338 .element(
339 "root",
340 Element::new("Text").prop("x", json!({ "$data": "/target" })),
341 )
342 .build()
343 .unwrap();
344 resolve_expressions(&mut spec);
345 assert_eq!(
346 spec.data, data,
347 "spec.data must be untouched even when it contains $data markers"
348 );
349 assert_eq!(
350 spec.elements.get("root").unwrap().props.get("x"),
351 Some(&json!("v"))
352 );
353 }
354
355 #[test]
356 fn single_pass_no_recursion() {
357 let out = run(
358 json!({ "outer": { "$data": "/inner" }, "inner": "never" }),
359 json!({ "$data": "/outer" }),
360 );
361 assert_eq!(
362 out,
363 json!({ "$data": "/inner" }),
364 "single-pass: $data output containing a marker must NOT be re-resolved"
365 );
366 }
367
368 #[test]
369 fn does_not_touch_children() {
370 let mut spec = Spec::builder()
371 .data(json!({ "child1": "resolved" }))
372 .element(
373 "root",
374 Element::new("Text")
375 .prop("x", json!("literal"))
376 .child("child1"),
377 )
378 .element("child1", Element::new("Text").prop("y", json!("leaf")))
379 .build()
380 .unwrap();
381 let before = spec.elements.get("root").unwrap().children.clone();
382 resolve_expressions(&mut spec);
383 assert_eq!(spec.elements.get("root").unwrap().children, before);
384 }
385
386 #[test]
387 fn does_not_touch_visible() {
388 let visible = Visibility::Condition(VisibilityCondition {
389 path: "/flag".to_string(),
390 operator: VisibilityOperator::Exists,
391 value: None,
392 });
393 let mut spec = Spec::builder()
394 .data(json!({ "flag": true }))
395 .element(
396 "root",
397 Element::new("Text")
398 .prop("x", json!("lit"))
399 .visible(visible.clone()),
400 )
401 .build()
402 .unwrap();
403 resolve_expressions(&mut spec);
404 assert_eq!(spec.elements.get("root").unwrap().visible, Some(visible));
405 }
406
407 #[test]
408 fn plugin_props_walk_identically() {
409 let mut spec = Spec::builder()
410 .data(json!({ "x": "resolved" }))
411 .element(
412 "root",
413 Element::new("MyPlugin").prop("x", json!({ "$data": "/x" })),
414 )
415 .build()
416 .unwrap();
417 resolve_expressions(&mut spec);
418 assert_eq!(
419 spec.elements.get("root").unwrap().props.get("x"),
420 Some(&json!("resolved")),
421 "plugin-typed props resolve identically to built-in props (D-14)"
422 );
423 }
424}