use std::error::Error;
use std::sync::{Arc, Mutex};
use log::{info, warn, error};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadFirstMetrics {
pub read_count: u64,
pub write_count: u64,
pub violation_count: u64,
pub last_reset: DateTime<Utc>,
}
impl ReadFirstMetrics {
pub fn new() -> Self {
Self {
read_count: 0,
write_count: 0,
violation_count: 0,
last_reset: Utc::now(),
}
}
pub fn reset(&mut self) {
self.read_count = 0;
self.write_count = 0;
self.violation_count = 0;
self.last_reset = Utc::now();
}
pub fn compliance_rate(&self) -> f64 {
if self.write_count == 0 {
return 100.0;
}
let compliant_writes = self.write_count.saturating_sub(self.violation_count);
(compliant_writes as f64 / self.write_count as f64) * 100.0
}
pub fn log_metrics(&self) {
info!(
"Read First Metrics: reads={}, writes={}, violations={}, compliance_rate={:.2}%",
self.read_count,
self.write_count,
self.violation_count,
self.compliance_rate()
);
}
}
#[derive(Debug)]
pub struct ReadFirstDwnManager {
web5_client: Arc<dyn Web5Client>,
metrics: Arc<Mutex<ReadFirstMetrics>>,
read_performed: Arc<Mutex<bool>>,
}
pub trait Web5Client: Send + Sync {
fn create_record(&self, options: &CreateRecordOptions) -> Result<Record, Web5Error>;
fn read_record(&self, record_id: &str) -> Result<Option<Record>, Web5Error>;
fn update_record(&self, record_id: &str, options: &UpdateRecordOptions) -> Result<Record, Web5Error>;
fn delete_record(&self, record_id: &str) -> Result<bool, Web5Error>;
fn query_records(&self, query: &QueryOptions) -> Result<Vec<Record>, Web5Error>;
fn create_record_async<'a>(&'a self, options: &'a CreateRecordOptions) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Record, Web5Error>> + Send + 'a>> {
Box::pin(async move { self.create_record(options) })
}
fn read_record_async<'a>(&'a self, record_id: &'a str) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Option<Record>, Web5Error>> + Send + 'a>> {
Box::pin(async move { self.read_record(record_id) })
}
fn update_record_async<'a>(&'a self, record_id: &'a str, options: &'a UpdateRecordOptions) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Record, Web5Error>> + Send + 'a>> {
Box::pin(async move { self.update_record(record_id, options) })
}
fn delete_record_async<'a>(&'a self, record_id: &'a str) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool, Web5Error>> + Send + 'a>> {
Box::pin(async move { self.delete_record(record_id) })
}
fn query_records_async<'a>(&'a self, query: &'a QueryOptions) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<Record>, Web5Error>> + Send + 'a>> {
Box::pin(async move { self.query_records(query) })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateRecordOptions {
pub data: String,
pub schema: String,
pub data_format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateRecordOptions {
pub data: String,
pub data_format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryOptions {
pub schema: Option<String>,
pub filter: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Record {
pub id: String,
pub data: String,
pub schema: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, thiserror::Error)]
pub enum Web5Error {
#[error("Record not found: {0}")]
RecordNotFound(String),
#[error("Read First violation: attempted to {0} without reading first")]
ReadFirstViolation(String),
#[error("Web5 client error: {0}")]
ClientError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Invalid operation: {0}")]
InvalidOperation(String),
}
impl ReadFirstDwnManager {
pub fn new(web5_client: Arc<dyn Web5Client>) -> Self {
Self {
web5_client,
metrics: Arc::new(Mutex::new(ReadFirstMetrics::new())),
read_performed: Arc::new(Mutex::new(false)),
}
}
fn reset_read_status(&self) {
if let Ok(mut status) = self.read_performed.lock() {
*status = false;
}
}
fn mark_read_performed(&self) {
if let Ok(mut status) = self.read_performed.lock() {
*status = true;
}
if let Ok(mut metrics) = self.metrics.lock() {
metrics.read_count += 1;
}
}
fn check_read_before_write(&self, operation: &str) -> Result<(), Web5Error> {
let read_performed = if let Ok(status) = self.read_performed.lock() {
*status
} else {
false
};
if let Ok(mut metrics) = self.metrics.lock() {
metrics.write_count += 1;
if !read_performed {
metrics.violation_count += 1;
warn!("Read First violation: {} operation performed without a preceding read", operation);
return Err(Web5Error::ReadFirstViolation(operation.to_string()));
}
}
Ok(())
}
pub fn get_metrics(&self) -> ReadFirstMetrics {
if let Ok(metrics) = self.metrics.lock() {
metrics.clone()
} else {
ReadFirstMetrics::new()
}
}
pub fn log_metrics(&self) {
if let Ok(metrics) = self.metrics.lock() {
metrics.log_metrics();
}
}
pub fn reset_metrics(&self) {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.reset();
}
}
pub fn create_record(&self, options: &CreateRecordOptions) -> Result<Record, Web5Error> {
self.reset_read_status();
let query_options = QueryOptions {
schema: Some(options.schema.clone()),
filter: None,
};
let _ = self.query_records(&query_options)?;
self.check_read_before_write("create")?;
self.web5_client.create_record(options)
}
pub fn read_record(&self, record_id: &str) -> Result<Option<Record>, Web5Error> {
self.mark_read_performed();
self.web5_client.read_record(record_id)
}
pub fn query_records(&self, query: &QueryOptions) -> Result<Vec<Record>, Web5Error> {
self.mark_read_performed();
self.web5_client.query_records(query)
}
pub fn update_record(&self, record_id: &str, options: &UpdateRecordOptions) -> Result<Record, Web5Error> {
self.reset_read_status();
let record = self.read_record(record_id)?;
let record = record.ok_or_else(|| Web5Error::RecordNotFound(record_id.to_string()))?;
self.check_read_before_write("update")?;
self.web5_client.update_record(record_id, options)
}
pub fn delete_record(&self, record_id: &str) -> Result<bool, Web5Error> {
self.reset_read_status();
let record = self.read_record(record_id)?;
let _ = record.ok_or_else(|| Web5Error::RecordNotFound(record_id.to_string()))?;
self.check_read_before_write("delete")?;
self.web5_client.delete_record(record_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::*;
use mockall::*;
mock! {
TestWeb5Client {}
impl Web5Client for TestWeb5Client {
fn create_record(&self, options: &CreateRecordOptions) -> Result<Record, Web5Error>;
fn read_record(&self, record_id: &str) -> Result<Option<Record>, Web5Error>;
fn update_record(&self, record_id: &str, options: &UpdateRecordOptions) -> Result<Record, Web5Error>;
fn delete_record(&self, record_id: &str) -> Result<bool, Web5Error>;
fn query_records(&self, query: &QueryOptions) -> Result<Vec<Record>, Web5Error>;
}
}
#[test]
fn test_create_record_enforces_read_first() {
let mut mock = MockTestWeb5Client::new();
mock.expect_query_records()
.times(1)
.returning(|_| Ok(vec![]));
mock.expect_create_record()
.times(1)
.returning(|_| {
Ok(Record {
id: "test-id".to_string(),
data: "test-data".to_string(),
schema: Some("test-schema".to_string()),
created_at: Utc::now(),
updated_at: None,
})
});
let manager = ReadFirstDwnManager::new(Arc::new(mock));
let result = manager.create_record(&CreateRecordOptions {
data: "test-data".to_string(),
schema: "test-schema".to_string(),
data_format: "application/json".to_string(),
});
assert!(result.is_ok());
let metrics = manager.get_metrics();
assert_eq!(metrics.read_count, 1);
assert_eq!(metrics.write_count, 1);
assert_eq!(metrics.violation_count, 0);
assert_eq!(metrics.compliance_rate(), 100.0);
}
#[test]
fn test_update_record_enforces_read_first() {
let mut mock = MockTestWeb5Client::new();
mock.expect_read_record()
.times(1)
.returning(|_| {
Ok(Some(Record {
id: "test-id".to_string(),
data: "original-data".to_string(),
schema: Some("test-schema".to_string()),
created_at: Utc::now(),
updated_at: None,
}))
});
mock.expect_update_record()
.times(1)
.returning(|_, _| {
Ok(Record {
id: "test-id".to_string(),
data: "updated-data".to_string(),
schema: Some("test-schema".to_string()),
created_at: Utc::now(),
updated_at: Some(Utc::now()),
})
});
let manager = ReadFirstDwnManager::new(Arc::new(mock));
let result = manager.update_record("test-id", &UpdateRecordOptions {
data: "updated-data".to_string(),
data_format: "application/json".to_string(),
});
assert!(result.is_ok());
let metrics = manager.get_metrics();
assert_eq!(metrics.read_count, 1);
assert_eq!(metrics.write_count, 1);
assert_eq!(metrics.violation_count, 0);
assert_eq!(metrics.compliance_rate(), 100.0);
}
#[test]
fn test_update_nonexistent_record_fails() {
let mut mock = MockTestWeb5Client::new();
mock.expect_read_record()
.times(1)
.returning(|_| Ok(None));
let manager = ReadFirstDwnManager::new(Arc::new(mock));
let result = manager.update_record("nonexistent-id", &UpdateRecordOptions {
data: "updated-data".to_string(),
data_format: "application/json".to_string(),
});
assert!(result.is_err());
match result {
Err(Web5Error::RecordNotFound(_)) => (),
_ => panic!("Expected RecordNotFound error"),
}
}
#[test]
fn test_delete_record_enforces_read_first() {
let mut mock = MockTestWeb5Client::new();
mock.expect_read_record()
.times(1)
.returning(|_| {
Ok(Some(Record {
id: "test-id".to_string(),
data: "test-data".to_string(),
schema: Some("test-schema".to_string()),
created_at: Utc::now(),
updated_at: None,
}))
});
mock.expect_delete_record()
.times(1)
.returning(|_| Ok(true));
let manager = ReadFirstDwnManager::new(Arc::new(mock));
let result = manager.delete_record("test-id");
assert!(result.is_ok());
assert!(result?);
let metrics = manager.get_metrics();
assert_eq!(metrics.read_count, 1);
assert_eq!(metrics.write_count, 1);
assert_eq!(metrics.violation_count, 0);
assert_eq!(metrics.compliance_rate(), 100.0);
}
}