Skip to main content

rust_webx_host/
endpoint.rs

1//! Endpoint —IEndpoint implementations for dual-mode dispatch.
2
3use rust_webx_core::error::Result;
4use rust_webx_core::http::IHttpContext;
5use rust_webx_core::routing::IEndpoint;
6use serde_json;
7use std::sync::Arc;
8
9use crate::authz::AuthorizerSet;
10use crate::problem_response::{write_forbidden, write_problem};
11use std::collections::HashMap;
12
13/// Endpoint that wraps a boxed async handler.
14#[allow(clippy::type_complexity)]
15pub struct RequestEndpoint {
16    handler: Box<
17        dyn Fn(
18                &mut dyn IHttpContext,
19            )
20                -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
21            + Send
22            + Sync,
23    >,
24}
25
26#[async_trait::async_trait]
27impl IEndpoint for RequestEndpoint {
28    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
29        (self.handler)(ctx).await
30    }
31}
32
33impl RequestEndpoint {
34    pub fn new<F>(f: F) -> Self
35    where
36        F: for<'a> Fn(
37                &'a mut dyn IHttpContext,
38            ) -> std::pin::Pin<
39                Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
40            > + Send
41            + Sync
42            + 'static,
43    {
44        Self {
45            handler: Box::new(f),
46        }
47    }
48}
49
50/// A route endpoint that dispatches to the registered handler via cache.
51///
52/// When a dispatch function is available (generated by the endpoint macros),
53/// it constructs the request from the HTTP request data, calls the handler
54/// via the HandlerCache call bridge, and writes the JSON response.
55/// Falls back to a descriptive stub message when no dispatch function is registered.
56#[allow(clippy::type_complexity)]
57pub struct StubEndpoint {
58    pub method: &'static str,
59    pub path: &'static str,
60    pub handler_type: &'static str,
61    pub dispatch_fn: Option<
62        fn(
63            Vec<u8>,
64            std::collections::HashMap<String, String>,
65            std::collections::HashMap<String, String>,
66            Option<Box<dyn rust_webx_core::auth::IClaims>>,
67        ) -> std::pin::Pin<
68            Box<dyn std::future::Future<Output = Result<rust_webx_core::route::scan::ResponseData>> + Send>,
69        >,
70    >,
71    pub auth_required_role: &'static str,
72    /// Dynamic authorizers (from `IDynamicAuthorizer` DI registrations).
73    /// Checked after static `#[authorize]` and before handler dispatch.
74    /// When `None`, no dynamic authorization checks run (pass-through).
75    pub authorizers: Option<Arc<AuthorizerSet>>,
76}
77
78#[async_trait::async_trait]
79impl IEndpoint for StubEndpoint {
80    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
81        if let Some(dispatch) = self.dispatch_fn {
82            let body_bytes = ctx.request().body_bytes().await.unwrap_or_default();
83            let route_params = ctx.request().route_params().clone();
84            let query_params = ctx.request().query().clone();
85
86            // Extract claims from the context so they can be forwarded explicitly
87            // to the handler via the dispatch function (fearless concurrency).
88            let claims = ctx.claims().map(|c| c.clone_box());
89
90            // ── Authorization check (declared on route via #[authorize]) ──
91            if !self.auth_required_role.is_empty() {
92                match ctx.claims() {
93                    None => {
94                        write_problem(ctx, 401, "Authentication required").await;
95                        return Ok(());
96                    }
97                    Some(claims) => {
98                        if self.auth_required_role != "authenticated"
99                            && !claims
100                                .roles()
101                                .iter()
102                                .any(|r| r.as_str() == self.auth_required_role)
103                        {
104                            let mut ext = HashMap::new();
105                            ext.insert(
106                                "required_role".into(),
107                                serde_json::Value::String(self.auth_required_role.to_string()),
108                            );
109                            write_forbidden(
110                                ctx,
111                                "Forbidden: insufficient permissions",
112                                ext,
113                            )
114                            .await;
115                            return Ok(());
116                        }
117                    }
118                }
119            }
120
121            // ── Dynamic authorization via IDynamicAuthorizer ──
122            if !self.auth_required_role.is_empty() {
123                if let Some(ref authorizers) = self.authorizers {
124                    if !authorizers.is_empty() {
125                        if let Some(claims) = ctx.claims() {
126                            let resource_key = ctx.request().route_pattern().unwrap_or(self.path);
127                            let method = ctx.request().method();
128                            authorizers.authorize(claims, resource_key, method).await?;
129                        }
130                    }
131                }
132            }
133
134            let response = dispatch(body_bytes, route_params, query_params, claims).await?;
135
136            ctx.response_mut().set_status(response.status);
137            ctx.response_mut()
138                .set_header("content-type", &response.content_type);
139            ctx.response_mut().write_bytes(response.body).await?;
140            return Ok(());
141        }
142
143        // Fallback: stub message (when no dispatch function is registered)
144        ctx.response_mut().set_status(200);
145        ctx.response_mut()
146            .write_text(&format!(
147                "Matched route: {} {} (handler: {})",
148                self.method, self.path, self.handler_type
149            ))
150            .await?;
151        Ok(())
152    }
153}
154
155/// Endpoint for controller-based methods.
156#[allow(clippy::type_complexity)]
157pub struct ControllerEndpoint {
158    handler: Box<
159        dyn Fn(
160                &mut dyn IHttpContext,
161            )
162                -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
163            + Send
164            + Sync,
165    >,
166}
167
168#[async_trait::async_trait]
169impl IEndpoint for ControllerEndpoint {
170    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
171        (self.handler)(ctx).await
172    }
173}
174
175impl ControllerEndpoint {
176    pub fn new<F>(f: F) -> Self
177    where
178        F: for<'a> Fn(
179                &'a mut dyn IHttpContext,
180            ) -> std::pin::Pin<
181                Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
182            > + Send
183            + Sync
184            + 'static,
185    {
186        Self {
187            handler: Box::new(f),
188        }
189    }
190}
191
192/// Endpoint that serves a static JSON payload.
193///
194/// Used for built-in endpoints like `/openapi.json` and `/health`.
195/// The `content_type` field allows RFC 8407 `application/health+json`
196/// for health endpoints while defaulting to `application/json`.
197pub struct StaticJsonEndpoint {
198    pub body: Vec<u8>,
199    pub content_type: &'static str,
200}
201
202impl StaticJsonEndpoint {
203    /// Create a new endpoint with the default `application/json` content type.
204    pub fn new(body: Vec<u8>) -> Self {
205        Self {
206            body,
207            content_type: "application/json",
208        }
209    }
210}
211
212#[async_trait::async_trait]
213impl IEndpoint for StaticJsonEndpoint {
214    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
215        ctx.response_mut().set_status(200);
216        ctx.response_mut()
217            .set_header("content-type", self.content_type);
218        ctx.response_mut().write_bytes(self.body.clone()).await
219    }
220}
221
222/// Endpoint that serves a static HTML payload.
223///
224/// Used for built-in endpoints like `/api/docs`.
225pub struct StaticHtmlEndpoint {
226    pub body: &'static str,
227}
228
229#[async_trait::async_trait]
230impl IEndpoint for StaticHtmlEndpoint {
231    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
232        ctx.response_mut().set_status(200);
233        ctx.response_mut()
234            .set_header("content-type", "text/html; charset=utf-8");
235        ctx.response_mut()
236            .write_bytes(self.body.as_bytes().to_vec())
237            .await
238    }
239}