use crate::{Result, ResultContext};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SavedObjectsManifest {
pub objects: Vec<SavedObject>,
#[serde(default = "default_exclude_export_details")]
pub exclude_export_details: bool,
#[serde(default = "default_include_references_deep")]
pub include_references_deep: bool,
}
fn default_exclude_export_details() -> bool {
true
}
fn default_include_references_deep() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct SavedObject {
#[serde(rename = "type")]
pub object_type: String,
pub id: String,
}
impl SavedObjectsManifest {
pub fn new() -> Self {
Self {
objects: Vec::new(),
exclude_export_details: true,
include_references_deep: true,
}
}
pub fn with_objects(objects: Vec<SavedObject>) -> Self {
Self {
objects,
exclude_export_details: true,
include_references_deep: true,
}
}
pub fn add_object(&mut self, object: SavedObject) {
if !self.objects.contains(&object) {
self.objects.push(object);
}
}
pub fn remove_object(&mut self, object_type: &str, id: &str) -> bool {
if let Some(pos) = self
.objects
.iter()
.position(|o| o.object_type == object_type && o.id == id)
{
self.objects.remove(pos);
true
} else {
false
}
}
pub fn contains(&self, object_type: &str, id: &str) -> bool {
self.objects
.iter()
.any(|o| o.object_type == object_type && o.id == id)
}
pub fn count(&self) -> usize {
self.objects.len()
}
pub fn sort(&mut self) {
self.objects
.sort_by(|a, b| a.object_type.cmp(&b.object_type).then(a.id.cmp(&b.id)));
}
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 saved objects manifest: {}",
path.as_ref().display()
)
})?;
let manifest: Self = serde_json::from_str(&content)
.with_context(|| "Failed to parse saved objects manifest JSON")?;
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 json = serde_json::to_string_pretty(self)
.with_context(|| "Failed to serialize saved objects manifest to JSON")?;
std::fs::write(path.as_ref(), json).with_context(|| {
format!(
"Failed to write saved objects manifest: {}",
path.as_ref().display()
)
})?;
Ok(())
}
}
impl Default for SavedObjectsManifest {
fn default() -> Self {
Self::new()
}
}
impl SavedObject {
pub fn new(object_type: impl Into<String>, id: impl Into<String>) -> Self {
Self {
object_type: object_type.into(),
id: id.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_new_manifest() {
let manifest = SavedObjectsManifest::new();
assert_eq!(manifest.count(), 0);
assert!(manifest.exclude_export_details);
assert!(manifest.include_references_deep);
}
#[test]
fn test_with_objects() {
let objects = vec![
SavedObject::new("dashboard", "dash-1"),
SavedObject::new("visualization", "viz-1"),
];
let manifest = SavedObjectsManifest::with_objects(objects);
assert_eq!(manifest.count(), 2);
assert!(manifest.contains("dashboard", "dash-1"));
assert!(manifest.contains("visualization", "viz-1"));
}
#[test]
fn test_add_object() {
let mut manifest = SavedObjectsManifest::new();
manifest.add_object(SavedObject::new("dashboard", "test"));
assert_eq!(manifest.count(), 1);
assert!(manifest.contains("dashboard", "test"));
manifest.add_object(SavedObject::new("dashboard", "test"));
assert_eq!(manifest.count(), 1);
}
#[test]
fn test_remove_object() {
let mut manifest = SavedObjectsManifest::with_objects(vec![
SavedObject::new("dashboard", "dash-1"),
SavedObject::new("visualization", "viz-1"),
]);
assert!(manifest.remove_object("dashboard", "dash-1"));
assert_eq!(manifest.count(), 1);
assert!(!manifest.contains("dashboard", "dash-1"));
assert!(!manifest.remove_object("dashboard", "nonexistent"));
}
#[test]
fn test_sort() {
let mut manifest = SavedObjectsManifest::with_objects(vec![
SavedObject::new("visualization", "viz-2"),
SavedObject::new("dashboard", "dash-2"),
SavedObject::new("dashboard", "dash-1"),
SavedObject::new("visualization", "viz-1"),
]);
manifest.sort();
assert_eq!(manifest.objects[0].object_type, "dashboard");
assert_eq!(manifest.objects[0].id, "dash-1");
assert_eq!(manifest.objects[1].object_type, "dashboard");
assert_eq!(manifest.objects[1].id, "dash-2");
assert_eq!(manifest.objects[2].object_type, "visualization");
assert_eq!(manifest.objects[2].id, "viz-1");
}
#[test]
fn test_read_write() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("manifest").join("saved_objects.json");
let mut original = SavedObjectsManifest::with_objects(vec![
SavedObject::new("dashboard", "dash-1"),
SavedObject::new("visualization", "viz-1"),
SavedObject::new("index-pattern", "idx-1"),
]);
original.exclude_export_details = true;
original.include_references_deep = false;
original.write(&manifest_path).unwrap();
assert!(manifest_path.exists());
let loaded = SavedObjectsManifest::read(&manifest_path).unwrap();
assert_eq!(loaded, original);
assert_eq!(loaded.count(), 3);
assert!(!loaded.include_references_deep);
}
#[test]
fn test_json_format() {
let manifest = SavedObjectsManifest::with_objects(vec![
SavedObject::new("dashboard", "dash-1"),
SavedObject::new("visualization", "viz-1"),
]);
let json = serde_json::to_string_pretty(&manifest).unwrap();
assert!(json.contains("\"objects\""));
assert!(json.contains("\"type\""));
assert!(json.contains("\"dashboard\""));
assert!(json.contains("\"excludeExportDetails\""));
assert!(json.contains("\"includeReferencesDeep\""));
}
}