1#![allow(clippy::pedantic, clippy::nursery)]
2#![cfg_attr(test, allow(clippy::all))]
3#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
7pub mod asyncapi;
13pub mod auth;
14#[cfg(not(target_arch = "wasm32"))]
16pub mod background;
17pub mod bindings;
18pub mod cors;
19#[cfg(feature = "di")]
20pub mod di_handler;
21#[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#[cfg(not(target_arch = "wasm32"))]
34pub mod server;
35#[cfg(not(target_arch = "wasm32"))]
37pub mod sse;
38#[cfg(not(target_arch = "wasm32"))]
40pub mod testing;
41#[cfg(not(target_arch = "wasm32"))]
43pub mod websocket;
44
45use serde::{Deserialize, Serialize};
46#[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
86pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct JwtConfig {
92 pub secret: String,
94 #[serde(default = "default_jwt_algorithm")]
96 pub algorithm: String,
97 pub audience: Option<Vec<String>>,
99 pub issuer: Option<String>,
101 #[serde(default)]
103 pub leeway: u64,
104}
105
106fn default_jwt_algorithm() -> String {
107 "HS256".to_string()
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ApiKeyConfig {
113 pub keys: Vec<String>,
115 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct StaticFilesConfig {
127 pub directory: String,
129 pub route_prefix: String,
131 #[serde(default = "default_true")]
133 pub index_file: bool,
134 pub cache_control: Option<String>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ServerConfig {
141 pub host: String,
143 pub port: u16,
145 pub workers: usize,
148
149 pub enable_request_id: bool,
151 pub max_body_size: Option<usize>,
153 pub request_timeout: Option<u64>,
155 pub compression: Option<CompressionConfig>,
157 pub rate_limit: Option<RateLimitConfig>,
161 pub jwt_auth: Option<JwtConfig>,
163 pub api_key_auth: Option<ApiKeyConfig>,
165 pub static_files: Vec<StaticFilesConfig>,
168 pub graceful_shutdown: bool,
171 pub shutdown_timeout: u64,
173 pub asyncapi: Option<crate::asyncapi::AsyncApiConfig>,
175 pub openapi: Option<crate::openapi::OpenApiConfig>,
177 pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
179 #[cfg(not(target_arch = "wasm32"))]
182 pub grpc: Option<crate::grpc::GrpcConfig>,
183 #[serde(skip)]
186 #[cfg_attr(alef, alef(skip))]
187 pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
188 #[cfg(not(target_arch = "wasm32"))]
191 pub background_tasks: BackgroundTaskConfig,
192 #[cfg(not(target_arch = "wasm32"))]
195 pub enable_http_trace: bool,
196 #[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 pub fn builder() -> ServerConfigBuilder {
250 ServerConfigBuilder::default()
251 }
252}
253
254#[derive(Debug, Clone, Default)]
290pub struct ServerConfigBuilder {
291 config: ServerConfig,
292}
293
294impl ServerConfigBuilder {
295 pub fn host(mut self, host: impl Into<String>) -> Self {
297 self.config.host = host.into();
298 self
299 }
300
301 pub fn port(mut self, port: u16) -> Self {
303 self.config.port = port;
304 self
305 }
306
307 pub fn workers(mut self, workers: usize) -> Self {
309 self.config.workers = workers;
310 self
311 }
312
313 pub fn enable_request_id(mut self, enable: bool) -> Self {
315 self.config.enable_request_id = enable;
316 self
317 }
318
319 #[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 pub fn max_body_size(mut self, size: Option<usize>) -> Self {
328 self.config.max_body_size = size;
329 self
330 }
331
332 pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
334 self.config.request_timeout = timeout;
335 self
336 }
337
338 pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
340 self.config.compression = compression;
341 self
342 }
343
344 pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
346 self.config.rate_limit = rate_limit;
347 self
348 }
349
350 pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
352 self.config.jwt_auth = jwt_auth;
353 self
354 }
355
356 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 pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
364 self.config.static_files = static_files;
365 self
366 }
367
368 pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
370 self.config.static_files.push(static_file);
371 self
372 }
373
374 pub fn graceful_shutdown(mut self, enable: bool) -> Self {
376 self.config.graceful_shutdown = enable;
377 self
378 }
379
380 pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
382 self.config.shutdown_timeout = timeout;
383 self
384 }
385
386 pub fn asyncapi(mut self, asyncapi: Option<crate::asyncapi::AsyncApiConfig>) -> Self {
388 self.config.asyncapi = asyncapi;
389 self
390 }
391
392 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 pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
404 self.config.openapi = openapi;
405 self
406 }
407
408 pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
410 self.config.jsonrpc = jsonrpc;
411 self
412 }
413
414 #[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 pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
423 self.config.lifecycle_hooks = hooks;
424 self
425 }
426
427 #[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 #[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 #[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 #[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 pub fn build(self) -> ServerConfig {
609 self.config
610 }
611}
612
613#[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}