1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct PachyConfig {
8 #[serde(default)]
10 pub name: Option<String>,
11
12 #[serde(default = "default_host")]
13 pub host: String,
14
15 #[serde(default = "default_port")]
16 pub port: u16,
17
18 #[serde(default = "default_watch_dirs")]
19 pub watch_dirs: Vec<String>,
20
21 #[serde(default)]
22 pub debug_mode: bool,
23
24 #[serde(default = "default_log_level")]
26 pub log_level: String,
27
28 #[serde(default = "default_shutdown_timeout_secs")]
30 pub shutdown_timeout_secs: u64,
31
32 #[serde(default = "default_deploy_target")]
34 pub deploy_target: DeployTarget,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
38#[serde(rename_all = "lowercase")]
39pub enum DeployTarget {
40 #[default]
42 Native,
43 Serverless,
45 Docker,
47}
48
49impl std::fmt::Display for DeployTarget {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 DeployTarget::Native => write!(f, "native"),
53 DeployTarget::Serverless => write!(f, "serverless"),
54 DeployTarget::Docker => write!(f, "docker"),
55 }
56 }
57}
58
59fn default_host() -> String {
60 "127.0.0.1".to_string()
61}
62
63fn default_port() -> u16 {
64 3000
65}
66
67fn default_watch_dirs() -> Vec<String> {
68 vec!["src".to_string(), "Cargo.toml".to_string()]
69}
70
71fn default_log_level() -> String {
72 "info".to_string()
73}
74
75fn default_shutdown_timeout_secs() -> u64 {
76 30
77}
78
79fn default_deploy_target() -> DeployTarget {
80 DeployTarget::Native
81}
82
83impl Default for PachyConfig {
84 fn default() -> Self {
85 Self {
86 name: None,
87 host: default_host(),
88 port: default_port(),
89 watch_dirs: default_watch_dirs(),
90 debug_mode: true,
91 log_level: default_log_level(),
92 shutdown_timeout_secs: default_shutdown_timeout_secs(),
93 deploy_target: DeployTarget::Native,
94 }
95 }
96}
97
98impl PachyConfig {
99 pub fn load(path: &Path) -> Result<Self> {
100 let content = std::fs::read_to_string(path)
101 .with_context(|| format!("failed to read config: {}", path.display()))?;
102 let mut cfg: Self = toml::from_str(&content)
103 .with_context(|| format!("invalid config at {}", path.display()))?;
104 cfg.apply_env_overrides();
105 Ok(cfg)
106 }
107
108 pub fn load_or_default(config_path: Option<&Path>) -> Result<Self> {
109 let candidates: Vec<PathBuf> = match config_path {
110 Some(p) => vec![p.to_path_buf()],
111 None => vec![
112 PathBuf::from("pachy.toml"),
113 PathBuf::from("Pachy.toml"),
114 ],
115 };
116
117 for path in &candidates {
118 if path.exists() {
119 return Self::load(path);
120 }
121 }
122
123 let mut cfg = Self::default();
124 cfg.apply_env_overrides();
125 Ok(cfg)
126 }
127
128 fn apply_env_overrides(&mut self) {
133 if let Ok(v) = std::env::var("PACHY_HOST") {
134 self.host = v;
135 }
136 if let Ok(v) = std::env::var("PACHY_PORT") {
137 if let Ok(p) = v.parse::<u16>() {
138 self.port = p;
139 }
140 }
141 if let Ok(v) = std::env::var("PACHY_LOG_LEVEL") {
142 self.log_level = v;
143 }
144 if let Ok(v) = std::env::var("PACHY_DEBUG") {
145 self.debug_mode = matches!(v.to_lowercase().as_str(), "1" | "true" | "yes");
146 }
147 if let Ok(v) = std::env::var("PACHY_SHUTDOWN_TIMEOUT") {
148 if let Ok(t) = v.parse::<u64>() {
149 self.shutdown_timeout_secs = t;
150 }
151 }
152 if let Ok(v) = std::env::var("PACHY_DEPLOY_TARGET") {
153 self.deploy_target = match v.to_lowercase().as_str() {
154 "serverless" => DeployTarget::Serverless,
155 "docker" => DeployTarget::Docker,
156 _ => DeployTarget::Native,
157 };
158 }
159 }
160
161 pub fn addr(&self) -> String {
162 format!("{}:{}", self.host, self.port)
163 }
164
165 pub fn bin_name(&self) -> String {
169 if let Some(n) = &self.name {
170 return n.clone();
171 }
172 let dir_name = std::env::current_dir()
177 .ok()
178 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()));
179
180 if let Some(name) = dir_name {
181 let cargo_toml = std::path::Path::new("Cargo.toml");
183 if cargo_toml.exists() {
184 if let Ok(content) = std::fs::read_to_string(cargo_toml) {
185 if content.contains(&format!("name = \"{name}\"")) {
187 return name;
188 }
189 }
190 }
191 }
192
193 "pachy".to_string()
194 }
195
196 pub fn effective_log_filter(&self) -> String {
197 std::env::var("RUST_LOG").unwrap_or_else(|_| self.log_level.clone())
199 }
200}