Skip to main content

plexus_substrate/activations/mustache/
activation.rs

1//! Mustache activation - template rendering for handle values
2//!
3//! This activation provides mustache template rendering as a core plugin.
4//! Other plugins can register their default templates and use this to render
5//! handle values consistently.
6
7use super::storage::{MustacheStorage, MustacheStorageConfig};
8use super::types::MustacheEvent;
9use async_stream::stream;
10use futures::Stream;
11use serde_json::Value;
12use std::sync::Arc;
13use uuid::Uuid;
14
15/// Mustache activation - renders values using mustache templates
16pub struct Mustache {
17    storage: Arc<MustacheStorage>,
18}
19
20impl Mustache {
21    /// Create a new Mustache activation with the given storage config
22    pub async fn new(config: MustacheStorageConfig) -> Result<Self, String> {
23        let storage = MustacheStorage::new(config)
24            .await
25            .map_err(|e| format!("Failed to initialize mustache storage: {}", e))?;
26
27        Ok(Self {
28            storage: Arc::new(storage),
29        })
30    }
31
32    /// Create with default configuration
33    pub async fn with_defaults() -> Result<Self, String> {
34        Self::new(MustacheStorageConfig::default()).await
35    }
36
37    /// Get access to the underlying storage
38    pub fn storage(&self) -> &MustacheStorage {
39        &self.storage
40    }
41
42    /// Register a template directly (non-streaming, for initialization)
43    ///
44    /// Use this method during plugin initialization to register default templates.
45    /// Unlike the RPC `register_template` method, this doesn't return a stream.
46    pub async fn register_template_direct(
47        &self,
48        plugin_id: Uuid,
49        method: &str,
50        name: &str,
51        template: &str,
52    ) -> Result<(), String> {
53        // Validate the template compiles
54        if let Err(e) = mustache::compile_str(template) {
55            return Err(format!("Invalid template: {}", e));
56        }
57
58        self.storage
59            .set_template(&plugin_id, method, name, template)
60            .await
61            .map_err(|e| format!("Failed to register template: {}", e))?;
62
63        Ok(())
64    }
65
66    /// Register multiple templates for a plugin
67    ///
68    /// Convenience method for registering multiple templates at once.
69    /// Takes a slice of (method, name, template) tuples.
70    pub async fn register_templates(
71        &self,
72        plugin_id: Uuid,
73        templates: &[(&str, &str, &str)],
74    ) -> Result<(), String> {
75        for (method, name, template) in templates {
76            self.register_template_direct(plugin_id, method, name, template).await?;
77        }
78        Ok(())
79    }
80}
81
82impl Clone for Mustache {
83    fn clone(&self) -> Self {
84        Self {
85            storage: Arc::clone(&self.storage),
86        }
87    }
88}
89
90/// Hub-macro generates all the boilerplate for this impl block
91#[plexus_macros::activation(namespace = "mustache",
92version = "1.0.0",
93description = "Mustache template rendering for handle values", crate_path = "plexus_core")]
94impl Mustache {
95    /// Render a value using a template
96    ///
97    /// Looks up the template for the given plugin/method/name combination
98    /// and renders the value using mustache templating. If template_name
99    /// is None, uses "default".
100    #[plexus_macros::method(description = "Render a value using a registered mustache template",
101    params(
102        plugin_id = "UUID of the plugin that owns the template",
103        method = "Method name the template is for",
104        template_name = "Template name (defaults to 'default' if not specified)",
105        value = "JSON value to render with the template"
106    ))]
107    async fn render(
108        &self,
109        plugin_id: Uuid,
110        method: String,
111        template_name: Option<String>,
112        value: Value,
113    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
114        let storage = Arc::clone(&self.storage);
115        let name = template_name.unwrap_or_else(|| "default".to_string());
116
117        stream! {
118            // Look up the template
119            match storage.get_template(&plugin_id, &method, &name).await {
120                Ok(Some(template_str)) => {
121                    // Compile and render the template
122                    match mustache::compile_str(&template_str) {
123                        Ok(template) => {
124                            let mut output = Vec::new();
125                            match template.render(&mut output, &value) {
126                                Ok(()) => {
127                                    match String::from_utf8(output) {
128                                        Ok(rendered) => {
129                                            yield MustacheEvent::Rendered { output: rendered };
130                                        }
131                                        Err(e) => {
132                                            yield MustacheEvent::Error {
133                                                message: format!("UTF-8 conversion error: {}", e),
134                                            };
135                                        }
136                                    }
137                                }
138                                Err(e) => {
139                                    yield MustacheEvent::Error {
140                                        message: format!("Template render error: {}", e),
141                                    };
142                                }
143                            }
144                        }
145                        Err(e) => {
146                            yield MustacheEvent::Error {
147                                message: format!("Template compile error: {}", e),
148                            };
149                        }
150                    }
151                }
152                Ok(None) => {
153                    yield MustacheEvent::NotFound {
154                        message: format!(
155                            "Template not found: plugin={}, method={}, name={}",
156                            plugin_id, method, name
157                        ),
158                    };
159                }
160                Err(e) => {
161                    yield MustacheEvent::Error {
162                        message: format!("Storage error: {}", e),
163                    };
164                }
165            }
166        }
167    }
168
169    /// Register a template for a plugin/method
170    ///
171    /// Templates are identified by (plugin_id, method, name). If a template
172    /// with the same identifier already exists, it will be updated.
173    #[plexus_macros::method(description = "Register a mustache template for a plugin method",
174    params(
175        plugin_id = "UUID of the plugin registering the template",
176        method = "Method name this template is for",
177        name = "Template name (e.g., 'default', 'compact', 'verbose')",
178        template = "Mustache template content"
179    ))]
180    async fn register_template(
181        &self,
182        plugin_id: Uuid,
183        method: String,
184        name: String,
185        template: String,
186    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
187        let storage = Arc::clone(&self.storage);
188
189        stream! {
190            // Validate the template compiles
191            if let Err(e) = mustache::compile_str(&template) {
192                yield MustacheEvent::Error {
193                    message: format!("Invalid template: {}", e),
194                };
195                return;
196            }
197
198            match storage.set_template(&plugin_id, &method, &name, &template).await {
199                Ok(info) => {
200                    yield MustacheEvent::Registered { template: info };
201                }
202                Err(e) => {
203                    yield MustacheEvent::Error {
204                        message: format!("Failed to register template: {}", e),
205                    };
206                }
207            }
208        }
209    }
210
211    /// List all templates for a plugin
212    #[plexus_macros::method(description = "List all templates registered for a plugin",
213    params(plugin_id = "UUID of the plugin to list templates for"))]
214    async fn list_templates(
215        &self,
216        plugin_id: Uuid,
217    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
218        let storage = Arc::clone(&self.storage);
219
220        stream! {
221            match storage.list_templates(&plugin_id).await {
222                Ok(templates) => {
223                    yield MustacheEvent::Templates { templates };
224                }
225                Err(e) => {
226                    yield MustacheEvent::Error {
227                        message: format!("Failed to list templates: {}", e),
228                    };
229                }
230            }
231        }
232    }
233
234    /// Get a specific template
235    #[plexus_macros::method(description = "Get a specific template by plugin, method, and name",
236    params(
237        plugin_id = "UUID of the plugin that owns the template",
238        method = "Method name the template is for",
239        name = "Template name"
240    ))]
241    async fn get_template(
242        &self,
243        plugin_id: Uuid,
244        method: String,
245        name: String,
246    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
247        let storage = Arc::clone(&self.storage);
248
249        stream! {
250            match storage.get_template(&plugin_id, &method, &name).await {
251                Ok(Some(template)) => {
252                    yield MustacheEvent::Template { template };
253                }
254                Ok(None) => {
255                    yield MustacheEvent::NotFound {
256                        message: format!(
257                            "Template not found: plugin={}, method={}, name={}",
258                            plugin_id, method, name
259                        ),
260                    };
261                }
262                Err(e) => {
263                    yield MustacheEvent::Error {
264                        message: format!("Failed to get template: {}", e),
265                    };
266                }
267            }
268        }
269    }
270
271    /// Delete a template
272    #[plexus_macros::method(description = "Delete a specific template",
273    params(
274        plugin_id = "UUID of the plugin that owns the template",
275        method = "Method name the template is for",
276        name = "Template name"
277    ))]
278    async fn delete_template(
279        &self,
280        plugin_id: Uuid,
281        method: String,
282        name: String,
283    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
284        let storage = Arc::clone(&self.storage);
285
286        stream! {
287            match storage.delete_template(&plugin_id, &method, &name).await {
288                Ok(deleted) => {
289                    yield MustacheEvent::Deleted {
290                        count: if deleted { 1 } else { 0 },
291                    };
292                }
293                Err(e) => {
294                    yield MustacheEvent::Error {
295                        message: format!("Failed to delete template: {}", e),
296                    };
297                }
298            }
299        }
300    }
301}