use crate::{Result, ResultContext};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpaceEntry {
pub id: String,
pub name: String,
}
impl SpaceEntry {
pub fn new(id: String, name: String) -> Self {
Self { id, name }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct KibanaMetadata {
pub version: String,
}
impl KibanaMetadata {
pub fn new(version: String) -> Self {
Self { version }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpacesManifest {
#[serde(default)]
pub kibana: Option<KibanaMetadata>,
#[serde(default)]
pub spaces: Vec<SpaceEntry>,
}
impl SpacesManifest {
pub fn new() -> Self {
Self {
kibana: None,
spaces: Vec::new(),
}
}
pub fn with_spaces(spaces: Vec<SpaceEntry>) -> Self {
Self {
kibana: None,
spaces,
}
}
pub fn set_kibana_version(&mut self, version: impl Into<String>) {
self.kibana = Some(KibanaMetadata::new(version.into()));
}
pub fn kibana_version(&self) -> Option<&str> {
self.kibana.as_ref().map(|k| k.version.as_str())
}
pub fn add_space(&mut self, id: String, name: String) -> bool {
if !self.spaces.iter().any(|s| s.id == id) {
self.spaces.push(SpaceEntry::new(id, name));
true
} else {
false
}
}
pub fn remove_space(&mut self, space_id: &str) -> bool {
if let Some(pos) = self.spaces.iter().position(|s| s.id == space_id) {
self.spaces.remove(pos);
true
} else {
false
}
}
pub fn contains(&self, space_id: &str) -> bool {
self.spaces.iter().any(|s| s.id == space_id)
}
pub fn ids(&self) -> Vec<String> {
self.spaces.iter().map(|s| s.id.clone()).collect()
}
pub fn count(&self) -> usize {
self.spaces.len()
}
pub fn read(path: impl AsRef<Path>) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref()).with_context(|| {
format!(
"Failed to read spaces manifest: {}",
path.as_ref().display()
)
})?;
let manifest: Self = yaml_serde::from_str(&content)
.with_context(|| "Failed to parse spaces manifest YAML")?;
Ok(manifest)
}
pub fn write(&self, path: impl AsRef<Path>) -> Result<()> {
if let Some(parent) = path.as_ref().parent() {
std::fs::create_dir_all(parent)?;
}
let yaml = yaml_serde::to_string(self)
.with_context(|| "Failed to serialize spaces manifest to YAML")?;
std::fs::write(path.as_ref(), yaml).with_context(|| {
format!(
"Failed to write spaces manifest: {}",
path.as_ref().display()
)
})?;
Ok(())
}
}
impl Default for SpacesManifest {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_new_manifest() {
let manifest = SpacesManifest::new();
assert_eq!(manifest.count(), 0);
}
#[test]
fn test_with_spaces() {
let manifest = SpacesManifest::with_spaces(vec![
SpaceEntry::new("default".to_string(), "Default".to_string()),
SpaceEntry::new("marketing".to_string(), "Marketing".to_string()),
]);
assert_eq!(manifest.kibana_version(), None);
assert_eq!(manifest.count(), 2);
assert!(manifest.contains("default"));
assert!(manifest.contains("marketing"));
}
#[test]
fn test_add_space() {
let mut manifest = SpacesManifest::new();
manifest.add_space("test".to_string(), "Test".to_string());
assert_eq!(manifest.count(), 1);
assert!(manifest.contains("test"));
manifest.add_space("test".to_string(), "Test".to_string());
assert_eq!(manifest.count(), 1);
}
#[test]
fn test_remove_space() {
let mut manifest = SpacesManifest::with_spaces(vec![
SpaceEntry::new("space1".to_string(), "Space 1".to_string()),
SpaceEntry::new("space2".to_string(), "Space 2".to_string()),
]);
assert!(manifest.remove_space("space1"));
assert_eq!(manifest.count(), 1);
assert!(!manifest.contains("space1"));
assert!(!manifest.remove_space("nonexistent"));
}
#[test]
fn test_read_write() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("manifest").join("spaces.yml");
let original = SpacesManifest::with_spaces(vec![
SpaceEntry::new("default".to_string(), "Default".to_string()),
SpaceEntry::new("marketing".to_string(), "Marketing".to_string()),
SpaceEntry::new("engineering".to_string(), "Engineering".to_string()),
]);
original.write(&manifest_path).unwrap();
assert!(manifest_path.exists());
let loaded = SpacesManifest::read(&manifest_path).unwrap();
assert_eq!(loaded, original);
assert_eq!(loaded.count(), 3);
}
#[test]
fn test_yaml_format() {
let mut manifest = SpacesManifest::with_spaces(vec![
SpaceEntry::new("space1".to_string(), "Space 1".to_string()),
SpaceEntry::new("space2".to_string(), "Space 2".to_string()),
]);
manifest.set_kibana_version("9.3.2");
let yaml = yaml_serde::to_string(&manifest).unwrap();
assert!(yaml.contains("kibana:"));
assert!(yaml.contains("version: 9.3.2"));
assert!(yaml.contains("spaces:"));
assert!(yaml.contains("id: space1"));
assert!(yaml.contains("name: Space 1"));
assert!(yaml.contains("id: space2"));
assert!(yaml.contains("name: Space 2"));
}
#[test]
fn test_backward_compatible_without_kibana_metadata() {
let yaml = "spaces:\n - id: default\n name: Default\n";
let manifest: SpacesManifest = yaml_serde::from_str(yaml).unwrap();
assert_eq!(manifest.kibana_version(), None);
assert_eq!(manifest.count(), 1);
assert!(manifest.contains("default"));
}
}