Skip to main content

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