helios_engine/
endpoint_builder.rs

1//! # Endpoint Builder Module
2//!
3//! This module provides a simple and ergonomic way to create custom endpoints
4//! for the Helios Engine HTTP server.
5
6use axum::{
7    extract::Json,
8    http::StatusCode,
9    response::{IntoResponse, Response},
10};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::HashMap;
14use std::sync::Arc;
15
16/// A custom endpoint handler function type.
17/// Takes optional request data and returns a response.
18pub type EndpointHandler = Arc<dyn Fn(Option<EndpointRequest>) -> EndpointResponse + Send + Sync>;
19
20/// Request data passed to endpoint handlers.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct EndpointRequest {
23    /// Query parameters from the URL.
24    #[serde(default)]
25    pub query: HashMap<String, String>,
26
27    /// Path parameters (e.g., /users/:id -> {"id": "123"}).
28    #[serde(default)]
29    pub params: HashMap<String, String>,
30
31    /// Request body as JSON (for POST, PUT, PATCH).
32    pub body: Option<Value>,
33}
34
35/// Response from an endpoint handler.
36#[derive(Debug, Clone)]
37pub struct EndpointResponse {
38    /// HTTP status code.
39    pub status: StatusCode,
40
41    /// Response body as JSON.
42    pub body: Value,
43}
44
45impl EndpointResponse {
46    /// Creates a new successful response (200 OK).
47    pub fn ok(body: Value) -> Self {
48        Self {
49            status: StatusCode::OK,
50            body,
51        }
52    }
53
54    /// Creates a new created response (201 CREATED).
55    pub fn created(body: Value) -> Self {
56        Self {
57            status: StatusCode::CREATED,
58            body,
59        }
60    }
61
62    /// Creates a new accepted response (202 ACCEPTED).
63    pub fn accepted(body: Value) -> Self {
64        Self {
65            status: StatusCode::ACCEPTED,
66            body,
67        }
68    }
69
70    /// Creates a new bad request response (400 BAD REQUEST).
71    pub fn bad_request(message: &str) -> Self {
72        Self {
73            status: StatusCode::BAD_REQUEST,
74            body: serde_json::json!({"error": message}),
75        }
76    }
77
78    /// Creates a new not found response (404 NOT FOUND).
79    pub fn not_found(message: &str) -> Self {
80        Self {
81            status: StatusCode::NOT_FOUND,
82            body: serde_json::json!({"error": message}),
83        }
84    }
85
86    /// Creates a new internal server error response (500 INTERNAL SERVER ERROR).
87    pub fn internal_error(message: &str) -> Self {
88        Self {
89            status: StatusCode::INTERNAL_SERVER_ERROR,
90            body: serde_json::json!({"error": message}),
91        }
92    }
93
94    /// Creates a custom response with a specific status code.
95    pub fn with_status(status: StatusCode, body: Value) -> Self {
96        Self { status, body }
97    }
98}
99
100impl IntoResponse for EndpointResponse {
101    fn into_response(self) -> Response {
102        (self.status, Json(self.body)).into_response()
103    }
104}
105
106/// Builder for creating custom endpoints with an ergonomic API.
107pub struct EndpointBuilder {
108    path: String,
109    method: HttpMethod,
110    handler: Option<EndpointHandler>,
111    description: Option<String>,
112}
113
114/// HTTP method for the endpoint.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum HttpMethod {
117    Get,
118    Post,
119    Put,
120    Delete,
121    Patch,
122}
123
124impl EndpointBuilder {
125    /// Creates a new GET endpoint builder.
126    pub fn get(path: impl Into<String>) -> Self {
127        Self {
128            path: path.into(),
129            method: HttpMethod::Get,
130            handler: None,
131            description: None,
132        }
133    }
134
135    /// Creates a new POST endpoint builder.
136    pub fn post(path: impl Into<String>) -> Self {
137        Self {
138            path: path.into(),
139            method: HttpMethod::Post,
140            handler: None,
141            description: None,
142        }
143    }
144
145    /// Creates a new PUT endpoint builder.
146    pub fn put(path: impl Into<String>) -> Self {
147        Self {
148            path: path.into(),
149            method: HttpMethod::Put,
150            handler: None,
151            description: None,
152        }
153    }
154
155    /// Creates a new DELETE endpoint builder.
156    pub fn delete(path: impl Into<String>) -> Self {
157        Self {
158            path: path.into(),
159            method: HttpMethod::Delete,
160            handler: None,
161            description: None,
162        }
163    }
164
165    /// Creates a new PATCH endpoint builder.
166    pub fn patch(path: impl Into<String>) -> Self {
167        Self {
168            path: path.into(),
169            method: HttpMethod::Patch,
170            handler: None,
171            description: None,
172        }
173    }
174
175    /// Sets a static JSON response for the endpoint.
176    /// This is the simplest way to create an endpoint that returns fixed data.
177    pub fn json(mut self, response: Value) -> Self {
178        self.handler = Some(Arc::new(move |_| EndpointResponse::ok(response.clone())));
179        self
180    }
181
182    /// Sets a handler function that receives request data and returns a response.
183    /// This allows for dynamic responses based on query params, path params, and body.
184    pub fn handle<F>(mut self, handler: F) -> Self
185    where
186        F: Fn(Option<EndpointRequest>) -> EndpointResponse + Send + Sync + 'static,
187    {
188        self.handler = Some(Arc::new(handler));
189        self
190    }
191
192    /// Sets a description for the endpoint (for documentation purposes).
193    pub fn description(mut self, description: impl Into<String>) -> Self {
194        self.description = Some(description.into());
195        self
196    }
197
198    /// Builds the endpoint.
199    pub fn build(self) -> CustomEndpoint {
200        CustomEndpoint {
201            path: self.path,
202            method: self.method,
203            handler: self
204                .handler
205                .expect("Handler must be set with .json() or .handle()"),
206            description: self.description,
207        }
208    }
209}
210
211/// A custom endpoint with a handler function.
212#[derive(Clone)]
213pub struct CustomEndpoint {
214    pub path: String,
215    pub method: HttpMethod,
216    pub handler: EndpointHandler,
217    pub description: Option<String>,
218}
219
220/// Helper function to create a simple GET endpoint with a static JSON response.
221///
222/// # Example
223/// ```
224/// use helios_engine::get;
225///
226/// let endpoint = get("/api/status", serde_json::json!({
227///     "status": "ok"
228/// }));
229/// ```
230pub fn get(path: impl Into<String>, response: Value) -> CustomEndpoint {
231    EndpointBuilder::get(path).json(response).build()
232}
233
234/// Helper function to create a simple POST endpoint with a static JSON response.
235pub fn post(path: impl Into<String>, response: Value) -> CustomEndpoint {
236    EndpointBuilder::post(path).json(response).build()
237}
238
239/// Helper function to create a simple PUT endpoint with a static JSON response.
240pub fn put(path: impl Into<String>, response: Value) -> CustomEndpoint {
241    EndpointBuilder::put(path).json(response).build()
242}
243
244/// Helper function to create a simple DELETE endpoint with a static JSON response.
245pub fn delete(path: impl Into<String>, response: Value) -> CustomEndpoint {
246    EndpointBuilder::delete(path).json(response).build()
247}
248
249/// Helper function to create a simple PATCH endpoint with a static JSON response.
250pub fn patch(path: impl Into<String>, response: Value) -> CustomEndpoint {
251    EndpointBuilder::patch(path).json(response).build()
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_endpoint_builder_get() {
260        let endpoint = EndpointBuilder::get("/api/test")
261            .json(serde_json::json!({"test": "data"}))
262            .build();
263
264        assert_eq!(endpoint.path, "/api/test");
265        assert_eq!(endpoint.method, HttpMethod::Get);
266    }
267
268    #[test]
269    fn test_endpoint_response_helpers() {
270        let ok_response = EndpointResponse::ok(serde_json::json!({"status": "ok"}));
271        assert_eq!(ok_response.status, StatusCode::OK);
272
273        let created_response = EndpointResponse::created(serde_json::json!({"id": 1}));
274        assert_eq!(created_response.status, StatusCode::CREATED);
275
276        let not_found = EndpointResponse::not_found("Resource not found");
277        assert_eq!(not_found.status, StatusCode::NOT_FOUND);
278    }
279
280    #[test]
281    fn test_helper_functions() {
282        let endpoint = get("/api/version", serde_json::json!({"version": "1.0.0"}));
283        assert_eq!(endpoint.path, "/api/version");
284        assert_eq!(endpoint.method, HttpMethod::Get);
285    }
286}