use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::str::FromStr;
use axum::extract::Request;
use axum::handler::Handler;
use axum::routing::MethodRouter;
use axum::Router;
use serde::{Deserialize, Serialize};
use tower_layer::Layer;
use tower_service::Service;
use crate::config::{BasicConfig, Config, ConfigBuilder, ConfigState, ConfigWrapper, GotchaConfigLoader};
use crate::router::{GotchaRouter, Responder};
use crate::GotchaContext;
#[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>,
}
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,
}
}
}
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,
}
}
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,
}
}
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,
}
}
}
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) -> Result<Self, crate::config::ConfigError>
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 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 + 'static,
L::Service: Service<Request> + Clone + Send + '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
}
#[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) -> Result<(), Box<dyn std::error::Error>>
where
A: AsRef<str>,
{
let addr_str = addr.as_ref();
let socket_addr: SocketAddr = addr_str.parse().map_err(|_| format!("Invalid address format: {}", addr_str))?;
self.listen_on(socket_addr).await
}
pub async fn listen_on(self, addr: SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("🚀 Starting Gotcha server on {}", addr);
let context = self.build_context().await?;
let app_router = self.build_app_router(context).await?;
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("✅ Server listening on http://{}", addr);
axum::serve(listener, app_router).await?;
Ok(())
}
pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
let host = self.host.clone();
let port = self.port;
let addr = SocketAddrV4::new(Ipv4Addr::from_str(&host)?, port);
self.listen_on(SocketAddr::V4(addr)).await
}
async fn build_context(&self) -> Result<GotchaContext<S, C>, Box<dyn std::error::Error>> {
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: {}, using defaults", e);
tracing::info!("💡 Check configuration files and environment variables");
ConfigWrapper {
basic: BasicConfig {
host: self.host.clone(),
port: self.port,
},
application: C::default(),
}
}
}
}
(None, None) => {
match std::panic::catch_unwind(|| GotchaConfigLoader::load::<ConfigWrapper<C>>(std::env::var("GOTCHA_ACTIVE_PROFILE").ok())) {
Ok(config) => config,
Err(_) => {
tracing::warn!("Failed to load configuration, using defaults");
ConfigWrapper {
basic: BasicConfig {
host: self.host.clone(),
port: self.port,
},
application: C::default(),
}
}
}
}
};
let state = match &self.state {
Some(state) => state.clone(),
None => S::default(),
};
Ok(GotchaContext { config, state })
}
async fn build_app_router(self, context: GotchaContext<S, C>) -> Result<Router, Box<dyn std::error::Error>> {
let GotchaRouter {
#[cfg(feature = "openapi")]
operations,
router: raw_router,
} = self.router;
#[cfg(feature = "openapi")]
let openapi_spec = crate::openapi::generate_openapi(operations);
cfg_if::cfg_if! {
if #[cfg(feature = "openapi")] {
use axum::Json;
let router = raw_router
.with_state(context.clone())
.route("/openapi.json", axum::routing::get(move || async move {
Json(openapi_spec.clone())
}))
.route("/redoc", axum::routing::get(crate::openapi::openapi_html))
.route("/scalar", axum::routing::get(crate::openapi::scalar_html));
} else {
let router = raw_router.with_state(context.clone());
}
}
Ok(router)
}
}
impl Gotcha<EmptyState, EmptyConfig> {
pub async fn quick_start() -> Result<Self, Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
Ok(Self::new())
}
}