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    /// Enable per-request HTTP tracing (tower-http `TraceLayer`)
148    pub enable_http_trace: bool,
149    /// Dependency injection container (requires 'di' feature)
150    #[cfg(feature = "di")]
151    pub di_container: Option<std::sync::Arc<spikard_core::di::DependencyContainer>>,
152}
153
154impl Default for ServerConfig {
155    fn default() -> Self {
156        Self {
157            host: "127.0.0.1".to_string(),
158            port: 8000,
159            workers: 1,
160            enable_request_id: false,
161            max_body_size: Some(10 * 1024 * 1024),
162            request_timeout: None,
163            compression: None,
164            rate_limit: None,
165            jwt_auth: None,
166            api_key_auth: None,
167            static_files: Vec::new(),
168            graceful_shutdown: true,
169            shutdown_timeout: 30,
170            openapi: None,
171            jsonrpc: None,
172            lifecycle_hooks: None,
173            background_tasks: BackgroundTaskConfig::default(),
174            enable_http_trace: false,
175            #[cfg(feature = "di")]
176            di_container: None,
177        }
178    }
179}
180
181impl ServerConfig {
182    /// Create a new builder for ServerConfig
183    ///
184    /// # Example
185    ///
186    /// ```ignorerust
187    /// use spikard_http::ServerConfig;
188    ///
189    /// let config = ServerConfig::builder()
190    ///     .port(3000)
191    ///     .host("0.0.0.0")
192    ///     .build();
193    /// ```
194    pub fn builder() -> ServerConfigBuilder {
195        ServerConfigBuilder::default()
196    }
197}
198
199/// Builder for ServerConfig
200///
201/// Provides a fluent API for configuring a Spikard server with dependency injection support.
202///
203/// # Dependency Injection
204///
205/// The builder provides methods to register dependencies that will be injected into handlers:
206///
207/// ```ignorerust
208/// # #[cfg(feature = "di")]
209/// # {
210/// use spikard_http::ServerConfig;
211/// use std::sync::Arc;
212///
213/// let config = ServerConfig::builder()
214///     .port(3000)
215///     .provide_value("app_name", "MyApp".to_string())
216///     .provide_value("max_connections", 100)
217///     .build();
218/// # }
219/// ```
220///
221/// For factory dependencies that create values on-demand:
222///
223/// ```ignorerust
224/// # #[cfg(feature = "di")]
225/// # {
226/// use spikard_http::ServerConfig;
227///
228/// let config = ServerConfig::builder()
229///     .port(3000)
230///     .provide_value("db_url", "postgresql://localhost/mydb".to_string())
231///     .build();
232/// # }
233/// ```
234#[derive(Debug, Clone, Default)]
235pub struct ServerConfigBuilder {
236    config: ServerConfig,
237}
238
239impl ServerConfigBuilder {
240    /// Set the host address to bind to
241    pub fn host(mut self, host: impl Into<String>) -> Self {
242        self.config.host = host.into();
243        self
244    }
245
246    /// Set the port to bind to
247    pub fn port(mut self, port: u16) -> Self {
248        self.config.port = port;
249        self
250    }
251
252    /// Set the number of worker threads (unused with tokio, kept for compatibility)
253    pub fn workers(mut self, workers: usize) -> Self {
254        self.config.workers = workers;
255        self
256    }
257
258    /// Enable or disable request ID generation and propagation
259    pub fn enable_request_id(mut self, enable: bool) -> Self {
260        self.config.enable_request_id = enable;
261        self
262    }
263
264    /// Enable or disable per-request HTTP tracing (tower-http `TraceLayer`)
265    pub fn enable_http_trace(mut self, enable: bool) -> Self {
266        self.config.enable_http_trace = enable;
267        self
268    }
269
270    /// Set maximum request body size in bytes (None = unlimited, not recommended)
271    pub fn max_body_size(mut self, size: Option<usize>) -> Self {
272        self.config.max_body_size = size;
273        self
274    }
275
276    /// Set request timeout in seconds (None = no timeout)
277    pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
278        self.config.request_timeout = timeout;
279        self
280    }
281
282    /// Set compression configuration
283    pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
284        self.config.compression = compression;
285        self
286    }
287
288    /// Set rate limiting configuration
289    pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
290        self.config.rate_limit = rate_limit;
291        self
292    }
293
294    /// Set JWT authentication configuration
295    pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
296        self.config.jwt_auth = jwt_auth;
297        self
298    }
299
300    /// Set API key authentication configuration
301    pub fn api_key_auth(mut self, api_key_auth: Option<ApiKeyConfig>) -> Self {
302        self.config.api_key_auth = api_key_auth;
303        self
304    }
305
306    /// Add static file serving configuration
307    pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
308        self.config.static_files = static_files;
309        self
310    }
311
312    /// Add a single static file serving configuration
313    pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
314        self.config.static_files.push(static_file);
315        self
316    }
317
318    /// Enable or disable graceful shutdown on SIGTERM/SIGINT
319    pub fn graceful_shutdown(mut self, enable: bool) -> Self {
320        self.config.graceful_shutdown = enable;
321        self
322    }
323
324    /// Set graceful shutdown timeout in seconds
325    pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
326        self.config.shutdown_timeout = timeout;
327        self
328    }
329
330    /// Set OpenAPI documentation configuration
331    pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
332        self.config.openapi = openapi;
333        self
334    }
335
336    /// Set JSON-RPC configuration
337    pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
338        self.config.jsonrpc = jsonrpc;
339        self
340    }
341
342    /// Set lifecycle hooks for request/response processing
343    pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
344        self.config.lifecycle_hooks = hooks;
345        self
346    }
347
348    /// Set background task executor configuration
349    pub fn background_tasks(mut self, config: BackgroundTaskConfig) -> Self {
350        self.config.background_tasks = config;
351        self
352    }
353
354    /// Register a value dependency (like Fastify decorate)
355    ///
356    /// Value dependencies are static values that are cloned when injected into handlers.
357    /// Use this for configuration objects, constants, or small shared state.
358    ///
359    /// # Example
360    ///
361    /// ```ignorerust
362    /// # #[cfg(feature = "di")]
363    /// # {
364    /// use spikard_http::ServerConfig;
365    ///
366    /// let config = ServerConfig::builder()
367    ///     .provide_value("app_name", "MyApp".to_string())
368    ///     .provide_value("version", "1.0.0".to_string())
369    ///     .provide_value("max_connections", 100)
370    ///     .build();
371    /// # }
372    /// ```
373    #[cfg(feature = "di")]
374    pub fn provide_value<T: Clone + Send + Sync + 'static>(mut self, key: impl Into<String>, value: T) -> Self {
375        use spikard_core::di::{DependencyContainer, ValueDependency};
376        use std::sync::Arc;
377
378        let key_str = key.into();
379
380        let container = if let Some(container) = self.config.di_container.take() {
381            Arc::try_unwrap(container).unwrap_or_else(|_arc| DependencyContainer::new())
382        } else {
383            DependencyContainer::new()
384        };
385
386        let mut container = container;
387
388        let dep = ValueDependency::new(key_str.clone(), value);
389
390        container
391            .register(key_str, Arc::new(dep))
392            .expect("Failed to register dependency");
393
394        self.config.di_container = Some(Arc::new(container));
395        self
396    }
397
398    /// Register a factory dependency (like Litestar Provide)
399    ///
400    /// Factory dependencies create values on-demand, optionally depending on other
401    /// registered dependencies. Factories are async and have access to resolved dependencies.
402    ///
403    /// # Type Parameters
404    ///
405    /// * `F` - Factory function type
406    /// * `Fut` - Future returned by the factory
407    /// * `T` - Type of value produced by the factory
408    ///
409    /// # Arguments
410    ///
411    /// * `key` - Unique identifier for this dependency
412    /// * `factory` - Async function that creates the dependency value
413    ///
414    /// # Example
415    ///
416    /// ```ignorerust
417    /// # #[cfg(feature = "di")]
418    /// # {
419    /// use spikard_http::ServerConfig;
420    /// use std::sync::Arc;
421    ///
422    /// let config = ServerConfig::builder()
423    ///     .provide_value("db_url", "postgresql://localhost/mydb".to_string())
424    ///     .provide_factory("db_pool", |resolved| async move {
425    ///         let url: Arc<String> = resolved.get("db_url").ok_or("Missing db_url")?;
426    ///         // Create database pool...
427    ///         Ok(format!("Pool: {}", url))
428    ///     })
429    ///     .build();
430    /// # }
431    /// ```
432    #[cfg(feature = "di")]
433    pub fn provide_factory<F, Fut, T>(mut self, key: impl Into<String>, factory: F) -> Self
434    where
435        F: Fn(&spikard_core::di::ResolvedDependencies) -> Fut + Send + Sync + Clone + 'static,
436        Fut: std::future::Future<Output = Result<T, String>> + Send + 'static,
437        T: Send + Sync + 'static,
438    {
439        use futures::future::BoxFuture;
440        use spikard_core::di::{DependencyContainer, DependencyError, FactoryDependency};
441        use std::sync::Arc;
442
443        let key_str = key.into();
444
445        let container = if let Some(container) = self.config.di_container.take() {
446            Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
447        } else {
448            DependencyContainer::new()
449        };
450
451        let mut container = container;
452
453        let factory_clone = factory.clone();
454
455        let dep = FactoryDependency::builder(key_str.clone())
456            .factory(
457                move |_req: &axum::http::Request<()>,
458                      _data: &spikard_core::RequestData,
459                      resolved: &spikard_core::di::ResolvedDependencies| {
460                    let factory = factory_clone.clone();
461                    let factory_result = factory(resolved);
462                    Box::pin(async move {
463                        let result = factory_result
464                            .await
465                            .map_err(|e| DependencyError::ResolutionFailed { message: e })?;
466                        Ok(Arc::new(result) as Arc<dyn std::any::Any + Send + Sync>)
467                    })
468                        as BoxFuture<'static, Result<Arc<dyn std::any::Any + Send + Sync>, DependencyError>>
469                },
470            )
471            .build();
472
473        container
474            .register(key_str, Arc::new(dep))
475            .expect("Failed to register dependency");
476
477        self.config.di_container = Some(Arc::new(container));
478        self
479    }
480
481    /// Register a dependency with full control (advanced API)
482    ///
483    /// This method allows you to register custom dependency implementations
484    /// that implement the `Dependency` trait. Use this for advanced use cases
485    /// where you need fine-grained control over dependency resolution.
486    ///
487    /// # Example
488    ///
489    /// ```ignorerust
490    /// # #[cfg(feature = "di")]
491    /// # {
492    /// use spikard_http::ServerConfig;
493    /// use spikard_core::di::ValueDependency;
494    /// use std::sync::Arc;
495    ///
496    /// let dep = ValueDependency::new("custom", "value".to_string());
497    ///
498    /// let config = ServerConfig::builder()
499    ///     .provide(Arc::new(dep))
500    ///     .build();
501    /// # }
502    /// ```
503    #[cfg(feature = "di")]
504    pub fn provide(mut self, dependency: std::sync::Arc<dyn spikard_core::di::Dependency>) -> Self {
505        use spikard_core::di::DependencyContainer;
506        use std::sync::Arc;
507
508        let key = dependency.key().to_string();
509
510        let container = if let Some(container) = self.config.di_container.take() {
511            Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
512        } else {
513            DependencyContainer::new()
514        };
515
516        let mut container = container;
517
518        container
519            .register(key, dependency)
520            .expect("Failed to register dependency");
521
522        self.config.di_container = Some(Arc::new(container));
523        self
524    }
525
526    /// Build the ServerConfig
527    pub fn build(self) -> ServerConfig {
528        self.config
529    }
530}
531
532const fn default_true() -> bool {
533    true
534}