1use serde_json::{Map, Value};
9
10use crate::event::read_path;
11
12#[derive(Debug, thiserror::Error)]
13pub enum TemplateError {
14 #[error("template '{0}' references $event but no event is in scope")]
15 EventOutOfScope(String),
16 #[error("template '{token}' did not resolve (path not found)")]
17 Unresolved { token: String },
18}
19
20pub fn resolve_config(
24 config: &Value,
25 instance_config: &Value,
26 instance_metadata: &Value,
27 event_payload: Option<&Value>,
28) -> Result<Value, TemplateError> {
29 match config {
30 Value::String(s) => resolve_string(s, instance_config, instance_metadata, event_payload),
31 Value::Array(items) => {
32 let mut out = Vec::with_capacity(items.len());
33 for it in items {
34 out.push(resolve_config(
35 it,
36 instance_config,
37 instance_metadata,
38 event_payload,
39 )?);
40 }
41 Ok(Value::Array(out))
42 }
43 Value::Object(map) => {
44 let mut out = Map::with_capacity(map.len());
45 for (k, v) in map {
46 out.insert(
47 k.clone(),
48 resolve_config(v, instance_config, instance_metadata, event_payload)?,
49 );
50 }
51 Ok(Value::Object(out))
52 }
53 other => Ok(other.clone()),
54 }
55}
56
57fn resolve_string(
58 s: &str,
59 instance_config: &Value,
60 instance_metadata: &Value,
61 event_payload: Option<&Value>,
62) -> Result<Value, TemplateError> {
63 let resolve_from = |src: &Value, path: &str| -> Option<Value> { read_path(src, path).cloned() };
64
65 if let Some(path) = s.strip_prefix("$config.") {
66 resolve_from(instance_config, path).ok_or_else(|| TemplateError::Unresolved {
67 token: s.to_string(),
68 })
69 } else if let Some(path) = s.strip_prefix("$instance.") {
70 resolve_from(instance_metadata, path).ok_or_else(|| TemplateError::Unresolved {
71 token: s.to_string(),
72 })
73 } else if let Some(path) = s.strip_prefix("$event.") {
74 let payload = event_payload.ok_or_else(|| TemplateError::EventOutOfScope(s.to_string()))?;
75 resolve_from(payload, path).ok_or_else(|| TemplateError::Unresolved {
76 token: s.to_string(),
77 })
78 } else {
79 Ok(Value::String(s.to_string()))
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn instance_tokens_never_fall_back_to_config() {
90 assert!(matches!(
91 resolve_config(
92 &Value::String("$instance.owner".into()),
93 &serde_json::json!({"owner":"legacy"}),
94 &serde_json::json!({}),
95 None,
96 ),
97 Err(TemplateError::Unresolved { .. })
98 ));
99 }
100 use serde_json::json;
101
102 #[test]
103 fn resolves_each_namespace() {
104 let cfg = json!({
105 "a": "$config.threshold",
106 "b": "$event.price",
107 "c": "literal",
108 "nested": {"d": "$instance.run_id"}
109 });
110 let out = resolve_config(
111 &cfg,
112 &json!({"threshold": 0.5}),
113 &json!({"run_id": "r1"}),
114 Some(&json!({"price": 42})),
115 )
116 .unwrap();
117 assert_eq!(
118 out,
119 json!({"a": 0.5, "b": 42, "c": "literal", "nested": {"d": "r1"}})
120 );
121 }
122
123 #[test]
124 fn event_template_without_event_errors() {
125 let cfg = json!("$event.x");
126 assert!(matches!(
127 resolve_config(&cfg, &json!({}), &json!({}), None),
128 Err(TemplateError::EventOutOfScope(_))
129 ));
130 }
131}