use super::resolve::{evaluate_binding, evaluate_binding_path};
use crate::ir::{ConditionalBranch, IRNode, RouterRoute, Value};
use crate::reactive::{build_evaluator, evaluate_template_string};
type DataSources = indexmap::IndexMap<String, serde_json::Value>;
fn warn_condition_error(what: &str, expr: &str, err: impl std::fmt::Display) {
crate::log_warn!(
crate::logger::LogScope::Reconciler,
"{} {:?} failed to evaluate: {} (branch treated as false)",
what,
expr,
err
);
}
fn resolve_binding(
binding: &crate::reactive::Binding,
state: &serde_json::Value,
data_sources: Option<&DataSources>,
) -> serde_json::Value {
if binding.is_data_source() {
if let (Some(provider), Some(ds_map)) = (binding.provider(), data_sources) {
if let Some(ds_state) = ds_map.get(provider) {
evaluate_binding_path(binding, ds_state).unwrap_or(serde_json::Value::Null)
} else {
serde_json::Value::Null
}
} else {
serde_json::Value::Null
}
} else {
evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null)
}
}
pub(crate) fn evaluate_value(
value: &Value,
state: &serde_json::Value,
data_sources: Option<&DataSources>,
) -> serde_json::Value {
match value {
Value::Static(v) => v.clone(),
Value::Binding(binding) => resolve_binding(binding, state, data_sources),
Value::TemplateString { template, .. } => {
let trimmed = template.trim();
if trimmed.starts_with("@{")
&& trimmed.ends_with('}')
&& !trimmed[2..trimmed.len() - 1].contains("@{")
{
let expr = &trimmed[2..trimmed.len() - 1];
let context = crate::reactive::build_expression_context(state, None, data_sources);
match crate::reactive::evaluate_expression(expr, &context) {
Ok(result) => result,
Err(e) => {
warn_condition_error("condition expression", expr, e);
serde_json::Value::Null
}
}
} else {
let evaluator = build_evaluator(state, None, data_sources);
match evaluate_template_string(template, &evaluator) {
Ok(result) => serde_json::Value::String(result),
Err(e) => {
warn_condition_error("condition template", template, e);
serde_json::Value::String(template.clone())
}
}
}
}
Value::Action(action) => serde_json::Value::String(format!("@{}", action)),
Value::Resource(name) => serde_json::Value::String(format!("@resources.{}", name)),
}
}
pub(crate) fn find_matching_branch<'a>(
evaluated_value: &serde_json::Value,
branches: &'a [ConditionalBranch],
fallback: Option<&'a [IRNode]>,
state: &serde_json::Value,
data_sources: Option<&DataSources>,
) -> Option<&'a [IRNode]> {
for branch in branches {
if pattern_matches(evaluated_value, &branch.pattern, state, data_sources) {
return Some(&branch.children);
}
}
fallback
}
pub(crate) fn find_matching_route_with_key<'a>(
location: &str,
routes: &'a [RouterRoute],
fallback: Option<&'a [IRNode]>,
) -> Option<(String, &'a [IRNode])> {
for route in routes {
if route_matches(&route.path, location) {
return Some((route.path.clone(), &route.children));
}
}
fallback.map(|children| ("__fallback__".to_string(), children))
}
fn route_matches(pattern: &str, location: &str) -> bool {
crate::portable::route::match_path(pattern, location).is_some()
}
fn pattern_matches(
evaluated_value: &serde_json::Value,
pattern: &Value,
state: &serde_json::Value,
data_sources: Option<&DataSources>,
) -> bool {
match pattern {
Value::Static(pattern_value) => values_match(evaluated_value, pattern_value, data_sources),
Value::Binding(binding) => {
let resolved = resolve_binding(binding, state, data_sources);
is_truthy(&resolved)
}
Value::TemplateString { template, .. } => {
evaluate_expression_pattern(template, evaluated_value, data_sources)
}
Value::Action(_) | Value::Resource(_) => false, }
}
fn evaluate_expression_pattern(
template: &str,
value: &serde_json::Value,
data_sources: Option<&DataSources>,
) -> bool {
use crate::reactive::evaluate_expression;
use std::collections::HashMap;
let mut context: HashMap<String, serde_json::Value> = HashMap::new();
context.insert("value".to_string(), value.clone());
let expr = if template.starts_with("@{") && template.ends_with("}") {
&template[2..template.len() - 1]
} else if template.contains("@{") {
let json_context = serde_json::json!({ "value": value });
let evaluator = build_evaluator(&json_context, None, data_sources);
match evaluate_template_string(template, &evaluator) {
Ok(result) => {
return is_truthy_string(&result);
}
Err(e) => {
warn_condition_error("pattern template", template, e);
return false;
}
}
} else {
template
};
match evaluate_expression(expr, &context) {
Ok(result) => is_truthy(&result),
Err(e) => {
warn_condition_error("pattern expression", expr, e);
false
}
}
}
pub(crate) fn is_truthy_string(s: &str) -> bool {
!s.is_empty() && s != "false" && s != "0" && s != "null" && s != "undefined"
}
pub(crate) fn is_truthy(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Null => false,
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
serde_json::Value::String(s) => is_truthy_string(s),
serde_json::Value::Array(arr) => !arr.is_empty(),
serde_json::Value::Object(obj) => !obj.is_empty(),
}
}
fn values_match(
value: &serde_json::Value,
pattern: &serde_json::Value,
data_sources: Option<&DataSources>,
) -> bool {
match pattern {
serde_json::Value::Array(patterns) => patterns
.iter()
.any(|p| values_match(value, p, data_sources)),
serde_json::Value::String(pattern_str) => {
if pattern_str == "_" || pattern_str == "*" {
return true;
}
if pattern_str.contains("@{") {
return evaluate_expression_pattern(pattern_str, value, data_sources);
}
matches!(value, serde_json::Value::String(s) if s == pattern_str)
}
_ => {
if value == pattern {
return true;
}
if let serde_json::Value::Bool(p) = pattern {
return is_truthy(value) == *p;
}
matches!(
(value, pattern),
(serde_json::Value::Null, serde_json::Value::Null)
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::Binding;
use serde_json::json;
#[test]
fn test_evaluate_value_template_expression_preserves_bool() {
let state = json!({"currentView": "feed"});
let value = Value::TemplateString {
template: "@{state.currentView == 'feed'}".to_string(),
bindings: vec![],
};
let result = evaluate_value(&value, &state, None);
assert_eq!(
result,
json!(true),
"Expression should evaluate to Bool(true), not String(\"true\")"
);
let value = Value::TemplateString {
template: "@{state.currentView == 'profile'}".to_string(),
bindings: vec![],
};
let result = evaluate_value(&value, &state, None);
assert_eq!(
result,
json!(false),
"Expression should evaluate to Bool(false)"
);
}
#[test]
fn test_evaluate_value_mixed_template_stays_string() {
let state = json!({"name": "Alice"});
let value = Value::TemplateString {
template: "Hello @{state.name}!".to_string(),
bindings: vec![],
};
let result = evaluate_value(&value, &state, None);
assert_eq!(result, json!("Hello Alice!"));
}
#[test]
fn test_pattern_matches_bool_coerces_truthy_value() {
let state = json!({});
let true_pattern = Value::Static(json!(true));
let false_pattern = Value::Static(json!(false));
let url = json!("https://example.com/img.jpg");
assert!(
pattern_matches(&url, &true_pattern, &state, None),
"truthy string URL must match Bool(true)",
);
assert!(
!pattern_matches(&url, &false_pattern, &state, None),
"truthy string URL must NOT match Bool(false)",
);
let empty = json!("");
assert!(!pattern_matches(&empty, &true_pattern, &state, None));
assert!(pattern_matches(&empty, &false_pattern, &state, None));
let null = json!(null);
assert!(!pattern_matches(&null, &true_pattern, &state, None));
assert!(pattern_matches(&null, &false_pattern, &state, None));
assert!(pattern_matches(&json!(42), &true_pattern, &state, None));
assert!(!pattern_matches(&json!(0), &true_pattern, &state, None));
assert!(pattern_matches(&json!([1]), &true_pattern, &state, None));
assert!(!pattern_matches(&json!([]), &true_pattern, &state, None));
}
#[test]
fn test_pattern_matches_binding_truthy() {
let state = json!({"isAdmin": true, "count": 0});
let evaluated = json!("anything");
let pattern = Value::Binding(Binding::state(vec!["isAdmin".to_string()]));
assert!(pattern_matches(&evaluated, &pattern, &state, None));
let pattern = Value::Binding(Binding::state(vec!["count".to_string()]));
assert!(!pattern_matches(&evaluated, &pattern, &state, None));
let pattern = Value::Binding(Binding::state(vec!["missing".to_string()]));
assert!(!pattern_matches(&evaluated, &pattern, &state, None));
}
#[test]
fn test_evaluate_value_data_source_binding() {
let state = json!({}); let mut ds = indexmap::IndexMap::new();
ds.insert(
"spacetime".to_string(),
json!({"status": "connected", "count": 42}),
);
let binding = Binding::data_source("spacetime", vec!["status".to_string()]);
let result = evaluate_value(&Value::Binding(binding), &state, Some(&ds));
assert_eq!(result, json!("connected"));
let binding = Binding::data_source("firebase", vec!["docs".to_string()]);
let result = evaluate_value(&Value::Binding(binding), &state, Some(&ds));
assert_eq!(result, json!(null));
let binding = Binding::data_source("spacetime", vec!["status".to_string()]);
let result = evaluate_value(&Value::Binding(binding), &state, None);
assert_eq!(result, json!(null));
}
#[test]
fn test_pattern_matches_data_source_binding() {
let state = json!({});
let mut ds = indexmap::IndexMap::new();
ds.insert("spacetime".to_string(), json!({"connected": true}));
let evaluated = json!("anything");
let pattern = Value::Binding(Binding::data_source(
"spacetime",
vec!["connected".to_string()],
));
assert!(pattern_matches(&evaluated, &pattern, &state, Some(&ds)));
let pattern = Value::Binding(Binding::data_source("firebase", vec!["ready".to_string()]));
assert!(!pattern_matches(&evaluated, &pattern, &state, Some(&ds)));
}
}