use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ServerPlatform {
GitHub,
GitLab,
}
impl std::fmt::Display for ServerPlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServerPlatform::GitHub => write!(f, "github"),
ServerPlatform::GitLab => write!(f, "gitlab"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub name: String,
pub platform: ServerPlatform,
pub api_url: String,
pub web_url: Option<String>,
pub push_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServerRegistry {
pub servers: Vec<ServerConfig>,
}
impl ServerRegistry {
pub fn load() -> Result<Self> {
let path = registry_path()?;
if !path.exists() {
return Ok(Self::default());
}
let content =
std::fs::read_to_string(&path).context("Failed to read server registry file")?;
serde_json::from_str(&content).context("Failed to parse server registry")
}
pub fn save(&self) -> Result<()> {
let path = registry_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create config directory")?;
}
let json = serde_json::to_string_pretty(self)?;
std::fs::write(&path, json).context("Failed to write server registry")?;
Ok(())
}
pub fn add(&mut self, server: ServerConfig) -> Result<()> {
if self.servers.iter().any(|s| s.name == server.name) {
bail!("Server '{}' already exists", server.name);
}
self.servers.push(server);
Ok(())
}
pub fn remove(&mut self, name: &str) -> Result<()> {
let len_before = self.servers.len();
self.servers.retain(|s| s.name != name);
if self.servers.len() == len_before {
bail!("Server '{}' not found", name);
}
Ok(())
}
pub fn get(&self, name: &str) -> Option<&ServerConfig> {
self.servers.iter().find(|s| s.name == name)
}
pub fn push_targets(&self) -> Vec<&ServerConfig> {
self.servers.iter().filter(|s| s.push_enabled).collect()
}
}
fn registry_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not determine home directory")?;
Ok(home.join(".config/securegit/servers.json"))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_server(name: &str, platform: ServerPlatform, push: bool) -> ServerConfig {
ServerConfig {
name: name.to_string(),
platform,
api_url: format!("https://{}.example.com/api", name),
web_url: None,
push_enabled: push,
}
}
#[test]
fn test_add_and_get_server() {
let mut reg = ServerRegistry::default();
let server = make_server("test-gh", ServerPlatform::GitHub, true);
reg.add(server).unwrap();
let found = reg.get("test-gh");
assert!(found.is_some());
assert_eq!(found.unwrap().platform, ServerPlatform::GitHub);
}
#[test]
fn test_add_duplicate_name_fails() {
let mut reg = ServerRegistry::default();
reg.add(make_server("dup", ServerPlatform::GitHub, true))
.unwrap();
let result = reg.add(make_server("dup", ServerPlatform::GitLab, false));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[test]
fn test_remove_server() {
let mut reg = ServerRegistry::default();
reg.add(make_server("to-remove", ServerPlatform::GitLab, false))
.unwrap();
assert!(reg.get("to-remove").is_some());
reg.remove("to-remove").unwrap();
assert!(reg.get("to-remove").is_none());
}
#[test]
fn test_remove_nonexistent_fails() {
let mut reg = ServerRegistry::default();
let result = reg.remove("ghost");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_push_targets_filters() {
let mut reg = ServerRegistry::default();
reg.add(make_server("push-yes", ServerPlatform::GitHub, true))
.unwrap();
reg.add(make_server("push-no", ServerPlatform::GitLab, false))
.unwrap();
reg.add(make_server("push-also", ServerPlatform::GitLab, true))
.unwrap();
let targets = reg.push_targets();
assert_eq!(targets.len(), 2);
assert!(targets.iter().any(|s| s.name == "push-yes"));
assert!(targets.iter().any(|s| s.name == "push-also"));
}
#[test]
fn test_save_and_load_roundtrip() {
let tmpdir = tempfile::tempdir().unwrap();
let path = tmpdir.path().join("servers.json");
let mut reg = ServerRegistry::default();
reg.add(make_server("roundtrip", ServerPlatform::GitHub, true))
.unwrap();
let json = serde_json::to_string_pretty(®).unwrap();
std::fs::write(&path, &json).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let loaded: ServerRegistry = serde_json::from_str(&content).unwrap();
assert_eq!(loaded.servers.len(), 1);
assert_eq!(loaded.servers[0].name, "roundtrip");
assert_eq!(loaded.servers[0].platform, ServerPlatform::GitHub);
}
#[test]
fn test_empty_registry() {
let reg = ServerRegistry::default();
assert!(reg.servers.is_empty());
assert!(reg.push_targets().is_empty());
assert!(reg.get("anything").is_none());
}
#[test]
fn test_server_platform_display() {
assert_eq!(ServerPlatform::GitHub.to_string(), "github");
assert_eq!(ServerPlatform::GitLab.to_string(), "gitlab");
}
}