Skip to main content

a2ui_base/model/
data_context.rs

1//! Scoped data access with dynamic value resolution.
2
3use std::collections::HashMap;
4
5use serde_json::Value;
6
7use super::data_model::DataModel;
8use crate::protocol::common_types::{
9    DynamicBoolean, DynamicBooleanCondition, DynamicNumber, DynamicString,
10    DynamicValue, FunctionCall,
11};
12
13/// A scoped window into the DataModel used during rendering.
14///
15/// Handles:
16/// - Absolute paths (starting with `/`)
17/// - Relative paths (no leading `/`, resolved against `base_path`)
18/// - Dynamic value resolution (literals, bindings, function calls)
19pub struct DataContext<'a> {
20    data_model: &'a DataModel,
21    base_path: String,
22    functions: &'a HashMap<String, Box<dyn crate::catalog::function_api::FunctionImplementation>>,
23}
24
25impl<'a> DataContext<'a> {
26    /// Create a new DataContext at the root scope.
27    pub fn new(
28        data_model: &'a DataModel,
29        functions: &'a HashMap<String, Box<dyn crate::catalog::function_api::FunctionImplementation>>,
30    ) -> Self {
31        Self {
32            data_model,
33            base_path: String::new(),
34            functions,
35        }
36    }
37
38    /// Create a nested context for template iteration.
39    pub fn nested(&self, relative_path: &str) -> DataContext<'a> {
40        let new_base = if self.base_path.is_empty() {
41            format!("/{}", relative_path)
42        } else {
43            format!("{}/{}", self.base_path, relative_path)
44        };
45        DataContext {
46            data_model: self.data_model,
47            base_path: new_base,
48            functions: self.functions,
49        }
50    }
51
52    /// Get the current base path.
53    pub fn base_path(&self) -> &str {
54        &self.base_path
55    }
56
57    /// Resolve a possibly-relative pointer to an absolute JSON Pointer.
58    pub fn resolve_pointer(&self, path: &str) -> String {
59        if path.starts_with('/') {
60            path.to_string()
61        } else if path.is_empty() {
62            self.base_path.clone()
63        } else if self.base_path.is_empty() {
64            format!("/{}", path)
65        } else {
66            format!("{}/{}", self.base_path, path)
67        }
68    }
69
70    /// Get a value at a (possibly relative) path.
71    pub fn get(&self, path: &str) -> Option<Value> {
72        let pointer = self.resolve_pointer(path);
73        self.data_model.get(&pointer).cloned()
74    }
75
76    /// Resolve a DynamicString to its current string value.
77    pub fn resolve_dynamic_string(&self, ds: &DynamicString) -> String {
78        match ds {
79            DynamicString::Literal(s) => s.clone(),
80            DynamicString::Binding(b) => self.resolve_binding_to_string(&b.path),
81            DynamicString::Function(fc) => {
82                let result = self.execute_function(fc);
83                value_to_string(&result)
84            }
85        }
86    }
87
88    /// Resolve a DynamicNumber.
89    pub fn resolve_dynamic_number(&self, dn: &DynamicNumber) -> f64 {
90        match dn {
91            DynamicNumber::Literal(n) => *n,
92            DynamicNumber::Binding(b) => self
93                .resolve_binding(&b.path)
94                .and_then(|v| v.as_f64())
95                .unwrap_or(0.0),
96            DynamicNumber::Function(fc) => {
97                let result = self.execute_function(fc);
98                result.as_f64().unwrap_or(0.0)
99            }
100        }
101    }
102
103    /// Resolve a DynamicBoolean.
104    pub fn resolve_dynamic_boolean(&self, db: &DynamicBoolean) -> bool {
105        match db {
106            DynamicBoolean::Literal(b) => *b,
107            DynamicBoolean::Binding(b) => self
108                .resolve_binding(&b.path)
109                .and_then(|v| v.as_bool())
110                .unwrap_or(false),
111            DynamicBoolean::Function(fc) => {
112                let result = self.execute_function(fc);
113                result.as_bool().unwrap_or(false)
114            }
115        }
116    }
117
118    /// Resolve a DynamicBooleanCondition (same logic as DynamicBoolean).
119    pub fn resolve_dynamic_boolean_condition(&self, db: &DynamicBooleanCondition) -> bool {
120        match db {
121            DynamicBooleanCondition::Literal(b) => *b,
122            DynamicBooleanCondition::Binding(b) => self
123                .resolve_binding(&b.path)
124                .and_then(|v| v.as_bool())
125                .unwrap_or(false),
126            DynamicBooleanCondition::Function(fc) => {
127                let result = self.execute_function(fc);
128                result.as_bool().unwrap_or(false)
129            }
130        }
131    }
132
133    /// Resolve a DynamicValue to a serde_json::Value.
134    pub fn resolve_dynamic_value(&self, dv: &DynamicValue) -> Value {
135        match dv {
136            DynamicValue::String(s) => Value::String(s.clone()),
137            DynamicValue::Number(n) => serde_json::json!(*n),
138            DynamicValue::Boolean(b) => Value::Bool(*b),
139            DynamicValue::Array(arr) => Value::Array(arr.clone()),
140            DynamicValue::Binding(b) => self.resolve_binding(&b.path).unwrap_or(Value::Null),
141            DynamicValue::Function(fc) => self.execute_function(fc),
142        }
143    }
144
145    /// Resolve a binding path to a Value.
146    fn resolve_binding(&self, path: &str) -> Option<Value> {
147        let pointer = self.resolve_pointer(path);
148        self.data_model.get(&pointer).cloned()
149    }
150
151    /// Resolve a binding to a string (type coercion).
152    fn resolve_binding_to_string(&self, path: &str) -> String {
153        self.resolve_binding(path)
154            .map(|v| value_to_string(&v))
155            .unwrap_or_default()
156    }
157
158    /// Call a function by name with pre-resolved arguments.
159    ///
160    /// Returns `None` if the function is not found or execution fails.
161    pub fn call_function_by_name(
162        &self,
163        name: &str,
164        args: &HashMap<String, Value>,
165    ) -> Option<Value> {
166        let func = self.functions.get(name)?;
167        // Resolve each argument value (may contain bindings or nested calls).
168        let mut resolved_args = HashMap::new();
169        for (key, val) in args {
170            let resolved = self.resolve_arg_value(val);
171            resolved_args.insert(key.clone(), resolved);
172        }
173        func.execute(&resolved_args, self).ok()
174    }
175
176    /// Execute a function call.
177    fn execute_function(&self, fc: &FunctionCall) -> Value {
178        let Some(func) = self.functions.get(&fc.call) else {
179            return Value::Null;
180        };
181
182        // Resolve each argument (they can contain DynamicValues)
183        let mut resolved_args = HashMap::new();
184        for (key, val) in &fc.args {
185            // Try to resolve as a DynamicValue if it's an object with "path" or "call"
186            let resolved = self.resolve_arg_value(val);
187            resolved_args.insert(key.clone(), resolved);
188        }
189
190        match func.execute(&resolved_args, self) {
191            Ok(v) => v,
192            Err(_) => Value::Null,
193        }
194    }
195
196    /// Resolve an argument value that might be a DynamicValue.
197    fn resolve_arg_value(&self, val: &Value) -> Value {
198        if let Some(obj) = val.as_object() {
199            if let Some(path) = obj.get("path").and_then(|v| v.as_str()) {
200                return self.resolve_binding(path).unwrap_or(Value::Null);
201            }
202            if let Some(call) = obj.get("call").and_then(|v| v.as_str()) {
203                let args = obj
204                    .get("args")
205                    .and_then(|v| v.as_object())
206                    .map(|m| {
207                        m.iter()
208                            .map(|(k, v)| (k.clone(), self.resolve_arg_value(v)))
209                            .collect::<HashMap<_, _>>()
210                    })
211                    .unwrap_or_default();
212                let fc = FunctionCall {
213                    call: call.to_string(),
214                    args,
215                };
216                return self.execute_function(&fc);
217            }
218        }
219        val.clone()
220    }
221}
222
223/// Convert a serde_json::Value to a display string per A2UI type coercion rules.
224pub fn value_to_string(v: &Value) -> String {
225    match v {
226        Value::String(s) => s.clone(),
227        Value::Number(n) => n.to_string(),
228        Value::Bool(b) => b.to_string(),
229        Value::Null => String::new(),
230        Value::Array(_) | Value::Object(_) => v.to_string(),
231    }
232}