Skip to main content

webx_host/
endpoint.rs

1//! Endpoint —IEndpoint implementations for dual-mode dispatch.
2
3use serde_json;
4use std::sync::Arc;
5use webx_core::error::Result;
6use webx_core::http::IHttpContext;
7use webx_core::routing::IEndpoint;
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 live HTTP context, calls the handler
54/// via the HandlerCache call bridge, and writes the response.
55/// Falls back to a descriptive stub message when no dispatch function is registered.
56pub struct StubEndpoint {
57    pub method: &'static str,
58    pub path: &'static str,
59    pub handler_type: &'static str,
60    pub dispatch_fn: Option<webx_core::route::scan::RouteDispatchFn>,
61    /// Rules collected from `#[authorize]`; empty means the endpoint is public.
62    /// Every rule must be satisfied.
63    pub authorization: &'static [webx_core::route::scan::AuthorizeRule],
64    /// Dynamic authorizers (from `IDynamicAuthorizer` DI registrations).
65    /// Checked after the declarative rules and before handler dispatch.
66    /// When `None`, no dynamic authorization checks run (pass-through).
67    pub authorizers: Option<Arc<AuthorizerSet>>,
68}
69
70#[async_trait::async_trait]
71impl IEndpoint for StubEndpoint {
72    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
73        if let Some(dispatch) = self.dispatch_fn {
74            // ── Declarative authorization from `#[authorize]` ──
75            if !self.authorization.is_empty() {
76                let denied = match ctx.claims() {
77                    None => {
78                        write_problem(ctx, 401, "Authentication required").await;
79                        return Ok(());
80                    }
81                    Some(claims) => self
82                        .authorization
83                        .iter()
84                        .find(|rule| !rule.matches(claims.roles(), claims.permissions()))
85                        .map(|rule| (rule.roles, rule.permissions)),
86                };
87
88                if let Some((roles, permissions)) = denied {
89                    let mut ext = HashMap::new();
90                    ext.insert("required_roles".into(), serde_json::json!(roles));
91                    ext.insert(
92                        "required_permissions".into(),
93                        serde_json::json!(permissions),
94                    );
95                    write_forbidden(ctx, "Forbidden: insufficient permissions", ext).await;
96                    return Ok(());
97                }
98
99                // ── Dynamic authorization via IDynamicAuthorizer ──
100                if let Some(ref authorizers) = self.authorizers {
101                    if !authorizers.is_empty() {
102                        if let Some(claims) = ctx.claims() {
103                            let resource_key = ctx.request().route_pattern().unwrap_or(self.path);
104                            let method = ctx.request().method();
105                            authorizers.authorize(claims, resource_key, method).await?;
106                        }
107                    }
108                }
109            }
110
111            let response = dispatch(&mut *ctx).await?;
112
113            ctx.response_mut().set_status(response.status);
114            if !response.content_type.is_empty() {
115                ctx.response_mut()
116                    .set_header("content-type", &response.content_type);
117            }
118            for (key, value) in &response.headers {
119                ctx.response_mut().set_header(key, value);
120            }
121            ctx.response_mut().write_body(response.body).await?;
122            return Ok(());
123        }
124
125        // Missing dispatch — route registered but handler bridge not linked.
126        write_problem(
127            ctx,
128            501,
129            &format!(
130                "Route handler not implemented: {} {} (request type: {})",
131                self.method, self.path, self.handler_type
132            ),
133        )
134        .await;
135        Ok(())
136    }
137}
138
139/// Endpoint for controller-based methods.
140#[allow(clippy::type_complexity)]
141pub struct ControllerEndpoint {
142    handler: Box<
143        dyn Fn(
144                &mut dyn IHttpContext,
145            )
146                -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
147            + Send
148            + Sync,
149    >,
150}
151
152#[async_trait::async_trait]
153impl IEndpoint for ControllerEndpoint {
154    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
155        (self.handler)(ctx).await
156    }
157}
158
159impl ControllerEndpoint {
160    pub fn new<F>(f: F) -> Self
161    where
162        F: for<'a> Fn(
163                &'a mut dyn IHttpContext,
164            ) -> std::pin::Pin<
165                Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
166            > + Send
167            + Sync
168            + 'static,
169    {
170        Self {
171            handler: Box::new(f),
172        }
173    }
174}
175
176/// Endpoint that serves a static JSON payload.
177///
178/// Used for built-in endpoints like `/openapi.json` and `/health`.
179/// The `content_type` field allows RFC 8407 `application/health+json`
180/// for health endpoints while defaulting to `application/json`.
181pub struct StaticJsonEndpoint {
182    pub body: Vec<u8>,
183    pub content_type: &'static str,
184}
185
186impl StaticJsonEndpoint {
187    /// Create a new endpoint with the default `application/json` content type.
188    pub fn new(body: Vec<u8>) -> Self {
189        Self {
190            body,
191            content_type: "application/json",
192        }
193    }
194}
195
196#[async_trait::async_trait]
197impl IEndpoint for StaticJsonEndpoint {
198    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
199        ctx.response_mut().set_status(200);
200        ctx.response_mut()
201            .set_header("content-type", self.content_type);
202        ctx.response_mut().write_bytes(self.body.clone()).await
203    }
204}
205
206/// Endpoint that serves a static HTML payload.
207///
208/// Used for built-in endpoints like `/api/docs`.
209pub struct StaticHtmlEndpoint {
210    pub body: &'static str,
211}
212
213#[async_trait::async_trait]
214impl IEndpoint for StaticHtmlEndpoint {
215    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
216        ctx.response_mut().set_status(200);
217        ctx.response_mut()
218            .set_header("content-type", "text/html; charset=utf-8");
219        ctx.response_mut()
220            .write_bytes(self.body.as_bytes().to_vec())
221            .await
222    }
223}