Skip to main content

hypen_engine/reactive/
expression.rs

1//! Expression evaluation using exprimo
2//!
3//! Supports JavaScript-like expressions in bindings:
4//! - `@{state.selected ? '#FFA7E1' : '#374151'}`
5//! - `@{item.count > 10 ? 'many' : 'few'}`
6//! - `@{state.user.name + ' (' + state.user.role + ')'}`
7
8use exprimo::{CustomFuncError, CustomFunction, Evaluator};
9use serde_json::Value;
10use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13use super::{Binding, BindingSource};
14use crate::error::EngineError;
15
16/// `length(x)` — JS-like size across the common container types.
17///
18/// Exprimo's built-in `.length` property only works on arrays and objects
19/// (0.6.1 rejects strings with "Cannot read property 'length' of
20/// non-array/non-object value"), which silently hid conditions like
21/// `@{state.searchQuery.length == 0}` when `searchQuery` was a string.
22/// Registering this as a custom function sidesteps the upstream gap
23/// without forking exprimo: the DSL syntax becomes `length(state.x)`
24/// instead of `.length`, and it works uniformly for strings, arrays,
25/// and objects.
26#[derive(Debug)]
27struct LengthFn;
28
29impl CustomFunction for LengthFn {
30    fn call(&self, args: &[Value]) -> Result<Value, CustomFuncError> {
31        if args.len() != 1 {
32            return Err(CustomFuncError::ArityError {
33                expected: 1,
34                got: args.len(),
35            });
36        }
37        let n = match &args[0] {
38            // `chars().count()` so grapheme-ish counting is unicode-aware.
39            // Cheap enough for the short strings that actually flow through
40            // template expressions; the full grapheme-cluster story is a
41            // separate feature if it ever matters.
42            Value::String(s) => s.chars().count(),
43            Value::Array(a) => a.len(),
44            Value::Object(o) => o.len(),
45            Value::Null => 0,
46            other => {
47                return Err(CustomFuncError::ArgumentError(format!(
48                    "length() expects string, array, object, or null; got {}",
49                    other
50                )));
51            }
52        };
53        // `len()` is `usize` and `serde_json::Number` accepts `u64` losslessly,
54        // so this can't fail on any 64-bit target.
55        Ok(Value::Number(serde_json::Number::from(n as u64)))
56    }
57}
58
59/// Build the custom-function registry shared by every evaluator the engine
60/// constructs. Centralised here so future additions (e.g. `keys()`,
61/// `values()`) land in one place rather than every `Evaluator::new` call
62/// site.
63fn builtin_functions() -> HashMap<String, Arc<dyn CustomFunction>> {
64    let mut funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
65    funcs.insert("length".to_string(), Arc::new(LengthFn));
66    funcs
67}
68
69/// Evaluate an expression string with the given context
70///
71/// The context should contain flattened variables like:
72/// - `state` -> the full state object
73/// - `item` -> the current item (in list iteration)
74///
75/// # Example
76/// ```
77/// use serde_json::json;
78/// use std::collections::HashMap;
79/// use hypen_engine::reactive::evaluate_expression;
80///
81/// let mut context = HashMap::new();
82/// context.insert("selected".to_string(), json!(true));
83///
84/// let result = evaluate_expression("selected ? 'yes' : 'no'", &context);
85/// assert_eq!(result, Ok(json!("yes")));
86/// ```
87pub fn evaluate_expression(
88    expr: &str,
89    context: &HashMap<String, Value>,
90) -> Result<Value, EngineError> {
91    let evaluator = Evaluator::new(context.clone(), builtin_functions());
92    evaluator
93        .evaluate(expr)
94        .map_err(|e| EngineError::ExpressionError(e.to_string()))
95}
96
97/// Build a context for expression evaluation from state, optional item, and data sources.
98///
99/// Flattens `state`, `item`, and data source objects so that expressions can access:
100/// - `state.user.name` via path traversal
101/// - `item.selected` via path traversal
102/// - `spacetime.messages` via data source path traversal
103pub fn build_expression_context(
104    state: &Value,
105    item: Option<&Value>,
106    data_sources: Option<&indexmap::IndexMap<String, Value>>,
107) -> HashMap<String, Value> {
108    let mut context = HashMap::new();
109
110    // Add the full state object
111    context.insert("state".to_string(), state.clone());
112
113    // Add the full item object if present
114    if let Some(item_value) = item {
115        context.insert("item".to_string(), item_value.clone());
116    }
117
118    // Add data source states as top-level context entries
119    if let Some(ds_map) = data_sources {
120        for (provider, ds_state) in ds_map {
121            context.insert(provider.clone(), ds_state.clone());
122        }
123    }
124
125    context
126}
127
128/// Extract all state, item, and data source bindings from an expression string
129///
130/// This scans the expression for `state.xxx`, `item.xxx`, and `provider.xxx`
131/// patterns and returns Binding objects for dependency tracking. Any
132/// `identifier.path` that isn't `state.` or `item.` is treated as a potential
133/// data source binding (validated at the dependency graph level).
134///
135/// # Example
136/// ```
137/// use hypen_engine::reactive::extract_bindings_from_expression;
138///
139/// let bindings = extract_bindings_from_expression("state.selected ? '#FFA7E1' : '#374151'");
140/// assert_eq!(bindings.len(), 1);
141/// assert_eq!(bindings[0].full_path(), "selected");
142/// ```
143pub fn extract_bindings_from_expression(expr: &str) -> Vec<Binding> {
144    let mut bindings = Vec::new();
145    let mut seen_paths: HashSet<String> = HashSet::new();
146
147    // Find all state.xxx and item.xxx patterns
148    for prefix in &["state.", "item."] {
149        let source = if *prefix == "state." {
150            BindingSource::State
151        } else {
152            BindingSource::Item
153        };
154
155        let mut search_pos = 0;
156        while let Some(start) = expr[search_pos..].find(prefix) {
157            let abs_start = search_pos + start;
158
159            // Check that this isn't in the middle of another identifier
160            // (e.g., "mystate.x" should not match)
161            if abs_start > 0 {
162                let prev_char = expr.chars().nth(abs_start - 1).unwrap_or(' ');
163                if prev_char.is_ascii_alphanumeric() || prev_char == '_' {
164                    search_pos = abs_start + prefix.len();
165                    continue;
166                }
167            }
168
169            // Extract the path after the prefix
170            let path_start = abs_start + prefix.len();
171            let mut path_end = path_start;
172
173            // Consume valid path characters (alphanumeric, underscore, and dots)
174            let chars: Vec<char> = expr.chars().collect();
175            while path_end < chars.len() {
176                let c = chars[path_end];
177                if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
178                    path_end += 1;
179                } else {
180                    break;
181                }
182            }
183
184            if path_end > path_start {
185                let path_str: String = chars[path_start..path_end].iter().collect();
186                // Remove trailing dots
187                let path_str = path_str.trim_end_matches('.');
188
189                if !path_str.is_empty() {
190                    let full_path = format!("{}{}", prefix, path_str);
191                    if !seen_paths.contains(&full_path) {
192                        seen_paths.insert(full_path);
193                        let path: Vec<String> =
194                            path_str.split('.').map(|s| s.to_string()).collect();
195                        bindings.push(Binding::new(source.clone(), path));
196                    }
197                }
198            }
199
200            search_pos = path_end.max(abs_start + prefix.len());
201        }
202    }
203
204    // Also find potential data source references: identifier.path patterns that
205    // aren't state.* or item.*. E.g., `spacetime.status == 'connected'`
206    extract_data_source_bindings_from_expression(expr, &mut bindings, &mut seen_paths);
207
208    bindings
209}
210
211/// Extract data source bindings (identifier.path patterns not starting with state/item)
212fn extract_data_source_bindings_from_expression(
213    expr: &str,
214    bindings: &mut Vec<Binding>,
215    seen_paths: &mut HashSet<String>,
216) {
217    let chars: Vec<char> = expr.chars().collect();
218    let len = chars.len();
219    let mut pos = 0;
220
221    // Reserved identifiers that are NOT data source providers
222    let reserved = ["state", "item", "true", "false", "null"];
223
224    while pos < len {
225        // Find the start of an identifier
226        if !chars[pos].is_ascii_alphabetic() && chars[pos] != '_' {
227            pos += 1;
228            continue;
229        }
230
231        // Check not preceded by alphanumeric (part of another identifier)
232        if pos > 0 && (chars[pos - 1].is_ascii_alphanumeric() || chars[pos - 1] == '_') {
233            pos += 1;
234            continue;
235        }
236
237        // Consume the identifier
238        let ident_start = pos;
239        while pos < len && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
240            pos += 1;
241        }
242        let ident: String = chars[ident_start..pos].iter().collect();
243
244        // Must be followed by a dot and more path segments
245        if pos >= len || chars[pos] != '.' {
246            continue;
247        }
248
249        // Skip reserved words
250        if reserved.contains(&ident.as_str()) {
251            continue;
252        }
253
254        // Consume the dot and path segments
255        let path_start = pos + 1; // skip the dot
256        let mut path_end = path_start;
257        while path_end < len
258            && (chars[path_end].is_ascii_alphanumeric()
259                || chars[path_end] == '_'
260                || chars[path_end] == '.')
261        {
262            path_end += 1;
263        }
264
265        if path_end > path_start {
266            let path_str: String = chars[path_start..path_end].iter().collect();
267            let path_str = path_str.trim_end_matches('.');
268            if !path_str.is_empty() {
269                let full_path = format!("{}.{}", ident, path_str);
270                if !seen_paths.contains(&full_path) {
271                    seen_paths.insert(full_path);
272                    let path: Vec<String> = path_str.split('.').map(|s| s.to_string()).collect();
273                    bindings.push(Binding::data_source(&ident, path));
274                }
275            }
276        }
277
278        pos = path_end;
279    }
280}
281
282/// Build a reusable [`Evaluator`] from state, optional item, and data sources.
283///
284/// Use this when you need to evaluate multiple expressions against the same
285/// state — `Evaluator::new` clones its context internally, so building once
286/// and reusing avoids paying the clone for every expression.
287pub fn build_evaluator(
288    state: &Value,
289    item: Option<&Value>,
290    data_sources: Option<&indexmap::IndexMap<String, Value>>,
291) -> Evaluator {
292    let context = build_expression_context(state, item, data_sources);
293    Evaluator::new(context, builtin_functions())
294}
295
296/// Evaluate a template string against a pre-built [`Evaluator`].
297///
298/// Substitutes every `@{...}` segment with the evaluator's result. Callers
299/// that need a one-shot evaluation against fresh state should pair this
300/// with [`build_evaluator`].
301pub fn evaluate_template_string(
302    template: &str,
303    evaluator: &Evaluator,
304) -> Result<String, EngineError> {
305    let mut result = template.to_string();
306    let mut pos = 0;
307
308    while let Some(rel_start) = result[pos..].find("@{") {
309        let abs_start = pos + rel_start;
310        let body_start = abs_start + 2;
311
312        // Walk the remainder of the string with char-boundary awareness to
313        // locate the matching closing brace. Byte offsets are used throughout
314        // so the arithmetic stays valid when the template contains multi-byte
315        // UTF-8 characters (·, —, emoji, accented letters, CJK, …) before or
316        // inside the expression.
317        let mut depth: i32 = 1;
318        let mut close_byte: Option<usize> = None;
319        for (off, ch) in result[body_start..].char_indices() {
320            match ch {
321                '{' => depth += 1,
322                '}' => {
323                    depth -= 1;
324                    if depth == 0 {
325                        close_byte = Some(body_start + off);
326                        break;
327                    }
328                }
329                _ => {}
330            }
331        }
332
333        let close_byte = match close_byte {
334            Some(b) => b,
335            None => {
336                return Err(EngineError::ExpressionError(
337                    "Unclosed expression in template".to_string(),
338                ));
339            }
340        };
341
342        // Copy the expression out of `result` so we can mutate `result` below.
343        let expr_content = result[body_start..close_byte].to_string();
344
345        // Evaluate the expression directly via the evaluator (no context clone)
346        let value = evaluator
347            .evaluate(&expr_content)
348            .map_err(|e| EngineError::ExpressionError(e.to_string()))?;
349
350        // Convert to string
351        let replacement = match &value {
352            Value::String(s) => s.clone(),
353            Value::Number(n) => n.to_string(),
354            Value::Bool(b) => b.to_string(),
355            Value::Null => "null".to_string(),
356            _ => serde_json::to_string(&value).unwrap_or_default(),
357        };
358
359        // `}` is ASCII so close_byte + 1 is a valid char boundary past it.
360        let end_byte = close_byte + 1;
361        result.replace_range(abs_start..end_byte, &replacement);
362
363        // Advance past the replacement so we don't re-scan substituted content.
364        pos = abs_start + replacement.len();
365    }
366
367    Ok(result)
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use serde_json::json;
374
375    #[test]
376    fn test_simple_expression() {
377        let mut context = HashMap::new();
378        context.insert("x".to_string(), json!(5));
379        context.insert("y".to_string(), json!(3));
380
381        let result = evaluate_expression("x + y", &context).unwrap();
382        // exprimo returns floats for arithmetic, so compare as f64
383        assert_eq!(result.as_f64().unwrap(), 8.0);
384    }
385
386    #[test]
387    fn test_ternary_expression() {
388        let mut context = HashMap::new();
389        context.insert("selected".to_string(), json!(true));
390
391        let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
392        assert_eq!(result, json!("yes"));
393    }
394
395    #[test]
396    fn test_ternary_with_colors() {
397        let mut context = HashMap::new();
398        context.insert("selected".to_string(), json!(true));
399
400        let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
401        assert_eq!(result, json!("#FFA7E1"));
402    }
403
404    #[test]
405    fn test_comparison_expression() {
406        let mut context = HashMap::new();
407        context.insert("count".to_string(), json!(15));
408
409        let result = evaluate_expression("count > 10", &context).unwrap();
410        assert_eq!(result, json!(true));
411    }
412
413    #[test]
414    fn test_state_object_access() {
415        let context =
416            build_expression_context(&json!({"user": {"name": "Alice", "age": 30}}), None, None);
417
418        let result = evaluate_expression("state.user.name", &context).unwrap();
419        assert_eq!(result, json!("Alice"));
420    }
421
422    #[test]
423    fn test_item_object_access() {
424        let context = build_expression_context(
425            &json!({}),
426            Some(&json!({"name": "Item 1", "selected": true})),
427            None,
428        );
429
430        let result = evaluate_expression("item.name", &context).unwrap();
431        assert_eq!(result, json!("Item 1"));
432    }
433
434    #[test]
435    fn test_item_ternary() {
436        let context = build_expression_context(&json!({}), Some(&json!({"selected": true})), None);
437
438        let result =
439            evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
440        assert_eq!(result, json!("#FFA7E1"));
441    }
442
443    #[test]
444    fn test_template_string_simple() {
445        let state = json!({"user": {"name": "Alice"}});
446        let evaluator = build_evaluator(&state, None, None);
447        let result = evaluate_template_string("Hello @{state.user.name}!", &evaluator).unwrap();
448        assert_eq!(result, "Hello Alice!");
449    }
450
451    #[test]
452    fn test_template_string_with_expression() {
453        let state = json!({"selected": true});
454        let evaluator = build_evaluator(&state, None, None);
455        let result = evaluate_template_string(
456            "Color: @{state.selected ? '#FFA7E1' : '#374151'}",
457            &evaluator,
458        )
459        .unwrap();
460        assert_eq!(result, "Color: #FFA7E1");
461    }
462
463    #[test]
464    fn test_template_string_multiple_expressions() {
465        let state = json!({"name": "Alice", "count": 5});
466        let evaluator = build_evaluator(&state, None, None);
467        let result =
468            evaluate_template_string("@{state.name} has @{state.count} items", &evaluator).unwrap();
469        assert_eq!(result, "Alice has 5 items");
470    }
471
472    #[test]
473    fn test_template_with_item() {
474        let state = json!({});
475        let item = json!({"name": "Product", "price": 99});
476        let evaluator = build_evaluator(&state, Some(&item), None);
477        let result = evaluate_template_string("@{item.name}: $@{item.price}", &evaluator).unwrap();
478        assert_eq!(result, "Product: $99");
479    }
480
481    #[test]
482    fn test_string_concatenation() {
483        let mut context = HashMap::new();
484        context.insert("first".to_string(), json!("Hello"));
485        context.insert("second".to_string(), json!("World"));
486
487        let result = evaluate_expression("first + ' ' + second", &context).unwrap();
488        assert_eq!(result, json!("Hello World"));
489    }
490
491    #[test]
492    fn test_logical_and() {
493        let mut context = HashMap::new();
494        context.insert("a".to_string(), json!(true));
495        context.insert("b".to_string(), json!(false));
496
497        let result = evaluate_expression("a && b", &context).unwrap();
498        assert_eq!(result, json!(false));
499    }
500
501    #[test]
502    fn test_logical_or() {
503        let mut context = HashMap::new();
504        context.insert("a".to_string(), json!(false));
505        context.insert("b".to_string(), json!(true));
506
507        let result = evaluate_expression("a || b", &context).unwrap();
508        assert_eq!(result, json!(true));
509    }
510
511    #[test]
512    fn test_template_string_multibyte_before_expression() {
513        // Regression: `find("@{")` returns a byte offset but the old code
514        // indexed into `Vec<char>` with it. Any multi-byte char before `@{`
515        // (·, —, emoji, accented letters, CJK …) desynchronised the two and
516        // the expression-extractor ate the leading byte of the identifier.
517        let state = json!({"a": "ALPHA", "b": "BETA"});
518        let evaluator = build_evaluator(&state, None, None);
519
520        // Middle-dot (U+00B7, 2 bytes in UTF-8)
521        let result = evaluate_template_string("@{state.a} · @{state.b}", &evaluator).unwrap();
522        assert_eq!(result, "ALPHA · BETA");
523
524        let result = evaluate_template_string("prefix · @{state.a}", &evaluator).unwrap();
525        assert_eq!(result, "prefix · ALPHA");
526
527        // Em dash (U+2014, 3 bytes)
528        let result = evaluate_template_string("@{state.a} — @{state.b}", &evaluator).unwrap();
529        assert_eq!(result, "ALPHA — BETA");
530
531        // Emoji (4 bytes)
532        let result = evaluate_template_string("🍕 @{state.a}", &evaluator).unwrap();
533        assert_eq!(result, "🍕 ALPHA");
534
535        // CJK (3 bytes each)
536        let result = evaluate_template_string("你好 @{state.a}", &evaluator).unwrap();
537        assert_eq!(result, "你好 ALPHA");
538
539        // Accented letter (2 bytes)
540        let result = evaluate_template_string("café @{state.a}", &evaluator).unwrap();
541        assert_eq!(result, "café ALPHA");
542    }
543
544    #[test]
545    fn test_template_string_multibyte_inside_replacement() {
546        // The replacement itself may contain multi-byte bytes, and the next
547        // `@{` may appear after them. Byte-offset advancement must still work.
548        let state = json!({"a": "café", "b": "naïve"});
549        let evaluator = build_evaluator(&state, None, None);
550
551        let result = evaluate_template_string("@{state.a} · @{state.b}", &evaluator).unwrap();
552        assert_eq!(result, "café · naïve");
553    }
554
555    #[test]
556    fn test_template_string_realistic_restaurant_example() {
557        // Mirrors the user-visible report: a Text containing two template
558        // expressions separated by a middle dot. Before the fix this returned
559        // Err and the caller silently surfaced the raw DSL template.
560        let state = json!({
561            "restaurant": {
562                "cuisine": "Italian",
563                "description": "Wood-fired pizza"
564            }
565        });
566        let evaluator = build_evaluator(&state, None, None);
567        let result = evaluate_template_string(
568            "@{state.restaurant.cuisine} · @{state.restaurant.description}",
569            &evaluator,
570        )
571        .unwrap();
572        assert_eq!(result, "Italian · Wood-fired pizza");
573    }
574
575    #[test]
576    fn test_length_function_strings_arrays_objects() {
577        // Regression: exprimo 0.6.1 rejects `.length` on strings with
578        // "Cannot read property 'length' of non-array/non-object value".
579        // We register `length(x)` as a custom function so the DSL can use
580        // a single syntax for strings / arrays / objects uniformly.
581        let state = json!({
582            "empty": "",
583            "hello": "hello",
584            "unicode": "café",        // 4 chars, 5 bytes
585            "items_empty": [],
586            "items": [1, 2, 3],
587            "obj": { "a": 1, "b": 2 },
588        });
589        let ev = build_evaluator(&state, None, None);
590
591        // strings
592        assert_eq!(
593            evaluate_template_string("@{length(state.empty)}", &ev).unwrap(),
594            "0"
595        );
596        assert_eq!(
597            evaluate_template_string("@{length(state.hello)}", &ev).unwrap(),
598            "5"
599        );
600        // unicode char count, not byte count
601        assert_eq!(
602            evaluate_template_string("@{length(state.unicode)}", &ev).unwrap(),
603            "4"
604        );
605
606        // arrays
607        assert_eq!(
608            evaluate_template_string("@{length(state.items_empty)}", &ev).unwrap(),
609            "0"
610        );
611        assert_eq!(
612            evaluate_template_string("@{length(state.items)}", &ev).unwrap(),
613            "3"
614        );
615
616        // objects (key count)
617        assert_eq!(
618            evaluate_template_string("@{length(state.obj)}", &ev).unwrap(),
619            "2"
620        );
621    }
622
623    #[test]
624    fn test_length_function_empty_search_query_condition() {
625        // The exact condition from the bug report: with `searchQuery = ""`
626        // the "no results / suggestions" branch was silently never rendering
627        // because exprimo errored on `.length`. `length(state.searchQuery) == 0`
628        // now evaluates cleanly.
629        let state = json!({ "searchQuery": "" });
630        let ev = build_evaluator(&state, None, None);
631
632        let mut ctx = HashMap::new();
633        ctx.insert("state".to_string(), state.clone());
634
635        let result = evaluate_expression("length(state.searchQuery) == 0", &ctx).unwrap();
636        assert_eq!(result, json!(true));
637
638        let populated = json!({ "searchQuery": "pizza" });
639        let mut ctx = HashMap::new();
640        ctx.insert("state".to_string(), populated);
641        let result = evaluate_expression("length(state.searchQuery) == 0", &ctx).unwrap();
642        assert_eq!(result, json!(false));
643
644        // `length()` inside a pure-expression template must preserve the Bool
645        // result (not stringify it), so `If(condition: "@{length(…) == 0}")`
646        // matches the Static(true) pattern the parser emits for If.
647        let _ = ev; // keep build_evaluator referenced so the evaluator path is also exercised
648    }
649
650    #[test]
651    fn test_length_function_null_and_errors() {
652        let state = json!({ "missing": null });
653        let ev = build_evaluator(&state, None, None);
654
655        // Null treated as 0 so `length(state.missing) == 0` doesn't throw
656        // for uninitialised state fields.
657        assert_eq!(
658            evaluate_template_string("@{length(state.missing)}", &ev).unwrap(),
659            "0"
660        );
661
662        // Wrong arity is a real error — surfaced, not silenced.
663        let err = evaluate_template_string("@{length()}", &ev).unwrap_err();
664        match err {
665            EngineError::ExpressionError(msg) => {
666                assert!(
667                    msg.contains("expected 1") || msg.contains("arg"),
668                    "expected arity error, got: {}",
669                    msg
670                );
671            }
672            _ => panic!("expected ExpressionError"),
673        }
674    }
675
676    #[test]
677    fn test_template_string_unclosed_expression() {
678        let state = json!({});
679        let evaluator = build_evaluator(&state, None, None);
680        let err = evaluate_template_string("prefix @{state.a", &evaluator).unwrap_err();
681        match err {
682            EngineError::ExpressionError(msg) => {
683                assert!(msg.contains("Unclosed"));
684            }
685            _ => panic!("expected ExpressionError"),
686        }
687    }
688
689    #[test]
690    fn test_complex_expression() {
691        let context = build_expression_context(
692            &json!({
693                "user": {
694                    "premium": true,
695                    "age": 25
696                }
697            }),
698            None,
699            None,
700        );
701
702        let result = evaluate_expression(
703            "state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
704            &context,
705        )
706        .unwrap();
707        assert_eq!(result, json!("VIP Adult"));
708    }
709}