Skip to main content

spikard_http/
lib.rs

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