espforge_lib/resolver/actions/
logic.rs1use crate::config::EspforgeConfiguration;
2use crate::manifest::ComponentManifest;
3use crate::register_action_strategy;
4use crate::resolver::actions::{ActionResolver, ActionStrategy, ValidationResult};
5use anyhow::{Result, anyhow};
6use espforge_macros::auto_register_action_strategy;
7use serde_yaml_ng::Value;
8use std::collections::HashMap;
9use tera::Tera;
10
11fn resolve_value(
19 val: &Value,
20 _config: &EspforgeConfiguration,
21 _manifests: &HashMap<String, ComponentManifest>,
22 _tera: &mut Tera,
23) -> Result<String> {
24 match val {
25 Value::String(s) => {
26 if let Some(var_name) = s.strip_prefix('$') {
27 Ok(var_name.to_string())
30 } else {
31 Ok(format!("\"{}\"", s))
33 }
34 }
35 Value::Bool(b) => Ok(b.to_string()),
36 Value::Number(n) => Ok(n.to_string()),
37 _ => Ok(format!("{:?}", val)),
38 }
39}
40
41#[derive(Default)]
44#[auto_register_action_strategy]
45pub struct SetActionStrategy;
46
47impl ActionStrategy for SetActionStrategy {
48 fn can_handle(&self, key: &str) -> bool {
49 key == "set"
50 }
51
52 fn validate(
53 &self,
54 _key: &str,
55 value: &Value,
56 config: &EspforgeConfiguration,
57 _manifests: &HashMap<String, ComponentManifest>,
58 ) -> ValidationResult {
59 let Some(map) = value.as_mapping() else {
60 return ValidationResult::Error("'set' value must be a map".to_string());
61 };
62
63 if let Some(variable) = map.get(Value::from("variable")).and_then(|v| v.as_str()) {
65 if let Some(app) = &config.app {
66 if !app.variables.contains_key(variable) {
67 return ValidationResult::Error(format!(
68 "Variable '{}' is not defined in app.variables",
69 variable
70 ));
71 }
72 } else {
73 return ValidationResult::Error("No variables defined".to_string());
74 }
75 } else {
76 return ValidationResult::Error("'set' missing 'variable' name".to_string());
77 }
78
79 ValidationResult::Ok("Validated set action".to_string())
80 }
81
82 fn render(
83 &self,
84 _key: &str,
85 value: &Value,
86 config: &EspforgeConfiguration,
87 manifests: &HashMap<String, ComponentManifest>,
88 tera: &mut Tera,
89 ) -> Result<String> {
90 let map = value.as_mapping().unwrap();
91 let variable = map.get(Value::from("variable")).unwrap().as_str().unwrap();
92
93 let resolved_value = if let Some(call_val) = map.get(Value::from("call")) {
95 let call_map = call_val
97 .as_mapping()
98 .ok_or_else(|| anyhow!("'call' must be a map"))?;
99
100 let target = call_map
101 .get(Value::from("target"))
102 .and_then(|v| v.as_str())
103 .ok_or_else(|| anyhow!("Call missing target"))?;
104
105 let method = call_map
106 .get(Value::from("method"))
107 .and_then(|v| v.as_str())
108 .ok_or_else(|| anyhow!("Call missing method"))?;
109
110 let key = format!("${}.{}", target, method);
112
113 let resolver = ActionResolver::new();
115 let mut code = resolver.resolve(&key, &Value::Null, config, manifests, tera)?;
116
117 if code.trim().ends_with(';') {
119 code = code.trim().trim_end_matches(';').to_string();
120 }
121 code
122 } else if let Some(val_node) = map.get(Value::from("value")) {
123 resolve_value(val_node, config, manifests, tera)?
124 } else {
125 return Err(anyhow!("Set action requires either 'value' or 'call'"));
126 };
127
128 Ok(format!("{} = {};", variable, resolved_value))
129 }
130}
131
132#[derive(Default)]
135#[auto_register_action_strategy]
136pub struct IfActionStrategy;
137
138impl ActionStrategy for IfActionStrategy {
139 fn can_handle(&self, key: &str) -> bool {
140 key == "if"
141 }
142
143 fn validate(
144 &self,
145 _key: &str,
146 value: &Value,
147 _config: &EspforgeConfiguration,
148 _manifests: &HashMap<String, ComponentManifest>,
149 ) -> ValidationResult {
150 let Some(map) = value.as_mapping() else {
151 return ValidationResult::Error("'if' value must be a map".to_string());
152 };
153
154 if !map.contains_key(Value::from("condition")) {
155 return ValidationResult::Error("'if' missing 'condition'".to_string());
156 }
157 if !map.contains_key(Value::from("then")) {
158 return ValidationResult::Error("'if' missing 'then' block".to_string());
159 }
160
161 ValidationResult::Ok("Validated if action".to_string())
162 }
163
164 fn render(
165 &self,
166 _key: &str,
167 value: &Value,
168 config: &EspforgeConfiguration,
169 manifests: &HashMap<String, ComponentManifest>,
170 tera: &mut Tera,
171 ) -> Result<String> {
172 let map = value.as_mapping().unwrap();
173
174 let cond_node = map
176 .get(Value::from("condition"))
177 .ok_or_else(|| anyhow!("Missing condition"))?;
178
179 let cond_map = cond_node
180 .as_mapping()
181 .ok_or_else(|| anyhow!("Condition must be a map"))?;
182
183 let lhs_node = cond_map.get(Value::from("lhs")).unwrap_or(&Value::Null);
184 let rhs_node = cond_map.get(Value::from("rhs")).unwrap_or(&Value::Null);
185 let op_node = cond_map
186 .get(Value::from("op"))
187 .and_then(|v| v.as_str())
188 .unwrap_or("==");
189
190 let lhs = resolve_value(lhs_node, config, manifests, tera)?;
191 let rhs = resolve_value(rhs_node, config, manifests, tera)?;
192
193 let op = match op_node {
194 "equals" => "==",
195 "not_equals" => "!=",
196 "gt" => ">",
197 "lt" => "<",
198 o => o,
199 };
200
201 let then_node = map
203 .get(Value::from("then"))
204 .ok_or_else(|| anyhow!("Missing then block"))?;
205
206 let then_list = then_node
207 .as_sequence()
208 .ok_or_else(|| anyhow!("'then' must be a list of actions"))?;
209
210 let mut then_code = String::new();
211 let resolver = ActionResolver::new();
212
213 for action_val in then_list {
214 if let Some(action_map) = action_val.as_mapping() {
215 for (k, v) in action_map {
216 let k_str = k.as_str().unwrap_or("unknown");
217 let code = resolver.resolve(k_str, v, config, manifests, tera)?;
218 then_code.push_str(&code);
219 then_code.push('\n');
220 }
221 }
222 }
223
224 Ok(format!("if {} {} {} {{\n{}\n}}", lhs, op, rhs, then_code))
225 }
226}