Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/api.rs

6.1 KiB, 61 runs

created by r1870400018:10141, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4/// API handler infrastructure.
5///
6/// Mirrors the webhook handler pattern but for general-purpose API
7/// endpoints. Steel provides the trait and registry; apps implement
8/// their own handlers and register them via an `AppExtension` before
9/// starting the server.
10///
11/// Webhooks are notifications from a third party, so the webhook
12/// layer is happy to acknowledge with 200 and return `None` when
13/// there is nothing to say back. API requests come from a client
14/// that expects a response every time, so `ApiHandler::handle`
15/// returns `HttpMessage` unconditionally.
16
17use crate::srv::cfg::ApiRoute;
18
19use oxedyne_fe2o3_core::prelude::*;
20use oxedyne_fe2o3_net::http::{
21 fields::HeaderFields,
22 header::HttpMethod,
23 loc::HttpLocator,
24 msg::HttpMessage,
25 status::HttpStatus,
26};
27
28use std::{
29 collections::HashMap,
30 future::Future,
31 pin::Pin,
32 sync::Arc,
33};
34use tokio_rustls::rustls::ClientConfig;
35
36
37// ┌───────────────────────────────────────────────────────────────────────────┐
38// │ API HANDLER TRAIT │
39// └───────────────────────────────────────────────────────────────────────────┘
40
41/// Apps implement this trait for each custom API endpoint they need -- a
42/// checkout builder that validates a cart and proxies to a payment provider, a
43/// geolocation lookup -- and register instances via an `AppExtension` before
44/// starting Steel.
45///
46/// The handler receives the full incoming request, so it can inspect method,
47/// query string, headers and body, and must always return a response.
48pub trait ApiHandler: Send + Sync + 'static {
49 fn handle<'a>(
50 &'a self,
51 route: &'a ApiRoute,
52 method: HttpMethod,
53 loc: &'a HttpLocator,
54 body: &'a [u8],
55 req_headers: &'a HeaderFields,
56 tls_client: &'a Option<Arc<ClientConfig>>,
57 id: &'a str,
58 ) -> Pin<Box<dyn Future<Output = Outcome<HttpMessage>> + Send + 'a>>;
59}
60
61
62// ┌───────────────────────────────────────────────────────────────────────────┐
63// │ API HANDLER REGISTRY │
64// └───────────────────────────────────────────────────────────────────────────┘
65
66/// Maps handler names, as written in config, to API handler implementations.
67/// Built by the app, usually from `AppExtension::api_handlers`, before server
68/// startup. Stock Steel starts with an empty registry.
69#[derive(Default)]
70pub struct ApiHandlerRegistry {
71 handlers: HashMap<String, Box<dyn ApiHandler>>,
72}
73
74impl ApiHandlerRegistry {
75 pub fn new() -> Self {
76 Self {
77 handlers: HashMap::new(),
78 }
79 }
80
81 /// The name must match the `handler` field in the corresponding `api_routes`
82 /// entry of `config.jdat`.
83 pub fn register<H: ApiHandler>(&mut self, name: &str, handler: H) {
84 self.handlers.insert(name.to_string(), Box::new(handler));
85 }
86
87 pub fn insert_boxed(&mut self, name: String, handler: Box<dyn ApiHandler>) {
88 self.handlers.insert(name, handler);
89 }
90
91 pub fn has(&self, name: &str) -> bool {
92 self.handlers.contains_key(name)
93 }
94
95 pub fn get(&self, name: &str) -> Option<&dyn ApiHandler> {
96 self.handlers.get(name).map(|b| b.as_ref())
97 }
98}
99
100// Manual Debug impl because Box<dyn ApiHandler> is not Debug.
101impl std::fmt::Debug for ApiHandlerRegistry {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 f.debug_struct("ApiHandlerRegistry")
104 .field("handlers", &self.handlers.keys().collect::<Vec<_>>())
105 .finish()
106 }
107}
108
109
110// ┌───────────────────────────────────────────────────────────────────────────┐
111// │ DISPATCH │
112// └───────────────────────────────────────────────────────────────────────────┘
113
114/// Called from the HTTPS server when an `ApiRoute` has its `handler` field set,
115/// meaning the route is served by an in-process handler rather than proxied to a
116/// remote upstream.
117pub async fn dispatch(
118 registry: &ApiHandlerRegistry,
119 route: &ApiRoute,
120 method: HttpMethod,
121 loc: &HttpLocator,
122 body: &[u8],
123 req_headers: &HeaderFields,
124 tls_client: &Option<Arc<ClientConfig>>,
125 id: &str,
126)
127 -> Outcome<HttpMessage>
128{
129 let handler_name = match &route.handler {
130 Some(n) => n,
131 None => {
132 warn!("{}: API route '{}' reached dispatch with no handler name.",
133 id, route.path);
134 return Ok(HttpMessage::respond_with_text(
135 HttpStatus::InternalServerError,
136 "API route misconfigured: no handler name.",
137 ));
138 }
139 };
140 match registry.handlers.get(handler_name) {
141 Some(handler) => handler.handle(
142 route, method, loc, body, req_headers, tls_client, id,
143 ).await,
144 None => {
145 warn!("{}: No registered API handler '{}'.", id, handler_name);
146 Ok(HttpMessage::respond_with_text(
147 HttpStatus::NotFound,
148 "Unknown API handler.",
149 ))
150 }
151 }
152}