1#![allow(clippy::pedantic, clippy::nursery)]
5#![cfg_attr(test, allow(clippy::all))]
6#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
10pub mod asyncapi;
16pub mod auth;
17#[cfg(not(target_arch = "wasm32"))]
19pub mod background;
20pub mod bindings;
21pub mod cors;
22#[cfg(feature = "di")]
23pub mod di_handler;
24#[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#[cfg(not(target_arch = "wasm32"))]
37pub mod server;
38#[cfg(not(target_arch = "wasm32"))]
40pub mod sse;
41#[cfg(not(target_arch = "wasm32"))]
43pub mod testing;
44#[cfg(not(target_arch = "wasm32"))]
46pub mod websocket;
47
48use serde::{Deserialize, Serialize};
49#[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
89pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct JwtConfig {
95 pub secret: String,
97 #[serde(default = "default_jwt_algorithm")]
99 pub algorithm: String,
100 pub audience: Option<Vec<String>>,
102 pub issuer: Option<String>,
104 #[serde(default)]
106 pub leeway: u64,
107}
108
109fn default_jwt_algorithm() -> String {
110 "HS256".to_string()
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ApiKeyConfig {
116 pub keys: Vec<String>,
118 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct StaticFilesConfig {
130 pub directory: String,
132 pub route_prefix: String,
134 #[serde(default = "default_true")]
136 pub index_file: bool,
137 pub cache_control: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(default)]
144pub struct ServerConfig {
145 pub host: String,
147 pub port: u16,
149 pub workers: usize,
152
153 pub enable_request_id: bool,
155 pub max_body_size: Option<usize>,
157 pub request_timeout: Option<u64>,
159 pub compression: Option<CompressionConfig>,
161 pub rate_limit: Option<RateLimitConfig>,
165 pub jwt_auth: Option<JwtConfig>,
167 pub api_key_auth: Option<ApiKeyConfig>,
169 pub static_files: Vec<StaticFilesConfig>,
172 pub graceful_shutdown: bool,
175 pub shutdown_timeout: u64,
177 pub asyncapi: Option<crate::asyncapi::AsyncApiConfig>,
179 pub openapi: Option<crate::openapi::OpenApiConfig>,
181 pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
183 #[cfg(not(target_arch = "wasm32"))]
186 pub grpc: Option<crate::grpc::GrpcConfig>,
187 #[serde(skip)]
190 #[cfg_attr(alef, alef(skip))]
191 pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
192 #[cfg(not(target_arch = "wasm32"))]
195 pub background_tasks: BackgroundTaskConfig,
196 #[cfg(not(target_arch = "wasm32"))]
199 pub enable_http_trace: bool,
200 #[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 pub fn builder() -> ServerConfigBuilder {
254 ServerConfigBuilder::default()
255 }
256}
257
258#[derive(Debug, Clone, Default)]
294pub struct ServerConfigBuilder {
295 config: ServerConfig,
296}
297
298impl ServerConfigBuilder {
299 pub fn host(mut self, host: impl Into<String>) -> Self {
301 self.config.host = host.into();
302 self
303 }
304
305 pub fn port(mut self, port: u16) -> Self {
307 self.config.port = port;
308 self
309 }
310
311 pub fn workers(mut self, workers: usize) -> Self {
313 self.config.workers = workers;
314 self
315 }
316
317 pub fn enable_request_id(mut self, enable: bool) -> Self {
319 self.config.enable_request_id = enable;
320 self
321 }
322
323 #[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 pub fn max_body_size(mut self, size: Option<usize>) -> Self {
332 self.config.max_body_size = size;
333 self
334 }
335
336 pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
338 self.config.request_timeout = timeout;
339 self
340 }
341
342 pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
344 self.config.compression = compression;
345 self
346 }
347
348 pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
350 self.config.rate_limit = rate_limit;
351 self
352 }
353
354 pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
356 self.config.jwt_auth = jwt_auth;
357 self
358 }
359
360 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 pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
368 self.config.static_files = static_files;
369 self
370 }
371
372 pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
374 self.config.static_files.push(static_file);
375 self
376 }
377
378 pub fn graceful_shutdown(mut self, enable: bool) -> Self {
380 self.config.graceful_shutdown = enable;
381 self
382 }
383
384 pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
386 self.config.shutdown_timeout = timeout;
387 self
388 }
389
390 pub fn asyncapi(mut self, asyncapi: Option<crate::asyncapi::AsyncApiConfig>) -> Self {
392 self.config.asyncapi = asyncapi;
393 self
394 }
395
396 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 pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
408 self.config.openapi = openapi;
409 self
410 }
411
412 pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
414 self.config.jsonrpc = jsonrpc;
415 self
416 }
417
418 #[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 pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
427 self.config.lifecycle_hooks = hooks;
428 self
429 }
430
431 #[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 #[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 #[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 #[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 pub fn build(self) -> ServerConfig {
613 self.config
614 }
615}
616
617#[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}