#[cfg(any(feature = "fs", feature = "network"))]
use crate::data::repository::{ISnapshotProvider, PayloadFuture};
#[cfg(any(feature = "fs", feature = "network"))]
use bistun_core::error::LmsError;
#[cfg(feature = "fs")]
#[derive(Debug, Clone)]
pub struct FileSnapshotProvider {
pub json_path: String,
pub sig_path: String,
}
#[cfg(feature = "fs")]
impl FileSnapshotProvider {
#[must_use]
pub fn new(json_path: String, sig_path: String) -> Self {
Self { json_path, sig_path }
}
}
#[cfg(feature = "fs")]
impl ISnapshotProvider for FileSnapshotProvider {
fn fetch_payload(&self) -> PayloadFuture<'_> {
Box::pin(async move {
let json_payload = tokio::fs::read_to_string(&self.json_path).await.map_err(|e| {
LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "FileSnapshotProvider".to_string(),
reason: format!("Failed to read JSON snapshot: {e}"),
}
})?;
let signature = tokio::fs::read_to_string(&self.sig_path).await.map_err(|e| {
LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "FileSnapshotProvider".to_string(),
reason: format!("Failed to read snapshot signature: {e}"),
}
})?;
Ok((json_payload, signature))
})
}
}
#[cfg(feature = "network")]
#[derive(Debug, Clone)]
pub struct HttpSnapshotProvider {
pub base_url: String,
}
#[cfg(feature = "network")]
impl HttpSnapshotProvider {
#[must_use]
pub fn new(base_url: String) -> Self {
Self { base_url }
}
}
#[cfg(feature = "network")]
impl ISnapshotProvider for HttpSnapshotProvider {
fn fetch_payload(&self) -> PayloadFuture<'_> {
Box::pin(async move {
let json_url = format!("{}/snapshot.json", self.base_url);
let sig_url = format!("{}/snapshot.sig", self.base_url);
let json_resp = reqwest::get(&json_url)
.await
.map_err(|e| LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "HttpSnapshotProvider".to_string(),
reason: format!("HTTP request failed for JSON: {e}"),
})?
.error_for_status()
.map_err(|e| LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "HttpSnapshotProvider".to_string(),
reason: format!("HTTP status error for JSON: {e}"),
})?;
let json_payload = json_resp.text().await.map_err(|e| LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "HttpSnapshotProvider".to_string(),
reason: format!("Failed to extract JSON text: {e}"),
})?;
let sig_resp = reqwest::get(&sig_url)
.await
.map_err(|e| LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "HttpSnapshotProvider".to_string(),
reason: format!("HTTP request failed for Signature: {e}"),
})?
.error_for_status()
.map_err(|e| LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "HttpSnapshotProvider".to_string(),
reason: format!("HTTP status error for Signature: {e}"),
})?;
let signature = sig_resp.text().await.map_err(|e| LmsError::PersistenceFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "HttpSnapshotProvider".to_string(),
reason: format!("Failed to extract Signature text: {e}"),
})?;
Ok((json_payload, signature))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "fs")]
mod fs_tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_file_provider_fetches_payload() {
let mut json_file =
NamedTempFile::new().expect("LMS-TEST: Failed to create temp JSON file");
let sig_file = NamedTempFile::new().expect("LMS-TEST: Failed to create temp SIG file");
writeln!(json_file, "[{{\"id\": \"ar-EG\"}}]")
.expect("LMS-TEST: Failed to write to temp file");
let json_path = json_file.path().to_str().expect("LMS-TEST: Invalid path").to_string();
let sig_path = sig_file.path().to_str().expect("LMS-TEST: Invalid path").to_string();
let provider = FileSnapshotProvider::new(json_path, sig_path);
let result = provider.fetch_payload().await;
assert!(result.is_ok());
let (payload, _) = result.expect("LMS-TEST: Provider failed to fetch payload");
assert!(payload.contains("ar-EG"));
}
#[tokio::test]
async fn test_file_provider_fails_gracefully_on_missing_file() {
let provider =
FileSnapshotProvider::new("does_not_exist.json".into(), "missing.sig".into());
let result = provider.fetch_payload().await;
assert!(matches!(result, Err(LmsError::PersistenceFault { .. })));
}
}
}