use a3s_box_core::error::{BoxError, Result};
use a3s_box_core::network::NetworkConfig;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct NetworkStore {
path: PathBuf,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
struct NetworksFile {
networks: HashMap<String, NetworkConfig>,
}
impl NetworkStore {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn default_path() -> Result<Self> {
let home = a3s_box_core::dirs_home();
Ok(Self::new(home.join("networks.json")))
}
pub fn load(&self) -> Result<HashMap<String, NetworkConfig>> {
if !self.path.exists() {
return Ok(HashMap::new());
}
let data = std::fs::read_to_string(&self.path).map_err(|e| {
BoxError::NetworkError(format!(
"failed to read networks file {}: {}",
self.path.display(),
e
))
})?;
let file: NetworksFile = serde_json::from_str(&data)
.map_err(|e| BoxError::NetworkError(format!("failed to parse networks file: {}", e)))?;
Ok(file.networks)
}
pub fn save(&self, networks: &HashMap<String, NetworkConfig>) -> Result<()> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
BoxError::NetworkError(format!(
"failed to create directory {}: {}",
parent.display(),
e
))
})?;
}
let file = NetworksFile {
networks: networks.clone(),
};
let json = serde_json::to_string_pretty(&file)
.map_err(|e| BoxError::NetworkError(format!("failed to serialize networks: {}", e)))?;
let tmp_path = self.path.with_extension("json.tmp");
std::fs::write(&tmp_path, &json).map_err(|e| {
BoxError::NetworkError(format!(
"failed to write tmp file {}: {}",
tmp_path.display(),
e
))
})?;
std::fs::rename(&tmp_path, &self.path).map_err(|e| {
BoxError::NetworkError(format!(
"failed to rename {} → {}: {}",
tmp_path.display(),
self.path.display(),
e
))
})?;
Ok(())
}
pub fn get(&self, name: &str) -> Result<Option<NetworkConfig>> {
let networks = self.load()?;
Ok(networks.get(name).cloned())
}
pub fn create(&self, config: NetworkConfig) -> Result<()> {
let mut networks = self.load()?;
if networks.contains_key(&config.name) {
return Err(BoxError::NetworkError(format!(
"network '{}' already exists",
config.name
)));
}
networks.insert(config.name.clone(), config);
self.save(&networks)
}
pub fn remove(&self, name: &str) -> Result<NetworkConfig> {
let mut networks = self.load()?;
let config = networks
.remove(name)
.ok_or_else(|| BoxError::NetworkError(format!("network '{}' not found", name)))?;
if !config.endpoints.is_empty() {
return Err(BoxError::NetworkError(format!(
"network '{}' has {} connected endpoint(s); disconnect them first or use --force",
name,
config.endpoints.len()
)));
}
self.save(&networks)?;
Ok(config)
}
pub fn list(&self) -> Result<Vec<NetworkConfig>> {
let networks = self.load()?;
Ok(networks.into_values().collect())
}
pub fn update(&self, config: &NetworkConfig) -> Result<()> {
let mut networks = self.load()?;
if !networks.contains_key(&config.name) {
return Err(BoxError::NetworkError(format!(
"network '{}' not found",
config.name
)));
}
networks.insert(config.name.clone(), config.clone());
self.save(&networks)
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl a3s_box_core::traits::NetworkStoreBackend for NetworkStore {
fn get(&self, name: &str) -> Result<Option<NetworkConfig>> {
self.get(name)
}
fn create(&self, config: NetworkConfig) -> Result<()> {
self.create(config)
}
fn remove(&self, name: &str) -> Result<NetworkConfig> {
self.remove(name)
}
fn list(&self) -> Result<Vec<NetworkConfig>> {
self.list()
}
fn update(&self, config: &NetworkConfig) -> Result<()> {
self.update(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_store() -> (tempfile::TempDir, NetworkStore) {
let dir = tempfile::tempdir().unwrap();
let store = NetworkStore::new(dir.path().join("networks.json"));
(dir, store)
}
#[test]
fn test_load_empty() {
let (_dir, store) = temp_store();
let networks = store.load().unwrap();
assert!(networks.is_empty());
}
#[test]
fn test_create_and_load() {
let (_dir, store) = temp_store();
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
store.create(net).unwrap();
let networks = store.load().unwrap();
assert_eq!(networks.len(), 1);
assert!(networks.contains_key("mynet"));
}
#[test]
fn test_create_duplicate() {
let (_dir, store) = temp_store();
let net1 = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let net2 = NetworkConfig::new("mynet", "10.89.0.0/24").unwrap();
store.create(net1).unwrap();
assert!(store.create(net2).is_err());
}
#[test]
fn test_get_existing() {
let (_dir, store) = temp_store();
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
store.create(net).unwrap();
let found = store.get("mynet").unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().name, "mynet");
}
#[test]
fn test_get_nonexistent() {
let (_dir, store) = temp_store();
let found = store.get("nope").unwrap();
assert!(found.is_none());
}
#[test]
fn test_remove() {
let (_dir, store) = temp_store();
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
store.create(net).unwrap();
let removed = store.remove("mynet").unwrap();
assert_eq!(removed.name, "mynet");
let networks = store.load().unwrap();
assert!(networks.is_empty());
}
#[test]
fn test_remove_nonexistent() {
let (_dir, store) = temp_store();
assert!(store.remove("nope").is_err());
}
#[test]
fn test_remove_with_endpoints() {
let (_dir, store) = temp_store();
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
store.create(net).unwrap();
assert!(store.remove("mynet").is_err());
}
#[test]
fn test_list() {
let (_dir, store) = temp_store();
store
.create(NetworkConfig::new("net1", "10.88.0.0/24").unwrap())
.unwrap();
store
.create(NetworkConfig::new("net2", "10.89.0.0/24").unwrap())
.unwrap();
let list = store.list().unwrap();
assert_eq!(list.len(), 2);
}
#[test]
fn test_update() {
let (_dir, store) = temp_store();
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
store.create(net).unwrap();
let mut net = store.get("mynet").unwrap().unwrap();
net.connect("box-1", "web").unwrap();
store.update(&net).unwrap();
let loaded = store.get("mynet").unwrap().unwrap();
assert_eq!(loaded.endpoints.len(), 1);
}
#[test]
fn test_update_nonexistent() {
let (_dir, store) = temp_store();
let net = NetworkConfig::new("nope", "10.88.0.0/24").unwrap();
assert!(store.update(&net).is_err());
}
#[test]
fn test_atomic_write() {
let (_dir, store) = temp_store();
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
store.create(net).unwrap();
let data = std::fs::read_to_string(store.path()).unwrap();
let _: serde_json::Value = serde_json::from_str(&data).unwrap();
let tmp = store.path().with_extension("json.tmp");
assert!(!tmp.exists());
}
#[test]
fn test_creates_parent_directory() {
let dir = tempfile::tempdir().unwrap();
let store = NetworkStore::new(dir.path().join("subdir").join("networks.json"));
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
store.create(net).unwrap();
assert!(store.path().exists());
}
}