glitcher_api/
registry.rs

1//! Action Registry - Maps action IDs to handlers
2//!
3//! Provides hybrid type-safety with String names for UI triggers and
4//! numeric IDs for fast execution path.
5//!
6//! ## Architecture
7//!
8//! - **UI Layer**: Triggers actions by string name ("reset", "randomize")
9//! - **Registry**: Maps names → numeric IDs (HashMap)
10//! - **Executor**: Dispatches by numeric ID (Vec indexing)
11//!
12//! ## Standard Actions
13//!
14//! Reserved IDs 0-99 for built-in actions:
15//! - `0`: Reset - Return parameters to defaults
16//! - `1`: Randomize - Randomize within widget ranges
17//!
18//! ## Custom Actions
19//!
20//! IDs 100+ for plugin-defined actions, executed via WASM.
21//!
22//! ## Usage
23//!
24//! ```rust,ignore
25//! use glitcher_api::registry::ActionRegistry;
26//! use glitcher_api::actions::ActionContext;
27//!
28//! let registry = ActionRegistry::from_manifest(manifest);
29//!
30//! // UI trigger by name
31//! let updates = registry.trigger("reset", &ctx, 0)?;
32//!
33//! // Fast path by ID
34//! let updates = registry.execute(standard_actions::RESET, &ctx, 0)?;
35//! ```
36
37use crate::{actions::ActionContext, ActionError, NodeManifest, ParamUpdate};
38use std::collections::HashMap;
39
40/// Numeric action ID for fast dispatch
41pub type ActionId = u32;
42
43/// Standard action ID constants (0-99 reserved)
44pub mod standard_actions {
45    use super::ActionId;
46    pub const RESET: ActionId = 0;
47    pub const RANDOMIZE: ActionId = 1;
48}
49
50/// Action handler variants
51#[derive(Debug, Clone)]
52pub enum ActionHandler {
53    /// Standard action handled by host
54    Standard(StandardAction),
55    /// Custom action requiring WASM execution
56    Custom(String),
57}
58
59/// Standard built-in actions
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum StandardAction {
62    Reset,
63    Randomize,
64}
65
66/// Action registry for a node
67///
68/// Maintains bidirectional mapping between action names and IDs,
69/// and dispatches to appropriate handlers.
70pub struct ActionRegistry {
71    /// String name → numeric ID mapping (for UI triggers)
72    id_map: HashMap<String, ActionId>,
73
74    /// Numeric ID → handler mapping (fast HashMap lookup)
75    handlers: HashMap<ActionId, ActionHandler>,
76
77    /// Reference to node manifest (needed for action execution)
78    manifest: NodeManifest,
79}
80
81impl ActionRegistry {
82    /// Create registry from node manifest
83    ///
84    /// Registers standard actions (IDs 0-2) and custom actions (IDs 100+).
85    ///
86    /// # Arguments
87    /// * `manifest` - Node manifest containing action definitions
88    ///
89    /// # Returns
90    /// * `Self` - Initialized action registry
91    pub fn from_manifest(manifest: NodeManifest) -> Self {
92        let mut id_map = HashMap::new();
93        let mut handlers = HashMap::new();
94
95        // Reserve standard action slots (0-1)
96        // These are always present, even if not declared in manifest
97        id_map.insert("reset".to_string(), standard_actions::RESET);
98        handlers.insert(
99            standard_actions::RESET,
100            ActionHandler::Standard(StandardAction::Reset),
101        );
102
103        id_map.insert("randomize".to_string(), standard_actions::RANDOMIZE);
104        handlers.insert(
105            standard_actions::RANDOMIZE,
106            ActionHandler::Standard(StandardAction::Randomize),
107        );
108
109        // Register custom actions from manifest (IDs 100+)
110        let mut next_id = 100;
111        for action in &manifest.actions {
112            // Skip if already registered as standard action
113            if !id_map.contains_key(&action.id) {
114                id_map.insert(action.id.clone(), next_id);
115                handlers.insert(next_id, ActionHandler::Custom(action.id.clone()));
116                next_id += 1;
117            }
118        }
119
120        Self {
121            id_map,
122            handlers,
123            manifest,
124        }
125    }
126
127    /// Trigger action by name (UI path)
128    ///
129    /// Looks up action ID by name and dispatches.
130    ///
131    /// # Arguments
132    /// * `name` - Action name (e.g., "reset", "randomize")
133    /// * `ctx` - Action context with current parameter state
134    /// * `seed` - Random seed for randomize action
135    ///
136    /// # Returns
137    /// * `Ok(Vec<ParamUpdate>)` - Parameter updates to apply
138    /// * `Err(ActionError)` - Action not found or execution failed
139    pub fn trigger(
140        &self,
141        name: &str,
142        ctx: &ActionContext,
143        seed: u64,
144    ) -> Result<Vec<ParamUpdate>, ActionError> {
145        let id = self
146            .id_map
147            .get(name)
148            .ok_or_else(|| ActionError::ActionNotFound(name.to_string()))?;
149
150        self.execute(*id, ctx, seed)
151    }
152
153    /// Execute action by numeric ID (fast path)
154    ///
155    /// Directly dispatches to handler without name lookup.
156    ///
157    /// # Arguments
158    /// * `id` - Numeric action ID
159    /// * `ctx` - Action context with current parameter state
160    /// * `seed` - Random seed for randomize action
161    ///
162    /// # Returns
163    /// * `Ok(Vec<ParamUpdate>)` - Parameter updates to apply
164    /// * `Err(ActionError)` - Invalid ID or execution failed
165    pub fn execute(
166        &self,
167        id: ActionId,
168        ctx: &ActionContext,
169        seed: u64,
170    ) -> Result<Vec<ParamUpdate>, ActionError> {
171        let handler = self
172            .handlers
173            .get(&id)
174            .ok_or_else(|| ActionError::ActionNotFound(id.to_string()))?;
175
176        match handler {
177            ActionHandler::Standard(action) => self.execute_standard(action, ctx, seed),
178            ActionHandler::Custom(name) => Err(ActionError::ExecutionFailed(format!(
179                "Custom action '{}' requires WASM execution (not available in registry)",
180                name
181            ))),
182        }
183    }
184
185    /// Execute standard built-in action
186    ///
187    /// Dispatches to appropriate action implementation.
188    fn execute_standard(
189        &self,
190        action: &StandardAction,
191        ctx: &ActionContext,
192        seed: u64,
193    ) -> Result<Vec<ParamUpdate>, ActionError> {
194        match action {
195            StandardAction::Reset => crate::actions::calculate_reset_values(&self.manifest, ctx)
196                .map_err(ActionError::ExecutionFailed),
197
198            StandardAction::Randomize => {
199                crate::actions::calculate_random_values(&self.manifest, ctx, seed)
200                    .map_err(ActionError::ExecutionFailed)
201            }
202        }
203    }
204
205    /// Get action ID by name (for debugging)
206    pub fn get_id(&self, name: &str) -> Option<ActionId> {
207        self.id_map.get(name).copied()
208    }
209
210    /// Get action count (standard + custom)
211    pub fn action_count(&self) -> usize {
212        self.handlers.len()
213    }
214
215    /// Check if action is standard (built-in)
216    pub fn is_standard(&self, id: ActionId) -> bool {
217        id < 100
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::{
225        ActionConfig, ActionDef, ExecutionModel, NodeCategory, ParamType, ShaderParam,
226        SliderConfig, WidgetConfig,
227    };
228
229    fn create_test_manifest() -> NodeManifest {
230        NodeManifest {
231            api_version: 1,
232            display_name: "Test Node".to_string(),
233            version: "1.0.0".to_string(),
234            author: "Test".to_string(),
235            description: "Test node".to_string(),
236            category: NodeCategory::Effector,
237            tags: vec![],
238            model: ExecutionModel::FragmentShader,
239            parameters: vec![ShaderParam {
240                name: "strength".to_string(),
241                data_type: ParamType::ScalarF32,
242                widget: WidgetConfig::Slider(SliderConfig {
243                    min: 0.0,
244                    max: 1.0,
245                    step: 0.01,
246                }),
247            }],
248            ports: vec![],
249            output_resolution_scale: 1.0,
250            output_hint: None,
251            actions: vec![
252                ActionDef {
253                    id: "custom-action-1".to_string(),
254                    label: "Custom Action 1".to_string(),
255                    config: ActionConfig::Trigger,
256                },
257                ActionDef {
258                    id: "custom-action-2".to_string(),
259                    label: "Custom Action 2".to_string(),
260                    config: ActionConfig::BeatSync,
261                },
262            ],
263            embedded_textures: vec![],
264        }
265    }
266
267    #[test]
268    fn test_registry_creation() {
269        let manifest = create_test_manifest();
270        let registry = ActionRegistry::from_manifest(manifest);
271
272        // Standard actions should be registered
273        assert_eq!(registry.get_id("reset"), Some(standard_actions::RESET));
274        assert_eq!(
275            registry.get_id("randomize"),
276            Some(standard_actions::RANDOMIZE)
277        );
278
279        // Custom actions should start at ID 100
280        assert_eq!(registry.get_id("custom-action-1"), Some(100));
281        assert_eq!(registry.get_id("custom-action-2"), Some(101));
282
283        // Total: 2 standard + 2 custom = 4
284        assert_eq!(registry.action_count(), 4);
285    }
286
287    #[test]
288    fn test_trigger_by_name() {
289        let manifest = create_test_manifest();
290        let registry = ActionRegistry::from_manifest(manifest);
291        let ctx = ActionContext {
292            params: &[],
293            beat_info: None,
294            is_high_frequency: false,
295        };
296
297        // Reset action should succeed
298        let result = registry.trigger("reset", &ctx, 0);
299        assert!(result.is_ok());
300        let updates = result.unwrap();
301        assert_eq!(updates.len(), 1); // One parameter
302
303        // Randomize action should succeed
304        let result = registry.trigger("randomize", &ctx, 42);
305        assert!(result.is_ok());
306
307        // Unknown action should fail
308        let result = registry.trigger("unknown", &ctx, 0);
309        assert!(matches!(result, Err(ActionError::ActionNotFound(_))));
310    }
311
312    #[test]
313    fn test_execute_by_id() {
314        let manifest = create_test_manifest();
315        let registry = ActionRegistry::from_manifest(manifest);
316        let ctx = ActionContext {
317            params: &[],
318            beat_info: None,
319            is_high_frequency: false,
320        };
321
322        // Execute reset by ID
323        let result = registry.execute(standard_actions::RESET, &ctx, 0);
324        assert!(result.is_ok());
325
326        // Execute randomize by ID
327        let result = registry.execute(standard_actions::RANDOMIZE, &ctx, 42);
328        assert!(result.is_ok());
329
330        // Invalid ID should fail
331        let result = registry.execute(999, &ctx, 0);
332        assert!(matches!(result, Err(ActionError::ActionNotFound(_))));
333    }
334
335    #[test]
336    fn test_custom_action_fails_without_wasm() {
337        let manifest = create_test_manifest();
338        let registry = ActionRegistry::from_manifest(manifest);
339        let ctx = ActionContext {
340            params: &[],
341            beat_info: None,
342            is_high_frequency: false,
343        };
344
345        // Custom action should fail (no WASM executor in registry)
346        let result = registry.trigger("custom-action-1", &ctx, 0);
347        assert!(matches!(result, Err(ActionError::ExecutionFailed(_))));
348    }
349
350    #[test]
351    fn test_is_standard() {
352        let manifest = create_test_manifest();
353        let registry = ActionRegistry::from_manifest(manifest);
354
355        assert!(registry.is_standard(0)); // RESET
356        assert!(registry.is_standard(1)); // RANDOMIZE
357        assert!(!registry.is_standard(100)); // Custom action
358        assert!(!registry.is_standard(101)); // Custom action
359    }
360}