use serde_json::{Map, Value};
use crate::event::read_path;
#[derive(Debug, thiserror::Error)]
pub enum TemplateError {
#[error("template '{0}' references $event but no event is in scope")]
EventOutOfScope(String),
#[error("template '{token}' did not resolve (path not found)")]
Unresolved {
token: String,
},
}
pub fn resolve_config(
config: &Value,
instance_config: &Value,
instance_metadata: &Value,
event_payload: Option<&Value>,
) -> Result<Value, TemplateError> {
match config {
Value::String(s) => resolve_string(s, instance_config, instance_metadata, event_payload),
Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for it in items {
out.push(resolve_config(
it,
instance_config,
instance_metadata,
event_payload,
)?);
}
Ok(Value::Array(out))
}
Value::Object(map) => {
let mut out = Map::with_capacity(map.len());
for (k, v) in map {
out.insert(
k.clone(),
resolve_config(v, instance_config, instance_metadata, event_payload)?,
);
}
Ok(Value::Object(out))
}
other => Ok(other.clone()),
}
}
fn resolve_string(
s: &str,
instance_config: &Value,
instance_metadata: &Value,
event_payload: Option<&Value>,
) -> Result<Value, TemplateError> {
let resolve_from = |src: &Value, path: &str| -> Option<Value> { read_path(src, path).cloned() };
if let Some(path) = s.strip_prefix("$config.") {
resolve_from(instance_config, path).ok_or_else(|| TemplateError::Unresolved {
token: s.to_string(),
})
} else if let Some(path) = s.strip_prefix("$instance.") {
resolve_from(instance_metadata, path).ok_or_else(|| TemplateError::Unresolved {
token: s.to_string(),
})
} else if let Some(path) = s.strip_prefix("$event.") {
let payload = event_payload.ok_or_else(|| TemplateError::EventOutOfScope(s.to_string()))?;
resolve_from(payload, path).ok_or_else(|| TemplateError::Unresolved {
token: s.to_string(),
})
} else {
Ok(Value::String(s.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instance_tokens_never_fall_back_to_config() {
assert!(matches!(
resolve_config(
&Value::String("$instance.owner".into()),
&serde_json::json!({"owner":"legacy"}),
&serde_json::json!({}),
None,
),
Err(TemplateError::Unresolved { .. })
));
}
use serde_json::json;
#[test]
fn resolves_each_namespace() {
let cfg = json!({
"a": "$config.threshold",
"b": "$event.price",
"c": "literal",
"nested": {"d": "$instance.run_id"}
});
let out = resolve_config(
&cfg,
&json!({"threshold": 0.5}),
&json!({"run_id": "r1"}),
Some(&json!({"price": 42})),
)
.unwrap();
assert_eq!(
out,
json!({"a": 0.5, "b": 42, "c": "literal", "nested": {"d": "r1"}})
);
}
#[test]
fn event_template_without_event_errors() {
let cfg = json!("$event.x");
assert!(matches!(
resolve_config(&cfg, &json!({}), &json!({}), None),
Err(TemplateError::EventOutOfScope(_))
));
}
}