use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PachyConfig {
#[serde(default)]
pub name: Option<String>,
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default = "default_watch_dirs")]
pub watch_dirs: Vec<String>,
#[serde(default)]
pub debug_mode: bool,
#[serde(default = "default_log_level")]
pub log_level: String,
#[serde(default = "default_shutdown_timeout_secs")]
pub shutdown_timeout_secs: u64,
#[serde(default = "default_deploy_target")]
pub deploy_target: DeployTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DeployTarget {
#[default]
Native,
Serverless,
Docker,
}
impl std::fmt::Display for DeployTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeployTarget::Native => write!(f, "native"),
DeployTarget::Serverless => write!(f, "serverless"),
DeployTarget::Docker => write!(f, "docker"),
}
}
}
fn default_host() -> String {
"127.0.0.1".to_string()
}
fn default_port() -> u16 {
3000
}
fn default_watch_dirs() -> Vec<String> {
vec!["src".to_string(), "Cargo.toml".to_string()]
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_shutdown_timeout_secs() -> u64 {
30
}
fn default_deploy_target() -> DeployTarget {
DeployTarget::Native
}
impl Default for PachyConfig {
fn default() -> Self {
Self {
name: None,
host: default_host(),
port: default_port(),
watch_dirs: default_watch_dirs(),
debug_mode: true,
log_level: default_log_level(),
shutdown_timeout_secs: default_shutdown_timeout_secs(),
deploy_target: DeployTarget::Native,
}
}
}
impl PachyConfig {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config: {}", path.display()))?;
let mut cfg: Self = toml::from_str(&content)
.with_context(|| format!("invalid config at {}", path.display()))?;
cfg.apply_env_overrides();
Ok(cfg)
}
pub fn load_or_default(config_path: Option<&Path>) -> Result<Self> {
let candidates: Vec<PathBuf> = match config_path {
Some(p) => vec![p.to_path_buf()],
None => vec![
PathBuf::from("pachy.toml"),
PathBuf::from("Pachy.toml"),
],
};
for path in &candidates {
if path.exists() {
return Self::load(path);
}
}
let mut cfg = Self::default();
cfg.apply_env_overrides();
Ok(cfg)
}
fn apply_env_overrides(&mut self) {
if let Ok(v) = std::env::var("PACHY_HOST") {
self.host = v;
}
if let Ok(v) = std::env::var("PACHY_PORT") {
if let Ok(p) = v.parse::<u16>() {
self.port = p;
}
}
if let Ok(v) = std::env::var("PACHY_LOG_LEVEL") {
self.log_level = v;
}
if let Ok(v) = std::env::var("PACHY_DEBUG") {
self.debug_mode = matches!(v.to_lowercase().as_str(), "1" | "true" | "yes");
}
if let Ok(v) = std::env::var("PACHY_SHUTDOWN_TIMEOUT") {
if let Ok(t) = v.parse::<u64>() {
self.shutdown_timeout_secs = t;
}
}
if let Ok(v) = std::env::var("PACHY_DEPLOY_TARGET") {
self.deploy_target = match v.to_lowercase().as_str() {
"serverless" => DeployTarget::Serverless,
"docker" => DeployTarget::Docker,
_ => DeployTarget::Native,
};
}
}
pub fn addr(&self) -> String {
format!("{}:{}", self.host, self.port)
}
pub fn bin_name(&self) -> String {
if let Some(n) = &self.name {
return n.clone();
}
let dir_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()));
if let Some(name) = dir_name {
let cargo_toml = std::path::Path::new("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(cargo_toml) {
if content.contains(&format!("name = \"{name}\"")) {
return name;
}
}
}
}
"pachy".to_string()
}
pub fn effective_log_filter(&self) -> String {
std::env::var("RUST_LOG").unwrap_or_else(|_| self.log_level.clone())
}
}