stygian_graph/ports/wasm_plugin.rs
1//! WASM plugin port — dynamic plugin system
2//!
3//! Defines the interface for loading and executing WebAssembly plugins that
4//! implement the [`ScrapingService`] interface. Any language that compiles to
5//! WASM + WASI can be used to write a Stygian plugin.
6//!
7//! # Architecture
8//!
9//! ```text
10//! stygian-graph
11//! └─ WasmPluginPort ← this file
12//! │
13//! └─ WasmPluginLoader (adapters/wasm_plugin.rs)
14//! │
15//! ├─ wasmtime Engine + Component (feature = "wasm-plugins")
16//! └─ WasmScrapingService implements ScrapingService
17//! ```
18//!
19//! # Feature gate
20//!
21//! The adapter that actually loads WASM files requires the `wasm-plugins`
22//! Cargo feature. The *port trait* is always available so application code
23//! can depend on it without pulling in `wasmtime`.
24//!
25//! # Plugin contract
26//!
27//! A WASM plugin must export two functions with these signatures (in WASI
28//! Preview 1 / C ABI style):
29//!
30//! | Export | Signature | Description |
31//! | -------- | ----------- | ------------- |
32//! | `plugin_name` | `() → *const u8` | Null-terminated UTF-8 name |
33//! | `plugin_execute` | `(url_ptr: i32, url_len: i32, params_ptr: i32, params_len: i32, out_ptr: *mut i32) → i32` | Execute and return output length |
34//!
35//! See `examples/wasm-plugin/` for a Rust template.
36
37use crate::domain::error::Result;
38use crate::ports::ScrapingService;
39use async_trait::async_trait;
40use serde::{Deserialize, Serialize};
41use std::path::PathBuf;
42use std::sync::Arc;
43
44// ─────────────────────────────────────────────────────────────────────────────
45// Metadata
46// ─────────────────────────────────────────────────────────────────────────────
47
48/// Static metadata about a loaded WASM plugin.
49///
50/// # Example
51///
52/// ```
53/// use stygian_graph::ports::wasm_plugin::WasmPluginMeta;
54///
55/// let meta = WasmPluginMeta {
56/// name: "my-scraper".to_string(),
57/// version: "0.1.0".to_string(),
58/// description: "Scrapes example.com".to_string(),
59/// path: std::path::PathBuf::from("plugins/my-scraper.wasm"),
60/// };
61/// assert_eq!(meta.name, "my-scraper");
62/// ```
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct WasmPluginMeta {
65 /// Human-readable plugin name (must match the `plugin_name` export)
66 pub name: String,
67 /// Semantic version string
68 pub version: String,
69 /// Short description of what the plugin scrapes
70 pub description: String,
71 /// Path to the `.wasm` file on disk
72 pub path: PathBuf,
73}
74
75// ─────────────────────────────────────────────────────────────────────────────
76// Port trait
77// ─────────────────────────────────────────────────────────────────────────────
78
79/// Port: load and manage WASM scraping plugins.
80///
81/// Implementations are responsible for:
82/// 1. Discovering `.wasm` files in a plugin directory.
83/// 2. Validating that they export the required functions.
84/// 3. Wrapping each plugin as an `Arc<dyn ScrapingService>` for the service
85/// registry.
86/// 4. Hot-reloading when the plugin file changes on disk.
87///
88/// # Example
89///
90/// ```no_run
91/// use stygian_graph::ports::wasm_plugin::WasmPluginPort;
92///
93/// // Implemented by WasmPluginLoader in adapters/wasm_plugin.rs
94/// async fn register_plugins<P: WasmPluginPort>(loader: &P) {
95/// let plugins = loader.discover().await.unwrap();
96/// for (meta, svc) in plugins {
97/// println!("loaded plugin: {}", meta.name);
98/// let _ = svc;
99/// }
100/// }
101/// ```
102#[async_trait]
103pub trait WasmPluginPort: Send + Sync {
104 /// Scan the configured plugin directory and load all `.wasm` files.
105 ///
106 /// Returns `(metadata, service)` pairs — the service can be registered
107 /// directly in a [`ServiceRegistry`].
108 ///
109 /// [`ServiceRegistry`]: crate::application::registry::ServiceRegistry
110 ///
111 /// # Example
112 ///
113 /// ```no_run
114 /// # use stygian_graph::ports::wasm_plugin::WasmPluginPort;
115 /// # async fn example(loader: impl WasmPluginPort) {
116 /// let plugins = loader.discover().await.unwrap();
117 /// println!("{} plugins found", plugins.len());
118 /// # }
119 /// ```
120 async fn discover(&self) -> Result<Vec<(WasmPluginMeta, Arc<dyn ScrapingService>)>>;
121
122 /// Load a single `.wasm` file by path.
123 ///
124 /// # Example
125 ///
126 /// ```no_run
127 /// # use stygian_graph::ports::wasm_plugin::WasmPluginPort;
128 /// # use std::path::PathBuf;
129 /// # async fn example(loader: impl WasmPluginPort) {
130 /// let path = PathBuf::from("plugins/my-scraper.wasm");
131 /// let (meta, _svc) = loader.load(&path).await.unwrap();
132 /// println!("loaded: {} v{}", meta.name, meta.version);
133 /// # }
134 /// ```
135 async fn load(
136 &self,
137 path: &std::path::Path,
138 ) -> Result<(WasmPluginMeta, Arc<dyn ScrapingService>)>;
139
140 /// List metadata for all currently loaded plugins (without reloading).
141 ///
142 /// # Example
143 ///
144 /// ```no_run
145 /// # use stygian_graph::ports::wasm_plugin::WasmPluginPort;
146 /// # async fn example(loader: impl WasmPluginPort) {
147 /// for meta in loader.loaded().await.unwrap() {
148 /// println!("{} v{}", meta.name, meta.version);
149 /// }
150 /// # }
151 /// ```
152 async fn loaded(&self) -> Result<Vec<WasmPluginMeta>>;
153}