From 7f5eebb43ed9495e73907bd56814654328495867 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Fri, 13 Mar 2026 11:25:16 -0400 Subject: [PATCH 1/3] Initial vibecoded changes for HTTP routes / webhooks --- Cargo.lock | 1 + crates/bindings-macro/src/lib.rs | 1 + crates/bindings-macro/src/procedure.rs | 159 ++++++++++++++-- .../src/lib/autogen/types.ts | 35 ++++ crates/bindings-typescript/src/lib/schema.ts | 2 + crates/bindings/src/http.rs | 86 +++++++++ crates/bindings/src/rt.rs | 23 +++ crates/client-api/Cargo.toml | 1 + crates/client-api/src/routes/database.rs | 153 ++++++++++++++- crates/core/src/host/module_host.rs | 29 +++ crates/lib/src/db/raw_def/v10.rs | 142 +++++++++++++- crates/lib/src/http.rs | 24 ++- crates/schema/src/def.rs | 72 ++++++- crates/schema/src/def/validate/v10.rs | 178 +++++++++++++++++- crates/schema/src/def/validate/v9.rs | 46 ++++- crates/schema/src/error.rs | 24 ++- .../tests/smoketests/http_routes.rs | 26 +++ crates/smoketests/tests/smoketests/mod.rs | 1 + 18 files changed, 957 insertions(+), 46 deletions(-) create mode 100644 crates/smoketests/tests/smoketests/http_routes.rs diff --git a/Cargo.lock b/Cargo.lock index 80a3c9de37a..2521911a7bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7723,6 +7723,7 @@ dependencies = [ "futures", "headers", "http 1.3.1", + "http-body-util", "humantime", "hyper 1.7.0", "hyper-util", diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index 8fa9705ca0e..8ef481c42dc 100644 --- a/crates/bindings-macro/src/lib.rs +++ b/crates/bindings-macro/src/lib.rs @@ -132,6 +132,7 @@ mod sym { symbol!(update); symbol!(default); symbol!(event); + symbol!(route); symbol!(u8); symbol!(i8); diff --git a/crates/bindings-macro/src/procedure.rs b/crates/bindings-macro/src/procedure.rs index 9f76e5b547f..db4c1cc2a7e 100644 --- a/crates/bindings-macro/src/procedure.rs +++ b/crates/bindings-macro/src/procedure.rs @@ -1,15 +1,22 @@ use crate::reducer::{assert_only_lifetime_generics, extract_typed_args, generate_explicit_names_impl}; use crate::sym; use crate::util::{check_duplicate, ident_to_litstr, match_meta}; -use proc_macro2::TokenStream; -use quote::quote; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; use syn::parse::Parser as _; -use syn::{ItemFn, LitStr}; +use syn::{Expr, ExprCall, ExprLit, ExprPath, ItemFn, Lit, LitStr}; #[derive(Default)] pub(crate) struct ProcedureArgs { /// For consistency with reducers: allow specifying a different export name than the Rust function name. name: Option, + route: Option, +} + +#[derive(Clone)] +pub(crate) struct RouteAttr { + method: syn::Ident, + path: LitStr, } impl ProcedureArgs { @@ -21,6 +28,11 @@ impl ProcedureArgs { check_duplicate(&args.name, &meta)?; args.name = Some(meta.value()?.parse()?); } + sym::route => { + check_duplicate(&args.route, &meta)?; + let expr: Expr = meta.value()?.parse()?; + args.route = Some(parse_route_expr(expr)?); + } }); Ok(()) }) @@ -29,16 +41,53 @@ impl ProcedureArgs { } } +fn parse_route_expr(expr: Expr) -> syn::Result { + let Expr::Call(ExprCall { func, args, .. }) = expr else { + return Err(syn::Error::new_spanned(expr, "expected `route = method(\"/path\")`")); + }; + + let Expr::Path(ExprPath { path, .. }) = *func else { + return Err(syn::Error::new_spanned(func, "expected `route = method(\"/path\")`")); + }; + + let method = path + .get_ident() + .cloned() + .ok_or_else(|| syn::Error::new_spanned(path, "expected method identifier like `get` or `post`"))?; + + if args.len() != 1 { + return Err(syn::Error::new_spanned(args, "expected a single path argument")); + } + + let Expr::Lit(ExprLit { lit: Lit::Str(path), .. }) = args.first().unwrap() else { + return Err(syn::Error::new_spanned(args, "expected a string literal path")); + }; + + Ok(RouteAttr { + method, + path: path.clone(), + }) +} + pub(crate) fn procedure_impl(_args: ProcedureArgs, original_function: &ItemFn) -> syn::Result { let func_name = &original_function.sig.ident; let vis = &original_function.vis; let explicit_name = _args.name.as_ref(); + let route = _args.route.as_ref(); let procedure_name = ident_to_litstr(func_name); assert_only_lifetime_generics(original_function, "procedures")?; let typed_args = extract_typed_args(original_function)?; + let is_http_route = route.is_some(); + + if is_http_route && typed_args.len() != 2 { + return Err(syn::Error::new_spanned( + original_function.sig.clone(), + "HTTP route procedures must take `(&mut ProcedureContext, Request)`", + )); + } // TODO: Require that procedures be `async` functions syntactically, // and use `futures_util::FutureExt::now_or_never` to poll them. @@ -80,31 +129,107 @@ pub(crate) fn procedure_impl(_args: ProcedureArgs, original_function: &ItemFn) - let lifetime_params = &original_function.sig.generics; let lifetime_where_clause = &lifetime_params.where_clause; - let generated_describe_function = quote! { - #[unsafe(export_name = #register_describer_symbol)] - pub extern "C" fn __register_describer() { - spacetimedb::rt::register_procedure::<_, _, #func_name>(#func_name) + let (generated_describe_function, wrapper_fn, invoke_target, fn_kind_ty, return_type_ty) = if let Some(route) = route { + let RouteAttr { method, path } = route.clone(); + let method_str = method.to_string(); + let method_expr = match method_str.as_str() { + "get" => quote!(spacetimedb::spacetimedb_lib::http::Method::Get), + "post" => quote!(spacetimedb::spacetimedb_lib::http::Method::Post), + "put" => quote!(spacetimedb::spacetimedb_lib::http::Method::Put), + "delete" => quote!(spacetimedb::spacetimedb_lib::http::Method::Delete), + "patch" => quote!(spacetimedb::spacetimedb_lib::http::Method::Patch), + _ => { + return Err(syn::Error::new( + Span::call_site(), + "unsupported HTTP method; expected get, post, put, delete, or patch", + )); + } + }; + + let path_value = path.value(); + let valid_path = path_value.starts_with('/') && !path_value[1..].is_empty() && !path_value[1..].contains('/'); + if !valid_path { + return Err(syn::Error::new_spanned(path, "route path must be a single segment starting with `/`")); } + + let wrapper_name = format_ident!("__spacetimedb_http_route_wrapper_{}", func_name); + let wrapper_fn = quote! { + fn #wrapper_name( + __ctx: &mut spacetimedb::ProcedureContext, + __request: spacetimedb::spacetimedb_lib::http::RequestAndBody, + ) -> spacetimedb::spacetimedb_lib::http::ResponseAndBody { + let __request = match spacetimedb::http::request_and_body_to_http(__request) { + Ok(req) => req, + Err(_) => { + let response = spacetimedb::http::Response::builder() + .status(400) + .body(spacetimedb::http::Body::empty()) + .expect("Failed to build error response"); + return spacetimedb::http::response_to_response_and_body(response); + } + }; + let __response = #func_name(__ctx, __request); + spacetimedb::http::response_to_response_and_body(__response) + } + }; + + let describe = quote! { + #[unsafe(export_name = #register_describer_symbol)] + pub extern "C" fn __register_describer() { + spacetimedb::rt::register_http_route_procedure::<#func_name>(#method_expr, #path) + } + }; + + ( + describe, + wrapper_fn, + quote!(#wrapper_name), + quote!(spacetimedb::rt::FnKindProcedure), + quote!(spacetimedb::spacetimedb_lib::http::ResponseAndBody), + ) + } else { + let describe = quote! { + #[unsafe(export_name = #register_describer_symbol)] + pub extern "C" fn __register_describer() { + spacetimedb::rt::register_procedure::<_, _, #func_name>(#func_name) + } + }; + ( + describe, + quote!(), + quote!(#func_name), + quote!(spacetimedb::rt::FnKindProcedure<#ret_ty_for_info>), + ret_ty_for_info.clone(), + ) }; let generate_explicit_names = generate_explicit_names_impl(&procedure_name.value(), func_name, explicit_name); + let assert_args_block = if is_http_route { + quote!() + } else { + quote! { + const _: () = { + fn _assert_args #lifetime_params () #lifetime_where_clause { + #(let _ = <#first_arg_ty as spacetimedb::rt::ProcedureContextArg>::_ITEM;)* + #(let _ = <#rest_arg_tys as spacetimedb::rt::ProcedureArg>::_ITEM;)* + #(let _ = <#ret_ty_for_assert as spacetimedb::rt::IntoProcedureResult>::to_result;)* + } + }; + } + }; + Ok(quote! { const _: () = { #generated_describe_function }; + #wrapper_fn #[allow(non_camel_case_types)] #vis struct #func_name { _never: ::core::convert::Infallible } - const _: () = { - fn _assert_args #lifetime_params () #lifetime_where_clause { - #(let _ = <#first_arg_ty as spacetimedb::rt::ProcedureContextArg>::_ITEM;)* - #(let _ = <#rest_arg_tys as spacetimedb::rt::ProcedureArg>::_ITEM;)* - #(let _ = <#ret_ty_for_assert as spacetimedb::rt::IntoProcedureResult>::to_result;)* - } - }; + #assert_args_block impl #func_name { fn invoke(__ctx: &mut spacetimedb::ProcedureContext, __args: &[u8]) -> spacetimedb::ProcedureResult { - spacetimedb::rt::invoke_procedure(#func_name, __ctx, __args) + spacetimedb::rt::invoke_procedure(#invoke_target, __ctx, __args) } } #[automatically_derived] @@ -113,7 +238,7 @@ pub(crate) fn procedure_impl(_args: ProcedureArgs, original_function: &ItemFn) - type Invoke = spacetimedb::rt::ProcedureFn; /// The function kind, which will cause scheduled tables to accept procedures. - type FnKind = spacetimedb::rt::FnKindProcedure<#ret_ty_for_info>; + type FnKind = #fn_kind_ty; /// The name of this function const NAME: &'static str = #procedure_name; @@ -126,7 +251,7 @@ pub(crate) fn procedure_impl(_args: ProcedureArgs, original_function: &ItemFn) - /// The return type of this function fn return_type(ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder) -> Option { - Some(<#ret_ty_for_info as spacetimedb::SpacetimeType>::make_type(ts)) + Some(<#return_type_ty as spacetimedb::SpacetimeType>::make_type(ts)) } } diff --git a/crates/bindings-typescript/src/lib/autogen/types.ts b/crates/bindings-typescript/src/lib/autogen/types.ts index 0ce535eca0f..de86432ed1e 100644 --- a/crates/bindings-typescript/src/lib/autogen/types.ts +++ b/crates/bindings-typescript/src/lib/autogen/types.ts @@ -133,6 +133,22 @@ export const HttpResponse = __t.object('HttpResponse', { }); export type HttpResponse = __Infer; +export const HttpRequestAndBody = __t.object('HttpRequestAndBody', { + get request() { + return HttpRequest; + }, + body: __t.byteArray(), +}); +export type HttpRequestAndBody = __Infer; + +export const HttpResponseAndBody = __t.object('HttpResponseAndBody', { + get response() { + return HttpResponse; + }, + body: __t.byteArray(), +}); +export type HttpResponseAndBody = __Infer; + // The tagged union or sum type for the algebraic type `HttpVersion`. export const HttpVersion = __t.enum('HttpVersion', { Http09: __t.unit(), @@ -358,6 +374,9 @@ export const RawModuleDefV10Section = __t.enum('RawModuleDefV10Section', { get ExplicitNames() { return ExplicitNames; }, + get HttpRoutes() { + return __t.array(RawHttpRouteDefV10); + }, }); export type RawModuleDefV10Section = __Infer; @@ -413,6 +432,22 @@ export const RawProcedureDefV10 = __t.object('RawProcedureDefV10', { }); export type RawProcedureDefV10 = __Infer; +export const Path = __t.object('Path', { + path: __t.string(), +}); +export type Path = __Infer; + +export const RawHttpRouteDefV10 = __t.object('RawHttpRouteDefV10', { + handlerFunction: __t.string(), + get method() { + return HttpMethod; + }, + get path() { + return Path; + }, +}); +export type RawHttpRouteDefV10 = __Infer; + export const RawProcedureDefV9 = __t.object('RawProcedureDefV9', { name: __t.string(), get params() { diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index be9edc9e113..d54509a9e89 100644 --- a/crates/bindings-typescript/src/lib/schema.ts +++ b/crates/bindings-typescript/src/lib/schema.ts @@ -195,6 +195,7 @@ export class ModuleContext { procedures: [], views: [], lifeCycleReducers: [], + httpRoutes: [], caseConversionPolicy: { tag: 'SnakeCase' }, explicitNames: { entries: [], @@ -221,6 +222,7 @@ export class ModuleContext { push(module.procedures && { tag: 'Procedures', value: module.procedures }); push(module.views && { tag: 'Views', value: module.views }); push(module.schedules && { tag: 'Schedules', value: module.schedules }); + push(module.httpRoutes && { tag: 'HttpRoutes', value: module.httpRoutes }); push( module.lifeCycleReducers && { tag: 'LifeCycleReducers', diff --git a/crates/bindings/src/http.rs b/crates/bindings/src/http.rs index e31eda8dde4..31dbd8330f1 100644 --- a/crates/bindings/src/http.rs +++ b/crates/bindings/src/http.rs @@ -199,6 +199,92 @@ fn convert_response(response: st_http::Response) -> http::Result http::Result> { + let st_http::RequestAndBody { request, body } = request; + let parts = convert_request_from_st(request)?; + Ok(http::Request::from_parts(parts, Body::from_bytes(body))) +} + +#[doc(hidden)] +pub fn response_to_response_and_body>(response: http::Response) -> st_http::ResponseAndBody { + let (parts, body) = response.map(Into::into).into_parts(); + let response = convert_response_from_http(parts); + let body = body.into_bytes().to_vec().into_boxed_slice(); + st_http::ResponseAndBody { response, body } +} + +fn convert_request_from_st(request: st_http::Request) -> http::Result { + let st_http::Request { + method, + headers, + timeout: _, + uri, + version, + } = request; + + let (mut req, ()) = http::Request::new(()).into_parts(); + req.method = match method { + st_http::Method::Get => http::Method::GET, + st_http::Method::Head => http::Method::HEAD, + st_http::Method::Post => http::Method::POST, + st_http::Method::Put => http::Method::PUT, + st_http::Method::Delete => http::Method::DELETE, + st_http::Method::Connect => http::Method::CONNECT, + st_http::Method::Options => http::Method::OPTIONS, + st_http::Method::Trace => http::Method::TRACE, + st_http::Method::Patch => http::Method::PATCH, + st_http::Method::Extension(method) => http::Method::from_bytes(method.as_bytes())?, + }; + req.uri = uri.parse().map_err(http::Error::from)?; + req.version = match version { + st_http::Version::Http09 => http::Version::HTTP_09, + st_http::Version::Http10 => http::Version::HTTP_10, + st_http::Version::Http11 => http::Version::HTTP_11, + st_http::Version::Http2 => http::Version::HTTP_2, + st_http::Version::Http3 => http::Version::HTTP_3, + }; + req.headers = headers + .into_iter() + .map(|(k, v)| { + let name = k.into_string().try_into()?; + let value = v.into_vec().try_into()?; + Ok((name, value)) + }) + .collect::>()?; + Ok(req) +} + +fn convert_response_from_http(response: http::response::Parts) -> st_http::Response { + let http::response::Parts { + extensions, + headers, + status, + version, + .. + } = response; + + let _ = extensions; + + st_http::Response { + headers: headers + .into_iter() + .map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into())) + .collect(), + version: match version { + http::Version::HTTP_09 => st_http::Version::Http09, + http::Version::HTTP_10 => st_http::Version::Http10, + http::Version::HTTP_11 => st_http::Version::Http11, + http::Version::HTTP_2 => st_http::Version::Http2, + http::Version::HTTP_3 => st_http::Version::Http3, + _ => unreachable!("Unknown HTTP version: {version:?}"), + }, + code: status.as_u16(), + } +} + /// Represents the body of an HTTP request or response. pub struct Body { inner: BodyInner, diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index d6d55eba5f4..80bfdbbde64 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -813,6 +813,29 @@ where }) } +#[cfg(feature = "unstable")] +pub fn register_http_route_procedure(method: spacetimedb_lib::http::Method, path: &'static str) +where + I: FnInfo, +{ + register_describer(move |module| { + let params = + <(spacetimedb_lib::http::RequestAndBody,) as Args>::schema::(&mut module.inner); + let ret_ty = + ::make_type(&mut module.inner); + module.inner.add_procedure_with_visibility( + I::NAME, + params, + ret_ty, + spacetimedb_lib::db::raw_def::v10::FunctionVisibility::Private, + ); + module.inner.add_http_route(I::NAME, method.clone(), path); + module.procedures.push(I::INVOKE); + + module.inner.add_explicit_names(I::explicit_names()); + }) +} + /// Registers a describer for the view `I` with arguments `A` and return type `Vec`. pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) where diff --git a/crates/client-api/Cargo.toml b/crates/client-api/Cargo.toml index a3da402e734..efcda883f72 100644 --- a/crates/client-api/Cargo.toml +++ b/crates/client-api/Cargo.toml @@ -34,6 +34,7 @@ axum-extra.workspace = true hyper.workspace = true hyper-util.workspace = true http.workspace = true +http-body-util.workspace = true headers.workspace = true mime = "0.3.17" tokio-stream = { version = "0.1.12", features = ["sync"] } diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index aeade589927..e23e3fe17ed 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -16,10 +16,12 @@ use crate::{ }; use axum::body::{Body, Bytes}; use axum::extract::{Path, Query, State}; +use axum::http::Request as AxumRequest; use axum::response::{ErrorResponse, IntoResponse}; use axum::routing::MethodRouter; use axum::Extension; use axum_extra::TypedHeader; +use http_body_util::BodyExt; use futures::TryStreamExt; use http::StatusCode; use log::{info, warn}; @@ -39,10 +41,12 @@ use spacetimedb_client_api_messages::name::{ }; use spacetimedb_lib::db::raw_def::v10::RawModuleDefV10; use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9; -use spacetimedb_lib::{sats, AlgebraicValue, Hash, ProductValue, Timestamp}; +use spacetimedb_lib::{bsatn, http as st_http, sats, AlgebraicValue, Hash, ProductValue, Timestamp}; +use spacetimedb_lib::de as st_de; use spacetimedb_schema::auto_migrate::{ MigrationPolicy as SchemaMigrationPolicy, MigrationToken, PrettyPrintStyle as AutoMigratePrettyPrintStyle, }; +use spacetimedb_lib::sats::algebraic_value::de::ValueDeserializer; use super::subscribe::{handle_websocket, HasWebSocketOptions}; @@ -215,6 +219,65 @@ pub async fn call( } } +#[derive(Deserialize)] +pub struct RouteParams { + name_or_identity: NameOrIdentity, + path: String, +} + +pub async fn http_route( + State(worker_ctx): State, + Extension(auth): Extension, + Path(RouteParams { name_or_identity, path }): Path, + request: AxumRequest, +) -> axum::response::Result { + let caller_identity = auth.claims.identity; + + let (module, _database) = find_module_and_database(&worker_ctx, name_or_identity).await?; + + let (parts, body) = request.into_parts(); + let route_path = format!("/{}", path); + let method = convert_method(parts.method.clone()); + + let Some((procedure_id, procedure_def)) = module.info.module_def.http_route(&method, &route_path) else { + return Ok(StatusCode::NOT_FOUND.into_response()); + }; + + let body_bytes = body + .collect() + .await + .map_err(|err| (StatusCode::BAD_REQUEST, format!("Failed to read request body: {err}")))? + .to_bytes(); + + let request_and_body = convert_request_to_st(parts, body_bytes); + let args = bsatn::to_vec(&request_and_body) + .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to encode request: {err}")))?; + + let call_result = module + .call_procedure_internal( + caller_identity, + None, + None, + procedure_id, + procedure_def, + FunctionArgs::Bsatn(args.into()), + ) + .await; + + let result = match call_result.result { + Ok(result) => result, + Err(_) => return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()), + }; + + let response = decode_response_and_body(result.return_val) + .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err))?; + + let response = response_and_body_to_axum(response) + .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid response: {err}")))?; + + Ok(response) +} + fn assert_content_type_json(content_type: headers::ContentType) -> axum::response::Result<()> { if content_type != headers::ContentType::json() { Err(axum::extract::rejection::MissingJsonContentType::default().into()) @@ -314,6 +377,88 @@ fn procedure_outcome_response(return_val: AlgebraicValue) -> (StatusCode, axum:: ) } +fn convert_method(method: http::Method) -> st_http::Method { + match method { + http::Method::GET => st_http::Method::Get, + http::Method::HEAD => st_http::Method::Head, + http::Method::POST => st_http::Method::Post, + http::Method::PUT => st_http::Method::Put, + http::Method::DELETE => st_http::Method::Delete, + http::Method::CONNECT => st_http::Method::Connect, + http::Method::OPTIONS => st_http::Method::Options, + http::Method::TRACE => st_http::Method::Trace, + http::Method::PATCH => st_http::Method::Patch, + _ => st_http::Method::Extension(method.to_string()), + } +} + +fn convert_version(version: http::Version) -> st_http::Version { + match version { + http::Version::HTTP_09 => st_http::Version::Http09, + http::Version::HTTP_10 => st_http::Version::Http10, + http::Version::HTTP_11 => st_http::Version::Http11, + http::Version::HTTP_2 => st_http::Version::Http2, + http::Version::HTTP_3 => st_http::Version::Http3, + _ => st_http::Version::Http11, + } +} + +fn convert_request_to_st(parts: http::request::Parts, body: Bytes) -> st_http::RequestAndBody { + let http::request::Parts { + method, + uri, + version, + headers, + .. + } = parts; + + let request = st_http::Request { + method: convert_method(method), + headers: headers + .into_iter() + .map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into())) + .collect(), + timeout: None, + uri: uri.to_string(), + version: convert_version(version), + }; + + st_http::RequestAndBody { + request, + body: body.to_vec().into_boxed_slice(), + } +} + +fn decode_response_and_body(return_val: AlgebraicValue) -> Result { + ::deserialize(ValueDeserializer::new(return_val)) + .map_err(|err| format!("Failed to decode HTTP response: {err:?}")) +} + +fn response_and_body_to_axum(response: st_http::ResponseAndBody) -> http::Result { + let st_http::ResponseAndBody { response, body } = response; + let parts = convert_st_response(response)?; + Ok(http::Response::from_parts(parts, Body::from(Vec::from(body)))) +} + +fn convert_st_response(response: st_http::Response) -> http::Result { + let st_http::Response { headers, version, code } = response; + + let (mut response, ()) = http::Response::new(()).into_parts(); + response.version = match version { + st_http::Version::Http09 => http::Version::HTTP_09, + st_http::Version::Http10 => http::Version::HTTP_10, + st_http::Version::Http11 => http::Version::HTTP_11, + st_http::Version::Http2 => http::Version::HTTP_2, + st_http::Version::Http3 => http::Version::HTTP_3, + }; + response.status = http::StatusCode::from_u16(code)?; + response.headers = headers + .into_iter() + .map(|(k, v)| Ok((k.into_string().try_into()?, v.into_vec().try_into()?))) + .collect::>()?; + Ok(response) +} + #[derive(Deserialize)] pub struct SchemaParams { name_or_identity: NameOrIdentity, @@ -1183,6 +1328,8 @@ pub struct DatabaseRoutes { pub subscribe_get: MethodRouter, /// POST: /database/:name_or_identity/call/:reducer pub call_reducer_procedure_post: MethodRouter, + /// ANY: /database/:name_or_identity/route/:path + pub route_any: MethodRouter, /// GET: /database/:name_or_identity/schema pub schema_get: MethodRouter, /// GET: /database/:name_or_identity/logs @@ -1202,7 +1349,7 @@ where S: NodeDelegate + ControlStateDelegate + HasWebSocketOptions + Authorization + Clone + 'static, { fn default() -> Self { - use axum::routing::{delete, get, post, put}; + use axum::routing::{any, delete, get, post, put}; Self { root_post: post(publish::), db_put: put(publish::), @@ -1214,6 +1361,7 @@ where identity_get: get(get_identity::), subscribe_get: get(handle_websocket::), call_reducer_procedure_post: post(call::), + route_any: any(http_route::), schema_get: get(schema::), logs_get: get(logs::), sql_post: post(sql::), @@ -1239,6 +1387,7 @@ where .route("/identity", self.identity_get) .route("/subscribe", self.subscribe_get) .route("/call/:reducer", self.call_reducer_procedure_post) + .route("/route/:path", self.route_any) .route("/schema", self.schema_get) .route("/logs", self.logs_get) .route("/sql", self.sql_post) diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index 1b46b12af08..c0238c6659a 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -1803,6 +1803,35 @@ impl ModuleHost { ret } + pub async fn call_procedure_internal( + &self, + caller_identity: Identity, + caller_connection_id: Option, + timer: Option, + procedure_id: ProcedureId, + procedure_def: &ProcedureDef, + args: FunctionArgs, + ) -> CallProcedureReturn { + let res = self + .call_procedure_inner( + caller_identity, + caller_connection_id, + timer, + procedure_id, + procedure_def, + args, + ) + .await; + + match res { + Ok(ret) => ret, + Err(err) => CallProcedureReturn { + result: Err(err), + tx_offset: None, + }, + } + } + async fn call_procedure_inner( &self, caller_identity: Identity, diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index a801ea286be..bb22bab8846 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -6,6 +6,7 @@ //! It allows easier future extensibility to add new kinds of definitions. use crate::db::raw_def::v9::{Lifecycle, RawIndexAlgorithm, TableAccess, TableType}; +use crate::http; use core::fmt; use spacetimedb_primitives::{ColId, ColList}; use spacetimedb_sats::raw_identifier::RawIdentifier; @@ -89,6 +90,9 @@ pub enum RawModuleDefV10Section { /// Names provided explicitly by the user that do not follow from the case conversion policy. ExplicitNames(ExplicitNames), + + /// HTTP route definitions. + HttpRoutes(Vec), } #[derive(Debug, Clone, Copy, Default, SpacetimeType)] @@ -131,6 +135,18 @@ pub struct NameMapping { pub canonical_name: RawIdentifier, } +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, Ord, PartialOrd))] +pub struct EnumVariantNameMapping { + /// The source name of the containing enum. + pub enum_source_name: RawIdentifier, + /// The source name of the variant. + pub variant_source_name: RawIdentifier, + /// The canonical name of the variant. + pub variant_canonical_name: RawIdentifier, +} + #[derive(Debug, Clone, SpacetimeType)] #[sats(crate = crate)] #[cfg_attr(feature = "test", derive(PartialEq, Eq, Ord, PartialOrd))] @@ -139,6 +155,7 @@ pub enum ExplicitNameEntry { Table(NameMapping), Function(NameMapping), Index(NameMapping), + EnumVariant(EnumVariantNameMapping), } #[derive(Debug, Default, Clone, SpacetimeType)] @@ -179,6 +196,19 @@ impl ExplicitNames { })); } + pub fn insert_enum_variant( + &mut self, + enum_source_name: impl Into, + variant_source_name: impl Into, + variant_canonical_name: impl Into, + ) { + self.insert(ExplicitNameEntry::EnumVariant(EnumVariantNameMapping { + enum_source_name: enum_source_name.into(), + variant_source_name: variant_source_name.into(), + variant_canonical_name: variant_canonical_name.into(), + })); + } + pub fn merge(&mut self, other: ExplicitNames) { self.entries.extend(other.entries); } @@ -357,6 +387,28 @@ pub struct RawProcedureDefV10 { pub visibility: FunctionVisibility, } +/// A path component of a URI. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct Path { + /// The trailing path component, including the leading `/`. + pub path: RawIdentifier, +} + +/// A definition binding a procedure to an HTTP route. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawHttpRouteDefV10 { + /// Identifier for a procedure defined by the module. + pub handler_function: RawIdentifier, + /// One of the supported HTTP methods. + pub method: http::Method, + /// The user-configurable trailing part of the path to listen on. + pub path: Path, +} + /// A sequence definition for a database table column. #[derive(Debug, Clone, SpacetimeType)] #[sats(crate = crate)] @@ -608,6 +660,14 @@ impl RawModuleDefV10 { _ => None, }) } + + /// Get the http routes section, if present. + pub fn http_routes(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::HttpRoutes(routes) => Some(routes), + _ => None, + }) + } } /// A builder for a [`RawModuleDefV10`]. @@ -785,6 +845,26 @@ impl RawModuleDefV10Builder { } } + /// Get mutable access to the http routes section, creating it if missing. + fn http_routes_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::HttpRoutes(_))) + .unwrap_or_else(|| { + self.module + .sections + .push(RawModuleDefV10Section::HttpRoutes(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::HttpRoutes(routes) => routes, + _ => unreachable!("Just ensured HttpRoutes section exists"), + } + } + /// Get mutable access to the case conversion policy, creating it if missing. fn explicit_names_mut(&mut self) -> &mut ExplicitNames { let idx = self @@ -967,12 +1047,28 @@ impl RawModuleDefV10Builder { source_name: impl Into, params: ProductType, return_type: AlgebraicType, + ) { + self.add_procedure_with_visibility( + source_name, + params, + return_type, + FunctionVisibility::ClientCallable, + ); + } + + /// Add a procedure to the in-progress module with explicit visibility. + pub fn add_procedure_with_visibility( + &mut self, + source_name: impl Into, + params: ProductType, + return_type: AlgebraicType, + visibility: FunctionVisibility, ) { self.procedures_mut().push(RawProcedureDefV10 { source_name: source_name.into(), params, return_type, - visibility: FunctionVisibility::ClientCallable, + visibility, }) } @@ -1050,6 +1146,20 @@ impl RawModuleDefV10Builder { .push(RawRowLevelSecurityDefV10 { sql: sql.into() }); } + /// Add an HTTP route definition to the module. + pub fn add_http_route( + &mut self, + handler_function: impl Into, + method: http::Method, + path: impl Into, + ) { + self.http_routes_mut().push(RawHttpRouteDefV10 { + handler_function: handler_function.into(), + method, + path: Path { path: path.into() }, + }); + } + pub fn add_explicit_names(&mut self, names: ExplicitNames) { self.explicit_names_mut().merge(names); } @@ -1088,7 +1198,7 @@ impl TypespaceBuilder for RawModuleDefV10Builder { if let btree_map::Entry::Occupied(o) = self.type_map.entry(typeid) { AlgebraicType::Ref(*o.get()) } else { - let slot_ref = { + let (slot_ref, enum_source_name) = { let ts = self.typespace_mut(); // Bind a fresh alias to the unit type. let slot_ref = ts.add(AlgebraicType::unit()); @@ -1096,8 +1206,10 @@ impl TypespaceBuilder for RawModuleDefV10Builder { self.type_map.insert(typeid, slot_ref); // Alias provided? Relate `name -> slot_ref`. - if let Some(sats_name) = source_name { + let enum_source_name = if let Some(sats_name) = source_name { let source_name = sats_name_to_scoped_name_v10(sats_name); + let enum_source_name = should_register_enum_variant_names(sats_name) + .then(|| source_name.source_name.clone()); self.types_mut().push(RawTypeDefV10 { source_name, @@ -1108,18 +1220,38 @@ impl TypespaceBuilder for RawModuleDefV10Builder { // macro doesn't know about the default ordering yet. custom_ordering: true, }); - } - slot_ref + enum_source_name + } else { + None + }; + (slot_ref, enum_source_name) }; // Borrow of `v` has ended here, so we can now convince the borrow checker. let ty = make_ty(self); self.typespace_mut()[slot_ref] = ty; + let enum_variants = match (&enum_source_name, &self.typespace_mut()[slot_ref]) { + (Some(enum_source_name), AlgebraicType::Sum(sum)) => Some(( + enum_source_name.clone(), + sum.variants.iter().filter_map(|variant| variant.name().cloned()).collect::>(), + )), + _ => None, + }; + if let Some((enum_source_name, variant_names)) = enum_variants { + for variant_name in variant_names { + self.explicit_names_mut() + .insert_enum_variant(enum_source_name.clone(), variant_name.clone(), variant_name); + } + } AlgebraicType::Ref(slot_ref) } } } +fn should_register_enum_variant_names(sats_name: &str) -> bool { + matches!(sats_name, "HttpMethod" | "HttpVersion") +} + pub fn reducer_default_ok_return_type() -> AlgebraicType { AlgebraicType::unit() } diff --git a/crates/lib/src/http.rs b/crates/lib/src/http.rs index cba80c30007..0334a4ba116 100644 --- a/crates/lib/src/http.rs +++ b/crates/lib/src/http.rs @@ -45,7 +45,7 @@ impl Request { } /// Represents an HTTP method. -#[derive(Clone, SpacetimeType, PartialEq, Eq)] +#[derive(Clone, SpacetimeType, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] #[sats(crate = crate, name = "HttpMethod")] pub enum Method { Get, @@ -165,3 +165,25 @@ impl Response { self.headers.size_in_bytes() } } + +/// A request paired with its body bytes. +/// +/// This is used for incoming HTTP route handling where the body bytes are kept separate +/// from the metadata-only [`Request`]. +#[derive(Clone, SpacetimeType)] +#[sats(crate = crate, name = "HttpRequestAndBody")] +pub struct RequestAndBody { + pub request: Request, + pub body: Box<[u8]>, +} + +/// A response paired with its body bytes. +/// +/// This is used for HTTP route handling where the body bytes are kept separate +/// from the metadata-only [`Response`]. +#[derive(Clone, SpacetimeType)] +#[sats(crate = crate, name = "HttpResponseAndBody")] +pub struct ResponseAndBody { + pub response: Response, + pub body: Box<[u8]>, +} diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 89c201e3f85..3df9b26e175 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -32,9 +32,10 @@ use spacetimedb_data_structures::error_stream::{CollectAllErrors, CombineErrors, use spacetimedb_data_structures::map::{Equivalent, HashMap}; use spacetimedb_lib::db::raw_def; use spacetimedb_lib::db::raw_def::v10::{ - ExplicitNames, RawConstraintDefV10, RawIndexDefV10, RawLifeCycleReducerDefV10, RawModuleDefV10, - RawModuleDefV10Section, RawProcedureDefV10, RawReducerDefV10, RawRowLevelSecurityDefV10, RawScheduleDefV10, - RawScopedTypeNameV10, RawSequenceDefV10, RawTableDefV10, RawTypeDefV10, RawViewDefV10, + ExplicitNames, Path as RawHttpPath, RawConstraintDefV10, RawHttpRouteDefV10, RawIndexDefV10, + RawLifeCycleReducerDefV10, RawModuleDefV10, RawModuleDefV10Section, RawProcedureDefV10, RawReducerDefV10, + RawRowLevelSecurityDefV10, RawScheduleDefV10, RawScopedTypeNameV10, RawSequenceDefV10, RawTableDefV10, + RawTypeDefV10, RawViewDefV10, }; use spacetimedb_lib::db::raw_def::v9::{ Lifecycle, RawColumnDefaultValueV9, RawConstraintDataV9, RawConstraintDefV9, RawIndexAlgorithm, RawIndexDefV9, @@ -42,7 +43,7 @@ use spacetimedb_lib::db::raw_def::v9::{ RawScheduleDefV9, RawScopedTypeNameV9, RawSequenceDefV9, RawSql, RawTableDefV9, RawTypeDefV9, RawUniqueConstraintDataV9, RawViewDefV9, TableAccess, TableType, }; -use spacetimedb_lib::{ProductType, RawModuleDef}; +use spacetimedb_lib::{http, ProductType, RawModuleDef}; use spacetimedb_primitives::{ColId, ColList, ColOrCols, ColSet, ProcedureId, ReducerId, TableId, ViewFnPtr}; use spacetimedb_sats::raw_identifier::RawIdentifier; use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, AlgebraicValue, Typespace}; @@ -147,6 +148,12 @@ pub struct ModuleDef { /// **Note**: Are only validated syntax-wise. row_level_security_raw: HashMap, + /// HTTP route definitions for this module. + http_routes: Vec, + + /// Lookup map for HTTP routes by method and path. + http_route_lookup: HashMap, + /// Indicates which raw module definition semantics this module /// was authored under. #[allow(unused)] @@ -222,6 +229,19 @@ impl ModuleDef { self.row_level_security_raw.values() } + /// The HTTP routes of the module definition. + pub fn http_routes(&self) -> impl Iterator { + self.http_routes.iter() + } + + /// Look up an HTTP route by method and path. + pub fn http_route(&self, method: &http::Method, path: &str) -> Option<(ProcedureId, &ProcedureDef)> { + let key = HttpRouteKey::new(method, path); + let procedure_id = self.http_route_lookup.get(&key).copied()?; + let def = self.get_procedure_by_id(procedure_id)?; + Some((procedure_id, def)) + } + /// The `Typespace` used by the module. /// /// `AlgebraicTypeRef`s in the table, reducer, and type alias declarations refer to this typespace. @@ -436,6 +456,8 @@ impl From for RawModuleDefV9 { refmap: _, row_level_security_raw, procedures, + http_routes: _, + http_route_lookup: _, raw_module_def_version: _, } = val; @@ -492,6 +514,8 @@ impl From for RawModuleDefV10 { refmap: _, row_level_security_raw, procedures, + http_routes, + http_route_lookup: _, raw_module_def_version: _, } = val; @@ -560,6 +584,19 @@ impl From for RawModuleDefV10 { } // Collect ExplicitNames for procedures: accessor_name → source_name, name → canonical_name. + let raw_http_routes: Vec = http_routes + .into_iter() + .filter_map(|route| { + let (_, def) = procedures.get_index(route.procedure_id.idx())?; + Some(RawHttpRouteDefV10 { + handler_function: def.accessor_name.clone().into(), + method: route.method, + path: RawHttpPath { + path: route.path.into(), + }, + }) + }) + .collect(); let raw_procedures: Vec = procedures .into_values() .map(|pd| { @@ -574,6 +611,10 @@ impl From for RawModuleDefV10 { sections.push(RawModuleDefV10Section::Procedures(raw_procedures)); } + if !raw_http_routes.is_empty() { + sections.push(RawModuleDefV10Section::HttpRoutes(raw_http_routes)); + } + // Collect ExplicitNames for views: accessor_name → source_name, name → canonical_name. let raw_views: Vec = views .into_values() @@ -1319,6 +1360,29 @@ pub struct ScheduleDef { pub function_kind: FunctionKind, } +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct HttpRouteDef { + pub method: http::Method, + pub path: String, + pub procedure_id: ProcedureId, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct HttpRouteKey { + method: http::Method, + path: String, +} + +impl HttpRouteKey { + fn new(method: &http::Method, path: &str) -> Self { + Self { + method: method.clone(), + path: path.to_string(), + } + } +} + impl From for RawScheduleDefV9 { fn from(val: ScheduleDef) -> Self { RawScheduleDefV9 { diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index 7ebbaae06d4..124d40f3cd6 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -3,14 +3,15 @@ use spacetimedb_lib::bsatn::Deserializer; use spacetimedb_lib::db::raw_def::v10::*; use spacetimedb_lib::db::view::{extract_view_return_product_type_ref, ViewKind}; use spacetimedb_lib::de::DeserializeSeed as _; -use spacetimedb_sats::{Typespace, WithTypespace}; +use spacetimedb_lib::http; +use spacetimedb_sats::{SpacetimeType, Typespace, WithTypespace}; use crate::def::validate::v9::{ check_function_names_are_unique, check_scheduled_functions_exist, generate_schedule_name, generate_unique_constraint_name, identifier, CoreValidator, TableValidator, ViewValidator, }; use crate::def::*; -use crate::error::ValidationError; +use crate::error::{PrettyAlgebraicType, ValidationError}; use crate::type_for_generate::ProductTypeDef; use crate::{def::validate::Result, error::TypeLocation}; @@ -21,6 +22,7 @@ pub struct ExplicitNamesLookup { pub tables: HashMap, pub functions: HashMap, pub indexes: HashMap, + pub enum_variants: HashMap>, } impl ExplicitNamesLookup { @@ -28,6 +30,7 @@ impl ExplicitNamesLookup { let mut tables = HashMap::default(); let mut functions = HashMap::default(); let mut indexes = HashMap::default(); + let mut enum_variants: HashMap> = HashMap::default(); for entry in ex.into_entries() { match entry { @@ -40,6 +43,12 @@ impl ExplicitNamesLookup { ExplicitNameEntry::Index(m) => { indexes.insert(m.source_name, m.canonical_name); } + ExplicitNameEntry::EnumVariant(m) => { + enum_variants + .entry(m.enum_source_name) + .or_default() + .insert(m.variant_source_name, m.variant_canonical_name); + } _ => {} } } @@ -48,6 +57,7 @@ impl ExplicitNamesLookup { tables, functions, indexes, + enum_variants, } } } @@ -76,6 +86,12 @@ pub fn validate(def: RawModuleDefV10) -> Result { let mut typespace = def.typespace().cloned().unwrap_or_else(|| Typespace::EMPTY.clone()); let known_type_definitions = def.types().into_iter().flatten().map(|def| def.ty); let case_policy = def.case_conversion_policy().into(); + let type_ref_names: HashMap = def + .types() + .into_iter() + .flatten() + .map(|def| (def.ty, def.source_name.source_name.clone())) + .collect(); let explicit_names = def .explicit_names() .cloned() @@ -85,7 +101,12 @@ pub fn validate(def: RawModuleDefV10) -> Result { // Original `typespace` needs to be preserved to be assign `accesor_name`s to columns. let typespace_with_accessor_names = typespace.clone(); // Apply case conversion to `typespace`. - CoreValidator::typespace_case_conversion(case_policy, &mut typespace); + CoreValidator::typespace_case_conversion( + case_policy, + &mut typespace, + &type_ref_names, + &explicit_names.enum_variants, + ); let mut validator = ModuleValidatorV10 { core: CoreValidator { @@ -249,13 +270,6 @@ pub fn validate(def: RawModuleDefV10) -> Result { Ok((tables, types, reducers, procedures, views)) }, ); - let CoreValidator { - stored_in_table_def, - typespace_for_generate, - lifecycle_reducers, - .. - } = validator.core; - let row_level_security_raw = def .row_level_security() .into_iter() @@ -266,6 +280,38 @@ pub fn validate(def: RawModuleDefV10) -> Result { let (tables, types, reducers, procedures, views) = (tables_types_reducers_procedures_views).map_err(|errors| errors.sort_deduplicate())?; + let (expected_request_ty, expected_response_ty) = http_route_expected_types(def.case_conversion_policy()); + + let http_routes = def + .http_routes() + .cloned() + .into_iter() + .flatten() + .map(|route| { + validator.validate_http_route_def(route, &procedures, &expected_request_ty, &expected_response_ty) + }) + .collect_all_errors::>() + .map_err(|errors| errors.sort_deduplicate())?; + + let mut http_route_lookup: HashMap = HashMap::default(); + for route in &http_routes { + let key = HttpRouteKey::new(&route.method, &route.path); + if http_route_lookup.insert(key, route.procedure_id).is_some() { + return Err(ValidationError::DuplicateHttpRoute { + method: route.method.clone(), + path: route.path.clone().into(), + } + .into()); + } + } + + let CoreValidator { + stored_in_table_def, + typespace_for_generate, + lifecycle_reducers, + .. + } = validator.core; + let typespace_for_generate = typespace_for_generate.finish(); Ok(ModuleDef { @@ -280,6 +326,8 @@ pub fn validate(def: RawModuleDefV10) -> Result { row_level_security_raw, lifecycle_reducers, procedures, + http_routes, + http_route_lookup, raw_module_def_version: RawModuleDefVersion::V10, }) } @@ -779,6 +827,116 @@ impl<'a> ModuleValidatorV10<'a> { param_columns, }) } + + fn validate_http_route_def( + &mut self, + route: RawHttpRouteDefV10, + procedures: &IndexMap, + expected_request: &AlgebraicType, + expected_response: &AlgebraicType, + ) -> Result { + let RawHttpRouteDefV10 { + handler_function, + method, + path, + } = route; + + if !matches!( + method, + http::Method::Get | http::Method::Post | http::Method::Put | http::Method::Delete | http::Method::Patch + ) { + return Err(ValidationError::InvalidHttpRouteMethod { method }.into()); + } + + let raw_path = path.path.clone(); + if !is_valid_http_route_path(raw_path.as_ref()) { + return Err(ValidationError::InvalidHttpRoutePath { path: raw_path }.into()); + } + + let handler_ident = self.core.resolve_function_ident(handler_function.clone())?; + let (procedure_id, procedure_def) = procedures + .iter() + .enumerate() + .find(|(_, (name, _))| *name == &handler_ident) + .map(|(idx, (_, def))| (ProcedureId(idx as u32), def)) + .ok_or_else(|| ValidationError::HttpRouteHandlerNotProcedure { + handler: handler_function.clone(), + })?; + + if !procedure_def.visibility.is_private() { + return Err(ValidationError::HttpRouteHandlerMustBePrivate { + handler: handler_function.clone(), + } + .into()); + } + + if procedure_def.params.elements.len() != 1 { + return Err(ValidationError::HttpRouteHandlerInvalidParams { + handler: handler_function.clone(), + expected: PrettyAlgebraicType(expected_request.clone()), + actual: PrettyAlgebraicType::from(procedure_def.params.clone()), + } + .into()); + } + + let actual_arg_ty = WithTypespace::new(self.core.typespace, &procedure_def.params.elements[0].algebraic_type) + .resolve_refs() + .expect("procedure arg types must be valid"); + if actual_arg_ty != *expected_request { + return Err(ValidationError::HttpRouteHandlerInvalidParams { + handler: handler_function.clone(), + expected: PrettyAlgebraicType(expected_request.clone()), + actual: PrettyAlgebraicType(actual_arg_ty), + } + .into()); + } + + let actual_return_ty = WithTypespace::new(self.core.typespace, &procedure_def.return_type) + .resolve_refs() + .expect("procedure return types must be valid"); + if actual_return_ty != *expected_response { + return Err(ValidationError::HttpRouteHandlerInvalidReturnType { + handler: handler_function.clone(), + expected: PrettyAlgebraicType(expected_response.clone()), + actual: PrettyAlgebraicType(actual_return_ty), + } + .into()); + } + + Ok(HttpRouteDef { + method, + path: raw_path.to_string(), + procedure_id, + }) + } +} + +fn http_route_expected_types(policy: CaseConversionPolicy) -> (AlgebraicType, AlgebraicType) { + let mut builder = RawModuleDefV10Builder::new(); + builder.set_case_conversion_policy(policy); + let request_ty = ::make_type(&mut builder); + let response_ty = ::make_type(&mut builder); + let typespace = builder + .finish() + .typespace() + .cloned() + .unwrap_or_else(|| Typespace::EMPTY.clone()); + + let request_ty = WithTypespace::new(&typespace, &request_ty) + .resolve_refs() + .expect("request type must be valid"); + let response_ty = WithTypespace::new(&typespace, &response_ty) + .resolve_refs() + .expect("response type must be valid"); + + (request_ty, response_ty) +} + +fn is_valid_http_route_path(path: &str) -> bool { + let Some(rest) = path.strip_prefix('/') else { + return false; + }; + !rest.is_empty() && !rest.contains('/') } fn attach_lifecycles_to_reducers( diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index d040435afe5..e12967274d6 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -165,6 +165,8 @@ pub fn validate(def: RawModuleDefV9) -> Result { row_level_security_raw, lifecycle_reducers, procedures, + http_routes: Vec::new(), + http_route_lookup: HashMap::default(), raw_module_def_version: RawModuleDefVersion::V9OrEarlier, }) } @@ -650,15 +652,28 @@ impl CoreValidator<'_> { // Recursive function to change typenames in the typespace according to the case conversion // policy. - pub(crate) fn typespace_case_conversion(case_policy: ValidationCase, typespace: &mut Typespace) { + pub(crate) fn typespace_case_conversion( + case_policy: ValidationCase, + typespace: &mut Typespace, + type_ref_names: &HashMap, + enum_variants: &HashMap>, + ) { let case_policy_for_enum_variants = if matches!(case_policy, ValidationCase::SnakeCase) { ValidationCase::CamelCase } else { case_policy }; - for ty in &mut typespace.types { - Self::convert_algebraic_type(ty, case_policy, case_policy_for_enum_variants); + for (index, ty) in typespace.types.iter_mut().enumerate() { + let type_ref = AlgebraicTypeRef(index as u32); + let enum_name = type_ref_names.get(&type_ref); + Self::convert_algebraic_type( + ty, + case_policy, + case_policy_for_enum_variants, + enum_name, + enum_variants, + ); } } @@ -667,6 +682,8 @@ impl CoreValidator<'_> { ty: &mut AlgebraicType, case_policy: ValidationCase, case_policy_for_enum_variants: ValidationCase, + enum_name: Option<&RawIdentifier>, + enum_variants: &HashMap>, ) { if ty.is_special() { return; @@ -684,6 +701,8 @@ impl CoreValidator<'_> { &mut element.algebraic_type, case_policy, case_policy_for_enum_variants, + None, + enum_variants, ); } } @@ -691,20 +710,35 @@ impl CoreValidator<'_> { for variant in &mut sum.variants.iter_mut() { // Convert the variant name if it exists if let Some(name) = variant.name() { - let new_name = convert(name.clone(), case_policy_for_enum_variants); - variant.name = Some(new_name.into()) + let explicit_name = enum_name + .and_then(|enum_name| enum_variants.get(enum_name)) + .and_then(|variants| variants.get(name)); + if let Some(canonical) = explicit_name { + variant.name = Some(canonical.clone()); + } else { + let new_name = convert(name.clone(), case_policy_for_enum_variants); + variant.name = Some(new_name.into()) + } } // Recursively convert the variant's type Self::convert_algebraic_type( &mut variant.algebraic_type, case_policy, case_policy_for_enum_variants, + None, + enum_variants, ); } } AlgebraicType::Array(array) => { // Arrays contain a base type that might need conversion - Self::convert_algebraic_type(&mut array.elem_ty, case_policy, case_policy_for_enum_variants); + Self::convert_algebraic_type( + &mut array.elem_ty, + case_policy, + case_policy_for_enum_variants, + None, + enum_variants, + ); } _ => {} } diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index 06f284998b5..6d31434da25 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -1,6 +1,6 @@ use spacetimedb_data_structures::error_stream::ErrorStream; use spacetimedb_lib::db::raw_def::v9::{Lifecycle, RawScopedTypeNameV9}; -use spacetimedb_lib::{ProductType, SumType}; +use spacetimedb_lib::{http, ProductType, SumType}; use spacetimedb_primitives::{ColId, ColList, ColSet}; use spacetimedb_sats::algebraic_type::fmt::fmt_algebraic_type; use spacetimedb_sats::{bsatn::DecodeError, raw_identifier::RawIdentifier, AlgebraicType, AlgebraicTypeRef}; @@ -119,6 +119,28 @@ pub enum ValidationError { expected: PrettyAlgebraicType, actual: PrettyAlgebraicType, }, + #[error("HTTP route method {method:?} is not supported")] + InvalidHttpRouteMethod { method: http::Method }, + #[error("HTTP route path `{path}` is invalid")] + InvalidHttpRoutePath { path: RawIdentifier }, + #[error("HTTP route handler `{handler}` does not refer to a procedure")] + HttpRouteHandlerNotProcedure { handler: RawIdentifier }, + #[error("HTTP route handler `{handler}` must be private")] + HttpRouteHandlerMustBePrivate { handler: RawIdentifier }, + #[error("HTTP route handler `{handler}` has invalid params: expected {expected}, got {actual}")] + HttpRouteHandlerInvalidParams { + handler: RawIdentifier, + expected: PrettyAlgebraicType, + actual: PrettyAlgebraicType, + }, + #[error("HTTP route handler `{handler}` has invalid return type: expected {expected}, got {actual}")] + HttpRouteHandlerInvalidReturnType { + handler: RawIdentifier, + expected: PrettyAlgebraicType, + actual: PrettyAlgebraicType, + }, + #[error("HTTP route {method:?} `{path}` is defined multiple times")] + DuplicateHttpRoute { method: http::Method, path: RawIdentifier }, #[error("Table name is reserved for system use: {table}")] TableNameReserved { table: Identifier }, #[error("Row-level security invalid: `{error}`, query: `{sql}")] diff --git a/crates/smoketests/tests/smoketests/http_routes.rs b/crates/smoketests/tests/smoketests/http_routes.rs new file mode 100644 index 00000000000..80a1b173e25 --- /dev/null +++ b/crates/smoketests/tests/smoketests/http_routes.rs @@ -0,0 +1,26 @@ +use spacetimedb_smoketests::Smoketest; + +#[test] +fn test_http_route_get() { + let module_code = r#" +use spacetimedb::{procedure, ProcedureContext, http::{Request, Response, Body}}; + +#[procedure(route = get("/hello"))] +fn hello(_ctx: &mut ProcedureContext, _request: Request) -> Response { + Response::builder() + .status(200) + .body(Body::from("HELLO WORLD")) + .unwrap() +} +"#; + + let test = Smoketest::builder().module_code(module_code).build(); + let identity = test.database_identity.as_ref().expect("No database published"); + + let response = test + .api_call("GET", &format!("/v1/database/{}/route/hello", identity)) + .expect("HTTP route request failed"); + + assert_eq!(response.status_code, 200); + assert_eq!(String::from_utf8_lossy(&response.body), "HELLO WORLD"); +} diff --git a/crates/smoketests/tests/smoketests/mod.rs b/crates/smoketests/tests/smoketests/mod.rs index f5053652dd3..8dc2561c505 100644 --- a/crates/smoketests/tests/smoketests/mod.rs +++ b/crates/smoketests/tests/smoketests/mod.rs @@ -19,6 +19,7 @@ mod domains; mod fail_initial_publish; mod filtering; mod http_egress; +mod http_routes; mod logs_level_filter; mod module_nested_op; mod modules; From 757990ae3c413422b54256417946e9aced70cccb Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Fri, 13 Mar 2026 11:53:54 -0400 Subject: [PATCH 2/3] Cargo fmt --- crates/bindings-macro/src/procedure.rs | 14 +++++++++++--- crates/bindings/src/http.rs | 4 +--- crates/bindings/src/rt.rs | 6 ++---- crates/client-api/src/routes/database.rs | 18 +++++++++++------- crates/lib/src/db/raw_def/v10.rs | 23 ++++++++++++----------- crates/schema/src/def/validate/v10.rs | 4 +--- crates/schema/src/def/validate/v9.rs | 8 +------- 7 files changed, 39 insertions(+), 38 deletions(-) diff --git a/crates/bindings-macro/src/procedure.rs b/crates/bindings-macro/src/procedure.rs index db4c1cc2a7e..8406031da22 100644 --- a/crates/bindings-macro/src/procedure.rs +++ b/crates/bindings-macro/src/procedure.rs @@ -59,7 +59,10 @@ fn parse_route_expr(expr: Expr) -> syn::Result { return Err(syn::Error::new_spanned(args, "expected a single path argument")); } - let Expr::Lit(ExprLit { lit: Lit::Str(path), .. }) = args.first().unwrap() else { + let Expr::Lit(ExprLit { + lit: Lit::Str(path), .. + }) = args.first().unwrap() + else { return Err(syn::Error::new_spanned(args, "expected a string literal path")); }; @@ -129,7 +132,9 @@ pub(crate) fn procedure_impl(_args: ProcedureArgs, original_function: &ItemFn) - let lifetime_params = &original_function.sig.generics; let lifetime_where_clause = &lifetime_params.where_clause; - let (generated_describe_function, wrapper_fn, invoke_target, fn_kind_ty, return_type_ty) = if let Some(route) = route { + let (generated_describe_function, wrapper_fn, invoke_target, fn_kind_ty, return_type_ty) = if let Some(route) = + route + { let RouteAttr { method, path } = route.clone(); let method_str = method.to_string(); let method_expr = match method_str.as_str() { @@ -149,7 +154,10 @@ pub(crate) fn procedure_impl(_args: ProcedureArgs, original_function: &ItemFn) - let path_value = path.value(); let valid_path = path_value.starts_with('/') && !path_value[1..].is_empty() && !path_value[1..].contains('/'); if !valid_path { - return Err(syn::Error::new_spanned(path, "route path must be a single segment starting with `/`")); + return Err(syn::Error::new_spanned( + path, + "route path must be a single segment starting with `/`", + )); } let wrapper_name = format_ident!("__spacetimedb_http_route_wrapper_{}", func_name); diff --git a/crates/bindings/src/http.rs b/crates/bindings/src/http.rs index 31dbd8330f1..2290b9e7ad3 100644 --- a/crates/bindings/src/http.rs +++ b/crates/bindings/src/http.rs @@ -200,9 +200,7 @@ fn convert_response(response: st_http::Response) -> http::Result http::Result> { +pub fn request_and_body_to_http(request: st_http::RequestAndBody) -> http::Result> { let st_http::RequestAndBody { request, body } = request; let parts = convert_request_from_st(request)?; Ok(http::Request::from_parts(parts, Body::from_bytes(body))) diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index 80bfdbbde64..5ac60318a88 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -819,10 +819,8 @@ where I: FnInfo, { register_describer(move |module| { - let params = - <(spacetimedb_lib::http::RequestAndBody,) as Args>::schema::(&mut module.inner); - let ret_ty = - ::make_type(&mut module.inner); + let params = <(spacetimedb_lib::http::RequestAndBody,) as Args>::schema::(&mut module.inner); + let ret_ty = ::make_type(&mut module.inner); module.inner.add_procedure_with_visibility( I::NAME, params, diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index e23e3fe17ed..b23f0d10295 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -21,9 +21,9 @@ use axum::response::{ErrorResponse, IntoResponse}; use axum::routing::MethodRouter; use axum::Extension; use axum_extra::TypedHeader; -use http_body_util::BodyExt; use futures::TryStreamExt; use http::StatusCode; +use http_body_util::BodyExt; use log::{info, warn}; use serde::Deserialize; use spacetimedb::database_logger::DatabaseLogger; @@ -41,12 +41,12 @@ use spacetimedb_client_api_messages::name::{ }; use spacetimedb_lib::db::raw_def::v10::RawModuleDefV10; use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9; -use spacetimedb_lib::{bsatn, http as st_http, sats, AlgebraicValue, Hash, ProductValue, Timestamp}; use spacetimedb_lib::de as st_de; +use spacetimedb_lib::sats::algebraic_value::de::ValueDeserializer; +use spacetimedb_lib::{bsatn, http as st_http, sats, AlgebraicValue, Hash, ProductValue, Timestamp}; use spacetimedb_schema::auto_migrate::{ MigrationPolicy as SchemaMigrationPolicy, MigrationToken, PrettyPrintStyle as AutoMigratePrettyPrintStyle, }; -use spacetimedb_lib::sats::algebraic_value::de::ValueDeserializer; use super::subscribe::{handle_websocket, HasWebSocketOptions}; @@ -250,8 +250,12 @@ pub async fn http_route( .to_bytes(); let request_and_body = convert_request_to_st(parts, body_bytes); - let args = bsatn::to_vec(&request_and_body) - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to encode request: {err}")))?; + let args = bsatn::to_vec(&request_and_body).map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to encode request: {err}"), + ) + })?; let call_result = module .call_procedure_internal( @@ -269,8 +273,8 @@ pub async fn http_route( Err(_) => return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()), }; - let response = decode_response_and_body(result.return_val) - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err))?; + let response = + decode_response_and_body(result.return_val).map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err))?; let response = response_and_body_to_axum(response) .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid response: {err}")))?; diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index bb22bab8846..5c67c72ee05 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -1048,12 +1048,7 @@ impl RawModuleDefV10Builder { params: ProductType, return_type: AlgebraicType, ) { - self.add_procedure_with_visibility( - source_name, - params, - return_type, - FunctionVisibility::ClientCallable, - ); + self.add_procedure_with_visibility(source_name, params, return_type, FunctionVisibility::ClientCallable); } /// Add a procedure to the in-progress module with explicit visibility. @@ -1208,8 +1203,8 @@ impl TypespaceBuilder for RawModuleDefV10Builder { // Alias provided? Relate `name -> slot_ref`. let enum_source_name = if let Some(sats_name) = source_name { let source_name = sats_name_to_scoped_name_v10(sats_name); - let enum_source_name = should_register_enum_variant_names(sats_name) - .then(|| source_name.source_name.clone()); + let enum_source_name = + should_register_enum_variant_names(sats_name).then(|| source_name.source_name.clone()); self.types_mut().push(RawTypeDefV10 { source_name, @@ -1233,14 +1228,20 @@ impl TypespaceBuilder for RawModuleDefV10Builder { let enum_variants = match (&enum_source_name, &self.typespace_mut()[slot_ref]) { (Some(enum_source_name), AlgebraicType::Sum(sum)) => Some(( enum_source_name.clone(), - sum.variants.iter().filter_map(|variant| variant.name().cloned()).collect::>(), + sum.variants + .iter() + .filter_map(|variant| variant.name().cloned()) + .collect::>(), )), _ => None, }; if let Some((enum_source_name, variant_names)) = enum_variants { for variant_name in variant_names { - self.explicit_names_mut() - .insert_enum_variant(enum_source_name.clone(), variant_name.clone(), variant_name); + self.explicit_names_mut().insert_enum_variant( + enum_source_name.clone(), + variant_name.clone(), + variant_name, + ); } } AlgebraicType::Ref(slot_ref) diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index 124d40f3cd6..e63fa186427 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -287,9 +287,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .cloned() .into_iter() .flatten() - .map(|route| { - validator.validate_http_route_def(route, &procedures, &expected_request_ty, &expected_response_ty) - }) + .map(|route| validator.validate_http_route_def(route, &procedures, &expected_request_ty, &expected_response_ty)) .collect_all_errors::>() .map_err(|errors| errors.sort_deduplicate())?; diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index e12967274d6..860fea2ee87 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -667,13 +667,7 @@ impl CoreValidator<'_> { for (index, ty) in typespace.types.iter_mut().enumerate() { let type_ref = AlgebraicTypeRef(index as u32); let enum_name = type_ref_names.get(&type_ref); - Self::convert_algebraic_type( - ty, - case_policy, - case_policy_for_enum_variants, - enum_name, - enum_variants, - ); + Self::convert_algebraic_type(ty, case_policy, case_policy_for_enum_variants, enum_name, enum_variants); } } From fbbe5b680b683e5ff4d6475237cf98adb9910384 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Mon, 16 Mar 2026 11:51:21 -0400 Subject: [PATCH 3/3] Special-ify some HTTP types Rather than having HTTP types participate specialy in the case-conversion infra, this commit (authored by Codex, with message by me) makes them into "special" types with double-underscore variant and field names, and with special recognition by codegen and schema validation. --- crates/codegen/src/cpp.rs | 3 + crates/codegen/src/csharp.rs | 9 +++ crates/codegen/src/rust.rs | 2 + crates/codegen/src/typescript.rs | 3 + crates/codegen/src/unrealcpp.rs | 27 +++++++++ crates/lib/src/db/raw_def/v10.rs | 62 ++----------------- crates/lib/src/http.rs | 32 ++++++++-- crates/sats/src/product_type.rs | 62 +++++++++++++++++++ crates/sats/src/sum_type.rs | 82 +++++++++++++++++++++++++- crates/schema/src/def/validate/v10.rs | 22 +------ crates/schema/src/def/validate/v9.rs | 38 ++---------- crates/schema/src/type_for_generate.rs | 14 +++++ 12 files changed, 239 insertions(+), 117 deletions(-) diff --git a/crates/codegen/src/cpp.rs b/crates/codegen/src/cpp.rs index 3f20b4d271a..32785ed6f7e 100644 --- a/crates/codegen/src/cpp.rs +++ b/crates/codegen/src/cpp.rs @@ -90,6 +90,9 @@ impl<'opts> Cpp<'opts> { AlgebraicTypeUse::TimeDuration => write!(output, "__sdk::TimeDuration"), AlgebraicTypeUse::ScheduleAt => write!(output, "__sdk::ScheduleAt"), AlgebraicTypeUse::Uuid => write!(output, "__sdk::Uuid"), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in C++ output") + } AlgebraicTypeUse::Unit => write!(output, "std::monostate"), AlgebraicTypeUse::Never => write!(output, "std::monostate"), AlgebraicTypeUse::Ref(type_ref) => { diff --git a/crates/codegen/src/csharp.rs b/crates/codegen/src/csharp.rs index e449f703181..b0d3047e3d0 100644 --- a/crates/codegen/src/csharp.rs +++ b/crates/codegen/src/csharp.rs @@ -1231,6 +1231,9 @@ fn ty_fmt<'a>(module: &'a ModuleDef, ty: &'a AlgebraicTypeUse) -> impl fmt::Disp PrimitiveType::F32 => "float", PrimitiveType::F64 => "double", }), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in C# output") + } AlgebraicTypeUse::Never => unimplemented!(), }) } @@ -1276,6 +1279,9 @@ fn ty_fmt_with_ns<'a>(module: &'a ModuleDef, ty: &'a AlgebraicTypeUse, namespace PrimitiveType::F32 => "float", PrimitiveType::F64 => "double", }), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in C# output") + } AlgebraicTypeUse::Never => unimplemented!(), }) } @@ -1307,6 +1313,9 @@ fn default_init(ctx: &TypespaceForGenerate, ty: &AlgebraicTypeUse) -> Option<&'s | AlgebraicTypeUse::Timestamp | AlgebraicTypeUse::TimeDuration | AlgebraicTypeUse::Uuid => None, + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in C# output") + } AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in C# output"), } } diff --git a/crates/codegen/src/rust.rs b/crates/codegen/src/rust.rs index c4a24c61b62..f818352767e 100644 --- a/crates/codegen/src/rust.rs +++ b/crates/codegen/src/rust.rs @@ -826,6 +826,8 @@ pub fn write_type(module: &ModuleDef, out: &mut W, ty: &AlgebraicTypeU AlgebraicTypeUse::Timestamp => write!(out, "__sdk::Timestamp")?, AlgebraicTypeUse::TimeDuration => write!(out, "__sdk::TimeDuration")?, AlgebraicTypeUse::Uuid => write!(out, "__sdk::Uuid")?, + AlgebraicTypeUse::HttpRequestAndBody => write!(out, "__sdk::HttpRequestAndBody")?, + AlgebraicTypeUse::HttpResponseAndBody => write!(out, "__sdk::HttpResponseAndBody")?, AlgebraicTypeUse::ScheduleAt => write!(out, "__sdk::ScheduleAt")?, AlgebraicTypeUse::Option(inner_ty) => { write!(out, "Option::<")?; diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 549530451df..5ce2d4bbf0d 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -789,6 +789,7 @@ fn write_type_builder(module: &ModuleDef, out: &mut W, ty: &AlgebraicT AlgebraicTypeUse::TimeDuration => write!(out, "__t.timeDuration()")?, AlgebraicTypeUse::ScheduleAt => write!(out, "__t.scheduleAt()")?, AlgebraicTypeUse::Uuid => write!(out, "__t.uuid()")?, + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => unimplemented!(), AlgebraicTypeUse::Option(inner_ty) => { write!(out, "__t.option(")?; write_type_builder(module, out, inner_ty)?; @@ -916,6 +917,7 @@ fn needs_parens_within_array(ty: &AlgebraicTypeUse) -> bool { AlgebraicTypeUse::ScheduleAt | AlgebraicTypeUse::Option(_) | AlgebraicTypeUse::Result { .. } => { true } + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => unimplemented!(), } } @@ -934,6 +936,7 @@ pub fn write_type( AlgebraicTypeUse::Timestamp => write!(out, "__Infer")?, AlgebraicTypeUse::TimeDuration => write!(out, "__Infer")?, AlgebraicTypeUse::Uuid => write!(out, "__Uuid")?, + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => unimplemented!(), AlgebraicTypeUse::ScheduleAt => write!( out, "{{ tag: \"Interval\", value: __Infer }} | {{ tag: \"Time\", value: __Infer }}" diff --git a/crates/codegen/src/unrealcpp.rs b/crates/codegen/src/unrealcpp.rs index c4b9fd81efb..44b5baed264 100644 --- a/crates/codegen/src/unrealcpp.rs +++ b/crates/codegen/src/unrealcpp.rs @@ -4163,6 +4163,9 @@ fn get_array_element_type_name(module: &ModuleDef, elem: &AlgebraicTypeUse) -> S AlgebraicTypeUse::Timestamp => "Timestamp".to_string(), AlgebraicTypeUse::TimeDuration => "TimeDuration".to_string(), AlgebraicTypeUse::Uuid => "Uuid".to_string(), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::ScheduleAt => "ScheduleAt".to_string(), AlgebraicTypeUse::Ref(r) => type_ref_name(module, *r), AlgebraicTypeUse::Option(nested_inner) => { @@ -4203,6 +4206,9 @@ fn get_optional_type_name(module: &ModuleDef, inner: &AlgebraicTypeUse) -> Strin AlgebraicTypeUse::Timestamp => "OptionalTimestamp".to_string(), AlgebraicTypeUse::TimeDuration => "OptionalTimeDuration".to_string(), AlgebraicTypeUse::Uuid => "OptionalUuid".to_string(), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::ScheduleAt => "OptionalScheduleAt".to_string(), AlgebraicTypeUse::Array(elem) => { // Generate specific optional array types based on element type @@ -4523,6 +4529,9 @@ fn get_type_name_for_result(module: &ModuleDef, ty: &AlgebraicTypeUse) -> String AlgebraicTypeUse::TimeDuration => "TimeDuration".to_string(), AlgebraicTypeUse::ScheduleAt => "ScheduleAt".to_string(), AlgebraicTypeUse::Uuid => "Uuid".to_string(), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::Unit => "Unit".to_string(), AlgebraicTypeUse::Array(elem) => { // Generate specific array types based on element type @@ -4918,6 +4927,9 @@ fn should_pass_by_value_in_delegate(_module: &ModuleDef, ty: &AlgebraicTypeUse) AlgebraicTypeUse::Timestamp => false, // FSpacetimeDBTimestamp is a USTRUCT AlgebraicTypeUse::TimeDuration => false, // FSpacetimeDBTimeDuration is a USTRUCT AlgebraicTypeUse::Uuid => false, // FSpacetimeDBUuid is a USTRUCT + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } // Custom structs/enums use const references AlgebraicTypeUse::Ref(_) => false, AlgebraicTypeUse::Array(_) => false, // Arrays use const references @@ -4961,6 +4973,9 @@ fn is_blueprintable(module: &ModuleDef, ty: &AlgebraicTypeUse) -> bool { AlgebraicTypeUse::Timestamp => true, AlgebraicTypeUse::TimeDuration => true, AlgebraicTypeUse::Uuid => true, + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::ScheduleAt => true, // ScheduleAt is blueprintable as a property (TObjectPtr) AlgebraicTypeUse::Unit => true, AlgebraicTypeUse::Ref(r) => { @@ -5000,6 +5015,9 @@ fn is_type_blueprintable_for_delegates(module: &ModuleDef, ty: &AlgebraicTypeUse AlgebraicTypeUse::Timestamp => true, AlgebraicTypeUse::TimeDuration => true, AlgebraicTypeUse::Uuid => true, + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::ScheduleAt => true, AlgebraicTypeUse::Unit => true, AlgebraicTypeUse::Ref(r) => { @@ -5428,6 +5446,9 @@ fn cpp_ty_fmt_impl<'a>( AlgebraicTypeUse::Timestamp => f.write_str("FSpacetimeDBTimestamp"), AlgebraicTypeUse::TimeDuration => f.write_str("FSpacetimeDBTimeDuration"), AlgebraicTypeUse::Uuid => f.write_str("FSpacetimeDBUuid"), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::ScheduleAt => f.write_str("FSpacetimeDBScheduleAt"), AlgebraicTypeUse::Unit => f.write_str("FSpacetimeDBUnit"), @@ -5497,6 +5518,9 @@ fn cpp_ty_init_fmt_impl(module: &ModuleDef, ty: &AlgebraicTypeUse) -> String { AlgebraicTypeUse::TimeDuration => String::new(), AlgebraicTypeUse::ScheduleAt => String::new(), AlgebraicTypeUse::Uuid => String::new(), + AlgebraicTypeUse::HttpRequestAndBody | AlgebraicTypeUse::HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } AlgebraicTypeUse::Unit => String::new(), // --------- references to user-defined types --------- AlgebraicTypeUse::Ref(r) => { @@ -5560,6 +5584,9 @@ fn collect_includes_for_type(module: &ModuleDef, ty: &AlgebraicTypeUse, out: &mu Identity | ConnectionId | Timestamp | TimeDuration | ScheduleAt | Uuid => { out.insert("Types/Builtins.h".to_string()); } + HttpRequestAndBody | HttpResponseAndBody => { + unimplemented!("Http request/response types are not supported in Unreal output") + } // Large integer primitives also need Builtins.h (for LargeIntegers.h) Primitive(PrimitiveType::I128) | Primitive(PrimitiveType::U128) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index 5c67c72ee05..4822228e2db 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -135,18 +135,6 @@ pub struct NameMapping { pub canonical_name: RawIdentifier, } -#[derive(Debug, Clone, SpacetimeType)] -#[sats(crate = crate)] -#[cfg_attr(feature = "test", derive(PartialEq, Eq, Ord, PartialOrd))] -pub struct EnumVariantNameMapping { - /// The source name of the containing enum. - pub enum_source_name: RawIdentifier, - /// The source name of the variant. - pub variant_source_name: RawIdentifier, - /// The canonical name of the variant. - pub variant_canonical_name: RawIdentifier, -} - #[derive(Debug, Clone, SpacetimeType)] #[sats(crate = crate)] #[cfg_attr(feature = "test", derive(PartialEq, Eq, Ord, PartialOrd))] @@ -155,7 +143,6 @@ pub enum ExplicitNameEntry { Table(NameMapping), Function(NameMapping), Index(NameMapping), - EnumVariant(EnumVariantNameMapping), } #[derive(Debug, Default, Clone, SpacetimeType)] @@ -196,19 +183,6 @@ impl ExplicitNames { })); } - pub fn insert_enum_variant( - &mut self, - enum_source_name: impl Into, - variant_source_name: impl Into, - variant_canonical_name: impl Into, - ) { - self.insert(ExplicitNameEntry::EnumVariant(EnumVariantNameMapping { - enum_source_name: enum_source_name.into(), - variant_source_name: variant_source_name.into(), - variant_canonical_name: variant_canonical_name.into(), - })); - } - pub fn merge(&mut self, other: ExplicitNames) { self.entries.extend(other.entries); } @@ -1193,7 +1167,7 @@ impl TypespaceBuilder for RawModuleDefV10Builder { if let btree_map::Entry::Occupied(o) = self.type_map.entry(typeid) { AlgebraicType::Ref(*o.get()) } else { - let (slot_ref, enum_source_name) = { + let slot_ref = { let ts = self.typespace_mut(); // Bind a fresh alias to the unit type. let slot_ref = ts.add(AlgebraicType::unit()); @@ -1201,10 +1175,8 @@ impl TypespaceBuilder for RawModuleDefV10Builder { self.type_map.insert(typeid, slot_ref); // Alias provided? Relate `name -> slot_ref`. - let enum_source_name = if let Some(sats_name) = source_name { + if let Some(sats_name) = source_name { let source_name = sats_name_to_scoped_name_v10(sats_name); - let enum_source_name = - should_register_enum_variant_names(sats_name).then(|| source_name.source_name.clone()); self.types_mut().push(RawTypeDefV10 { source_name, @@ -1215,44 +1187,18 @@ impl TypespaceBuilder for RawModuleDefV10Builder { // macro doesn't know about the default ordering yet. custom_ordering: true, }); - enum_source_name - } else { - None - }; - (slot_ref, enum_source_name) + } + slot_ref }; // Borrow of `v` has ended here, so we can now convince the borrow checker. let ty = make_ty(self); self.typespace_mut()[slot_ref] = ty; - let enum_variants = match (&enum_source_name, &self.typespace_mut()[slot_ref]) { - (Some(enum_source_name), AlgebraicType::Sum(sum)) => Some(( - enum_source_name.clone(), - sum.variants - .iter() - .filter_map(|variant| variant.name().cloned()) - .collect::>(), - )), - _ => None, - }; - if let Some((enum_source_name, variant_names)) = enum_variants { - for variant_name in variant_names { - self.explicit_names_mut().insert_enum_variant( - enum_source_name.clone(), - variant_name.clone(), - variant_name, - ); - } - } AlgebraicType::Ref(slot_ref) } } } -fn should_register_enum_variant_names(sats_name: &str) -> bool { - matches!(sats_name, "HttpMethod" | "HttpVersion") -} - pub fn reducer_default_ok_return_type() -> AlgebraicType { AlgebraicType::unit() } diff --git a/crates/lib/src/http.rs b/crates/lib/src/http.rs index 0334a4ba116..dd4e53b44df 100644 --- a/crates/lib/src/http.rs +++ b/crates/lib/src/http.rs @@ -18,7 +18,13 @@ //! Instead, if/when we want to add new functionality which requires sending additional information, //! we'll define a new versioned ABI call which uses new types for interchange. -use spacetimedb_sats::{time_duration::TimeDuration, SpacetimeType}; +use crate::de::Deserialize; +use crate::ser::Serialize; +use spacetimedb_sats::{ + product_type::{HTTP_BODY_TAG, HTTP_REQUEST_TAG, HTTP_RESPONSE_TAG}, + time_duration::TimeDuration, + AlgebraicType, SpacetimeType, +}; /// Represents an HTTP request which can be made from a procedure running in a SpacetimeDB database. #[derive(Clone, SpacetimeType)] @@ -170,20 +176,36 @@ impl Response { /// /// This is used for incoming HTTP route handling where the body bytes are kept separate /// from the metadata-only [`Request`]. -#[derive(Clone, SpacetimeType)] -#[sats(crate = crate, name = "HttpRequestAndBody")] +#[derive(Clone, Serialize, Deserialize)] pub struct RequestAndBody { pub request: Request, pub body: Box<[u8]>, } +impl SpacetimeType for RequestAndBody { + fn make_type(typespace: &mut S) -> AlgebraicType { + AlgebraicType::product([ + (HTTP_REQUEST_TAG, Request::make_type(typespace)), + (HTTP_BODY_TAG, AlgebraicType::array(AlgebraicType::U8)), + ]) + } +} + /// A response paired with its body bytes. /// /// This is used for HTTP route handling where the body bytes are kept separate /// from the metadata-only [`Response`]. -#[derive(Clone, SpacetimeType)] -#[sats(crate = crate, name = "HttpResponseAndBody")] +#[derive(Clone, Serialize, Deserialize)] pub struct ResponseAndBody { pub response: Response, pub body: Box<[u8]>, } + +impl SpacetimeType for ResponseAndBody { + fn make_type(typespace: &mut S) -> AlgebraicType { + AlgebraicType::product([ + (HTTP_RESPONSE_TAG, Response::make_type(typespace)), + (HTTP_BODY_TAG, AlgebraicType::array(AlgebraicType::U8)), + ]) + } +} diff --git a/crates/sats/src/product_type.rs b/crates/sats/src/product_type.rs index 0131c9365b7..5b56f8c080a 100644 --- a/crates/sats/src/product_type.rs +++ b/crates/sats/src/product_type.rs @@ -22,6 +22,12 @@ pub const TIME_DURATION_TAG: &str = "__time_duration_micros__"; /// The tag used inside the special `UUID` product type. pub const UUID_TAG: &str = "__uuid__"; +/// The tag used inside the special HTTP request-and-body product type. +pub const HTTP_REQUEST_TAG: &str = "__http_request__"; +/// The tag used inside the special HTTP response-and-body product type. +pub const HTTP_RESPONSE_TAG: &str = "__http_response__"; +/// The tag used inside the special HTTP request/response body field. +pub const HTTP_BODY_TAG: &str = "__http_body__"; /// A structural product type of the factors given by `elements`. /// @@ -165,11 +171,62 @@ impl ProductType { tag_name == UUID_TAG } + /// Returns whether this is the special tag of an HTTP request field. + pub fn is_http_request_tag(tag_name: &str) -> bool { + tag_name == HTTP_REQUEST_TAG + } + + /// Returns whether this is the special tag of an HTTP response field. + pub fn is_http_response_tag(tag_name: &str) -> bool { + tag_name == HTTP_RESPONSE_TAG + } + + /// Returns whether this is the special tag of an HTTP body field. + pub fn is_http_body_tag(tag_name: &str) -> bool { + tag_name == HTTP_BODY_TAG + } + /// Returns whether this is the special case of [`crate::uuid::Uuid`]. pub fn is_uuid(&self) -> bool { self.is_newtype_of(UUID_TAG, AlgebraicType::U128) } + /// Returns whether this is the special case of an HTTP request paired with body bytes. + /// Does not follow `Ref`s. + pub fn is_http_request_and_body(&self) -> bool { + matches!( + &*self.elements, + [ + ProductTypeElement { + name: Some(request_name), + .. + }, + ProductTypeElement { + name: Some(body_name), + .. + } + ] if &**request_name == HTTP_REQUEST_TAG && &**body_name == HTTP_BODY_TAG + ) + } + + /// Returns whether this is the special case of an HTTP response paired with body bytes. + /// Does not follow `Ref`s. + pub fn is_http_response_and_body(&self) -> bool { + matches!( + &*self.elements, + [ + ProductTypeElement { + name: Some(response_name), + .. + }, + ProductTypeElement { + name: Some(body_name), + .. + } + ] if &**response_name == HTTP_RESPONSE_TAG && &**body_name == HTTP_BODY_TAG + ) + } + /// Returns whether this is a special known `tag`, /// currently `Address`, `Identity`, `Timestamp`, `TimeDuration`, `ConnectionId` or `UUID`. pub fn is_special_tag(tag_name: &str) -> bool { @@ -179,6 +236,9 @@ impl ProductType { TIMESTAMP_TAG, TIME_DURATION_TAG, UUID_TAG, + HTTP_REQUEST_TAG, + HTTP_RESPONSE_TAG, + HTTP_BODY_TAG, ] .contains(&tag_name) } @@ -192,6 +252,8 @@ impl ProductType { || self.is_timestamp() || self.is_time_duration() || self.is_uuid() + || self.is_http_request_and_body() + || self.is_http_response_and_body() } /// Returns whether this is a unit type, that is, has no elements. diff --git a/crates/sats/src/sum_type.rs b/crates/sats/src/sum_type.rs index 93e257bf6e8..d788d60b7a0 100644 --- a/crates/sats/src/sum_type.rs +++ b/crates/sats/src/sum_type.rs @@ -17,6 +17,36 @@ pub const OPTION_NONE_TAG: &str = "none"; pub const RESULT_OK_TAG: &str = "ok"; /// The tag used for the `err` variant of the special `result` sum type. pub const RESULT_ERR_TAG: &str = "err"; +/// The tag used for the `Extension` variant of the special HTTP method sum type. +pub const HTTP_METHOD_EXTENSION_TAG: &str = "Extension"; +/// The tag used for the `Get` variant of the special HTTP method sum type. +pub const HTTP_METHOD_GET_TAG: &str = "Get"; +/// The tag used for the `Head` variant of the special HTTP method sum type. +pub const HTTP_METHOD_HEAD_TAG: &str = "Head"; +/// The tag used for the `Post` variant of the special HTTP method sum type. +pub const HTTP_METHOD_POST_TAG: &str = "Post"; +/// The tag used for the `Put` variant of the special HTTP method sum type. +pub const HTTP_METHOD_PUT_TAG: &str = "Put"; +/// The tag used for the `Delete` variant of the special HTTP method sum type. +pub const HTTP_METHOD_DELETE_TAG: &str = "Delete"; +/// The tag used for the `Connect` variant of the special HTTP method sum type. +pub const HTTP_METHOD_CONNECT_TAG: &str = "Connect"; +/// The tag used for the `Options` variant of the special HTTP method sum type. +pub const HTTP_METHOD_OPTIONS_TAG: &str = "Options"; +/// The tag used for the `Trace` variant of the special HTTP method sum type. +pub const HTTP_METHOD_TRACE_TAG: &str = "Trace"; +/// The tag used for the `Patch` variant of the special HTTP method sum type. +pub const HTTP_METHOD_PATCH_TAG: &str = "Patch"; +/// The tag used for the `Http09` variant of the special HTTP version sum type. +pub const HTTP_VERSION_09_TAG: &str = "Http09"; +/// The tag used for the `Http10` variant of the special HTTP version sum type. +pub const HTTP_VERSION_10_TAG: &str = "Http10"; +/// The tag used for the `Http11` variant of the special HTTP version sum type. +pub const HTTP_VERSION_11_TAG: &str = "Http11"; +/// The tag used for the `Http2` variant of the special HTTP version sum type. +pub const HTTP_VERSION_2_TAG: &str = "Http2"; +/// The tag used for the `Http3` variant of the special HTTP version sum type. +pub const HTTP_VERSION_3_TAG: &str = "Http3"; /// A structural sum type. /// @@ -193,9 +223,59 @@ impl SumType { } } + /// Return whether this sum type is the special HTTP method type. + /// Does not follow `Ref`s. + pub fn is_http_method(&self) -> bool { + match &*self.variants { + [get, head, post, put, delete, connect, options, trace, patch, extension] => { + get.has_name(HTTP_METHOD_GET_TAG) + && get.is_unit() + && head.has_name(HTTP_METHOD_HEAD_TAG) + && head.is_unit() + && post.has_name(HTTP_METHOD_POST_TAG) + && post.is_unit() + && put.has_name(HTTP_METHOD_PUT_TAG) + && put.is_unit() + && delete.has_name(HTTP_METHOD_DELETE_TAG) + && delete.is_unit() + && connect.has_name(HTTP_METHOD_CONNECT_TAG) + && connect.is_unit() + && options.has_name(HTTP_METHOD_OPTIONS_TAG) + && options.is_unit() + && trace.has_name(HTTP_METHOD_TRACE_TAG) + && trace.is_unit() + && patch.has_name(HTTP_METHOD_PATCH_TAG) + && patch.is_unit() + && extension.has_name(HTTP_METHOD_EXTENSION_TAG) + && extension.algebraic_type == AlgebraicType::String + } + _ => false, + } + } + + /// Return whether this sum type is the special HTTP version type. + /// Does not follow `Ref`s. + pub fn is_http_version(&self) -> bool { + match &*self.variants { + [http09, http10, http11, http2, http3] => { + http09.has_name(HTTP_VERSION_09_TAG) + && http09.is_unit() + && http10.has_name(HTTP_VERSION_10_TAG) + && http10.is_unit() + && http11.has_name(HTTP_VERSION_11_TAG) + && http11.is_unit() + && http2.has_name(HTTP_VERSION_2_TAG) + && http2.is_unit() + && http3.has_name(HTTP_VERSION_3_TAG) + && http3.is_unit() + } + _ => false, + } + } + /// Returns whether this sum type is a special known type, currently `Option`, `ScheduleAt`, or `Result`. pub fn is_special(&self) -> bool { - self.is_option() || self.is_schedule_at() || self.is_result() + self.is_option() || self.is_schedule_at() || self.is_result() || self.is_http_method() || self.is_http_version() } /// Returns whether this sum type is like on in C without data attached to the variants. diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index e63fa186427..8934c24eeee 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -22,7 +22,6 @@ pub struct ExplicitNamesLookup { pub tables: HashMap, pub functions: HashMap, pub indexes: HashMap, - pub enum_variants: HashMap>, } impl ExplicitNamesLookup { @@ -30,7 +29,6 @@ impl ExplicitNamesLookup { let mut tables = HashMap::default(); let mut functions = HashMap::default(); let mut indexes = HashMap::default(); - let mut enum_variants: HashMap> = HashMap::default(); for entry in ex.into_entries() { match entry { @@ -43,12 +41,6 @@ impl ExplicitNamesLookup { ExplicitNameEntry::Index(m) => { indexes.insert(m.source_name, m.canonical_name); } - ExplicitNameEntry::EnumVariant(m) => { - enum_variants - .entry(m.enum_source_name) - .or_default() - .insert(m.variant_source_name, m.variant_canonical_name); - } _ => {} } } @@ -57,7 +49,6 @@ impl ExplicitNamesLookup { tables, functions, indexes, - enum_variants, } } } @@ -86,12 +77,6 @@ pub fn validate(def: RawModuleDefV10) -> Result { let mut typespace = def.typespace().cloned().unwrap_or_else(|| Typespace::EMPTY.clone()); let known_type_definitions = def.types().into_iter().flatten().map(|def| def.ty); let case_policy = def.case_conversion_policy().into(); - let type_ref_names: HashMap = def - .types() - .into_iter() - .flatten() - .map(|def| (def.ty, def.source_name.source_name.clone())) - .collect(); let explicit_names = def .explicit_names() .cloned() @@ -101,12 +86,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { // Original `typespace` needs to be preserved to be assign `accesor_name`s to columns. let typespace_with_accessor_names = typespace.clone(); // Apply case conversion to `typespace`. - CoreValidator::typespace_case_conversion( - case_policy, - &mut typespace, - &type_ref_names, - &explicit_names.enum_variants, - ); + CoreValidator::typespace_case_conversion(case_policy, &mut typespace); let mut validator = ModuleValidatorV10 { core: CoreValidator { diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 860fea2ee87..0c5eecb3022 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -652,22 +652,15 @@ impl CoreValidator<'_> { // Recursive function to change typenames in the typespace according to the case conversion // policy. - pub(crate) fn typespace_case_conversion( - case_policy: ValidationCase, - typespace: &mut Typespace, - type_ref_names: &HashMap, - enum_variants: &HashMap>, - ) { + pub(crate) fn typespace_case_conversion(case_policy: ValidationCase, typespace: &mut Typespace) { let case_policy_for_enum_variants = if matches!(case_policy, ValidationCase::SnakeCase) { ValidationCase::CamelCase } else { case_policy }; - for (index, ty) in typespace.types.iter_mut().enumerate() { - let type_ref = AlgebraicTypeRef(index as u32); - let enum_name = type_ref_names.get(&type_ref); - Self::convert_algebraic_type(ty, case_policy, case_policy_for_enum_variants, enum_name, enum_variants); + for ty in &mut typespace.types { + Self::convert_algebraic_type(ty, case_policy, case_policy_for_enum_variants); } } @@ -676,8 +669,6 @@ impl CoreValidator<'_> { ty: &mut AlgebraicType, case_policy: ValidationCase, case_policy_for_enum_variants: ValidationCase, - enum_name: Option<&RawIdentifier>, - enum_variants: &HashMap>, ) { if ty.is_special() { return; @@ -695,8 +686,6 @@ impl CoreValidator<'_> { &mut element.algebraic_type, case_policy, case_policy_for_enum_variants, - None, - enum_variants, ); } } @@ -704,35 +693,20 @@ impl CoreValidator<'_> { for variant in &mut sum.variants.iter_mut() { // Convert the variant name if it exists if let Some(name) = variant.name() { - let explicit_name = enum_name - .and_then(|enum_name| enum_variants.get(enum_name)) - .and_then(|variants| variants.get(name)); - if let Some(canonical) = explicit_name { - variant.name = Some(canonical.clone()); - } else { - let new_name = convert(name.clone(), case_policy_for_enum_variants); - variant.name = Some(new_name.into()) - } + let new_name = convert(name.clone(), case_policy_for_enum_variants); + variant.name = Some(new_name.into()) } // Recursively convert the variant's type Self::convert_algebraic_type( &mut variant.algebraic_type, case_policy, case_policy_for_enum_variants, - None, - enum_variants, ); } } AlgebraicType::Array(array) => { // Arrays contain a base type that might need conversion - Self::convert_algebraic_type( - &mut array.elem_ty, - case_policy, - case_policy_for_enum_variants, - None, - enum_variants, - ); + Self::convert_algebraic_type(&mut array.elem_ty, case_policy, case_policy_for_enum_variants); } _ => {} } diff --git a/crates/schema/src/type_for_generate.rs b/crates/schema/src/type_for_generate.rs index edeabdadd99..928d291db48 100644 --- a/crates/schema/src/type_for_generate.rs +++ b/crates/schema/src/type_for_generate.rs @@ -305,6 +305,10 @@ pub enum AlgebraicTypeUse { /// The special `Uuid` type. Uuid, + /// The special HTTP request-and-body type. + HttpRequestAndBody, + /// The special HTTP response-and-body type. + HttpResponseAndBody, /// The unit type (empty product). /// This is *distinct* from a use of a definition of a product type with no elements. @@ -406,6 +410,16 @@ impl TypespaceForGenerateBuilder<'_> { Ok(AlgebraicTypeUse::TimeDuration) } else if ty.is_uuid() { Ok(AlgebraicTypeUse::Uuid) + } else if ty + .as_product() + .is_some_and(|product| product.is_http_request_and_body()) + { + Ok(AlgebraicTypeUse::HttpRequestAndBody) + } else if ty + .as_product() + .is_some_and(|product| product.is_http_response_and_body()) + { + Ok(AlgebraicTypeUse::HttpResponseAndBody) } else if ty.is_unit() { Ok(AlgebraicTypeUse::Unit) } else if ty.is_never() {