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}