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