Skip to content

Commit 3f61a28

Browse files
committed
Implement HTTP handlers / webhooks in Rust modules
Codex-assisted changes to implement HTTP handlers / webhooks, based on proposal discussed elsewhere. I've done a reasonably thorough review of these changes for code quality, though they remain largely untested as of this commit. I've made a few minor changes to the LLM's output, and left TODO comments anywhere I feel more substative changes will be necessary. Most notably, we'll need more testing, documentation, and better detection of suspicious-or-invalid path components. TypeScript, C# and C++ support is also not included, as per title. There are also notes that this initial implementation uses `Vec<Route>` for its router, meaning route matching is O(num_routes), and router construction (with error checking) is O(num_routes ^ 2). I don't expect this to matter much in the short term, as I expect modules to define a pretty small number of routes.
1 parent b2fb04a commit 3f61a28

File tree

24 files changed

+1877
-27
lines changed

24 files changed

+1877
-27
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-macro/src/http.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use crate::reducer::{assert_only_lifetime_generics, extract_typed_args};
2+
use crate::util::ident_to_litstr;
3+
use proc_macro2::TokenStream;
4+
use quote::quote;
5+
use syn::{ItemFn, ReturnType};
6+
7+
pub(crate) fn handler_impl(args: TokenStream, original_function: &ItemFn) -> syn::Result<TokenStream> {
8+
if !args.is_empty() {
9+
return Err(syn::Error::new_spanned(
10+
args,
11+
"The `handler` attribute does not accept arguments",
12+
));
13+
}
14+
15+
let func_name = &original_function.sig.ident;
16+
let vis = &original_function.vis;
17+
let handler_name = ident_to_litstr(func_name);
18+
19+
assert_only_lifetime_generics(original_function, "http handlers")?;
20+
21+
// TODO(error-reporting): Prefer emitting tyck code rather than checking in the macro.
22+
let typed_args = extract_typed_args(original_function)?;
23+
if typed_args.len() != 2 {
24+
return Err(syn::Error::new_spanned(
25+
original_function.sig.clone(),
26+
"HTTP handlers must take exactly two arguments",
27+
));
28+
}
29+
30+
let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::<Vec<_>>();
31+
let first_arg_ty = &arg_tys[0];
32+
let second_arg_ty = &arg_tys[1];
33+
34+
// TODO(error-reporting): Prefer emitting tyck code rather than checking in the macro.
35+
let ret_ty = match &original_function.sig.output {
36+
ReturnType::Type(_, t) => t.as_ref(),
37+
ReturnType::Default => {
38+
return Err(syn::Error::new_spanned(
39+
original_function.sig.clone(),
40+
"HTTP handlers must return `spacetimedb::http::Response`",
41+
));
42+
}
43+
};
44+
45+
let internal_ident = syn::Ident::new(&format!("__spacetimedb_http_handler_{func_name}"), func_name.span());
46+
let mut inner_fn = original_function.clone();
47+
inner_fn.sig.ident = internal_ident.clone();
48+
49+
let register_describer_symbol = format!("__preinit__20_register_http_handler_{}", handler_name.value());
50+
51+
let lifetime_params = &original_function.sig.generics;
52+
let lifetime_where_clause = &lifetime_params.where_clause;
53+
54+
let generated_describe_function = quote! {
55+
#[unsafe(export_name = #register_describer_symbol)]
56+
pub extern "C" fn __register_describer() {
57+
spacetimedb::rt::register_http_handler(#handler_name, #internal_ident)
58+
}
59+
};
60+
61+
Ok(quote! {
62+
#inner_fn
63+
64+
#vis const #func_name: spacetimedb::http::Handler = spacetimedb::http::Handler::new(#handler_name);
65+
66+
const _: () = {
67+
#generated_describe_function
68+
};
69+
70+
const _: () = {
71+
// TODO(error-reporting): It should be sufficient to just cast the function to a particular `fn` type,
72+
// rather than doing all this stuff with particular args implementing traits.
73+
fn _assert_args #lifetime_params () #lifetime_where_clause {
74+
let _ = <#first_arg_ty as spacetimedb::rt::HttpHandlerContextArg>::_ITEM;
75+
let _ = <#second_arg_ty as spacetimedb::rt::HttpHandlerRequestArg>::_ITEM;
76+
let _ = <#ret_ty as spacetimedb::rt::HttpHandlerReturn>::_ITEM;
77+
}
78+
};
79+
})
80+
}
81+
82+
pub(crate) fn router_impl(args: TokenStream, original_function: &ItemFn) -> syn::Result<TokenStream> {
83+
if !args.is_empty() {
84+
return Err(syn::Error::new_spanned(
85+
args,
86+
"The `router` attribute does not accept arguments",
87+
));
88+
}
89+
90+
if !original_function.sig.inputs.is_empty() {
91+
return Err(syn::Error::new_spanned(
92+
original_function.sig.clone(),
93+
"HTTP router functions must take no arguments",
94+
));
95+
}
96+
97+
let func_name = &original_function.sig.ident;
98+
let register_symbol = "__preinit__30_register_http_router";
99+
100+
Ok(quote! {
101+
#original_function
102+
103+
const _: () = {
104+
fn _assert_router() {
105+
// TODO(cleanup): Why two bindings here?
106+
let _f: fn() -> spacetimedb::http::Router = #func_name;
107+
let _ = _f;
108+
}
109+
};
110+
111+
const _: () = {
112+
#[unsafe(export_name = #register_symbol)]
113+
pub extern "C" fn __register_router() {
114+
spacetimedb::rt::register_http_router(#func_name)
115+
}
116+
};
117+
})
118+
}

crates/bindings-macro/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//
99
// (private documentation for the macro authors is totally fine here and you SHOULD write that!)
1010

11+
mod http;
1112
mod procedure;
1213

1314
#[proc_macro_attribute]
@@ -17,6 +18,24 @@ pub fn procedure(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
1718
procedure::procedure_impl(args, original_function)
1819
})
1920
}
21+
22+
#[proc_macro_attribute]
23+
pub fn http_handler(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
24+
ok_or_compile_error(|| {
25+
let item_ts: TokenStream = item.into();
26+
let original_function: ItemFn = syn::parse2(item_ts)?;
27+
http::handler_impl(args.into(), &original_function)
28+
})
29+
}
30+
31+
#[proc_macro_attribute]
32+
pub fn http_router(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
33+
ok_or_compile_error(|| {
34+
let item_ts: TokenStream = item.into();
35+
let original_function: ItemFn = syn::parse2(item_ts)?;
36+
http::router_impl(args.into(), &original_function)
37+
})
38+
}
2039
mod reducer;
2140

2241
#[proc_macro_attribute]

0 commit comments

Comments
 (0)