use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::{StorageResult, TransactionError};
use crate::tenant::TenantContext;
use crate::types::StoredResource;
use super::storage::ResourceStorage;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IsolationLevel {
#[default]
ReadCommitted,
RepeatableRead,
Serializable,
Snapshot,
}
impl std::fmt::Display for IsolationLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IsolationLevel::ReadCommitted => write!(f, "read-committed"),
IsolationLevel::RepeatableRead => write!(f, "repeatable-read"),
IsolationLevel::Serializable => write!(f, "serializable"),
IsolationLevel::Snapshot => write!(f, "snapshot"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LockingStrategy {
#[default]
Optimistic,
Pessimistic,
None,
}
#[derive(Debug, Clone, Default)]
pub struct TransactionOptions {
pub isolation_level: IsolationLevel,
pub locking_strategy: LockingStrategy,
pub timeout_ms: u64,
pub read_only: bool,
}
impl TransactionOptions {
pub fn new() -> Self {
Self::default()
}
pub fn isolation_level(mut self, level: IsolationLevel) -> Self {
self.isolation_level = level;
self
}
pub fn locking_strategy(mut self, strategy: LockingStrategy) -> Self {
self.locking_strategy = strategy;
self
}
pub fn timeout_ms(mut self, timeout: u64) -> Self {
self.timeout_ms = timeout;
self
}
pub fn read_only(mut self) -> Self {
self.read_only = true;
self.locking_strategy = LockingStrategy::None;
self
}
}
#[async_trait]
pub trait Transaction: Send + Sync {
async fn create(
&mut self,
resource_type: &str,
resource: Value,
) -> StorageResult<StoredResource>;
async fn read(
&mut self,
resource_type: &str,
id: &str,
) -> StorageResult<Option<StoredResource>>;
async fn update(
&mut self,
current: &StoredResource,
resource: Value,
) -> StorageResult<StoredResource>;
async fn delete(&mut self, resource_type: &str, id: &str) -> StorageResult<()>;
async fn commit(self: Box<Self>) -> StorageResult<()>;
async fn rollback(self: Box<Self>) -> StorageResult<()>;
fn tenant(&self) -> &TenantContext;
fn is_active(&self) -> bool;
}
#[async_trait]
pub trait TransactionProvider: ResourceStorage {
type Transaction: Transaction;
async fn begin_transaction(
&self,
tenant: &TenantContext,
options: TransactionOptions,
) -> StorageResult<Self::Transaction>;
async fn with_transaction<F, Fut, R>(
&self,
tenant: &TenantContext,
options: TransactionOptions,
f: F,
) -> StorageResult<R>
where
F: FnOnce(Self::Transaction) -> Fut + Send,
Fut: std::future::Future<Output = StorageResult<R>> + Send,
R: Send,
{
let tx = self.begin_transaction(tenant, options).await?;
f(tx).await
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BundleEntry {
#[serde(default)]
pub method: BundleMethod,
#[serde(default)]
pub url: String,
#[serde(default)]
pub resource: Option<Value>,
#[serde(default)]
pub if_match: Option<String>,
#[serde(default)]
pub if_none_match: Option<String>,
#[serde(default)]
pub if_none_exist: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "UPPERCASE")]
pub enum BundleMethod {
#[default]
Get,
Post,
Put,
Patch,
Delete,
}
impl std::fmt::Display for BundleMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BundleMethod::Get => write!(f, "GET"),
BundleMethod::Post => write!(f, "POST"),
BundleMethod::Put => write!(f, "PUT"),
BundleMethod::Patch => write!(f, "PATCH"),
BundleMethod::Delete => write!(f, "DELETE"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleEntryResult {
pub status: u16,
pub location: Option<String>,
pub etag: Option<String>,
pub last_modified: Option<String>,
pub resource: Option<Value>,
pub outcome: Option<Value>,
}
impl BundleEntryResult {
pub fn created(resource: StoredResource) -> Self {
Self {
status: 201,
location: Some(resource.versioned_url()),
etag: Some(resource.etag().to_string()),
last_modified: Some(resource.last_modified().to_rfc3339()),
resource: Some(resource.into_content()),
outcome: None,
}
}
pub fn ok(resource: StoredResource) -> Self {
Self {
status: 200,
location: None,
etag: Some(resource.etag().to_string()),
last_modified: Some(resource.last_modified().to_rfc3339()),
resource: Some(resource.into_content()),
outcome: None,
}
}
pub fn deleted() -> Self {
Self {
status: 204,
location: None,
etag: None,
last_modified: None,
resource: None,
outcome: None,
}
}
pub fn error(status: u16, outcome: Value) -> Self {
Self {
status,
location: None,
etag: None,
last_modified: None,
resource: None,
outcome: Some(outcome),
}
}
}
#[derive(Debug, Clone)]
pub struct BundleResult {
pub bundle_type: BundleType,
pub entries: Vec<BundleEntryResult>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BundleType {
Transaction,
Batch,
}
#[async_trait]
pub trait BundleProvider: ResourceStorage {
async fn process_transaction(
&self,
tenant: &TenantContext,
entries: Vec<BundleEntry>,
) -> Result<BundleResult, TransactionError>;
async fn process_batch(
&self,
tenant: &TenantContext,
entries: Vec<BundleEntry>,
) -> StorageResult<BundleResult>;
}
#[cfg(test)]
mod tests {
use super::*;
use helios_fhir::FhirVersion;
#[test]
fn test_isolation_level_display() {
assert_eq!(IsolationLevel::ReadCommitted.to_string(), "read-committed");
assert_eq!(IsolationLevel::Serializable.to_string(), "serializable");
}
#[test]
fn test_transaction_options_builder() {
let opts = TransactionOptions::new()
.isolation_level(IsolationLevel::Serializable)
.timeout_ms(5000);
assert_eq!(opts.isolation_level, IsolationLevel::Serializable);
assert_eq!(opts.timeout_ms, 5000);
}
#[test]
fn test_transaction_options_read_only() {
let opts = TransactionOptions::new().read_only();
assert!(opts.read_only);
assert_eq!(opts.locking_strategy, LockingStrategy::None);
}
#[test]
fn test_bundle_method_display() {
assert_eq!(BundleMethod::Get.to_string(), "GET");
assert_eq!(BundleMethod::Post.to_string(), "POST");
assert_eq!(BundleMethod::Delete.to_string(), "DELETE");
}
#[test]
fn test_bundle_entry_result_created() {
let resource = StoredResource::new(
"Patient",
"123",
crate::tenant::TenantId::new("t1"),
serde_json::json!({}),
FhirVersion::default(),
);
let result = BundleEntryResult::created(resource);
assert_eq!(result.status, 201);
assert!(result.location.is_some());
assert!(result.etag.is_some());
}
#[test]
fn test_bundle_entry_result_error() {
let outcome = serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "error", "code": "not-found"}]
});
let result = BundleEntryResult::error(404, outcome);
assert_eq!(result.status, 404);
assert!(result.outcome.is_some());
assert!(result.resource.is_none());
}
}