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::{InitArguments, InitResult};
45
46pub 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 ::trailbase_wasm::wit::export!($impl);
56 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}