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::InitResult;
45pub use crate::wit::trailbase::runtime::host_endpoint::thread_id;
46
47// Needed for export macro
48pub use static_assertions::assert_impl_all;
49pub use wstd::wasip2 as __wasi;
50
51#[macro_export]
52macro_rules! export {
53    ($impl:ident) => {
54        ::trailbase_wasm::assert_impl_all!($impl: ::trailbase_wasm::Guest);
55        // Register InitEndpoint.
56        ::trailbase_wasm::wit::export!($impl);
57        // Register Incoming HTTP handler.
58        type _HttpHandlerIdent = ::trailbase_wasm::HttpIncomingHandler<$impl>;
59        ::trailbase_wasm::__wasi::http::proxy::export!(
60            _HttpHandlerIdent with_types_in ::trailbase_wasm::__wasi);
61    };
62}
63
64pub trait Guest {
65  fn http_handlers() -> Vec<HttpRoute> {
66    return vec![];
67  }
68
69  fn job_handlers() -> Vec<Job> {
70    return vec![];
71  }
72}
73
74impl<T: Guest> crate::wit::exports::trailbase::runtime::init_endpoint::Guest for T {
75  fn init() -> InitResult {
76    return InitResult {
77      http_handlers: T::http_handlers()
78        .into_iter()
79        .map(|route| (to_method_type(route.method), route.path))
80        .collect(),
81      job_handlers: T::job_handlers()
82        .into_iter()
83        .map(|config| (config.name, config.spec))
84        .collect(),
85    };
86  }
87}
88
89pub struct HttpIncomingHandler<T: Guest> {
90  phantom: std::marker::PhantomData<T>,
91}
92
93impl<T: Guest> HttpIncomingHandler<T> {
94  async fn handle(request: Request<IncomingBody>, responder: Responder) -> Finished {
95    let path = request.uri().path();
96    let method = request.method();
97
98    let Some(context) = request
99      .headers()
100      .get("__context")
101      .and_then(|h| serde_json::from_slice::<HttpContext>(h.as_bytes()).ok())
102    else {
103      return responder
104        .respond(empty_error_response(StatusCode::INTERNAL_SERVER_ERROR))
105        .await;
106    };
107
108    log::debug!("WASM guest received HTTP request {path}: {context:?}");
109
110    match context.kind {
111      HttpContextKind::Http => {
112        if let Some(HttpRoute { handler, .. }) = T::http_handlers()
113          .into_iter()
114          .find(|route| route.method == method && route.path == context.registered_path)
115        {
116          return handler(context, request, responder).await;
117        }
118      }
119      HttpContextKind::Job => {
120        if let Some(Job { handler, .. }) = T::job_handlers()
121          .into_iter()
122          .find(|config| method == Method::GET && config.name == context.registered_path)
123        {
124          return handler(responder).await;
125        }
126      }
127    }
128
129    return responder
130      .respond(empty_error_response(StatusCode::NOT_FOUND))
131      .await;
132  }
133}
134
135impl<T: Guest> ::wstd::wasip2::exports::http::incoming_handler::Guest for HttpIncomingHandler<T> {
136  fn handle(
137    request: ::wstd::wasip2::http::types::IncomingRequest,
138    response_out: ::wstd::wasip2::http::types::ResponseOutparam,
139  ) {
140    let responder = Responder::new(response_out);
141
142    let _finished: Finished = match ::wstd::http::request::try_from_incoming(request) {
143      Ok(request) => ::wstd::runtime::block_on(async { Self::handle(request, responder).await }),
144      Err(err) => responder.fail(err),
145    };
146  }
147}
148
149fn to_method_type(m: Method) -> crate::wit::exports::trailbase::runtime::init_endpoint::MethodType {
150  use crate::wit::exports::trailbase::runtime::init_endpoint::MethodType;
151
152  return match m {
153    Method::GET => MethodType::Get,
154    Method::POST => MethodType::Post,
155    Method::HEAD => MethodType::Head,
156    Method::OPTIONS => MethodType::Options,
157    Method::PATCH => MethodType::Patch,
158    Method::DELETE => MethodType::Delete,
159    Method::PUT => MethodType::Put,
160    Method::TRACE => MethodType::Trace,
161    Method::CONNECT => MethodType::Connect,
162    _ => panic!("extension"),
163  };
164}