use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::json;
use crate::engine::{EngineError, EngineResult};
use crate::storage::{StorageBackend, StorageEntry};
pub struct ConfigRoleStore {
pub config_prefix: &'static str,
pub role_prefix: &'static str,
}
impl ConfigRoleStore {
pub const fn new(config_prefix: &'static str, role_prefix: &'static str) -> Self {
Self {
config_prefix,
role_prefix,
}
}
pub async fn load_config<C: DeserializeOwned>(
&self,
storage: &dyn StorageBackend,
name: &str,
) -> EngineResult<Option<C>> {
load(storage, self.config_prefix, name).await
}
pub async fn load_role<R: DeserializeOwned>(
&self,
storage: &dyn StorageBackend,
name: &str,
) -> EngineResult<Option<R>> {
load(storage, self.role_prefix, name).await
}
pub async fn require_role<R: DeserializeOwned>(
&self,
storage: &dyn StorageBackend,
name: &str,
) -> EngineResult<R> {
self.load_role(storage, name).await?.ok_or(EngineError::NotFound)
}
pub async fn require_config<C: DeserializeOwned>(
&self,
storage: &dyn StorageBackend,
name: &str,
) -> EngineResult<C> {
self.load_config(storage, name).await?.ok_or_else(|| {
EngineError::InvalidRequest(format!("unknown config '{name}' — POST it first"))
})
}
pub async fn handle_read<R: DeserializeOwned + Serialize>(
&self,
storage: &dyn StorageBackend,
path: &str,
) -> EngineResult<serde_json::Value> {
if let Some(name) = path.strip_prefix("roles/") {
let role: R = self.require_role(storage, name).await?;
serde_json::to_value(role).map_err(|e| EngineError::Other(e.to_string()))
} else if let Some(name) = path.strip_prefix("config/") {
let exists = storage.get(&format!("{}{name}", self.config_prefix)).await?.is_some();
if exists {
Ok(json!({
"configured": true,
"note": "config is write-only — it holds this engine's root \
credential and is never returned. POST to replace it.",
}))
} else {
Err(EngineError::NotFound)
}
} else {
Err(EngineError::InvalidRequest(
"expected config/{name} or roles/{name}".into(),
))
}
}
pub async fn handle_write<C, R>(
&self,
storage: &dyn StorageBackend,
path: &str,
data: serde_json::Value,
) -> EngineResult<()>
where
C: DeserializeOwned + Serialize,
R: DeserializeOwned + Serialize,
{
if let Some(name) = path.strip_prefix("config/") {
let config: C =
serde_json::from_value(data).map_err(|e| EngineError::InvalidRequest(e.to_string()))?;
save(storage, self.config_prefix, name, &config).await
} else if let Some(name) = path.strip_prefix("roles/") {
let role: R =
serde_json::from_value(data).map_err(|e| EngineError::InvalidRequest(e.to_string()))?;
save(storage, self.role_prefix, name, &role).await
} else {
Err(EngineError::InvalidRequest(
"expected config/{name} or roles/{name}".into(),
))
}
}
pub async fn handle_delete(
&self,
storage: &dyn StorageBackend,
path: &str,
) -> EngineResult<()> {
if let Some(name) = path.strip_prefix("config/") {
storage.delete(&format!("{}{name}", self.config_prefix)).await?;
Ok(())
} else if let Some(name) = path.strip_prefix("roles/") {
storage.delete(&format!("{}{name}", self.role_prefix)).await?;
Ok(())
} else {
Err(EngineError::InvalidRequest(
"expected config/{name} or roles/{name}".into(),
))
}
}
pub async fn handle_list(
&self,
storage: &dyn StorageBackend,
prefix: &str,
) -> EngineResult<Vec<String>> {
let (storage_prefix, rest) = if let Some(rest) = prefix.strip_prefix("roles/") {
(self.role_prefix, rest)
} else if let Some(rest) = prefix.strip_prefix("config/") {
(self.config_prefix, rest)
} else {
return Err(EngineError::InvalidRequest(
"expected config/ or roles/ prefix".into(),
));
};
let keys = storage.list(&format!("{storage_prefix}{rest}")).await?;
Ok(keys
.into_iter()
.filter_map(|k| k.strip_prefix(storage_prefix).map(|s| s.to_string()))
.collect())
}
}
async fn load<T: DeserializeOwned>(
storage: &dyn StorageBackend,
prefix: &str,
name: &str,
) -> EngineResult<Option<T>> {
let Some(entry) = storage.get(&format!("{prefix}{name}")).await? else {
return Ok(None);
};
Ok(Some(
serde_json::from_slice(&entry.value).map_err(|e| EngineError::Other(e.to_string()))?,
))
}
async fn save<T: Serialize>(
storage: &dyn StorageBackend,
prefix: &str,
name: &str,
value: &T,
) -> EngineResult<()> {
let bytes = serde_json::to_vec(value).map_err(|e| EngineError::Other(e.to_string()))?;
storage
.put(
&format!("{prefix}{name}"),
StorageEntry {
value: bytes,
expires_at: None,
},
)
.await?;
Ok(())
}