use std::path::{Path, PathBuf};
use foundation_core::valtron::ThreadedValue;
use super::traits::{StateStore, StateStoreStream};
use super::types::ResourceState;
use crate::core::errors::StorageError;
pub struct FileStateStore {
root_dir: PathBuf,
}
impl FileStateStore {
#[must_use]
pub fn new(project_dir: &Path, project: &str, stage: &str) -> Self {
Self {
root_dir: project_dir.join(".deployment").join(project).join(stage),
}
}
#[must_use]
pub fn with_root(root_dir: PathBuf) -> Self {
Self { root_dir }
}
fn resource_path(&self, resource_id: &str) -> PathBuf {
let safe_id = resource_id.replace('/', ":");
self.root_dir.join(format!("{safe_id}.json"))
}
fn wrap_value<T: Send + 'static>(val: T) -> StateStoreStream<T> {
Box::new(std::iter::once(ThreadedValue::Value(Ok(val))))
}
fn wrap_vec<T: Send + 'static>(vals: Vec<T>) -> StateStoreStream<T> {
Box::new(vals.into_iter().map(|v| ThreadedValue::Value(Ok(v))))
}
}
impl StateStore for FileStateStore {
fn init(&self) -> Result<(), StorageError> {
std::fs::create_dir_all(&self.root_dir)?;
Ok(())
}
fn list(&self) -> Result<StateStoreStream<String>, StorageError> {
let mut ids = Vec::new();
if self.root_dir.exists() {
for entry in std::fs::read_dir(&self.root_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "json") {
if let Some(name) = path.file_stem() {
ids.push(name.to_string_lossy().replace(':', "/"));
}
}
}
}
ids.sort();
Ok(Self::wrap_vec(ids))
}
fn count(&self) -> Result<StateStoreStream<usize>, StorageError> {
let mut count = 0;
if self.root_dir.exists() {
for entry in std::fs::read_dir(&self.root_dir)? {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "json") {
count += 1;
}
}
}
Ok(Self::wrap_value(count))
}
fn get(
&self,
resource_id: &str,
) -> Result<StateStoreStream<Option<ResourceState>>, StorageError> {
let path = self.resource_path(resource_id);
if !path.exists() {
return Ok(Self::wrap_value(None));
}
let content = std::fs::read_to_string(&path)?;
let state: ResourceState = serde_json::from_str(&content)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
Ok(Self::wrap_value(Some(state)))
}
fn get_batch(&self, ids: &[&str]) -> Result<StateStoreStream<ResourceState>, StorageError> {
let mut results = Vec::new();
for id in ids {
let path = self.resource_path(id);
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let state: ResourceState = serde_json::from_str(&content)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
results.push(state);
}
}
Ok(Self::wrap_vec(results))
}
fn all(&self) -> Result<StateStoreStream<ResourceState>, StorageError> {
let mut results = Vec::new();
if self.root_dir.exists() {
for entry in std::fs::read_dir(&self.root_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "json") {
let content = std::fs::read_to_string(&path)?;
let state: ResourceState = serde_json::from_str(&content)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
results.push(state);
}
}
}
results.sort_by(|a, b| a.id.cmp(&b.id));
Ok(Self::wrap_vec(results))
}
fn set(
&self,
_resource_id: &str,
state: &ResourceState,
) -> Result<StateStoreStream<()>, StorageError> {
self.init()?;
let path = self.resource_path(&state.id);
let content = serde_json::to_string_pretty(state)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
std::fs::write(&path, content)?;
Ok(Self::wrap_value(()))
}
fn delete(&self, resource_id: &str) -> Result<StateStoreStream<()>, StorageError> {
let path = self.resource_path(resource_id);
if path.exists() {
std::fs::remove_file(&path)?;
}
Ok(Self::wrap_value(()))
}
}