Skip to main content

spikard_http/
lib.rs

1// reason: pedantic and nursery lints produce ~500 actionable items across this large crate
2// (missing_errors_doc, must_use_candidate, module_name_repetitions, uninlined_format_args, …);
3// tracked for incremental cleanup — do not expand these suppression categories.
4#![allow(clippy::pedantic, clippy::nursery)]
5#![cfg_attr(test, allow(clippy::all))]
6// On wasm32 the server/background/grpc/sse/websocket modules are gated out, leaving
7// some helper functions used only by native code paths. Suppress dead-code warnings
8// crate-wide on wasm — those helpers ARE used on native builds.
9#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
10//! Spikard HTTP Server
11//!
12//! Pure Rust HTTP server with language-agnostic handler trait.
13//! Language bindings (Python, Node, WASM) implement the Handler trait.
14
15pub mod asyncapi;
16pub mod auth;
17// wasm: BackgroundRuntime uses tokio::spawn + JoinSet — not available on wasm32
18#[cfg(not(target_arch = "wasm32"))]
19pub mod background;
20pub mod bindings;
21pub mod cors;
22#[cfg(feature = "di")]
23pub mod di_handler;
24// wasm: tonic server transport pulls hyper/mio; gate the whole module
25#[cfg(not(target_arch = "wasm32"))]
26pub mod grpc;
27pub mod handler_response;
28pub mod handler_trait;
29pub mod jsonrpc;
30pub mod lifecycle;
31pub(crate) mod middleware;
32pub mod openapi;
33pub(crate) mod query_parser;
34pub mod response;
35// wasm: axum::serve + tokio::net::TcpListener are not available on wasm32
36#[cfg(not(target_arch = "wasm32"))]
37pub mod server;
38// wasm: SSE sse_handler spawns a tokio task with time keepalive
39#[cfg(not(target_arch = "wasm32"))]
40pub mod sse;
41// wasm: axum-test spins a real server; axum::extract::ws needs tokio_tungstenite
42#[cfg(not(target_arch = "wasm32"))]
43pub mod testing;
44// wasm: axum::extract::ws depends on tokio_tungstenite which pulls mio
45#[cfg(not(target_arch = "wasm32"))]
46pub mod websocket;
47
48use serde::{Deserialize, Serialize};
49// wasm: tokio::runtime::Runtime requires the full tokio rt feature set
50#[cfg(not(target_arch = "wasm32"))]
51use tokio::runtime::Runtime;
52
53#[cfg(test)]
54mod handler_trait_tests;
55
56pub use asyncapi::{
57    AsyncApiConfig, ParseResult, ParsedChannel, ParsedMessage, ParsedOperation, ValidateRequest, ValidationResponse,
58    parse_asyncapi_value, validate_message,
59};
60pub use auth::{Claims, api_key_auth_middleware, jwt_auth_middleware};
61#[cfg(not(target_arch = "wasm32"))]
62pub use background::{BackgroundHandle, BackgroundJobError, BackgroundJobMetadata, BackgroundTaskConfig};
63#[cfg(feature = "di")]
64pub use di_handler::DependencyInjectingHandler;
65#[cfg(not(target_arch = "wasm32"))]
66pub use grpc::GrpcConfig;
67pub use handler_response::HandlerResponse;
68pub use handler_trait::{Handler, HandlerResult, RequestData, StaticResponse, StaticResponseHandler, ValidatedParams};
69pub use jsonrpc::JsonRpcConfig;
70pub use lifecycle::{HookResult, LifecycleHook, LifecycleHooks, LifecycleHooksBuilder, request_hook, response_hook};
71pub use openapi::{ContactInfo, LicenseInfo, OpenApiConfig, SecuritySchemeInfo, ServerInfo};
72pub use response::Response;
73#[cfg(not(target_arch = "wasm32"))]
74pub use server::Server;
75pub use spikard_core::errors::StructuredError;
76pub use spikard_core::parameters::ParameterSource;
77pub use spikard_core::router::JsonRpcMethodInfo;
78pub use spikard_core::{
79    CompressionConfig, CorsConfig, Method, ParameterValidator, ProblemDetails, RateLimitConfig, Route, RouteMetadata,
80    SchemaRegistry, SchemaValidator, ValidationError, ValidationErrorDetail,
81};
82#[cfg(not(target_arch = "wasm32"))]
83pub use sse::{SseEvent, SseEventProducer, SseState, sse_handler};
84#[cfg(not(target_arch = "wasm32"))]
85pub use testing::{ResponseSnapshot, SnapshotError, snapshot_response};
86#[cfg(not(target_arch = "wasm32"))]
87pub use websocket::{WebSocketHandler, WebSocketState, websocket_handler};
88
89/// Reexport from spikard_core for convenience
90pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
91
92/// JWT authentication configuration
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct JwtConfig {
95    /// Secret key for JWT verification
96    pub secret: String,
97    /// Required algorithm (HS256, HS384, HS512, RS256, etc.)
98    #[serde(default = "default_jwt_algorithm")]
99    pub algorithm: String,
100    /// Required audience claim
101    pub audience: Option<Vec<String>>,
102    /// Required issuer claim
103    pub issuer: Option<String>,
104    /// Leeway for expiration checks (seconds)
105    #[serde(default)]
106    pub leeway: u64,
107}
108
109fn default_jwt_algorithm() -> String {
110    "HS256".to_string()
111}
112
113/// API Key authentication configuration
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ApiKeyConfig {
116    /// Valid API keys
117    pub keys: Vec<String>,
118    /// Header name to check (e.g., "X-API-Key")
119    #[serde(default = "default_api_key_header")]
120    pub header_name: String,
121}
122
123fn default_api_key_header() -> String {
124    "X-API-Key".to_string()
125}
126
127/// Static file serving configuration
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct StaticFilesConfig {
130    /// Directory path to serve
131    pub directory: String,
132    /// URL path prefix (e.g., "/static")
133    pub route_prefix: String,
134    /// Fallback to index.html for directories
135    #[serde(default = "default_true")]
136    pub index_file: bool,
137    /// Cache-Control header value
138    pub cache_control: Option<String>,
139}
140
141/// Server configuration
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(default)]
144pub struct ServerConfig {
145    /// Host to bind to
146    pub host: String,
147    /// Port to bind to
148    pub port: u16,
149    /// Number of Tokio runtime worker threads used by binding-managed server runtimes
150    // wasm: field kept but only meaningful on native; wasm runtimes ignore it
151    pub workers: usize,
152
153    /// Enable request ID generation and propagation
154    pub enable_request_id: bool,
155    /// Maximum request body size in bytes (None = unlimited, not recommended)
156    pub max_body_size: Option<usize>,
157    /// Request timeout in seconds (None = no timeout)
158    pub request_timeout: Option<u64>,
159    /// Enable compression middleware
160    pub compression: Option<CompressionConfig>,
161    /// Enable rate limiting
162    // wasm: RateLimitConfig is defined in spikard-core (pure data), so the field itself is
163    // wasm-compatible; tower_governor (the consumer) is native-only via target.cfg
164    pub rate_limit: Option<RateLimitConfig>,
165    /// JWT authentication configuration
166    pub jwt_auth: Option<JwtConfig>,
167    /// API Key authentication configuration
168    pub api_key_auth: Option<ApiKeyConfig>,
169    /// Static file serving configuration
170    // wasm: StaticFilesConfig is pure data; ServeDir (the consumer) is native-only
171    pub static_files: Vec<StaticFilesConfig>,
172    /// Enable graceful shutdown on SIGTERM/SIGINT
173    // wasm: field kept for API uniformity; graceful shutdown is a no-op on wasm
174    pub graceful_shutdown: bool,
175    /// Graceful shutdown timeout (seconds)
176    pub shutdown_timeout: u64,
177    /// AsyncAPI HTTP endpoint configuration
178    pub asyncapi: Option<crate::asyncapi::AsyncApiConfig>,
179    /// OpenAPI documentation configuration
180    pub openapi: Option<crate::openapi::OpenApiConfig>,
181    /// JSON-RPC configuration
182    pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
183    /// gRPC configuration
184    // wasm: GrpcConfig / mod grpc are native-only (tonic transport pulls mio)
185    #[cfg(not(target_arch = "wasm32"))]
186    pub grpc: Option<crate::grpc::GrpcConfig>,
187    /// Lifecycle hooks for request/response processing
188    // Not serializable: contains function pointers/closures
189    #[serde(skip)]
190    #[cfg_attr(alef, alef(skip))]
191    pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
192    /// Background task executor configuration
193    // wasm: BackgroundTaskConfig is pure data (Serialize/Deserialize); the runtime is native-only
194    #[cfg(not(target_arch = "wasm32"))]
195    pub background_tasks: BackgroundTaskConfig,
196    /// Enable per-request HTTP tracing (tower-http `TraceLayer`)
197    // wasm: tower-http TraceLayer is native-only (tower-http dep is native-only)
198    #[cfg(not(target_arch = "wasm32"))]
199    pub enable_http_trace: bool,
200    /// Dependency injection container (requires 'di' feature)
201    // Not serializable: contains runtime dependency injection state
202    #[cfg(feature = "di")]
203    #[serde(skip)]
204    #[cfg_attr(alef, alef(skip))]
205    pub di_container: Option<std::sync::Arc<spikard_core::di::DependencyContainer>>,
206}
207
208impl Default for ServerConfig {
209    fn default() -> Self {
210        Self {
211            host: "127.0.0.1".to_string(),
212            port: 8000,
213            workers: 1,
214            enable_request_id: false,
215            max_body_size: Some(10 * 1024 * 1024),
216            request_timeout: None,
217            compression: None,
218            rate_limit: None,
219            jwt_auth: None,
220            api_key_auth: None,
221            static_files: Vec::new(),
222            graceful_shutdown: true,
223            shutdown_timeout: 30,
224            asyncapi: None,
225            openapi: None,
226            jsonrpc: None,
227            #[cfg(not(target_arch = "wasm32"))]
228            grpc: None,
229            lifecycle_hooks: None,
230            #[cfg(not(target_arch = "wasm32"))]
231            background_tasks: BackgroundTaskConfig::default(),
232            #[cfg(not(target_arch = "wasm32"))]
233            enable_http_trace: false,
234            #[cfg(feature = "di")]
235            di_container: None,
236        }
237    }
238}
239
240impl ServerConfig {
241    /// Create a new builder for ServerConfig
242    ///
243    /// # Example
244    ///
245    /// ```ignorerust
246    /// use spikard_http::ServerConfig;
247    ///
248    /// let config = ServerConfig::builder()
249    ///     .port(3000)
250    ///     .host("0.0.0.0")
251    ///     .build();
252    /// ```
253    pub fn builder() -> ServerConfigBuilder {
254        ServerConfigBuilder::default()
255    }
256}
257
258/// Builder for ServerConfig
259///
260/// Provides a fluent API for configuring a Spikard server with dependency injection support.
261///
262/// # Dependency Injection
263///
264/// The builder provides methods to register dependencies that will be injected into handlers:
265///
266/// ```ignorerust
267/// # #[cfg(feature = "di")]
268/// # {
269/// use spikard_http::ServerConfig;
270/// use std::sync::Arc;
271///
272/// let config = ServerConfig::builder()
273///     .port(3000)
274///     .provide_value("app_name", "MyApp".to_string())
275///     .provide_value("max_connections", 100)
276///     .build();
277/// # }
278/// ```
279///
280/// For factory dependencies that create values on-demand:
281///
282/// ```ignorerust
283/// # #[cfg(feature = "di")]
284/// # {
285/// use spikard_http::ServerConfig;
286///
287/// let config = ServerConfig::builder()
288///     .port(3000)
289///     .provide_value("db_url", "postgresql://localhost/mydb".to_string())
290///     .build();
291/// # }
292/// ```
293#[derive(Debug, Clone, Default)]
294pub struct ServerConfigBuilder {
295    config: ServerConfig,
296}
297
298impl ServerConfigBuilder {
299    /// Set the host address to bind to
300    pub fn host(mut self, host: impl Into<String>) -> Self {
301        self.config.host = host.into();
302        self
303    }
304
305    /// Set the port to bind to
306    pub fn port(mut self, port: u16) -> Self {
307        self.config.port = port;
308        self
309    }
310
311    /// Set the number of Tokio runtime worker threads
312    pub fn workers(mut self, workers: usize) -> Self {
313        self.config.workers = workers;
314        self
315    }
316
317    /// Enable or disable request ID generation and propagation
318    pub fn enable_request_id(mut self, enable: bool) -> Self {
319        self.config.enable_request_id = enable;
320        self
321    }
322
323    /// Enable or disable per-request HTTP tracing (tower-http `TraceLayer`)
324    #[cfg(not(target_arch = "wasm32"))]
325    pub fn enable_http_trace(mut self, enable: bool) -> Self {
326        self.config.enable_http_trace = enable;
327        self
328    }
329
330    /// Set maximum request body size in bytes (None = unlimited, not recommended)
331    pub fn max_body_size(mut self, size: Option<usize>) -> Self {
332        self.config.max_body_size = size;
333        self
334    }
335
336    /// Set request timeout in seconds (None = no timeout)
337    pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
338        self.config.request_timeout = timeout;
339        self
340    }
341
342    /// Set compression configuration
343    pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
344        self.config.compression = compression;
345        self
346    }
347
348    /// Set rate limiting configuration
349    pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
350        self.config.rate_limit = rate_limit;
351        self
352    }
353
354    /// Set JWT authentication configuration
355    pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
356        self.config.jwt_auth = jwt_auth;
357        self
358    }
359
360    /// Set API key authentication configuration
361    pub fn api_key_auth(mut self, api_key_auth: Option<ApiKeyConfig>) -> Self {
362        self.config.api_key_auth = api_key_auth;
363        self
364    }
365
366    /// Add static file serving configuration
367    pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
368        self.config.static_files = static_files;
369        self
370    }
371
372    /// Add a single static file serving configuration
373    pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
374        self.config.static_files.push(static_file);
375        self
376    }
377
378    /// Enable or disable graceful shutdown on SIGTERM/SIGINT
379    pub fn graceful_shutdown(mut self, enable: bool) -> Self {
380        self.config.graceful_shutdown = enable;
381        self
382    }
383
384    /// Set graceful shutdown timeout in seconds
385    pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
386        self.config.shutdown_timeout = timeout;
387        self
388    }
389
390    /// Set AsyncAPI HTTP endpoint configuration
391    pub fn asyncapi(mut self, asyncapi: Option<crate::asyncapi::AsyncApiConfig>) -> Self {
392        self.config.asyncapi = asyncapi;
393        self
394    }
395
396    /// Register an AsyncAPI spec and enable the /asyncapi.json endpoint
397    pub fn with_asyncapi_spec(mut self, spec: serde_json::Value) -> Self {
398        let config = crate::asyncapi::AsyncApiConfig {
399            enabled: true,
400            spec: Some(spec),
401        };
402        self.config.asyncapi = Some(config);
403        self
404    }
405
406    /// Set OpenAPI documentation configuration
407    pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
408        self.config.openapi = openapi;
409        self
410    }
411
412    /// Set JSON-RPC configuration
413    pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
414        self.config.jsonrpc = jsonrpc;
415        self
416    }
417
418    /// Set gRPC configuration
419    #[cfg(not(target_arch = "wasm32"))]
420    pub fn grpc(mut self, grpc: Option<crate::grpc::GrpcConfig>) -> Self {
421        self.config.grpc = grpc;
422        self
423    }
424
425    /// Set lifecycle hooks for request/response processing
426    pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
427        self.config.lifecycle_hooks = hooks;
428        self
429    }
430
431    /// Set background task executor configuration
432    #[cfg(not(target_arch = "wasm32"))]
433    pub fn background_tasks(mut self, config: BackgroundTaskConfig) -> Self {
434        self.config.background_tasks = config;
435        self
436    }
437
438    /// Register a value dependency (like Fastify decorate)
439    ///
440    /// Value dependencies are static values that are cloned when injected into handlers.
441    /// Use this for configuration objects, constants, or small shared state.
442    ///
443    /// # Example
444    ///
445    /// ```ignorerust
446    /// # #[cfg(feature = "di")]
447    /// # {
448    /// use spikard_http::ServerConfig;
449    ///
450    /// let config = ServerConfig::builder()
451    ///     .provide_value("app_name", "MyApp".to_string())
452    ///     .provide_value("version", "1.0.0".to_string())
453    ///     .provide_value("max_connections", 100)
454    ///     .build();
455    /// # }
456    /// ```
457    #[cfg(feature = "di")]
458    pub fn provide_value<T: Clone + Send + Sync + 'static>(mut self, key: impl Into<String>, value: T) -> Self {
459        use spikard_core::di::{DependencyContainer, ValueDependency};
460        use std::sync::Arc;
461
462        let key_str = key.into();
463
464        let container = if let Some(container) = self.config.di_container.take() {
465            Arc::try_unwrap(container).unwrap_or_else(|_arc| DependencyContainer::new())
466        } else {
467            DependencyContainer::new()
468        };
469
470        let mut container = container;
471
472        let dep = ValueDependency::new(key_str.clone(), value);
473
474        container
475            .register(key_str, Arc::new(dep))
476            .expect("Failed to register dependency");
477
478        self.config.di_container = Some(Arc::new(container));
479        self
480    }
481
482    /// Register a factory dependency (like Litestar Provide)
483    ///
484    /// Factory dependencies create values on-demand, optionally depending on other
485    /// registered dependencies. Factories are async and have access to resolved dependencies.
486    ///
487    /// # Type Parameters
488    ///
489    /// * `F` - Factory function type
490    /// * `Fut` - Future returned by the factory
491    /// * `T` - Type of value produced by the factory
492    ///
493    /// # Arguments
494    ///
495    /// * `key` - Unique identifier for this dependency
496    /// * `factory` - Async function that creates the dependency value
497    ///
498    /// # Example
499    ///
500    /// ```ignorerust
501    /// # #[cfg(feature = "di")]
502    /// # {
503    /// use spikard_http::ServerConfig;
504    /// use std::sync::Arc;
505    ///
506    /// let config = ServerConfig::builder()
507    ///     .provide_value("db_url", "postgresql://localhost/mydb".to_string())
508    ///     .provide_factory("db_pool", |resolved| async move {
509    ///         let url: Arc<String> = resolved.get("db_url").ok_or("Missing db_url")?;
510    ///         // Create database pool...
511    ///         Ok(format!("Pool: {}", url))
512    ///     })
513    ///     .build();
514    /// # }
515    /// ```
516    #[cfg(feature = "di")]
517    pub fn provide_factory<F, Fut, T>(mut self, key: impl Into<String>, factory: F) -> Self
518    where
519        F: Fn(&spikard_core::di::ResolvedDependencies) -> Fut + Send + Sync + Clone + 'static,
520        Fut: std::future::Future<Output = Result<T, String>> + Send + 'static,
521        T: Send + Sync + 'static,
522    {
523        use futures::future::BoxFuture;
524        use spikard_core::di::{DependencyContainer, DependencyError, FactoryDependency};
525        use std::sync::Arc;
526
527        let key_str = key.into();
528
529        let container = if let Some(container) = self.config.di_container.take() {
530            Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
531        } else {
532            DependencyContainer::new()
533        };
534
535        let mut container = container;
536
537        let factory_clone = factory.clone();
538
539        let dep = FactoryDependency::builder(key_str.clone())
540            .factory(
541                move |_req: &axum::http::Request<()>,
542                      _data: &spikard_core::RequestData,
543                      resolved: &spikard_core::di::ResolvedDependencies| {
544                    let factory = factory_clone.clone();
545                    let factory_result = factory(resolved);
546                    Box::pin(async move {
547                        let result = factory_result
548                            .await
549                            .map_err(|e| DependencyError::ResolutionFailed { message: e })?;
550                        Ok(Arc::new(result) as Arc<dyn std::any::Any + Send + Sync>)
551                    })
552                        as BoxFuture<'static, Result<Arc<dyn std::any::Any + Send + Sync>, DependencyError>>
553                },
554            )
555            .build()
556            .expect("Factory dependency must have a configured factory function");
557
558        container
559            .register(key_str, Arc::new(dep))
560            .expect("Failed to register dependency");
561
562        self.config.di_container = Some(Arc::new(container));
563        self
564    }
565
566    /// Register a dependency with full control (advanced API)
567    ///
568    /// This method allows you to register custom dependency implementations
569    /// that implement the `Dependency` trait. Use this for advanced use cases
570    /// where you need fine-grained control over dependency resolution.
571    ///
572    /// # Example
573    ///
574    /// ```ignorerust
575    /// # #[cfg(feature = "di")]
576    /// # {
577    /// use spikard_http::ServerConfig;
578    /// use spikard_core::di::ValueDependency;
579    /// use std::sync::Arc;
580    ///
581    /// let dep = ValueDependency::new("custom", "value".to_string());
582    ///
583    /// let config = ServerConfig::builder()
584    ///     .provide(Arc::new(dep))
585    ///     .build();
586    /// # }
587    /// ```
588    #[cfg(feature = "di")]
589    pub fn provide(mut self, dependency: std::sync::Arc<dyn spikard_core::di::Dependency>) -> Self {
590        use spikard_core::di::DependencyContainer;
591        use std::sync::Arc;
592
593        let key = dependency.key().to_string();
594
595        let container = if let Some(container) = self.config.di_container.take() {
596            Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
597        } else {
598            DependencyContainer::new()
599        };
600
601        let mut container = container;
602
603        container
604            .register(key, dependency)
605            .expect("Failed to register dependency");
606
607        self.config.di_container = Some(Arc::new(container));
608        self
609    }
610
611    /// Build the ServerConfig
612    pub fn build(self) -> ServerConfig {
613        self.config
614    }
615}
616
617/// Build a Tokio runtime for serving HTTP requests with the configured worker count.
618///
619/// `workers == 1` uses a current-thread runtime to minimize scheduling overhead.
620/// `workers > 1` uses a multi-thread runtime with an explicit worker thread count.
621///
622/// Not available on `wasm32-unknown-unknown` — the host runtime drives execution there.
623#[cfg(not(target_arch = "wasm32"))]
624pub fn build_server_runtime(config: &ServerConfig) -> std::io::Result<Runtime> {
625    let mut builder = if config.workers <= 1 {
626        tokio::runtime::Builder::new_current_thread()
627    } else {
628        let mut builder = tokio::runtime::Builder::new_multi_thread();
629        builder.worker_threads(config.workers);
630        builder
631    };
632
633    builder.enable_all().build()
634}
635
636const fn default_true() -> bool {
637    true
638}