spikard_http/
lib.rs

1#![allow(clippy::pedantic, clippy::nursery)]
2#![cfg_attr(test, allow(clippy::all))]
3//! Spikard HTTP Server
4//!
5//! Pure Rust HTTP server with language-agnostic handler trait.
6//! Language bindings (Python, Node, WASM) implement the Handler trait.
7
8pub mod auth;
9pub mod background;
10pub mod bindings;
11pub mod body_metadata;
12pub mod cors;
13pub mod debug;
14#[cfg(feature = "di")]
15pub mod di_handler;
16pub mod handler_response;
17pub mod handler_trait;
18pub mod jsonrpc;
19pub mod lifecycle;
20pub mod middleware;
21pub mod openapi;
22pub mod query_parser;
23pub mod response;
24pub mod server;
25pub mod sse;
26pub mod testing;
27pub mod websocket;
28
29use serde::{Deserialize, Serialize};
30
31#[cfg(test)]
32mod handler_trait_tests;
33
34pub use auth::{Claims, api_key_auth_middleware, jwt_auth_middleware};
35pub use background::{
36    BackgroundHandle, BackgroundJobError, BackgroundJobMetadata, BackgroundRuntime, BackgroundSpawnError,
37    BackgroundTaskConfig,
38};
39pub use body_metadata::ResponseBodySize;
40#[cfg(feature = "di")]
41pub use di_handler::DependencyInjectingHandler;
42pub use handler_response::HandlerResponse;
43pub use handler_trait::{Handler, HandlerResult, RequestData, ValidatedParams};
44pub use jsonrpc::JsonRpcConfig;
45pub use lifecycle::{HookResult, LifecycleHook, LifecycleHooks, LifecycleHooksBuilder, request_hook, response_hook};
46pub use openapi::{ContactInfo, LicenseInfo, OpenApiConfig, SecuritySchemeInfo, ServerInfo};
47pub use response::Response;
48pub use server::Server;
49pub use spikard_core::{
50    CompressionConfig, CorsConfig, Method, ParameterValidator, ProblemDetails, RateLimitConfig, Route, RouteHandler,
51    RouteMetadata, Router, SchemaRegistry, SchemaValidator,
52};
53pub use sse::{SseEvent, SseEventProducer, SseState, sse_handler};
54pub use testing::{ResponseSnapshot, SnapshotError, snapshot_response};
55pub use websocket::{WebSocketHandler, WebSocketState, websocket_handler};
56
57/// Reexport from spikard_core for convenience
58pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
59
60/// JWT authentication configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct JwtConfig {
63    /// Secret key for JWT verification
64    pub secret: String,
65    /// Required algorithm (HS256, HS384, HS512, RS256, etc.)
66    #[serde(default = "default_jwt_algorithm")]
67    pub algorithm: String,
68    /// Required audience claim
69    pub audience: Option<Vec<String>>,
70    /// Required issuer claim
71    pub issuer: Option<String>,
72    /// Leeway for expiration checks (seconds)
73    #[serde(default)]
74    pub leeway: u64,
75}
76
77fn default_jwt_algorithm() -> String {
78    "HS256".to_string()
79}
80
81/// API Key authentication configuration
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ApiKeyConfig {
84    /// Valid API keys
85    pub keys: Vec<String>,
86    /// Header name to check (e.g., "X-API-Key")
87    #[serde(default = "default_api_key_header")]
88    pub header_name: String,
89}
90
91fn default_api_key_header() -> String {
92    "X-API-Key".to_string()
93}
94
95/// Static file serving configuration
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct StaticFilesConfig {
98    /// Directory path to serve
99    pub directory: String,
100    /// URL path prefix (e.g., "/static")
101    pub route_prefix: String,
102    /// Fallback to index.html for directories
103    #[serde(default = "default_true")]
104    pub index_file: bool,
105    /// Cache-Control header value
106    pub cache_control: Option<String>,
107}
108
109/// Server configuration
110#[derive(Debug, Clone)]
111pub struct ServerConfig {
112    /// Host to bind to
113    pub host: String,
114    /// Port to bind to
115    pub port: u16,
116    /// Number of worker threads (unused with tokio)
117    pub workers: usize,
118
119    /// Enable request ID generation and propagation
120    pub enable_request_id: bool,
121    /// Maximum request body size in bytes (None = unlimited, not recommended)
122    pub max_body_size: Option<usize>,
123    /// Request timeout in seconds (None = no timeout)
124    pub request_timeout: Option<u64>,
125    /// Enable compression middleware
126    pub compression: Option<CompressionConfig>,
127    /// Enable rate limiting
128    pub rate_limit: Option<RateLimitConfig>,
129    /// JWT authentication configuration
130    pub jwt_auth: Option<JwtConfig>,
131    /// API Key authentication configuration
132    pub api_key_auth: Option<ApiKeyConfig>,
133    /// Static file serving configuration
134    pub static_files: Vec<StaticFilesConfig>,
135    /// Enable graceful shutdown on SIGTERM/SIGINT
136    pub graceful_shutdown: bool,
137    /// Graceful shutdown timeout (seconds)
138    pub shutdown_timeout: u64,
139    /// OpenAPI documentation configuration
140    pub openapi: Option<crate::openapi::OpenApiConfig>,
141    /// JSON-RPC configuration
142    pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
143    /// Lifecycle hooks for request/response processing
144    pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
145    /// Background task executor configuration
146    pub background_tasks: BackgroundTaskConfig,
147    /// Dependency injection container (requires 'di' feature)
148    #[cfg(feature = "di")]
149    pub di_container: Option<std::sync::Arc<spikard_core::di::DependencyContainer>>,
150}
151
152impl Default for ServerConfig {
153    fn default() -> Self {
154        Self {
155            host: "127.0.0.1".to_string(),
156            port: 8000,
157            workers: 1,
158            enable_request_id: true,
159            max_body_size: Some(10 * 1024 * 1024),
160            request_timeout: Some(30),
161            compression: Some(CompressionConfig::default()),
162            rate_limit: None,
163            jwt_auth: None,
164            api_key_auth: None,
165            static_files: Vec::new(),
166            graceful_shutdown: true,
167            shutdown_timeout: 30,
168            openapi: None,
169            jsonrpc: None,
170            lifecycle_hooks: None,
171            background_tasks: BackgroundTaskConfig::default(),
172            #[cfg(feature = "di")]
173            di_container: None,
174        }
175    }
176}
177
178impl ServerConfig {
179    /// Create a new builder for ServerConfig
180    ///
181    /// # Example
182    ///
183    /// ```ignorerust
184    /// use spikard_http::ServerConfig;
185    ///
186    /// let config = ServerConfig::builder()
187    ///     .port(3000)
188    ///     .host("0.0.0.0")
189    ///     .build();
190    /// ```
191    pub fn builder() -> ServerConfigBuilder {
192        ServerConfigBuilder::default()
193    }
194}
195
196/// Builder for ServerConfig
197///
198/// Provides a fluent API for configuring a Spikard server with dependency injection support.
199///
200/// # Dependency Injection
201///
202/// The builder provides methods to register dependencies that will be injected into handlers:
203///
204/// ```ignorerust
205/// # #[cfg(feature = "di")]
206/// # {
207/// use spikard_http::ServerConfig;
208/// use std::sync::Arc;
209///
210/// let config = ServerConfig::builder()
211///     .port(3000)
212///     .provide_value("app_name", "MyApp".to_string())
213///     .provide_value("max_connections", 100)
214///     .build();
215/// # }
216/// ```
217///
218/// For factory dependencies that create values on-demand:
219///
220/// ```ignorerust
221/// # #[cfg(feature = "di")]
222/// # {
223/// use spikard_http::ServerConfig;
224///
225/// let config = ServerConfig::builder()
226///     .port(3000)
227///     .provide_value("db_url", "postgresql://localhost/mydb".to_string())
228///     .build();
229/// # }
230/// ```
231#[derive(Debug, Clone, Default)]
232pub struct ServerConfigBuilder {
233    config: ServerConfig,
234}
235
236impl ServerConfigBuilder {
237    /// Set the host address to bind to
238    pub fn host(mut self, host: impl Into<String>) -> Self {
239        self.config.host = host.into();
240        self
241    }
242
243    /// Set the port to bind to
244    pub fn port(mut self, port: u16) -> Self {
245        self.config.port = port;
246        self
247    }
248
249    /// Set the number of worker threads (unused with tokio, kept for compatibility)
250    pub fn workers(mut self, workers: usize) -> Self {
251        self.config.workers = workers;
252        self
253    }
254
255    /// Enable or disable request ID generation and propagation
256    pub fn enable_request_id(mut self, enable: bool) -> Self {
257        self.config.enable_request_id = enable;
258        self
259    }
260
261    /// Set maximum request body size in bytes (None = unlimited, not recommended)
262    pub fn max_body_size(mut self, size: Option<usize>) -> Self {
263        self.config.max_body_size = size;
264        self
265    }
266
267    /// Set request timeout in seconds (None = no timeout)
268    pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
269        self.config.request_timeout = timeout;
270        self
271    }
272
273    /// Set compression configuration
274    pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
275        self.config.compression = compression;
276        self
277    }
278
279    /// Set rate limiting configuration
280    pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
281        self.config.rate_limit = rate_limit;
282        self
283    }
284
285    /// Set JWT authentication configuration
286    pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
287        self.config.jwt_auth = jwt_auth;
288        self
289    }
290
291    /// Set API key authentication configuration
292    pub fn api_key_auth(mut self, api_key_auth: Option<ApiKeyConfig>) -> Self {
293        self.config.api_key_auth = api_key_auth;
294        self
295    }
296
297    /// Add static file serving configuration
298    pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
299        self.config.static_files = static_files;
300        self
301    }
302
303    /// Add a single static file serving configuration
304    pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
305        self.config.static_files.push(static_file);
306        self
307    }
308
309    /// Enable or disable graceful shutdown on SIGTERM/SIGINT
310    pub fn graceful_shutdown(mut self, enable: bool) -> Self {
311        self.config.graceful_shutdown = enable;
312        self
313    }
314
315    /// Set graceful shutdown timeout in seconds
316    pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
317        self.config.shutdown_timeout = timeout;
318        self
319    }
320
321    /// Set OpenAPI documentation configuration
322    pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
323        self.config.openapi = openapi;
324        self
325    }
326
327    /// Set JSON-RPC configuration
328    pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
329        self.config.jsonrpc = jsonrpc;
330        self
331    }
332
333    /// Set lifecycle hooks for request/response processing
334    pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
335        self.config.lifecycle_hooks = hooks;
336        self
337    }
338
339    /// Set background task executor configuration
340    pub fn background_tasks(mut self, config: BackgroundTaskConfig) -> Self {
341        self.config.background_tasks = config;
342        self
343    }
344
345    /// Register a value dependency (like Fastify decorate)
346    ///
347    /// Value dependencies are static values that are cloned when injected into handlers.
348    /// Use this for configuration objects, constants, or small shared state.
349    ///
350    /// # Example
351    ///
352    /// ```ignorerust
353    /// # #[cfg(feature = "di")]
354    /// # {
355    /// use spikard_http::ServerConfig;
356    ///
357    /// let config = ServerConfig::builder()
358    ///     .provide_value("app_name", "MyApp".to_string())
359    ///     .provide_value("version", "1.0.0".to_string())
360    ///     .provide_value("max_connections", 100)
361    ///     .build();
362    /// # }
363    /// ```
364    #[cfg(feature = "di")]
365    pub fn provide_value<T: Clone + Send + Sync + 'static>(mut self, key: impl Into<String>, value: T) -> Self {
366        use spikard_core::di::{DependencyContainer, ValueDependency};
367        use std::sync::Arc;
368
369        let key_str = key.into();
370
371        let container = if let Some(container) = self.config.di_container.take() {
372            Arc::try_unwrap(container).unwrap_or_else(|_arc| DependencyContainer::new())
373        } else {
374            DependencyContainer::new()
375        };
376
377        let mut container = container;
378
379        let dep = ValueDependency::new(key_str.clone(), value);
380
381        container
382            .register(key_str, Arc::new(dep))
383            .expect("Failed to register dependency");
384
385        self.config.di_container = Some(Arc::new(container));
386        self
387    }
388
389    /// Register a factory dependency (like Litestar Provide)
390    ///
391    /// Factory dependencies create values on-demand, optionally depending on other
392    /// registered dependencies. Factories are async and have access to resolved dependencies.
393    ///
394    /// # Type Parameters
395    ///
396    /// * `F` - Factory function type
397    /// * `Fut` - Future returned by the factory
398    /// * `T` - Type of value produced by the factory
399    ///
400    /// # Arguments
401    ///
402    /// * `key` - Unique identifier for this dependency
403    /// * `factory` - Async function that creates the dependency value
404    ///
405    /// # Example
406    ///
407    /// ```ignorerust
408    /// # #[cfg(feature = "di")]
409    /// # {
410    /// use spikard_http::ServerConfig;
411    /// use std::sync::Arc;
412    ///
413    /// let config = ServerConfig::builder()
414    ///     .provide_value("db_url", "postgresql://localhost/mydb".to_string())
415    ///     .provide_factory("db_pool", |resolved| async move {
416    ///         let url: Arc<String> = resolved.get("db_url").ok_or("Missing db_url")?;
417    ///         // Create database pool...
418    ///         Ok(format!("Pool: {}", url))
419    ///     })
420    ///     .build();
421    /// # }
422    /// ```
423    #[cfg(feature = "di")]
424    pub fn provide_factory<F, Fut, T>(mut self, key: impl Into<String>, factory: F) -> Self
425    where
426        F: Fn(&spikard_core::di::ResolvedDependencies) -> Fut + Send + Sync + Clone + 'static,
427        Fut: std::future::Future<Output = Result<T, String>> + Send + 'static,
428        T: Send + Sync + 'static,
429    {
430        use futures::future::BoxFuture;
431        use spikard_core::di::{DependencyContainer, DependencyError, FactoryDependency};
432        use std::sync::Arc;
433
434        let key_str = key.into();
435
436        let container = if let Some(container) = self.config.di_container.take() {
437            Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
438        } else {
439            DependencyContainer::new()
440        };
441
442        let mut container = container;
443
444        let factory_clone = factory.clone();
445
446        let dep = FactoryDependency::builder(key_str.clone())
447            .factory(
448                move |_req: &axum::http::Request<()>,
449                      _data: &spikard_core::RequestData,
450                      resolved: &spikard_core::di::ResolvedDependencies| {
451                    let factory = factory_clone.clone();
452                    let factory_result = factory(resolved);
453                    Box::pin(async move {
454                        let result = factory_result
455                            .await
456                            .map_err(|e| DependencyError::ResolutionFailed { message: e })?;
457                        Ok(Arc::new(result) as Arc<dyn std::any::Any + Send + Sync>)
458                    })
459                        as BoxFuture<'static, Result<Arc<dyn std::any::Any + Send + Sync>, DependencyError>>
460                },
461            )
462            .build();
463
464        container
465            .register(key_str, Arc::new(dep))
466            .expect("Failed to register dependency");
467
468        self.config.di_container = Some(Arc::new(container));
469        self
470    }
471
472    /// Register a dependency with full control (advanced API)
473    ///
474    /// This method allows you to register custom dependency implementations
475    /// that implement the `Dependency` trait. Use this for advanced use cases
476    /// where you need fine-grained control over dependency resolution.
477    ///
478    /// # Example
479    ///
480    /// ```ignorerust
481    /// # #[cfg(feature = "di")]
482    /// # {
483    /// use spikard_http::ServerConfig;
484    /// use spikard_core::di::ValueDependency;
485    /// use std::sync::Arc;
486    ///
487    /// let dep = ValueDependency::new("custom", "value".to_string());
488    ///
489    /// let config = ServerConfig::builder()
490    ///     .provide(Arc::new(dep))
491    ///     .build();
492    /// # }
493    /// ```
494    #[cfg(feature = "di")]
495    pub fn provide(mut self, dependency: std::sync::Arc<dyn spikard_core::di::Dependency>) -> Self {
496        use spikard_core::di::DependencyContainer;
497        use std::sync::Arc;
498
499        let key = dependency.key().to_string();
500
501        let container = if let Some(container) = self.config.di_container.take() {
502            Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
503        } else {
504            DependencyContainer::new()
505        };
506
507        let mut container = container;
508
509        container
510            .register(key, dependency)
511            .expect("Failed to register dependency");
512
513        self.config.di_container = Some(Arc::new(container));
514        self
515    }
516
517    /// Build the ServerConfig
518    pub fn build(self) -> ServerConfig {
519        self.config
520    }
521}
522
523const fn default_true() -> bool {
524    true
525}