Skip to main content

pachy_config/
lib.rs

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    /// Application / binary name. Defaults to the current directory name.
9    #[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    /// Minimum log level: error | warn | info | debug | trace
25    #[serde(default = "default_log_level")]
26    pub log_level: String,
27
28    /// Seconds to wait for in-flight requests before forceful shutdown.
29    #[serde(default = "default_shutdown_timeout_secs")]
30    pub shutdown_timeout_secs: u64,
31
32    /// Deployment target for `pachy build --target`.
33    #[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    /// Standard native binary (default).
41    #[default]
42    Native,
43    /// AWS Lambda / Vercel serverless function.
44    Serverless,
45    /// OCI container image via Docker.
46    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    /// Environment variables override pachy.toml values.
129    ///
130    /// PACHY_HOST, PACHY_PORT, PACHY_LOG_LEVEL, PACHY_DEBUG,
131    /// PACHY_SHUTDOWN_TIMEOUT, PACHY_DEPLOY_TARGET
132    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    /// The binary name to pass to `cargo build --bin`.
166    /// Uses `name` from pachy.toml if set, otherwise falls back to the
167    /// current directory name, then "app".
168    pub fn bin_name(&self) -> String {
169        if let Some(n) = &self.name {
170            return n.clone();
171        }
172        // Try current directory name, but only if it looks like a valid bin name
173        // (no spaces, not a framework root like "Pachycephalosaurus").
174        // Fall back to "pachy" so `cargo build --bin pachy` always works from
175        // the framework root.
176        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            // Check if a Cargo.toml in the current dir declares this bin.
182            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 the Cargo.toml has [[bin]] with this name, use it.
186                    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        // RUST_LOG takes final priority over everything.
198        std::env::var("RUST_LOG").unwrap_or_else(|_| self.log_level.clone())
199    }
200}