feature (plugin): make rust runtime plugin testable

This commit is contained in:
MickaelK
2026-09-05 01:26:22 +10:00
parent d2d21a06c7
commit 78f756eb94
14 changed files with 138 additions and 61 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ func (in *Instance) Http() func(*mux.Router) error {
}
router.HandleFunc(rt.path, middleware.NewMiddlewareChain(
func(app *App, w http.ResponseWriter, r *http.Request) {
if err := in.rt.Call(r.Context(), "http", httpKey{}, &httpData{r: r, w: w}); err != nil {
if err := in.rt.Call(r.Context(), "http", httpKey{}, &httpData{r: r, w: w, ctx: app}); err != nil {
utils.Log.Error("extension::adapter http path=%s err=%s", r.URL.Path, err.Error())
}
},
+19
View File
@@ -2,8 +2,10 @@ package adapter
import (
"context"
"encoding/json"
"net/http"
. "github.com/mickael-kerjean/filestash/server/pkg/core"
"github.com/mickael-kerjean/filestash/server/pkg/extension/adapter/runtime"
"github.com/mickael-kerjean/filestash/server/pkg/utils"
)
@@ -41,6 +43,7 @@ type appKey struct{}
type httpKey struct{}
type httpData struct {
ctx *App
r *http.Request
w http.ResponseWriter
next bool
@@ -79,5 +82,21 @@ func exportShared(b *runtime.HostModuleBuilder) {
return mem.WriteString(outPtr, outCap, stateHttp(ctx).r.URL.Path)
}).Export("req_header", func(ctx context.Context, mem runtime.IMemory, nPtr, nLen, outPtr, outCap uint32) uint32 {
return mem.WriteString(outPtr, outCap, stateHttp(ctx).r.Header.Get(string(mem.Read(nPtr, nLen))))
}).Export("req_url_query", func(ctx context.Context, mem runtime.IMemory, nPtr, nLen, outPtr, outCap uint32) uint32 {
return mem.WriteString(outPtr, outCap, stateHttp(ctx).r.URL.Query().Get(string(mem.Read(nPtr, nLen))))
}).Export("app_body", func(ctx context.Context, mem runtime.IMemory, outPtr, outCap uint32) uint32 {
body := stateHttp(ctx).ctx.Body
byt, err := json.Marshal(body)
if err != nil || body == nil {
return mem.WriteString(outPtr, outCap, "{}")
}
return mem.Write(outPtr, outCap, byt)
}).Export("app_session", func(ctx context.Context, mem runtime.IMemory, outPtr, outCap uint32) uint32 {
session := stateHttp(ctx).ctx.Session
byt, err := json.Marshal(session)
if err != nil || session == nil {
return mem.WriteString(outPtr, outCap, "{}")
}
return mem.Write(outPtr, outCap, byt)
})
}
+2 -1
View File
@@ -1 +1,2 @@
target
target
Cargo.lock
-16
View File
@@ -1,16 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "filestash"
version = "0.1.0"
dependencies = [
"log",
]
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+4
View File
@@ -5,3 +5,7 @@ edition = "2021"
[dependencies]
log = "0.4"
mockall = { version = "0.13", optional = true }
[features]
mock = ["dep:mockall"]
+13 -13
View File
@@ -21,43 +21,43 @@ macro_rules! register {
#[no_mangle]
pub extern "C" fn authorisation_ls() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::ls(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::ls(app, &$crate::ContextImpl, &path)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_cat() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::cat(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::cat(app, &$crate::ContextImpl, &path)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_stat() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::stat(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::stat(app, &$crate::ContextImpl, &path)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_mkdir() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::mkdir(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::mkdir(app, &$crate::ContextImpl, &path)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_rm() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::rm(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::rm(app, &$crate::ContextImpl, &path)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_mv() {
let from = $crate::authorisation::authorisation_pull_path();
let to = $crate::authorisation::authorisation_pull_target();
$anchor.with(|app| <$app as $crate::Authorisation>::mv(app, &$crate::Context, &from, &to)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::mv(app, &$crate::ContextImpl, &from, &to)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_save() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::save(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::save(app, &$crate::ContextImpl, &path)).apply();
}
#[no_mangle]
pub extern "C" fn authorisation_touch() {
let path = $crate::authorisation::authorisation_pull_path();
$anchor.with(|app| <$app as $crate::Authorisation>::touch(app, &$crate::Context, &path)).apply();
$anchor.with(|app| <$app as $crate::Authorisation>::touch(app, &$crate::ContextImpl, &path)).apply();
}
};
@@ -72,12 +72,12 @@ macro_rules! register {
}
#[no_mangle]
pub extern "C" fn http() {
let method = $crate::Request.method();
let path = $crate::Request.path();
let method = $crate::RequestImpl.method();
let path = $crate::RequestImpl.path();
let mut router = $crate::Router::new();
<$app as $crate::Http>::routes(&mut router);
let mut res = $crate::Response;
$anchor.with(|app| router.dispatch(app, &method, &path, &$crate::Request, &mut res));
let mut res = $crate::ResponseImpl;
$anchor.with(|app| router.dispatch(app, &method, &path, &$crate::RequestImpl, &mut res));
}
};
@@ -115,7 +115,7 @@ macro_rules! register {
pub extern "C" fn middleware() {
let mut res = $crate::Response;
$anchor
.with(|app| <$app as $crate::Middleware>::handle(app, &$crate::Request, &mut res))
.with(|app| <$app as $crate::Middleware>::handle(app, &$crate::RequestImpl, &mut res))
.apply();
}
};
@@ -29,28 +29,28 @@ impl Decision {
}
pub trait Authorisation {
fn ls(&self, _ctx: &Context, _path: &str) -> Decision {
fn ls(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
fn cat(&self, _ctx: &Context, _path: &str) -> Decision {
fn cat(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
fn stat(&self, _ctx: &Context, _path: &str) -> Decision {
fn stat(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
fn mkdir(&self, _ctx: &Context, _path: &str) -> Decision {
fn mkdir(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
fn rm(&self, _ctx: &Context, _path: &str) -> Decision {
fn rm(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
fn mv(&self, _ctx: &Context, _from: &str, _to: &str) -> Decision {
fn mv(&self, _ctx: &impl Context, _from: &str, _to: &str) -> Decision {
Decision::Deny
}
fn save(&self, _ctx: &Context, _path: &str) -> Decision {
fn save(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
fn touch(&self, _ctx: &Context, _path: &str) -> Decision {
fn touch(&self, _ctx: &impl Context, _path: &str) -> Decision {
Decision::Deny
}
}
+20 -1
View File
@@ -1 +1,20 @@
pub struct Context;
use crate::{read, ffi};
#[derive(Default)]
pub struct ContextImpl;
#[cfg_attr(feature = "mock", mockall::automock)]
pub trait Context {
fn body(&self) -> Vec<u8>;
fn session(&self) -> Vec<u8>;
}
impl Context for ContextImpl {
fn body(&self) -> Vec<u8> {
read(|ptr, cap| unsafe { ffi::app_body(ptr, cap) }).into_bytes()
}
fn session(&self) -> Vec<u8> {
read(|ptr, cap| unsafe { ffi::app_session(ptr, cap) }).into_bytes()
}
}
+4
View File
@@ -3,10 +3,14 @@ extern "C" {
pub fn req_method(out_ptr: *mut u8, out_cap: u32) -> u32;
pub fn req_path(out_ptr: *mut u8, out_cap: u32) -> u32;
pub fn req_header(name_ptr: *const u8, name_len: u32, out_ptr: *mut u8, out_cap: u32) -> u32;
pub fn req_url_query(key_ptr: *const u8, key_len: u32, out_ptr: *mut u8, out_cap: u32) -> u32;
pub fn resp_status(code: u32);
pub fn resp_header(key: *const u8, key_len: u32, value: *const u8, value_len: u32);
pub fn resp_write(ptr: *const u8, len: u32);
pub fn app_body(out_ptr: *mut u8, out_cap: u32) -> u32;
pub fn app_session(out_ptr: *mut u8, out_cap: u32) -> u32;
pub fn log(level: u32, ptr: *const u8, len: u32);
}
+21 -4
View File
@@ -1,4 +1,4 @@
use crate::{Request, Response};
use crate::{RequestImpl, Response, ResponseImpl, ContextImpl};
#[link(wasm_import_module = "env")]
extern "C" {
@@ -10,7 +10,7 @@ extern "C" {
);
}
pub type Handler<A> = fn(&A, &Request, &mut Response);
pub type Handler<A> = fn(&A, &ContextImpl, &RequestImpl, &mut ResponseImpl);
struct Route<A> {
method: &'static str,
@@ -28,6 +28,10 @@ impl<A> Router<A> {
Router { routes: Vec::new() }
}
pub fn head(&mut self, path: &'static str, middleware: &'static [&'static str], handler: Handler<A>) -> &mut Self {
self.route("HEAD", path, middleware, handler)
}
pub fn get(&mut self, path: &'static str, middleware: &'static [&'static str], handler: Handler<A>) -> &mut Self {
self.route("GET", path, middleware, handler)
}
@@ -36,14 +40,27 @@ impl<A> Router<A> {
self.route("PUT", path, middleware, handler)
}
pub fn dispatch(&self, app: &A, method: &str, path: &str, req: &Request, res: &mut Response) {
pub fn post(&mut self, path: &'static str, middleware: &'static [&'static str], handler: Handler<A>) -> &mut Self {
self.route("POST", path, middleware, handler)
}
pub fn delete(&mut self, path: &'static str, middleware: &'static [&'static str], handler: Handler<A>) -> &mut Self {
self.route("DELETE", path, middleware, handler)
}
pub fn patch(&mut self, path: &'static str, middleware: &'static [&'static str], handler: Handler<A>) -> &mut Self {
self.route("PATCH", path, middleware, handler)
}
pub fn dispatch(&self, app: &A, method: &str, path: &str, req: &RequestImpl, res: &mut ResponseImpl) {
for route in &self.routes {
if route.method == method && route.path == path {
(route.handler)(app, req, res);
(route.handler)(app, &ContextImpl{}, req, res);
return;
}
}
res.status(404);
res.write(b"{\"status\": \"error\", \"message\": \"not_found\"}");
}
pub fn describe(&self) {
+10 -3
View File
@@ -10,12 +10,12 @@ mod request;
mod response;
pub use authorisation::{Authorisation, Decision};
pub use context::Context;
pub use http::{Handler, Http, Router};
pub use lifecycle::{Lifecycle, OnChanges, OnDestroy, OnInit};
pub use middleware::{Middleware, Next};
pub use request::Request;
pub use response::Response;
pub use request::{Request, RequestImpl};
pub use response::{Response, ResponseImpl};
pub use context::{Context, ContextImpl};
pub use log;
@@ -25,3 +25,10 @@ pub(crate) fn read(call: impl Fn(*mut u8, u32) -> u32) -> String {
buf.truncate(n);
String::from_utf8_lossy(&buf).into_owned()
}
#[cfg(feature = "mock")]
pub use request::MockRequest;
#[cfg(feature = "mock")]
pub use response::MockResponse;
#[cfg(feature = "mock")]
pub use context::MockContext;
@@ -19,5 +19,5 @@ impl Next {
}
pub trait Middleware {
fn handle(&self, req: &Request, res: &mut Response) -> Next;
fn handle(&self, req: &impl Request, res: &mut impl Response) -> Next;
}
+22 -5
View File
@@ -1,17 +1,34 @@
use crate::{ffi, read};
pub struct Request;
#[derive(Default)]
pub struct RequestImpl;
impl Request {
pub fn method(&self) -> String {
#[cfg_attr(feature = "mock", mockall::automock)]
pub trait Request {
fn method(&self) -> String;
fn path(&self) -> String;
fn header(&self, name: &str) -> String;
fn url_query(&self, key: &str) -> Option<String>;
}
impl Request for RequestImpl {
fn method(&self) -> String {
read(|ptr, cap| unsafe { ffi::req_method(ptr, cap) })
}
pub fn path(&self) -> String {
fn path(&self) -> String {
read(|ptr, cap| unsafe { ffi::req_path(ptr, cap) })
}
pub fn header(&self, name: &str) -> String {
fn header(&self, name: &str) -> String {
read(|ptr, cap| unsafe { ffi::req_header(name.as_ptr(), name.len() as u32, ptr, cap) })
}
fn url_query(&self, key: &str) -> Option<String> {
let value = read(|ptr, cap| unsafe { ffi::req_url_query(key.as_ptr(), key.len() as u32, ptr, cap) });
if value.is_empty() {
return None
}
return Some(value)
}
}
+13 -8
View File
@@ -1,22 +1,27 @@
use crate::ffi;
pub struct Response;
#[derive(Default)]
pub struct ResponseImpl;
impl Response {
pub fn status(&mut self, code: u16) -> &mut Self {
#[cfg_attr(feature = "mock", mockall::automock)]
pub trait Response {
fn status(&mut self, code: u16);
fn header(&mut self, key: &str, value: &str);
fn write(&mut self, data: &[u8]);
}
impl Response for ResponseImpl {
fn status(&mut self, code: u16) {
unsafe { ffi::resp_status(code as u32) };
self
}
pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
fn header(&mut self, key: &str, value: &str) {
unsafe {
ffi::resp_header(key.as_ptr(), key.len() as u32, value.as_ptr(), value.len() as u32)
};
self
}
pub fn write(&mut self, data: &[u8]) -> &mut Self {
fn write(&mut self, data: &[u8]) {
unsafe { ffi::resp_write(data.as_ptr(), data.len() as u32) };
self
}
}