use crate::error::{PortForgeError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct PortForgeConfig {
pub general: GeneralConfig,
pub health: HealthConfig,
pub detectors: Vec<CustomDetector>,
pub ports: HashMap<u16, PortOverride>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub refresh_interval: u64,
pub show_all: bool,
pub docker_enabled: bool,
pub health_checks_enabled: bool,
pub max_concurrent_health_checks: usize,
pub theme: String,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
refresh_interval: 2,
show_all: false,
docker_enabled: true,
health_checks_enabled: true,
max_concurrent_health_checks: 10,
theme: "dark".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HealthConfig {
pub timeout_ms: u64,
pub default_endpoints: Vec<String>,
pub framework_endpoints: HashMap<String, String>,
}
impl Default for HealthConfig {
fn default() -> Self {
let mut framework_endpoints = HashMap::new();
framework_endpoints.insert("next.js".to_string(), "/api/health".to_string());
framework_endpoints.insert("express".to_string(), "/health".to_string());
framework_endpoints.insert("actix".to_string(), "/health".to_string());
framework_endpoints.insert("axum".to_string(), "/health".to_string());
framework_endpoints.insert("fastapi".to_string(), "/health".to_string());
framework_endpoints.insert("rails".to_string(), "/up".to_string());
framework_endpoints.insert("django".to_string(), "/health/".to_string());
framework_endpoints.insert("spring".to_string(), "/actuator/health".to_string());
Self {
timeout_ms: 2000,
default_endpoints: vec![
"/health".to_string(),
"/healthz".to_string(),
"/api/health".to_string(),
"/".to_string(),
],
framework_endpoints,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomDetector {
pub kind: String,
pub framework: String,
pub detect_files: Vec<String>,
pub health_endpoint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortOverride {
pub label: Option<String>,
pub health_endpoint: Option<String>,
pub hidden: bool,
}
impl PortForgeConfig {
pub fn load() -> Result<Self> {
let path = Self::config_path();
if path.exists() {
let content = std::fs::read_to_string(&path).map_err(|e| {
PortForgeError::ConfigError(format!(
"Failed to read config at {}: {}",
path.display(),
e
))
})?;
let config: PortForgeConfig = toml::from_str(&content).map_err(|e| {
PortForgeError::ConfigError(format!("Failed to parse config: {}", e))
})?;
Ok(config)
} else {
Ok(Self::default())
}
}
pub fn write_default() -> Result<PathBuf> {
let path = Self::config_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
PortForgeError::ConfigError(format!("Failed to create config directory: {}", e))
})?;
}
let default_config = Self::default();
let content = toml::to_string_pretty(&default_config).map_err(|e| {
PortForgeError::ConfigError(format!("Failed to serialize config: {}", e))
})?;
let header = r#"# PortForge Configuration
# Location: ~/.config/portforge.toml
# Documentation: https://github.com/kabudu/portforge#configuration
"#;
std::fs::write(&path, format!("{}{}", header, content))
.map_err(|e| PortForgeError::ConfigError(format!("Failed to write config: {}", e)))?;
Ok(path)
}
pub fn config_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("portforge.toml")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = PortForgeConfig::default();
assert_eq!(config.general.refresh_interval, 2);
assert!(!config.general.show_all);
assert!(config.general.docker_enabled);
assert_eq!(config.health.timeout_ms, 2000);
assert!(!config.health.default_endpoints.is_empty());
}
#[test]
fn test_config_serialization() {
let config = PortForgeConfig::default();
let toml_str = toml::to_string_pretty(&config).unwrap();
let parsed: PortForgeConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(
parsed.general.refresh_interval,
config.general.refresh_interval
);
}
}