use async_trait::async_trait;
use serde_json::Value;
use crate::error::{ConcurrencyError, StorageError, StorageResult};
use crate::tenant::TenantContext;
use crate::types::StoredResource;
use super::storage::ResourceStorage;
#[async_trait]
pub trait VersionedStorage: ResourceStorage {
async fn vread(
&self,
tenant: &TenantContext,
resource_type: &str,
id: &str,
version_id: &str,
) -> StorageResult<Option<StoredResource>>;
async fn update_with_match(
&self,
tenant: &TenantContext,
resource_type: &str,
id: &str,
expected_version: &str,
resource: Value,
) -> StorageResult<StoredResource>;
async fn delete_with_match(
&self,
tenant: &TenantContext,
resource_type: &str,
id: &str,
expected_version: &str,
) -> StorageResult<()>;
async fn current_version(
&self,
tenant: &TenantContext,
resource_type: &str,
id: &str,
) -> StorageResult<Option<String>> {
Ok(self
.read(tenant, resource_type, id)
.await?
.map(|r| r.version_id().to_string()))
}
async fn list_versions(
&self,
tenant: &TenantContext,
resource_type: &str,
id: &str,
) -> StorageResult<Vec<String>>;
}
#[derive(Debug, Clone)]
pub struct VersionConflictInfo {
pub resource_type: String,
pub id: String,
pub expected_version: String,
pub actual_version: String,
pub current_content: Option<Value>,
}
impl VersionConflictInfo {
pub fn new(
resource_type: impl Into<String>,
id: impl Into<String>,
expected_version: impl Into<String>,
actual_version: impl Into<String>,
) -> Self {
Self {
resource_type: resource_type.into(),
id: id.into(),
expected_version: expected_version.into(),
actual_version: actual_version.into(),
current_content: None,
}
}
pub fn with_content(mut self, content: Value) -> Self {
self.current_content = Some(content);
self
}
pub fn into_error(self) -> StorageError {
StorageError::Concurrency(ConcurrencyError::VersionConflict {
resource_type: self.resource_type,
id: self.id,
expected_version: self.expected_version,
actual_version: self.actual_version,
})
}
}
pub fn check_version_match(
resource_type: &str,
id: &str,
expected: &str,
actual: &str,
) -> StorageResult<()> {
if expected == actual {
Ok(())
} else {
Err(VersionConflictInfo::new(resource_type, id, expected, actual).into_error())
}
}
pub fn normalize_etag(etag: &str) -> &str {
etag.trim_start_matches("W/")
.trim_start_matches('"')
.trim_end_matches('"')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_conflict_info() {
let info = VersionConflictInfo::new("Patient", "123", "1", "2");
assert_eq!(info.resource_type, "Patient");
assert_eq!(info.id, "123");
assert_eq!(info.expected_version, "1");
assert_eq!(info.actual_version, "2");
}
#[test]
fn test_version_conflict_with_content() {
let info = VersionConflictInfo::new("Patient", "123", "1", "2")
.with_content(serde_json::json!({"name": "test"}));
assert!(info.current_content.is_some());
}
#[test]
fn test_version_conflict_into_error() {
let info = VersionConflictInfo::new("Patient", "123", "1", "2");
let error = info.into_error();
assert!(matches!(error, StorageError::Concurrency(_)));
}
#[test]
fn test_check_version_match_success() {
let result = check_version_match("Patient", "123", "1", "1");
assert!(result.is_ok());
}
#[test]
fn test_check_version_match_failure() {
let result = check_version_match("Patient", "123", "1", "2");
assert!(result.is_err());
}
#[test]
fn test_normalize_etag() {
assert_eq!(normalize_etag("W/\"1\""), "1");
assert_eq!(normalize_etag("\"1\""), "1");
assert_eq!(normalize_etag("1"), "1");
assert_eq!(normalize_etag("W/\"abc\""), "abc");
}
}