trailbase_wasm/
lib.rs

1#![forbid(unsafe_code, clippy::unwrap_used)]
2#![allow(clippy::needless_return)]
3#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
4
5pub mod wit {
6  wit_bindgen::generate!({
7      world: "trailbase:runtime/trailbase",
8      path: [
9          // Order-sensitive: will import *.wit from the folder.
10          "wit/deps-0.2.6/random",
11          "wit/deps-0.2.6/io",
12          "wit/deps-0.2.6/clocks",
13          "wit/deps-0.2.6/filesystem",
14          "wit/deps-0.2.6/sockets",
15          "wit/deps-0.2.6/cli",
16          "wit/deps-0.2.6/http",
17          "wit/keyvalue-0.2.0-draft",
18          // Ours:
19          "wit/trailbase.wit",
20      ],
21      pub_export_macro: true,
22      default_bindings_module: "trailbase_wasm::wit",
23      // additional_derives: [PartialEq, Eq, Hash, Clone],
24      generate_all,
25  });
26}
27
28pub mod db;
29pub mod fetch;
30pub mod fs;
31pub mod http;
32pub mod job;
33pub mod kv;
34pub mod time;
35
36use trailbase_wasm_common::{HttpContext, HttpContextKind};
37use wstd::http::Request;
38use wstd::http::body::IncomingBody;
39use wstd::http::server::{Finished, Responder};
40
41use crate::http::{HttpRoute, Method, StatusCode, empty_error_response};
42use crate::job::Job;
43
44pub use crate::wit::exports::trailbase::runtime::init_endpoint::{InitArguments, InitResult};
45
46// Needed for export macro
47pub use static_assertions::assert_impl_all;
48pub use wstd::wasip2 as __wasi;
49
50#[macro_export]
51macro_rules! export {
52    ($impl:ident) => {
53        ::trailbase_wasm::assert_impl_all!($impl: ::trailbase_wasm::Guest);
54        // Register InitEndpoint.
55        ::trailbase_wasm::wit::export!($impl);
56        // Register Incoming HTTP handler.
57        type _HttpHandlerIdent = ::trailbase_wasm::HttpIncomingHandler<$impl>;
58        ::trailbase_wasm::__wasi::http::proxy::export!(
59            _HttpHandlerIdent with_types_in ::trailbase_wasm::__wasi);
60    };
61}
62
63#[derive(Debug)]
64pub struct Args {
65  pub version: Option<String>,
66}
67
68pub trait Guest {
69  fn init(_: Args) {}
70
71  fn http_handlers() -> Vec<HttpRoute> {
72    return vec![];
73  }
74
75  fn job_handlers() -> Vec<Job> {
76    return vec![];
77  }
78}
79
80impl<T: Guest> crate::wit::exports::trailbase::runtime::init_endpoint::Guest for T {
81  fn init(args: InitArguments) -> InitResult {
82    T::init(Args {
83      version: args.version,
84    });
85
86    return InitResult {
87      http_handlers: T::http_handlers()
88        .into_iter()
89        .map(|route| (to_method_type(route.method), route.path))
90        .collect(),
91      job_handlers: T::job_handlers()
92        .into_iter()
93        .map(|config| (config.name, config.spec))
94        .collect(),
95    };
96  }
97}
98
99pub struct HttpIncomingHandler<T: Guest> {
100  phantom: std::marker::PhantomData<T>,
101}
102
103impl<T: Guest> HttpIncomingHandler<T> {
104  async fn handle(request: Request<IncomingBody>, responder: Responder) -> Finished {
105    let path = request.uri().path();
106    let method = request.method();
107
108    let Some(context) = request
109      .headers()
110      .get("__context")
111      .and_then(|h| serde_json::from_slice::<HttpContext>(h.as_bytes()).ok())
112    else {
113      return responder
114        .respond(empty_error_response(StatusCode::INTERNAL_SERVER_ERROR))
115        .await;
116    };
117
118    log::debug!("WASM guest received HTTP request {path}: {context:?}");
119
120    match context.kind {
121      HttpContextKind::Http => {
122        if let Some(HttpRoute { handler, .. }) = T::http_handlers()
123          .into_iter()
124          .find(|route| route.method == method && route.path == context.registered_path)
125        {
126          return handler(context, request, responder).await;
127        }
128      }
129      HttpContextKind::Job => {
130        if let Some(Job { handler, .. }) = T::job_handlers()
131          .into_iter()
132          .find(|config| method == Method::GET && config.name == context.registered_path)
133        {
134          return handler(responder).await;
135        }
136      }
137    }
138
139    return responder
140      .respond(empty_error_response(StatusCode::NOT_FOUND))
141      .await;
142  }
143}
144
145impl<T: Guest> ::wstd::wasip2::exports::http::incoming_handler::Guest for HttpIncomingHandler<T> {
146  fn handle(
147    request: ::wstd::wasip2::http::types::IncomingRequest,
148    response_out: ::wstd::wasip2::http::types::ResponseOutparam,
149  ) {
150    let responder = Responder::new(response_out);
151
152    let _finished: Finished = match ::wstd::http::request::try_from_incoming(request) {
153      Ok(request) => ::wstd::runtime::block_on(async { Self::handle(request, responder).await }),
154      Err(err) => responder.fail(err),
155    };
156  }
157}
158
159fn to_method_type(m: Method) -> crate::wit::exports::trailbase::runtime::init_endpoint::MethodType {
160  use crate::wit::exports::trailbase::runtime::init_endpoint::MethodType;
161
162  return match m {
163    Method::GET => MethodType::Get,
164    Method::POST => MethodType::Post,
165    Method::HEAD => MethodType::Head,
166    Method::OPTIONS => MethodType::Options,
167    Method::PATCH => MethodType::Patch,
168    Method::DELETE => MethodType::Delete,
169    Method::PUT => MethodType::Put,
170    Method::TRACE => MethodType::Trace,
171    Method::CONNECT => MethodType::Connect,
172    _ => panic!("extension"),
173  };
174}