use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::str::FromStr;
use axum::extract::Request;
use axum::handler::Handler;
use axum::routing::MethodRouter;
use serde::{Deserialize, Serialize};
use tower_layer::Layer;
use tower_service::Service;
use crate::config::{Config, ConfigBuilder, ConfigState, ConfigWrapper, GotchaConfigLoader, ServerConfig};
use crate::error::{GotchaError, GotchaResult};
use crate::router::{GotchaRouter, Responder};
use crate::GotchaContext;
#[cfg(feature = "task")]
type TaskRegistrar<S, C> = Box<dyn FnOnce(&mut crate::TaskScheduler<S, C>) + Send>;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct EmptyConfig {}
#[derive(Clone, Debug, Default)]
pub struct EmptyState {}
pub struct Gotcha<S = EmptyState, C = EmptyConfig>
where
S: Clone + Send + Sync + 'static,
C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
{
router: GotchaRouter<GotchaContext<S, C>>,
host: String,
port: u16,
state: Option<S>,
config: Option<ConfigWrapper<C>>,
config_builder: Option<ConfigState>,
#[cfg(feature = "task")]
tasks: Vec<TaskRegistrar<S, C>>,
}
impl Default for Gotcha<EmptyState, EmptyConfig> {
fn default() -> Self {
Self::new()
}
}
impl Gotcha<EmptyState, EmptyConfig> {
pub fn new() -> Self {
Self {
router: GotchaRouter::default(),
host: "127.0.0.1".to_string(),
port: 3000,
state: None,
config: None,
config_builder: None,
#[cfg(feature = "task")]
tasks: Vec::new(),
}
}
}
impl Gotcha {
pub fn with_types<S, C>() -> Gotcha<S, C>
where
S: Clone + Send + Sync + 'static + Default,
C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
{
Gotcha {
router: GotchaRouter::default(),
host: "127.0.0.1".to_string(),
port: 3000,
state: None,
config: None,
config_builder: None,
#[cfg(feature = "task")]
tasks: Vec::new(),
}
}
pub fn with_state<S>() -> Gotcha<S, EmptyConfig>
where
S: Clone + Send + Sync + 'static + Default,
{
Gotcha {
router: GotchaRouter::default(),
host: "127.0.0.1".to_string(),
port: 3000,
state: None,
config: None,
config_builder: None,
#[cfg(feature = "task")]
tasks: Vec::new(),
}
}
pub fn with_config<C>() -> Gotcha<EmptyState, C>
where
C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
{
Gotcha {
router: GotchaRouter::default(),
host: "127.0.0.1".to_string(),
port: 3000,
state: None,
config: None,
config_builder: None,
#[cfg(feature = "task")]
tasks: Vec::new(),
}
}
}
impl<S, C> Gotcha<S, C>
where
S: Clone + Send + Sync + 'static + Default,
C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
{
pub fn state(mut self, state: S) -> Self {
self.state = Some(state);
self
}
pub fn config(mut self, config: ConfigWrapper<C>) -> Self {
self.config = Some(config);
self
}
pub fn build_config<F>(mut self, builder_fn: F) -> GotchaResult<Self>
where
F: FnOnce(ConfigBuilder) -> ConfigBuilder,
{
let builder = Config::builder();
let configured_builder = builder_fn(builder);
let config: ConfigWrapper<C> = configured_builder.build()?;
self.config = Some(config);
self.config_builder = None; Ok(self)
}
pub fn with_default_config(self) -> Self {
self.with_default_files().with_default_env()
}
pub fn with_env_config<P: AsRef<str>>(mut self, prefix: P) -> Self {
let mut state = self.config_builder.take().unwrap_or_else(|| ConfigState {
file_paths: Vec::new(),
env_prefixes: Vec::new(),
enable_vars: true,
});
state.env_prefixes.push(prefix.as_ref().to_string());
self.config_builder = Some(state);
self
}
pub fn with_file_config<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
let mut state = self.config_builder.take().unwrap_or_else(|| ConfigState {
file_paths: Vec::new(),
env_prefixes: Vec::new(),
enable_vars: true,
});
state.file_paths.push(path.as_ref().to_path_buf());
self.config_builder = Some(state);
self
}
pub fn with_optional_config<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
let mut state = self.config_builder.take().unwrap_or_else(|| ConfigState {
file_paths: Vec::new(),
env_prefixes: Vec::new(),
enable_vars: true,
});
state.file_paths.push(path.as_ref().to_path_buf());
self.config_builder = Some(state);
self
}
pub fn with_default_files(mut self) -> Self {
let mut state = self.config_builder.take().unwrap_or_default();
state.file_paths.push("configurations/application.toml".into());
if let Ok(profile) = std::env::var("GOTCHA_ACTIVE_PROFILE") {
let profile_path = format!("configurations/application_{}.toml", profile);
state.file_paths.push(profile_path.into());
}
self.config_builder = Some(state);
self
}
pub fn with_default_env(mut self) -> Self {
let mut state = self.config_builder.take().unwrap_or_default();
state.env_prefixes.push("APP".to_string());
self.config_builder = Some(state);
self
}
pub fn enable_variable_substitution(mut self) -> Self {
let mut state = self.config_builder.take().unwrap_or_default();
state.enable_vars = true;
self.config_builder = Some(state);
self
}
pub fn host<H: Into<String>>(mut self, host: H) -> Self {
self.host = host.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn get<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.get(path, handler);
self
}
pub fn post<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.post(path, handler);
self
}
pub fn put<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.put(path, handler);
self
}
pub fn delete<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.delete(path, handler);
self
}
pub fn patch<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.patch(path, handler);
self
}
pub fn head<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.head(path, handler);
self
}
pub fn options<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.options(path, handler);
self
}
pub fn trace<H, T>(mut self, path: &str, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.trace(path, handler);
self
}
pub fn route(mut self, path: &str, method_router: MethodRouter<GotchaContext<S, C>>) -> Self {
self.router = self.router.route(path, method_router);
self
}
pub fn routes<F>(mut self, routes_fn: F) -> Self
where
F: FnOnce(GotchaRouter<GotchaContext<S, C>>) -> GotchaRouter<GotchaContext<S, C>>,
{
self.router = routes_fn(self.router);
self
}
pub fn nest(mut self, path: &str, other: Self) -> Self {
self.router = self.router.nest(path, other.router);
self
}
pub fn merge(mut self, other: Self) -> Self {
self.router = self.router.merge(other.router);
self
}
pub fn layer<L>(mut self, layer: L) -> Self
where
L: Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
L::Service: Service<Request> + Clone + Send + Sync + 'static,
<L::Service as Service<Request>>::Response: Responder + 'static,
<L::Service as Service<Request>>::Error: Into<std::convert::Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,
{
self.router = self.router.layer(layer);
self
}
pub fn fallback<H, T>(mut self, handler: H) -> Self
where
H: Handler<T, GotchaContext<S, C>>,
T: 'static,
{
self.router = self.router.fallback(handler);
self
}
#[cfg(feature = "openapi")]
pub fn openapi<F>(mut self, transform: F) -> Self
where
F: FnOnce(oas::OpenAPIV3) -> oas::OpenAPIV3 + Send + 'static,
{
self.router = self.router.openapi(transform);
self
}
#[cfg(feature = "task")]
pub fn tasks<F>(mut self, register: F) -> Self
where
F: FnOnce(&mut crate::TaskScheduler<S, C>) + Send + 'static,
{
self.tasks.push(Box::new(register));
self
}
#[cfg(feature = "cors")]
pub fn with_cors(self) -> Self {
use crate::layers::CorsLayer;
self.layer(CorsLayer::permissive())
}
#[cfg(feature = "openapi")]
pub fn with_openapi(self) -> Self {
self
}
pub async fn listen<A>(self, addr: A) -> GotchaResult<()>
where
A: AsRef<str>,
{
let addr_str = addr.as_ref();
let socket_addr: SocketAddr = addr_str.parse().map_err(|_| GotchaError::InvalidAddress(addr_str.to_string()))?;
self.listen_on(socket_addr).await
}
pub async fn listen_on(self, addr: SocketAddr) -> GotchaResult<()> {
tracing::info!("🚀 Starting Gotcha server on {}", addr);
let context = self.build_context().await?;
#[cfg(feature = "task")]
{
let tasks = self.tasks;
if !tasks.is_empty() {
let mut scheduler = crate::TaskScheduler::new(context.clone());
for register in tasks {
register(&mut scheduler);
}
}
}
let app_router = self.router.into_axum_router(context);
let listener = tokio::net::TcpListener::bind(addr).await.map_err(|source| GotchaError::Bind {
addr: addr.to_string(),
source,
})?;
tracing::info!("✅ Server listening on http://{}", addr);
axum::serve(listener, app_router).await.map_err(GotchaError::Io)?;
Ok(())
}
pub async fn run(self) -> GotchaResult<()> {
let ip = Ipv4Addr::from_str(&self.host).map_err(|_| GotchaError::InvalidAddress(self.host.clone()))?;
let addr = SocketAddrV4::new(ip, self.port);
self.listen_on(SocketAddr::V4(addr)).await
}
async fn build_context(&self) -> GotchaResult<GotchaContext<S, C>> {
let config = match (&self.config, &self.config_builder) {
(Some(config), _) => config.clone(),
(None, Some(state)) => {
let builder = ConfigBuilder::from_state(state.clone());
match builder.build::<ConfigWrapper<C>>() {
Ok(config) => {
tracing::info!("Configuration loaded successfully from accumulated sources");
config
}
Err(e) => {
tracing::warn!("Failed to load accumulated configuration: {e}, using defaults");
ConfigWrapper {
server: ServerConfig {
host: self.host.clone(),
port: self.port,
},
app: C::default(),
}
}
}
}
(None, None) => match GotchaConfigLoader::load::<ConfigWrapper<C>>(std::env::var("GOTCHA_ACTIVE_PROFILE").ok()) {
Ok(config) => config,
Err(e) => {
tracing::warn!("Failed to load configuration: {e}, using defaults");
ConfigWrapper {
server: ServerConfig {
host: self.host.clone(),
port: self.port,
},
app: C::default(),
}
}
},
};
let state = match &self.state {
Some(state) => state.clone(),
None => S::default(),
};
Ok(GotchaContext { config, state })
}
}
impl Gotcha<EmptyState, EmptyConfig> {
pub async fn quick_start() -> GotchaResult<Self> {
tracing_subscriber::fmt::init();
Ok(Self::new())
}
}