espforge_lib/resolver/actions/
component.rs

1use crate::config::EspforgeConfiguration;
2use crate::manifest::ComponentManifest;
3use crate::register_action_strategy;
4use crate::resolver::actions::{ActionStrategy, ValidationResult};
5use anyhow::{Context, Result, anyhow};
6use espforge_macros::auto_register_action_strategy;
7use serde_yaml_ng::Value;
8use std::collections::HashMap;
9use tera::Tera;
10
11#[derive(Default)]
12#[auto_register_action_strategy]
13pub struct ComponentActionStrategy;
14
15impl ComponentActionStrategy {
16    fn parse_key<'a>(&self, key: &'a str) -> Option<(&'a str, &'a str)> {
17        // format: $component_name.method_name
18        if !key.starts_with('$') {
19            return None;
20        }
21
22        let parts: Vec<&str> = key.split('.').collect();
23        if parts.len() < 2 {
24            return None;
25        }
26
27        let instance_name = &parts[0][1..]; // strip '$'
28        let method_name = parts[1];
29        Some((instance_name, method_name))
30    }
31
32    fn get_manifest<'a>(
33        &self,
34        instance_name: &str,
35        config: &EspforgeConfiguration,
36        manifests: &'a HashMap<String, ComponentManifest>,
37    ) -> Result<(&'a ComponentManifest, String)> {
38        let components = config
39            .components
40            .as_ref()
41            .ok_or_else(|| anyhow!("No components defined"))?;
42
43        let instance = components
44            .get(instance_name)
45            .ok_or_else(|| anyhow!("Component instance '{}' not found", instance_name))?;
46
47        let manifest = manifests.get(&instance.using).ok_or_else(|| {
48            anyhow!(
49                "Manifest '{}' not found for instance '{}'",
50                instance.using,
51                instance_name
52            )
53        })?;
54
55        Ok((manifest, instance.using.clone()))
56    }
57}
58
59impl ActionStrategy for ComponentActionStrategy {
60    fn can_handle(&self, key: &str) -> bool {
61        key.starts_with('$')
62    }
63
64    fn validate(
65        &self,
66        key: &str,
67        _value: &Value,
68        config: &EspforgeConfiguration,
69        manifests: &HashMap<String, ComponentManifest>,
70    ) -> ValidationResult {
71        let Some((instance_name, method_name)) = self.parse_key(key) else {
72            return ValidationResult::Error(format!(
73                "Invalid component syntax '{}'. Expected $name.method",
74                key
75            ));
76        };
77
78        match self.get_manifest(instance_name, config, manifests) {
79            Ok((manifest, _)) => {
80                if manifest.methods.contains_key(method_name) {
81                    ValidationResult::Ok(format!(
82                        "Validated action '{}' on component '{}'",
83                        method_name, instance_name
84                    ))
85                } else {
86                    ValidationResult::Error(format!(
87                        "Method '{}' not found on component '{}' (type {})",
88                        method_name, instance_name, manifest.name
89                    ))
90                }
91            }
92            Err(e) => ValidationResult::Error(e.to_string()),
93        }
94    }
95
96    fn render(
97        &self,
98        key: &str,
99        value: &Value,
100        config: &EspforgeConfiguration,
101        manifests: &HashMap<String, ComponentManifest>,
102        tera: &mut Tera,
103    ) -> Result<String> {
104        let (instance_name, method_name) = self
105            .parse_key(key)
106            .ok_or_else(|| anyhow!("Invalid key format"))?;
107
108        let (manifest, _) = self.get_manifest(instance_name, config, manifests)?;
109
110        let method_def = manifest
111            .methods
112            .get(method_name)
113            .ok_or_else(|| anyhow!("Method not found"))?;
114
115        let mut context = tera::Context::new();
116        context.insert("target", instance_name);
117        context.insert("args", value);
118
119        tera.render_str(&method_def.template, &context)
120            .with_context(|| format!("Failed to render component action {}", key))
121    }
122}
123
124//register_action_strategy!(ComponentActionStrategy);