use async_trait::async_trait;
use mockforge_plugin_core::{
AuthRequest, AuthResponse, DataQuery, DataResult, PluginContext, PluginError, PluginId,
ResolutionContext, ResponseData, ResponseRequest,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use wasmparser::{Parser, Payload};
use wasmtime::{Engine, Instance, Linker, Module, Store};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuntimeType {
Rust,
TinyGo,
AssemblyScript,
Remote(RemoteRuntimeConfig),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteRuntimeConfig {
pub protocol: RemoteProtocol,
pub endpoint: String,
pub timeout_ms: u64,
pub max_retries: u32,
pub auth: Option<RemoteAuthConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemoteProtocol {
Http,
Grpc,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteAuthConfig {
pub auth_type: String,
pub value: String,
}
#[async_trait]
pub trait RuntimeAdapter: Send + Sync {
fn runtime_type(&self) -> RuntimeType;
async fn initialize(&mut self) -> Result<(), PluginError>;
async fn call_auth(
&self,
context: &PluginContext,
request: &AuthRequest,
) -> Result<AuthResponse, PluginError>;
async fn call_template_function(
&self,
function_name: &str,
args: &[serde_json::Value],
context: &ResolutionContext,
) -> Result<serde_json::Value, PluginError>;
async fn call_response_generator(
&self,
context: &PluginContext,
request: &ResponseRequest,
) -> Result<ResponseData, PluginError>;
async fn call_datasource_query(
&self,
query: &DataQuery,
context: &PluginContext,
) -> Result<DataResult, PluginError>;
async fn health_check(&self) -> Result<bool, PluginError>;
async fn cleanup(&mut self) -> Result<(), PluginError>;
fn get_metrics(&self) -> HashMap<String, serde_json::Value> {
HashMap::new()
}
}
pub fn detect_runtime_type(wasm_bytes: &[u8]) -> Result<RuntimeType, PluginError> {
if has_tinygo_signature(wasm_bytes) {
return Ok(RuntimeType::TinyGo);
}
if has_assemblyscript_signature(wasm_bytes) {
return Ok(RuntimeType::AssemblyScript);
}
Ok(RuntimeType::Rust)
}
fn has_tinygo_signature(wasm_bytes: &[u8]) -> bool {
let (exports, custom_sections) = extract_wasm_signatures(wasm_bytes);
let has_runtime_exports = exports.contains("resume") && exports.contains("getsp");
let has_tinygo_custom = custom_sections.iter().any(|s| s.contains("tinygo"));
has_runtime_exports
|| has_tinygo_custom
|| String::from_utf8_lossy(wasm_bytes).contains("tinygo")
}
fn has_assemblyscript_signature(wasm_bytes: &[u8]) -> bool {
let (exports, custom_sections) = extract_wasm_signatures(wasm_bytes);
let has_alloc_exports =
exports.contains("__new") && (exports.contains("__pin") || exports.contains("__unpin"));
let has_as_custom = custom_sections
.iter()
.any(|s| s.contains("assemblyscript") || s.contains("asc"));
has_alloc_exports
|| has_as_custom
|| String::from_utf8_lossy(wasm_bytes).contains("assemblyscript")
}
fn extract_wasm_signatures(wasm_bytes: &[u8]) -> (std::collections::HashSet<String>, Vec<String>) {
use std::collections::HashSet;
let mut exports = HashSet::new();
let mut custom_sections = Vec::new();
for payload in Parser::new(0).parse_all(wasm_bytes) {
match payload {
Ok(Payload::ExportSection(section)) => {
for export in section.into_iter().flatten() {
exports.insert(export.name.to_string());
}
}
Ok(Payload::CustomSection(section)) => {
custom_sections.push(section.name().to_string());
}
Ok(_) => {}
Err(_) => break,
}
}
(exports, custom_sections)
}
pub struct RuntimeAdapterFactory;
impl RuntimeAdapterFactory {
pub fn create(
runtime_type: RuntimeType,
plugin_id: PluginId,
wasm_bytes: Vec<u8>,
) -> Result<Box<dyn RuntimeAdapter>, PluginError> {
match runtime_type {
RuntimeType::Rust => Ok(Box::new(RustAdapter::new(plugin_id, wasm_bytes)?)),
RuntimeType::TinyGo => Ok(Box::new(TinyGoAdapter::new(plugin_id, wasm_bytes)?)),
RuntimeType::AssemblyScript => {
Ok(Box::new(AssemblyScriptAdapter::new(plugin_id, wasm_bytes)?))
}
RuntimeType::Remote(config) => Ok(Box::new(RemoteAdapter::new(plugin_id, config)?)),
}
}
}
pub struct RustAdapter {
plugin_id: PluginId,
engine: Arc<Engine>,
module: Module,
runtime: Mutex<Option<WasmRuntime>>,
}
struct WasmRuntime {
store: Store<WasiCtx>,
instance: Instance,
}
impl RustAdapter {
pub fn new(plugin_id: PluginId, wasm_bytes: Vec<u8>) -> Result<Self, PluginError> {
let engine = Arc::new(Engine::default());
let module = Module::from_binary(&engine, &wasm_bytes)
.map_err(|e| PluginError::execution(format!("Failed to load WASM module: {}", e)))?;
Ok(Self {
plugin_id,
engine,
module,
runtime: Mutex::new(None),
})
}
fn call_wasm_json(
&self,
function_name: &str,
input_data: serde_json::Value,
) -> Result<serde_json::Value, PluginError> {
let mut runtime_guard =
self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let runtime = runtime_guard.as_mut().ok_or_else(|| {
PluginError::execution("Runtime not initialized. Call initialize() first.".to_string())
})?;
let input_json = serde_json::to_string(&input_data)
.map_err(|e| PluginError::execution(format!("Failed to serialize input: {}", e)))?;
let input_bytes = input_json.as_bytes();
let input_len = input_bytes.len() as i32;
let memory =
runtime.instance.get_memory(&mut runtime.store, "memory").ok_or_else(|| {
PluginError::execution("WASM module must export 'memory'".to_string())
})?;
let alloc_func = runtime
.instance
.get_typed_func::<i32, i32>(&mut runtime.store, "alloc")
.map_err(|e| {
PluginError::execution(format!("Failed to get alloc function: {}", e))
})?;
let input_ptr = alloc_func
.call(&mut runtime.store, input_len)
.map_err(|e| PluginError::execution(format!("Failed to allocate memory: {}", e)))?;
memory
.write(&mut runtime.store, input_ptr as usize, input_bytes)
.map_err(|e| PluginError::execution(format!("Failed to write input: {}", e)))?;
let plugin_func = runtime
.instance
.get_typed_func::<(i32, i32), (i32, i32)>(&mut runtime.store, function_name)
.map_err(|e| {
PluginError::execution(format!("Function '{}' not found: {}", function_name, e))
})?;
let (output_ptr, output_len) =
plugin_func.call(&mut runtime.store, (input_ptr, input_len)).map_err(|e| {
PluginError::execution(format!(
"Failed to call function '{}': {}",
function_name, e
))
})?;
let mut output_bytes = vec![0u8; output_len as usize];
memory
.read(&runtime.store, output_ptr as usize, &mut output_bytes)
.map_err(|e| PluginError::execution(format!("Failed to read output: {}", e)))?;
if let Ok(dealloc_func) =
runtime.instance.get_typed_func::<(i32, i32), ()>(&mut runtime.store, "dealloc")
{
let _ = dealloc_func.call(&mut runtime.store, (input_ptr, input_len));
let _ = dealloc_func.call(&mut runtime.store, (output_ptr, output_len));
}
let output_str = String::from_utf8(output_bytes)
.map_err(|e| PluginError::execution(format!("Failed to decode output: {}", e)))?;
serde_json::from_str(&output_str)
.map_err(|e| PluginError::execution(format!("Failed to parse output JSON: {}", e)))
}
}
#[async_trait]
impl RuntimeAdapter for RustAdapter {
fn runtime_type(&self) -> RuntimeType {
RuntimeType::Rust
}
async fn initialize(&mut self) -> Result<(), PluginError> {
tracing::info!("Initializing Rust plugin: {}", self.plugin_id);
let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
let mut store = Store::new(&self.engine, wasi_ctx);
let linker = Linker::new(&self.engine);
let instance = linker
.instantiate(&mut store, &self.module)
.map_err(|e| PluginError::execution(format!("Failed to instantiate module: {}", e)))?;
let mut runtime_guard =
self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
*runtime_guard = Some(WasmRuntime { store, instance });
tracing::info!("Successfully initialized Rust plugin: {}", self.plugin_id);
Ok(())
}
async fn call_auth(
&self,
context: &PluginContext,
request: &AuthRequest,
) -> Result<AuthResponse, PluginError> {
let input = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri.to_string(),
"query_params": request.query_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_wasm_json("authenticate", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
}
async fn call_template_function(
&self,
function_name: &str,
args: &[serde_json::Value],
context: &ResolutionContext,
) -> Result<serde_json::Value, PluginError> {
let input = serde_json::json!({
"function_name": function_name,
"args": args,
"context": context,
});
self.call_wasm_json("template_function", input)
}
async fn call_response_generator(
&self,
context: &PluginContext,
request: &ResponseRequest,
) -> Result<ResponseData, PluginError> {
let input = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri,
"path": request.path,
"query_params": request.query_params,
"path_params": request.path_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_wasm_json("generate_response", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
}
async fn call_datasource_query(
&self,
query: &DataQuery,
context: &PluginContext,
) -> Result<DataResult, PluginError> {
let input = serde_json::json!({
"query_type": format!("{:?}", query.query_type),
"query": query.query,
"parameters": query.parameters,
"limit": query.limit,
"offset": query.offset,
"context": context,
});
let result = self.call_wasm_json("query_datasource", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
}
async fn health_check(&self) -> Result<bool, PluginError> {
Ok(true)
}
async fn cleanup(&mut self) -> Result<(), PluginError> {
Ok(())
}
}
pub struct TinyGoAdapter {
plugin_id: PluginId,
engine: Arc<Engine>,
module: Module,
runtime: Mutex<Option<WasmRuntime>>,
}
impl TinyGoAdapter {
pub fn new(plugin_id: PluginId, wasm_bytes: Vec<u8>) -> Result<Self, PluginError> {
let engine = Arc::new(Engine::default());
let module = Module::from_binary(&engine, &wasm_bytes).map_err(|e| {
PluginError::execution(format!("Failed to load TinyGo WASM module: {}", e))
})?;
Ok(Self {
plugin_id,
engine,
module,
runtime: Mutex::new(None),
})
}
fn call_wasm_json(
&self,
function_name: &str,
input_data: serde_json::Value,
) -> Result<serde_json::Value, PluginError> {
let mut runtime_guard =
self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let runtime = runtime_guard.as_mut().ok_or_else(|| {
PluginError::execution("Runtime not initialized. Call initialize() first.".to_string())
})?;
let input_json = serde_json::to_string(&input_data)
.map_err(|e| PluginError::execution(format!("Failed to serialize input: {}", e)))?;
let input_bytes = input_json.as_bytes();
let input_len = input_bytes.len() as i32;
let memory =
runtime.instance.get_memory(&mut runtime.store, "memory").ok_or_else(|| {
PluginError::execution("TinyGo WASM module must export 'memory'".to_string())
})?;
let malloc_func = runtime
.instance
.get_typed_func::<i32, i32>(&mut runtime.store, "malloc")
.map_err(|e| {
PluginError::execution(format!(
"Failed to get malloc function (TinyGo specific): {}",
e
))
})?;
let input_ptr = malloc_func
.call(&mut runtime.store, input_len)
.map_err(|e| PluginError::execution(format!("Failed to allocate memory: {}", e)))?;
memory
.write(&mut runtime.store, input_ptr as usize, input_bytes)
.map_err(|e| PluginError::execution(format!("Failed to write input: {}", e)))?;
let plugin_func = runtime
.instance
.get_typed_func::<(i32, i32), (i32, i32)>(&mut runtime.store, function_name)
.map_err(|e| {
PluginError::execution(format!("Function '{}' not found: {}", function_name, e))
})?;
let (output_ptr, output_len) =
plugin_func.call(&mut runtime.store, (input_ptr, input_len)).map_err(|e| {
PluginError::execution(format!(
"Failed to call function '{}': {}",
function_name, e
))
})?;
let mut output_bytes = vec![0u8; output_len as usize];
memory
.read(&runtime.store, output_ptr as usize, &mut output_bytes)
.map_err(|e| PluginError::execution(format!("Failed to read output: {}", e)))?;
if let Ok(free_func) =
runtime.instance.get_typed_func::<i32, ()>(&mut runtime.store, "free")
{
let _ = free_func.call(&mut runtime.store, input_ptr);
let _ = free_func.call(&mut runtime.store, output_ptr);
}
let output_str = String::from_utf8(output_bytes)
.map_err(|e| PluginError::execution(format!("Failed to decode output: {}", e)))?;
serde_json::from_str(&output_str)
.map_err(|e| PluginError::execution(format!("Failed to parse output JSON: {}", e)))
}
}
#[async_trait]
impl RuntimeAdapter for TinyGoAdapter {
fn runtime_type(&self) -> RuntimeType {
RuntimeType::TinyGo
}
async fn initialize(&mut self) -> Result<(), PluginError> {
tracing::info!("Initializing TinyGo plugin: {}", self.plugin_id);
let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
let mut store = Store::new(&self.engine, wasi_ctx);
let linker = Linker::new(&self.engine);
let instance = linker.instantiate(&mut store, &self.module).map_err(|e| {
PluginError::execution(format!("Failed to instantiate TinyGo module: {}", e))
})?;
let mut runtime_guard =
self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
*runtime_guard = Some(WasmRuntime { store, instance });
tracing::info!("Successfully initialized TinyGo plugin: {}", self.plugin_id);
Ok(())
}
async fn call_auth(
&self,
context: &PluginContext,
request: &AuthRequest,
) -> Result<AuthResponse, PluginError> {
let input = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri.to_string(),
"query_params": request.query_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_wasm_json("authenticate", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
}
async fn call_template_function(
&self,
function_name: &str,
args: &[serde_json::Value],
context: &ResolutionContext,
) -> Result<serde_json::Value, PluginError> {
let input = serde_json::json!({
"function_name": function_name,
"args": args,
"context": context,
});
self.call_wasm_json("template_function", input)
}
async fn call_response_generator(
&self,
context: &PluginContext,
request: &ResponseRequest,
) -> Result<ResponseData, PluginError> {
let input = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri,
"path": request.path,
"query_params": request.query_params,
"path_params": request.path_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_wasm_json("generate_response", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
}
async fn call_datasource_query(
&self,
query: &DataQuery,
context: &PluginContext,
) -> Result<DataResult, PluginError> {
let input = serde_json::json!({
"query_type": format!("{:?}", query.query_type),
"query": query.query,
"parameters": query.parameters,
"limit": query.limit,
"offset": query.offset,
"context": context,
});
let result = self.call_wasm_json("query_datasource", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
}
async fn health_check(&self) -> Result<bool, PluginError> {
Ok(true)
}
async fn cleanup(&mut self) -> Result<(), PluginError> {
Ok(())
}
}
pub struct AssemblyScriptAdapter {
plugin_id: PluginId,
engine: Arc<Engine>,
module: Module,
runtime: Mutex<Option<WasmRuntime>>,
}
impl AssemblyScriptAdapter {
pub fn new(plugin_id: PluginId, wasm_bytes: Vec<u8>) -> Result<Self, PluginError> {
let engine = Arc::new(Engine::default());
let module = Module::from_binary(&engine, &wasm_bytes).map_err(|e| {
PluginError::execution(format!("Failed to load AssemblyScript WASM module: {}", e))
})?;
Ok(Self {
plugin_id,
engine,
module,
runtime: Mutex::new(None),
})
}
fn call_wasm_json(
&self,
function_name: &str,
input_data: serde_json::Value,
) -> Result<serde_json::Value, PluginError> {
let mut runtime_guard =
self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let runtime = runtime_guard.as_mut().ok_or_else(|| {
PluginError::execution("Runtime not initialized. Call initialize() first.".to_string())
})?;
let input_json = serde_json::to_string(&input_data)
.map_err(|e| PluginError::execution(format!("Failed to serialize input: {}", e)))?;
let input_bytes = input_json.as_bytes();
let input_len = input_bytes.len() as i32;
let memory =
runtime.instance.get_memory(&mut runtime.store, "memory").ok_or_else(|| {
PluginError::execution(
"AssemblyScript WASM module must export 'memory'".to_string(),
)
})?;
let new_func = runtime
.instance
.get_typed_func::<(i32, i32), i32>(&mut runtime.store, "__new")
.map_err(|e| {
PluginError::execution(format!(
"Failed to get __new function (AssemblyScript specific): {}",
e
))
})?;
let input_ptr = new_func
.call(&mut runtime.store, (input_len, 1))
.map_err(|e| PluginError::execution(format!("Failed to allocate memory: {}", e)))?;
if let Ok(pin_func) =
runtime.instance.get_typed_func::<i32, i32>(&mut runtime.store, "__pin")
{
let _ = pin_func.call(&mut runtime.store, input_ptr);
}
memory
.write(&mut runtime.store, input_ptr as usize, input_bytes)
.map_err(|e| PluginError::execution(format!("Failed to write input: {}", e)))?;
let plugin_func = runtime
.instance
.get_typed_func::<(i32, i32), (i32, i32)>(&mut runtime.store, function_name)
.map_err(|e| {
PluginError::execution(format!("Function '{}' not found: {}", function_name, e))
})?;
let (output_ptr, output_len) =
plugin_func.call(&mut runtime.store, (input_ptr, input_len)).map_err(|e| {
PluginError::execution(format!(
"Failed to call function '{}': {}",
function_name, e
))
})?;
let mut output_bytes = vec![0u8; output_len as usize];
memory
.read(&runtime.store, output_ptr as usize, &mut output_bytes)
.map_err(|e| PluginError::execution(format!("Failed to read output: {}", e)))?;
if let Ok(unpin_func) =
runtime.instance.get_typed_func::<i32, ()>(&mut runtime.store, "__unpin")
{
let _ = unpin_func.call(&mut runtime.store, input_ptr);
let _ = unpin_func.call(&mut runtime.store, output_ptr);
}
let output_str = String::from_utf8(output_bytes)
.map_err(|e| PluginError::execution(format!("Failed to decode output: {}", e)))?;
serde_json::from_str(&output_str)
.map_err(|e| PluginError::execution(format!("Failed to parse output JSON: {}", e)))
}
}
#[async_trait]
impl RuntimeAdapter for AssemblyScriptAdapter {
fn runtime_type(&self) -> RuntimeType {
RuntimeType::AssemblyScript
}
async fn initialize(&mut self) -> Result<(), PluginError> {
tracing::info!("Initializing AssemblyScript plugin: {}", self.plugin_id);
let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
let mut store = Store::new(&self.engine, wasi_ctx);
let linker = Linker::new(&self.engine);
let instance = linker.instantiate(&mut store, &self.module).map_err(|e| {
PluginError::execution(format!("Failed to instantiate AssemblyScript module: {}", e))
})?;
let mut runtime_guard =
self.runtime.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
*runtime_guard = Some(WasmRuntime { store, instance });
tracing::info!("Successfully initialized AssemblyScript plugin: {}", self.plugin_id);
Ok(())
}
async fn call_auth(
&self,
context: &PluginContext,
request: &AuthRequest,
) -> Result<AuthResponse, PluginError> {
let input = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri.to_string(),
"query_params": request.query_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_wasm_json("authenticate", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
}
async fn call_template_function(
&self,
function_name: &str,
args: &[serde_json::Value],
context: &ResolutionContext,
) -> Result<serde_json::Value, PluginError> {
let input = serde_json::json!({
"function_name": function_name,
"args": args,
"context": context,
});
self.call_wasm_json("template_function", input)
}
async fn call_response_generator(
&self,
context: &PluginContext,
request: &ResponseRequest,
) -> Result<ResponseData, PluginError> {
let input = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri,
"path": request.path,
"query_params": request.query_params,
"path_params": request.path_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_wasm_json("generate_response", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
}
async fn call_datasource_query(
&self,
query: &DataQuery,
context: &PluginContext,
) -> Result<DataResult, PluginError> {
let input = serde_json::json!({
"query_type": format!("{:?}", query.query_type),
"query": query.query,
"parameters": query.parameters,
"limit": query.limit,
"offset": query.offset,
"context": context,
});
let result = self.call_wasm_json("query_datasource", input)?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
}
async fn health_check(&self) -> Result<bool, PluginError> {
Ok(true)
}
async fn cleanup(&mut self) -> Result<(), PluginError> {
Ok(())
}
}
pub struct RemoteAdapter {
plugin_id: PluginId,
config: RemoteRuntimeConfig,
client: reqwest::Client,
}
impl RemoteAdapter {
pub fn new(plugin_id: PluginId, config: RemoteRuntimeConfig) -> Result<Self, PluginError> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(config.timeout_ms))
.build()
.map_err(|e| PluginError::execution(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
plugin_id,
config,
client,
})
}
async fn call_remote_plugin(
&self,
endpoint: &str,
body: serde_json::Value,
) -> Result<serde_json::Value, PluginError> {
let url = format!("{}{}", self.config.endpoint, endpoint);
let mut request = self.client.post(&url).json(&body);
if let Some(auth) = &self.config.auth {
request = match auth.auth_type.as_str() {
"bearer" => request.bearer_auth(&auth.value),
"api_key" => request.header("X-API-Key", &auth.value),
_ => request,
};
}
let response = request
.send()
.await
.map_err(|e| PluginError::execution(format!("Remote plugin call failed: {}", e)))?;
if !response.status().is_success() {
return Err(PluginError::execution(format!(
"Remote plugin returned error status: {}",
response.status()
)));
}
let result: serde_json::Value = response
.json()
.await
.map_err(|e| PluginError::execution(format!("Failed to parse response: {}", e)))?;
Ok(result)
}
}
#[async_trait]
impl RuntimeAdapter for RemoteAdapter {
fn runtime_type(&self) -> RuntimeType {
RuntimeType::Remote(self.config.clone())
}
async fn initialize(&mut self) -> Result<(), PluginError> {
tracing::info!("Initializing remote plugin: {}", self.plugin_id);
self.health_check().await?;
Ok(())
}
async fn call_auth(
&self,
context: &PluginContext,
request: &AuthRequest,
) -> Result<AuthResponse, PluginError> {
let body = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri.to_string(),
"query_params": request.query_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_remote_plugin("/plugin/authenticate", body).await?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse AuthResponse: {}", e)))
}
async fn call_template_function(
&self,
function_name: &str,
args: &[serde_json::Value],
context: &ResolutionContext,
) -> Result<serde_json::Value, PluginError> {
let body = serde_json::json!({
"function_name": function_name,
"args": args,
"context": context,
});
self.call_remote_plugin("/plugin/template/execute", body).await
}
async fn call_response_generator(
&self,
context: &PluginContext,
request: &ResponseRequest,
) -> Result<ResponseData, PluginError> {
let body = serde_json::json!({
"context": context,
"method": request.method.to_string(),
"uri": request.uri,
"path": request.path,
"query_params": request.query_params,
"path_params": request.path_params,
"client_ip": request.client_ip,
"user_agent": request.user_agent,
});
let result = self.call_remote_plugin("/plugin/response/generate", body).await?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse ResponseData: {}", e)))
}
async fn call_datasource_query(
&self,
query: &DataQuery,
context: &PluginContext,
) -> Result<DataResult, PluginError> {
let body = serde_json::json!({
"query_type": format!("{:?}", query.query_type),
"query": query.query,
"parameters": query.parameters,
"limit": query.limit,
"offset": query.offset,
"context": context,
});
let result = self.call_remote_plugin("/plugin/datasource/query", body).await?;
serde_json::from_value(result)
.map_err(|e| PluginError::execution(format!("Failed to parse DataResult: {}", e)))
}
async fn health_check(&self) -> Result<bool, PluginError> {
let url = format!("{}/health", self.config.endpoint);
match self.client.get(&url).send().await {
Ok(response) => Ok(response.status().is_success()),
Err(_) => Ok(false),
}
}
async fn cleanup(&mut self) -> Result<(), PluginError> {
tracing::info!("Cleaning up remote plugin: {}", self.plugin_id);
Ok(())
}
fn get_metrics(&self) -> HashMap<String, serde_json::Value> {
let mut metrics = HashMap::new();
metrics.insert("plugin_id".to_string(), serde_json::json!(self.plugin_id.as_str()));
metrics.insert("endpoint".to_string(), serde_json::json!(self.config.endpoint));
metrics.insert(
"protocol".to_string(),
serde_json::json!(format!("{:?}", self.config.protocol)),
);
metrics
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runtime_type_equality() {
assert_eq!(RuntimeType::Rust, RuntimeType::Rust);
assert_eq!(RuntimeType::TinyGo, RuntimeType::TinyGo);
assert_eq!(RuntimeType::AssemblyScript, RuntimeType::AssemblyScript);
assert_ne!(RuntimeType::Rust, RuntimeType::TinyGo);
}
#[test]
fn test_runtime_type_clone() {
let rt = RuntimeType::Rust;
let cloned = rt.clone();
assert_eq!(rt, cloned);
}
#[test]
fn test_runtime_type_detection() {
let empty_bytes = vec![];
let runtime = detect_runtime_type(&empty_bytes).unwrap();
assert_eq!(runtime, RuntimeType::Rust);
}
#[test]
fn test_runtime_type_detection_tinygo() {
let tinygo_bytes = b"wasm module with tinygo runtime".to_vec();
let runtime = detect_runtime_type(&tinygo_bytes).unwrap();
assert_eq!(runtime, RuntimeType::TinyGo);
}
#[test]
fn test_runtime_type_detection_assemblyscript() {
let as_bytes = b"wasm module with assemblyscript runtime".to_vec();
let runtime = detect_runtime_type(&as_bytes).unwrap();
assert_eq!(runtime, RuntimeType::AssemblyScript);
}
#[test]
fn test_has_tinygo_signature() {
assert!(has_tinygo_signature(b"this contains tinygo"));
assert!(!has_tinygo_signature(b"this does not contain it"));
}
#[test]
fn test_has_assemblyscript_signature() {
assert!(has_assemblyscript_signature(b"this contains assemblyscript"));
assert!(!has_assemblyscript_signature(b"this does not contain it"));
}
#[test]
fn test_remote_runtime_config() {
let config = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: Some(RemoteAuthConfig {
auth_type: "bearer".to_string(),
value: "secret-token".to_string(),
}),
};
assert_eq!(config.endpoint, "http://localhost:8080");
assert_eq!(config.timeout_ms, 5000);
assert_eq!(config.max_retries, 3);
assert!(config.auth.is_some());
}
#[test]
fn test_remote_runtime_config_without_auth() {
let config = RemoteRuntimeConfig {
protocol: RemoteProtocol::Grpc,
endpoint: "grpc://localhost:9090".to_string(),
timeout_ms: 10000,
max_retries: 5,
auth: None,
};
assert_eq!(config.protocol, RemoteProtocol::Grpc);
assert!(config.auth.is_none());
}
#[test]
fn test_remote_runtime_config_clone() {
let config = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: None,
};
let cloned = config.clone();
assert_eq!(config.endpoint, cloned.endpoint);
assert_eq!(config.timeout_ms, cloned.timeout_ms);
}
#[test]
fn test_remote_runtime_config_equality() {
let config1 = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: None,
};
let config2 = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: None,
};
assert_eq!(config1, config2);
}
#[test]
fn test_remote_protocol_equality() {
assert_eq!(RemoteProtocol::Http, RemoteProtocol::Http);
assert_eq!(RemoteProtocol::Grpc, RemoteProtocol::Grpc);
assert_ne!(RemoteProtocol::Http, RemoteProtocol::Grpc);
}
#[test]
fn test_remote_protocol_clone() {
let proto = RemoteProtocol::Http;
let cloned = proto.clone();
assert_eq!(proto, cloned);
}
#[test]
fn test_remote_auth_config() {
let auth = RemoteAuthConfig {
auth_type: "bearer".to_string(),
value: "secret-token".to_string(),
};
assert_eq!(auth.auth_type, "bearer");
assert_eq!(auth.value, "secret-token");
}
#[test]
fn test_remote_auth_config_api_key() {
let auth = RemoteAuthConfig {
auth_type: "api_key".to_string(),
value: "my-api-key".to_string(),
};
assert_eq!(auth.auth_type, "api_key");
}
#[test]
fn test_remote_auth_config_clone() {
let auth = RemoteAuthConfig {
auth_type: "bearer".to_string(),
value: "token".to_string(),
};
let cloned = auth.clone();
assert_eq!(auth.auth_type, cloned.auth_type);
assert_eq!(auth.value, cloned.value);
}
#[test]
fn test_remote_auth_config_equality() {
let auth1 = RemoteAuthConfig {
auth_type: "bearer".to_string(),
value: "token".to_string(),
};
let auth2 = RemoteAuthConfig {
auth_type: "bearer".to_string(),
value: "token".to_string(),
};
assert_eq!(auth1, auth2);
}
#[test]
fn test_factory_create_rust_adapter() {
let plugin_id = PluginId::new("test-plugin");
let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
let result = RuntimeAdapterFactory::create(RuntimeType::Rust, plugin_id, wasm_bytes);
assert!(result.is_ok());
let adapter = result.unwrap();
assert_eq!(adapter.runtime_type(), RuntimeType::Rust);
}
#[test]
fn test_factory_create_tinygo_adapter() {
let plugin_id = PluginId::new("test-plugin");
let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
let result = RuntimeAdapterFactory::create(RuntimeType::TinyGo, plugin_id, wasm_bytes);
assert!(result.is_ok());
let adapter = result.unwrap();
assert_eq!(adapter.runtime_type(), RuntimeType::TinyGo);
}
#[test]
fn test_factory_create_assemblyscript_adapter() {
let plugin_id = PluginId::new("test-plugin");
let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
let result =
RuntimeAdapterFactory::create(RuntimeType::AssemblyScript, plugin_id, wasm_bytes);
assert!(result.is_ok());
let adapter = result.unwrap();
assert_eq!(adapter.runtime_type(), RuntimeType::AssemblyScript);
}
#[test]
fn test_factory_create_remote_adapter() {
let plugin_id = PluginId::new("test-plugin");
let config = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: None,
};
let result =
RuntimeAdapterFactory::create(RuntimeType::Remote(config.clone()), plugin_id, vec![]);
assert!(result.is_ok());
let adapter = result.unwrap();
assert_eq!(adapter.runtime_type(), RuntimeType::Remote(config));
}
#[test]
fn test_factory_create_with_invalid_wasm() {
let plugin_id = PluginId::new("test-plugin");
let wasm_bytes = vec![0x00, 0x00, 0x00, 0x00];
let result = RuntimeAdapterFactory::create(RuntimeType::Rust, plugin_id, wasm_bytes);
assert!(result.is_err());
}
#[test]
fn test_remote_adapter_creation() {
let plugin_id = PluginId::new("test-plugin");
let config = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: None,
};
let result = RemoteAdapter::new(plugin_id, config.clone());
assert!(result.is_ok());
let adapter = result.unwrap();
assert_eq!(adapter.runtime_type(), RuntimeType::Remote(config));
}
#[test]
fn test_remote_adapter_get_metrics() {
let plugin_id = PluginId::new("test-plugin");
let config = RemoteRuntimeConfig {
protocol: RemoteProtocol::Http,
endpoint: "http://localhost:8080".to_string(),
timeout_ms: 5000,
max_retries: 3,
auth: None,
};
let adapter = RemoteAdapter::new(plugin_id.clone(), config).unwrap();
let metrics = adapter.get_metrics();
assert!(metrics.contains_key("plugin_id"));
assert!(metrics.contains_key("endpoint"));
assert!(metrics.contains_key("protocol"));
assert_eq!(metrics.get("plugin_id").unwrap(), &serde_json::json!(plugin_id.as_str()));
}
#[test]
fn test_runtime_type_with_mixed_signatures() {
let mixed_bytes = b"tinygo and assemblyscript".to_vec();
let runtime = detect_runtime_type(&mixed_bytes).unwrap();
assert_eq!(runtime, RuntimeType::TinyGo);
}
#[test]
fn test_empty_wasm_bytes() {
let plugin_id = PluginId::new("test");
let result = RustAdapter::new(plugin_id, vec![]);
assert!(result.is_err());
}
#[test]
fn test_runtime_type_debug() {
let rt = RuntimeType::Rust;
let debug_str = format!("{:?}", rt);
assert!(debug_str.contains("Rust"));
}
#[test]
fn test_remote_protocol_debug() {
let proto = RemoteProtocol::Http;
let debug_str = format!("{:?}", proto);
assert!(debug_str.contains("Http"));
}
}