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 "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 "wit/trailbase.wit",
20 ],
21 pub_export_macro: true,
22 default_bindings_module: "trailbase_wasm::wit",
23 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
47pub 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 ::trailbase_wasm::wit::export!($impl);
57 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}