#![allow(clippy::pedantic, clippy::nursery)]
#![cfg_attr(test, allow(clippy::all))]
pub mod auth;
pub mod background;
pub mod bindings;
pub mod body_metadata;
pub mod cors;
pub mod debug;
#[cfg(feature = "di")]
pub mod di_handler;
pub mod grpc;
pub mod handler_response;
pub mod handler_trait;
pub mod jsonrpc;
pub mod lifecycle;
pub mod middleware;
pub mod openapi;
pub mod query_parser;
pub mod response;
pub mod server;
pub mod sse;
pub mod testing;
pub mod websocket;
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
#[cfg(test)]
mod handler_trait_tests;
pub use auth::{Claims, api_key_auth_middleware, jwt_auth_middleware};
pub use background::{
BackgroundHandle, BackgroundJobError, BackgroundJobMetadata, BackgroundRuntime, BackgroundSpawnError,
BackgroundTaskConfig,
};
pub use body_metadata::ResponseBodySize;
#[cfg(feature = "di")]
pub use di_handler::DependencyInjectingHandler;
pub use grpc::{
GrpcConfig, GrpcHandler, GrpcHandlerResult, GrpcRegistry, GrpcRequestData, GrpcResponseData, MessageStream,
StreamingRequest, StreamingResponse,
};
pub use handler_response::HandlerResponse;
pub use handler_trait::{Handler, HandlerResult, RequestData, StaticResponse, StaticResponseHandler, ValidatedParams};
pub use jsonrpc::JsonRpcConfig;
pub use lifecycle::{HookResult, LifecycleHook, LifecycleHooks, LifecycleHooksBuilder, request_hook, response_hook};
pub use openapi::{ContactInfo, LicenseInfo, OpenApiConfig, SecuritySchemeInfo, ServerInfo};
pub use response::Response;
pub use server::Server;
pub use spikard_core::{
CompressionConfig, CorsConfig, Method, ParameterValidator, ProblemDetails, RateLimitConfig, Route, RouteHandler,
RouteMetadata, Router, SchemaRegistry, SchemaValidator,
};
pub use sse::{SseEvent, SseEventProducer, SseState, sse_handler};
pub use testing::{ResponseSnapshot, SnapshotError, snapshot_response};
pub use websocket::{WebSocketHandler, WebSocketState, websocket_handler};
pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtConfig {
pub secret: String,
#[serde(default = "default_jwt_algorithm")]
pub algorithm: String,
pub audience: Option<Vec<String>>,
pub issuer: Option<String>,
#[serde(default)]
pub leeway: u64,
}
fn default_jwt_algorithm() -> String {
"HS256".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKeyConfig {
pub keys: Vec<String>,
#[serde(default = "default_api_key_header")]
pub header_name: String,
}
fn default_api_key_header() -> String {
"X-API-Key".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticFilesConfig {
pub directory: String,
pub route_prefix: String,
#[serde(default = "default_true")]
pub index_file: bool,
pub cache_control: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub workers: usize,
pub enable_request_id: bool,
pub max_body_size: Option<usize>,
pub request_timeout: Option<u64>,
pub compression: Option<CompressionConfig>,
pub rate_limit: Option<RateLimitConfig>,
pub jwt_auth: Option<JwtConfig>,
pub api_key_auth: Option<ApiKeyConfig>,
pub static_files: Vec<StaticFilesConfig>,
pub graceful_shutdown: bool,
pub shutdown_timeout: u64,
pub openapi: Option<crate::openapi::OpenApiConfig>,
pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
pub grpc: Option<crate::grpc::GrpcConfig>,
pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
pub background_tasks: BackgroundTaskConfig,
pub enable_http_trace: bool,
#[cfg(feature = "di")]
pub di_container: Option<std::sync::Arc<spikard_core::di::DependencyContainer>>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8000,
workers: 1,
enable_request_id: false,
max_body_size: Some(10 * 1024 * 1024),
request_timeout: None,
compression: None,
rate_limit: None,
jwt_auth: None,
api_key_auth: None,
static_files: Vec::new(),
graceful_shutdown: true,
shutdown_timeout: 30,
openapi: None,
jsonrpc: None,
grpc: None,
lifecycle_hooks: None,
background_tasks: BackgroundTaskConfig::default(),
enable_http_trace: false,
#[cfg(feature = "di")]
di_container: None,
}
}
}
impl ServerConfig {
pub fn builder() -> ServerConfigBuilder {
ServerConfigBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct ServerConfigBuilder {
config: ServerConfig,
}
impl ServerConfigBuilder {
pub fn host(mut self, host: impl Into<String>) -> Self {
self.config.host = host.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.config.port = port;
self
}
pub fn workers(mut self, workers: usize) -> Self {
self.config.workers = workers;
self
}
pub fn enable_request_id(mut self, enable: bool) -> Self {
self.config.enable_request_id = enable;
self
}
pub fn enable_http_trace(mut self, enable: bool) -> Self {
self.config.enable_http_trace = enable;
self
}
pub fn max_body_size(mut self, size: Option<usize>) -> Self {
self.config.max_body_size = size;
self
}
pub fn request_timeout(mut self, timeout: Option<u64>) -> Self {
self.config.request_timeout = timeout;
self
}
pub fn compression(mut self, compression: Option<CompressionConfig>) -> Self {
self.config.compression = compression;
self
}
pub fn rate_limit(mut self, rate_limit: Option<RateLimitConfig>) -> Self {
self.config.rate_limit = rate_limit;
self
}
pub fn jwt_auth(mut self, jwt_auth: Option<JwtConfig>) -> Self {
self.config.jwt_auth = jwt_auth;
self
}
pub fn api_key_auth(mut self, api_key_auth: Option<ApiKeyConfig>) -> Self {
self.config.api_key_auth = api_key_auth;
self
}
pub fn static_files(mut self, static_files: Vec<StaticFilesConfig>) -> Self {
self.config.static_files = static_files;
self
}
pub fn add_static_files(mut self, static_file: StaticFilesConfig) -> Self {
self.config.static_files.push(static_file);
self
}
pub fn graceful_shutdown(mut self, enable: bool) -> Self {
self.config.graceful_shutdown = enable;
self
}
pub fn shutdown_timeout(mut self, timeout: u64) -> Self {
self.config.shutdown_timeout = timeout;
self
}
pub fn openapi(mut self, openapi: Option<crate::openapi::OpenApiConfig>) -> Self {
self.config.openapi = openapi;
self
}
pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
self.config.jsonrpc = jsonrpc;
self
}
pub fn grpc(mut self, grpc: Option<crate::grpc::GrpcConfig>) -> Self {
self.config.grpc = grpc;
self
}
pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
self.config.lifecycle_hooks = hooks;
self
}
pub fn background_tasks(mut self, config: BackgroundTaskConfig) -> Self {
self.config.background_tasks = config;
self
}
#[cfg(feature = "di")]
pub fn provide_value<T: Clone + Send + Sync + 'static>(mut self, key: impl Into<String>, value: T) -> Self {
use spikard_core::di::{DependencyContainer, ValueDependency};
use std::sync::Arc;
let key_str = key.into();
let container = if let Some(container) = self.config.di_container.take() {
Arc::try_unwrap(container).unwrap_or_else(|_arc| DependencyContainer::new())
} else {
DependencyContainer::new()
};
let mut container = container;
let dep = ValueDependency::new(key_str.clone(), value);
container
.register(key_str, Arc::new(dep))
.expect("Failed to register dependency");
self.config.di_container = Some(Arc::new(container));
self
}
#[cfg(feature = "di")]
pub fn provide_factory<F, Fut, T>(mut self, key: impl Into<String>, factory: F) -> Self
where
F: Fn(&spikard_core::di::ResolvedDependencies) -> Fut + Send + Sync + Clone + 'static,
Fut: std::future::Future<Output = Result<T, String>> + Send + 'static,
T: Send + Sync + 'static,
{
use futures::future::BoxFuture;
use spikard_core::di::{DependencyContainer, DependencyError, FactoryDependency};
use std::sync::Arc;
let key_str = key.into();
let container = if let Some(container) = self.config.di_container.take() {
Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
} else {
DependencyContainer::new()
};
let mut container = container;
let factory_clone = factory.clone();
let dep = FactoryDependency::builder(key_str.clone())
.factory(
move |_req: &axum::http::Request<()>,
_data: &spikard_core::RequestData,
resolved: &spikard_core::di::ResolvedDependencies| {
let factory = factory_clone.clone();
let factory_result = factory(resolved);
Box::pin(async move {
let result = factory_result
.await
.map_err(|e| DependencyError::ResolutionFailed { message: e })?;
Ok(Arc::new(result) as Arc<dyn std::any::Any + Send + Sync>)
})
as BoxFuture<'static, Result<Arc<dyn std::any::Any + Send + Sync>, DependencyError>>
},
)
.build()
.expect("Factory dependency must have a configured factory function");
container
.register(key_str, Arc::new(dep))
.expect("Failed to register dependency");
self.config.di_container = Some(Arc::new(container));
self
}
#[cfg(feature = "di")]
pub fn provide(mut self, dependency: std::sync::Arc<dyn spikard_core::di::Dependency>) -> Self {
use spikard_core::di::DependencyContainer;
use std::sync::Arc;
let key = dependency.key().to_string();
let container = if let Some(container) = self.config.di_container.take() {
Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
} else {
DependencyContainer::new()
};
let mut container = container;
container
.register(key, dependency)
.expect("Failed to register dependency");
self.config.di_container = Some(Arc::new(container));
self
}
pub fn build(self) -> ServerConfig {
self.config
}
}
pub fn build_server_runtime(config: &ServerConfig) -> std::io::Result<Runtime> {
let mut builder = if config.workers <= 1 {
tokio::runtime::Builder::new_current_thread()
} else {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.worker_threads(config.workers);
builder
};
builder.enable_all().build()
}
const fn default_true() -> bool {
true
}