use std::collections::HashMap;
use serde_json::Value;
use super::data_model::DataModel;
use crate::protocol::common_types::{
DynamicBoolean, DynamicBooleanCondition, DynamicNumber, DynamicString,
DynamicValue, FunctionCall,
};
pub struct DataContext<'a> {
data_model: &'a DataModel,
base_path: String,
functions: &'a HashMap<String, Box<dyn crate::catalog::function_api::FunctionImplementation>>,
template_index: Option<usize>,
}
impl<'a> DataContext<'a> {
pub fn new(
data_model: &'a DataModel,
functions: &'a HashMap<String, Box<dyn crate::catalog::function_api::FunctionImplementation>>,
) -> Self {
Self {
data_model,
base_path: String::new(),
functions,
template_index: None,
}
}
pub fn nested(&self, relative_path: &str) -> DataContext<'a> {
let new_base = if self.base_path.is_empty() {
format!("/{}", relative_path)
} else {
format!("{}/{}", self.base_path, relative_path)
};
DataContext {
data_model: self.data_model,
base_path: new_base,
functions: self.functions,
template_index: None,
}
}
pub fn template_index(&self) -> Option<usize> {
self.template_index
}
pub fn with_template_index(mut self, index: Option<usize>) -> Self {
self.template_index = index;
self
}
pub fn set_template_index(&mut self, index: Option<usize>) {
self.template_index = index;
}
fn resolve_index(&self, args: &HashMap<String, Value>) -> Option<Value> {
self.template_index.map(|idx| {
let offset = args
.get("offset")
.map(|v| self.resolve_arg_value(v).as_f64().unwrap_or(0.0))
.unwrap_or(0.0);
serde_json::json!(idx as f64 + offset)
})
}
pub fn base_path(&self) -> &str {
&self.base_path
}
pub fn resolve_pointer(&self, path: &str) -> String {
if path.starts_with('/') {
path.to_string()
} else if path.is_empty() {
self.base_path.clone()
} else if self.base_path.is_empty() {
format!("/{}", path)
} else {
format!("{}/{}", self.base_path, path)
}
}
pub fn get(&self, path: &str) -> Option<Value> {
let pointer = self.resolve_pointer(path);
self.data_model.get(&pointer).cloned()
}
pub fn resolve_dynamic_string(&self, ds: &DynamicString) -> String {
match ds {
DynamicString::Literal(s) => s.clone(),
DynamicString::Binding(b) => self.resolve_binding_to_string(&b.path),
DynamicString::Function(fc) => {
let result = self.execute_function(fc);
value_to_string(&result)
}
}
}
pub fn resolve_dynamic_number(&self, dn: &DynamicNumber) -> f64 {
match dn {
DynamicNumber::Literal(n) => *n,
DynamicNumber::Binding(b) => self
.resolve_binding(&b.path)
.and_then(|v| v.as_f64())
.unwrap_or(0.0),
DynamicNumber::Function(fc) => {
let result = self.execute_function(fc);
result.as_f64().unwrap_or(0.0)
}
}
}
pub fn resolve_dynamic_boolean(&self, db: &DynamicBoolean) -> bool {
match db {
DynamicBoolean::Literal(b) => *b,
DynamicBoolean::Binding(b) => self
.resolve_binding(&b.path)
.and_then(|v| v.as_bool())
.unwrap_or(false),
DynamicBoolean::Function(fc) => {
let result = self.execute_function(fc);
result.as_bool().unwrap_or(false)
}
}
}
pub fn resolve_dynamic_boolean_condition(&self, db: &DynamicBooleanCondition) -> bool {
match db {
DynamicBooleanCondition::Literal(b) => *b,
DynamicBooleanCondition::Binding(b) => self
.resolve_binding(&b.path)
.and_then(|v| v.as_bool())
.unwrap_or(false),
DynamicBooleanCondition::Function(fc) => {
let result = self.execute_function(fc);
result.as_bool().unwrap_or(false)
}
}
}
pub fn resolve_dynamic_value(&self, dv: &DynamicValue) -> Value {
match dv {
DynamicValue::String(s) => Value::String(s.clone()),
DynamicValue::Number(n) => serde_json::json!(*n),
DynamicValue::Boolean(b) => Value::Bool(*b),
DynamicValue::Array(arr) => Value::Array(arr.clone()),
DynamicValue::Binding(b) => self.resolve_binding(&b.path).unwrap_or(Value::Null),
DynamicValue::Function(fc) => self.execute_function(fc),
}
}
fn resolve_binding(&self, path: &str) -> Option<Value> {
let pointer = self.resolve_pointer(path);
self.data_model.get(&pointer).cloned()
}
fn resolve_binding_to_string(&self, path: &str) -> String {
self.resolve_binding(path)
.map(|v| value_to_string(&v))
.unwrap_or_default()
}
pub fn call_function_by_name(
&self,
name: &str,
args: &HashMap<String, Value>,
) -> Option<Value> {
if name == "@index" {
return self.resolve_index(args);
}
let func = self.functions.get(name)?;
let mut resolved_args = HashMap::new();
for (key, val) in args {
let resolved = self.resolve_arg_value(val);
resolved_args.insert(key.clone(), resolved);
}
func.execute(&resolved_args, self).ok()
}
fn execute_function(&self, fc: &FunctionCall) -> Value {
if fc.call == "@index" {
return self.resolve_index(&fc.args).unwrap_or(Value::Null);
}
let Some(func) = self.functions.get(&fc.call) else {
return Value::Null;
};
let mut resolved_args = HashMap::new();
for (key, val) in &fc.args {
let resolved = self.resolve_arg_value(val);
resolved_args.insert(key.clone(), resolved);
}
match func.execute(&resolved_args, self) {
Ok(v) => v,
Err(_) => Value::Null,
}
}
fn resolve_arg_value(&self, val: &Value) -> Value {
if let Some(obj) = val.as_object() {
if let Some(path) = obj.get("path").and_then(|v| v.as_str()) {
return self.resolve_binding(path).unwrap_or(Value::Null);
}
if let Some(call) = obj.get("call").and_then(|v| v.as_str()) {
let args = obj
.get("args")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.map(|(k, v)| (k.clone(), self.resolve_arg_value(v)))
.collect::<HashMap<_, _>>()
})
.unwrap_or_default();
let fc = FunctionCall {
call: call.to_string(),
args,
};
return self.execute_function(&fc);
}
}
val.clone()
}
}
pub fn value_to_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => String::new(),
Value::Array(_) | Value::Object(_) => v.to_string(),
}
}
#[cfg(test)]
mod index_tests {
use super::*;
use crate::model::data_model::DataModel;
use crate::protocol::common_types::{DynamicNumber, FunctionCall};
use serde_json::json;
fn ctx(idx: Option<usize>) -> DataContext<'static> {
let dm = Box::leak(Box::new(DataModel::new()));
let fns = Box::leak(Box::new(HashMap::new()));
DataContext::new(dm, fns).with_template_index(idx)
}
fn index_call(args: &[(&str, Value)]) -> FunctionCall {
let mut map = HashMap::new();
for (k, v) in args {
map.insert((*k).to_string(), v.clone());
}
FunctionCall {
call: "@index".to_string(),
args: map,
}
}
#[test]
fn at_index_without_template_context_is_null() {
let ctx = ctx(None);
let dn = DynamicNumber::Function(index_call(&[]));
assert_eq!(ctx.resolve_dynamic_number(&dn), 0.0);
assert_eq!(ctx.call_function_by_name("@index", &HashMap::new()), None);
}
#[test]
fn at_index_returns_zero_based_index() {
let ctx = ctx(Some(2));
let dn = DynamicNumber::Function(index_call(&[]));
assert_eq!(ctx.resolve_dynamic_number(&dn), 2.0);
}
#[test]
fn at_index_with_literal_offset() {
let ctx = ctx(Some(2));
let dn = DynamicNumber::Function(index_call(&[("offset", json!(1))]));
assert_eq!(ctx.resolve_dynamic_number(&dn), 3.0);
}
#[test]
fn at_index_with_bound_offset() {
let dm = Box::leak(Box::new(DataModel::from_value(json!({"step": 10}))));
let fns = Box::leak(Box::new(HashMap::new()));
let ctx = DataContext::new(dm, fns).with_template_index(Some(0));
let dn = DynamicNumber::Function(index_call(&[("offset", json!({"path": "/step"}))]));
assert_eq!(ctx.resolve_dynamic_number(&dn), 10.0); }
#[test]
fn at_index_via_call_function_by_name() {
let ctx = ctx(Some(4));
let mut args = HashMap::new();
args.insert("offset".to_string(), json!(1));
assert_eq!(ctx.call_function_by_name("@index", &args), Some(json!(5.0)));
}
#[test]
fn at_index_zero_plus_offset() {
let ctx = ctx(Some(0));
let dn = DynamicNumber::Function(index_call(&[("offset", json!(1))]));
assert_eq!(ctx.resolve_dynamic_number(&dn), 1.0);
}
}