espforge_lib/resolver/actions/
logic.rs

1use 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
11// --- Helper Functions ---
12
13/// Resolves a YAML value into a Rust code fragment.
14/// Handles:
15/// - "$var" -> var (Variable reference)
16/// - Strings -> "string" (Literal)
17/// - Numbers/Bools -> literal
18fn 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                // It's a variable reference (e.g. "$buttonstate")
28                // Return just the name so it refers to the Rust variable
29                Ok(var_name.to_string())
30            } else {
31                // It's a string literal, wrap in quotes
32                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// --- Set Strategy ---
42
43#[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        // Check if variable exists in config
64        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        // Check for 'call' (function call assignment) OR 'value' (literal/expression)
94        let resolved_value = if let Some(call_val) = map.get(Value::from("call")) {
95            // Synthesize a component call action
96            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            // Construct "$target.method" key
111            let key = format!("${}.{}", target, method);
112
113            // Use a temporary resolver to render that specific action string
114            let resolver = ActionResolver::new();
115            let mut code = resolver.resolve(&key, &Value::Null, config, manifests, tera)?;
116
117            // Strip trailing semicolon if present, because we are using it as an expression
118            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// --- If Strategy ---
133
134#[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        // condition: { lhs: ..., op: ..., rhs: ... }
175        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        // Process 'then' block
202        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}