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::hub_methods(
92    namespace = "mustache",
93    version = "1.0.0",
94    description = "Mustache template rendering for handle values"
95)]
96impl Mustache {
97    /// Render a value using a template
98    ///
99    /// Looks up the template for the given plugin/method/name combination
100    /// and renders the value using mustache templating. If template_name
101    /// is None, uses "default".
102    #[plexus_macros::hub_method(
103        description = "Render a value using a registered mustache template",
104        params(
105            plugin_id = "UUID of the plugin that owns the template",
106            method = "Method name the template is for",
107            template_name = "Template name (defaults to 'default' if not specified)",
108            value = "JSON value to render with the template"
109        )
110    )]
111    async fn render(
112        &self,
113        plugin_id: Uuid,
114        method: String,
115        template_name: Option<String>,
116        value: Value,
117    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
118        let storage = Arc::clone(&self.storage);
119        let name = template_name.unwrap_or_else(|| "default".to_string());
120
121        stream! {
122            // Look up the template
123            match storage.get_template(&plugin_id, &method, &name).await {
124                Ok(Some(template_str)) => {
125                    // Compile and render the template
126                    match mustache::compile_str(&template_str) {
127                        Ok(template) => {
128                            let mut output = Vec::new();
129                            match template.render(&mut output, &value) {
130                                Ok(()) => {
131                                    match String::from_utf8(output) {
132                                        Ok(rendered) => {
133                                            yield MustacheEvent::Rendered { output: rendered };
134                                        }
135                                        Err(e) => {
136                                            yield MustacheEvent::Error {
137                                                message: format!("UTF-8 conversion error: {}", e),
138                                            };
139                                        }
140                                    }
141                                }
142                                Err(e) => {
143                                    yield MustacheEvent::Error {
144                                        message: format!("Template render error: {}", e),
145                                    };
146                                }
147                            }
148                        }
149                        Err(e) => {
150                            yield MustacheEvent::Error {
151                                message: format!("Template compile error: {}", e),
152                            };
153                        }
154                    }
155                }
156                Ok(None) => {
157                    yield MustacheEvent::NotFound {
158                        message: format!(
159                            "Template not found: plugin={}, method={}, name={}",
160                            plugin_id, method, name
161                        ),
162                    };
163                }
164                Err(e) => {
165                    yield MustacheEvent::Error {
166                        message: format!("Storage error: {}", e),
167                    };
168                }
169            }
170        }
171    }
172
173    /// Register a template for a plugin/method
174    ///
175    /// Templates are identified by (plugin_id, method, name). If a template
176    /// with the same identifier already exists, it will be updated.
177    #[plexus_macros::hub_method(
178        description = "Register a mustache template for a plugin method",
179        params(
180            plugin_id = "UUID of the plugin registering the template",
181            method = "Method name this template is for",
182            name = "Template name (e.g., 'default', 'compact', 'verbose')",
183            template = "Mustache template content"
184        )
185    )]
186    async fn register_template(
187        &self,
188        plugin_id: Uuid,
189        method: String,
190        name: String,
191        template: String,
192    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
193        let storage = Arc::clone(&self.storage);
194
195        stream! {
196            // Validate the template compiles
197            if let Err(e) = mustache::compile_str(&template) {
198                yield MustacheEvent::Error {
199                    message: format!("Invalid template: {}", e),
200                };
201                return;
202            }
203
204            match storage.set_template(&plugin_id, &method, &name, &template).await {
205                Ok(info) => {
206                    yield MustacheEvent::Registered { template: info };
207                }
208                Err(e) => {
209                    yield MustacheEvent::Error {
210                        message: format!("Failed to register template: {}", e),
211                    };
212                }
213            }
214        }
215    }
216
217    /// List all templates for a plugin
218    #[plexus_macros::hub_method(
219        description = "List all templates registered for a plugin",
220        params(plugin_id = "UUID of the plugin to list templates for")
221    )]
222    async fn list_templates(
223        &self,
224        plugin_id: Uuid,
225    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
226        let storage = Arc::clone(&self.storage);
227
228        stream! {
229            match storage.list_templates(&plugin_id).await {
230                Ok(templates) => {
231                    yield MustacheEvent::Templates { templates };
232                }
233                Err(e) => {
234                    yield MustacheEvent::Error {
235                        message: format!("Failed to list templates: {}", e),
236                    };
237                }
238            }
239        }
240    }
241
242    /// Get a specific template
243    #[plexus_macros::hub_method(
244        description = "Get a specific template by plugin, method, and name",
245        params(
246            plugin_id = "UUID of the plugin that owns the template",
247            method = "Method name the template is for",
248            name = "Template name"
249        )
250    )]
251    async fn get_template(
252        &self,
253        plugin_id: Uuid,
254        method: String,
255        name: String,
256    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
257        let storage = Arc::clone(&self.storage);
258
259        stream! {
260            match storage.get_template(&plugin_id, &method, &name).await {
261                Ok(Some(template)) => {
262                    yield MustacheEvent::Template { template };
263                }
264                Ok(None) => {
265                    yield MustacheEvent::NotFound {
266                        message: format!(
267                            "Template not found: plugin={}, method={}, name={}",
268                            plugin_id, method, name
269                        ),
270                    };
271                }
272                Err(e) => {
273                    yield MustacheEvent::Error {
274                        message: format!("Failed to get template: {}", e),
275                    };
276                }
277            }
278        }
279    }
280
281    /// Delete a template
282    #[plexus_macros::hub_method(
283        description = "Delete a specific template",
284        params(
285            plugin_id = "UUID of the plugin that owns the template",
286            method = "Method name the template is for",
287            name = "Template name"
288        )
289    )]
290    async fn delete_template(
291        &self,
292        plugin_id: Uuid,
293        method: String,
294        name: String,
295    ) -> impl Stream<Item = MustacheEvent> + Send + 'static {
296        let storage = Arc::clone(&self.storage);
297
298        stream! {
299            match storage.delete_template(&plugin_id, &method, &name).await {
300                Ok(deleted) => {
301                    yield MustacheEvent::Deleted {
302                        count: if deleted { 1 } else { 0 },
303                    };
304                }
305                Err(e) => {
306                    yield MustacheEvent::Error {
307                        message: format!("Failed to delete template: {}", e),
308                    };
309                }
310            }
311        }
312    }
313}