1#![allow(clippy::pedantic, clippy::nursery)]
2#![cfg_attr(test, allow(clippy::all))]
3pub 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
57pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct JwtConfig {
63 pub secret: String,
65 #[serde(default = "default_jwt_algorithm")]
67 pub algorithm: String,
68 pub audience: Option<Vec<String>>,
70 pub issuer: Option<String>,
72 #[serde(default)]
74 pub leeway: u64,
75}
76
77fn default_jwt_algorithm() -> String {
78 "HS256".to_string()
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ApiKeyConfig {
84 pub keys: Vec<String>,
86 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct StaticFilesConfig {
98 pub directory: String,
100 pub route_prefix: String,
102 #[serde(default = "default_true")]
104 pub index_file: bool,
105 pub cache_control: Option<String>,
107}
108
109#[derive(Debug, Clone)]
111pub struct ServerConfig {
112 pub host: String,
114 pub port: u16,
116 pub workers: usize,
118
119 pub enable_request_id: bool,
121 pub max_body_size: Option<usize>,
123 pub request_timeout: Option<u64>,
125 pub compression: Option<CompressionConfig>,
127 pub rate_limit: Option<RateLimitConfig>,
129 pub jwt_auth: Option<JwtConfig>,
131 pub api_key_auth: Option<ApiKeyConfig>,
133 pub static_files: Vec<StaticFilesConfig>,
135 pub graceful_shutdown: bool,
137 pub shutdown_timeout: u64,
139 pub openapi: Option<crate::openapi::OpenApiConfig>,
141 pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
143 pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
145 pub background_tasks: BackgroundTaskConfig,
147 pub enable_http_trace: bool,
149 #[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 pub fn builder() -> ServerConfigBuilder {
195 ServerConfigBuilder::default()
196 }
197}
198
199#[derive(Debug, Clone, Default)]
235pub struct ServerConfigBuilder {
236 config: ServerConfig,
237}
238
239impl ServerConfigBuilder {
240 pub fn host(mut self, host: impl Into<String>) -> Self {
242 self.config.host = host.into();
243 self
244 }
245
246 pub fn port(mut self, port: u16) -> Self {
248 self.config.port = port;
249 self
250 }
251
252 pub fn workers(mut self, workers: usize) -> Self {
254 self.config.workers = workers;
255 self
256 }
257
258 pub fn enable_request_id(mut self, enable: bool) -> Self {
260 self.config.enable_request_id = enable;
261 self
262 }
263
264 pub fn enable_http_trace(mut self, enable: bool) -> Self {
266 self.config.enable_http_trace = enable;
267 self
268 }
269
270 pub fn max_body_size(mut self, size: Option<usize>) -> Self {
272 self.config.max_body_size = size;
273 self
274 }
275
276 pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
278 self.config.request_timeout = timeout;
279 self
280 }
281
282 pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
284 self.config.compression = compression;
285 self
286 }
287
288 pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
290 self.config.rate_limit = rate_limit;
291 self
292 }
293
294 pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
296 self.config.jwt_auth = jwt_auth;
297 self
298 }
299
300 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 pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
308 self.config.static_files = static_files;
309 self
310 }
311
312 pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
314 self.config.static_files.push(static_file);
315 self
316 }
317
318 pub fn graceful_shutdown(mut self, enable: bool) -> Self {
320 self.config.graceful_shutdown = enable;
321 self
322 }
323
324 pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
326 self.config.shutdown_timeout = timeout;
327 self
328 }
329
330 pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
332 self.config.openapi = openapi;
333 self
334 }
335
336 pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
338 self.config.jsonrpc = jsonrpc;
339 self
340 }
341
342 pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
344 self.config.lifecycle_hooks = hooks;
345 self
346 }
347
348 pub fn background_tasks(mut self, config: BackgroundTaskConfig) -> Self {
350 self.config.background_tasks = config;
351 self
352 }
353
354 #[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 #[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 #[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 pub fn build(self) -> ServerConfig {
528 self.config
529 }
530}
531
532const fn default_true() -> bool {
533 true
534}