use std::sync::Arc;
use std::{error::Error, fmt};
use batpak::coordinate::Coordinate;
use batpak::store::{AppendOptions, Open, Store, StoreError};
use crate::operation_status::OperationStatusFactV1;
const OPERATION_STATUS_SCOPE: &str = "scope:operation-status";
const OPERATION_STATUS_ENTITY_PREFIX: &str = "syncbat:operation-status:";
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OperationStatusSinkError {
message: String,
}
impl OperationStatusSinkError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for OperationStatusSinkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for OperationStatusSinkError {}
impl From<StoreError> for OperationStatusSinkError {
fn from(error: StoreError) -> Self {
Self::new(error.to_string())
}
}
pub trait OperationStatusSink: Send + Sync {
fn record_fact(&self, fact: &OperationStatusFactV1) -> Result<(), OperationStatusSinkError>;
}
pub fn operation_status_entity(
operation: &str,
) -> Result<String, batpak::coordinate::CoordinateError> {
let entity = format!("{OPERATION_STATUS_ENTITY_PREFIX}{operation}");
Coordinate::new(&entity, OPERATION_STATUS_SCOPE)?;
Ok(entity)
}
pub struct StoreOperationStatusSink {
store: Arc<Store<Open>>,
base_options: AppendOptions,
}
impl StoreOperationStatusSink {
#[must_use]
pub fn new(store: Arc<Store<Open>>) -> Self {
Self {
store,
base_options: AppendOptions::new(),
}
}
#[must_use]
pub fn with_options(mut self, options: AppendOptions) -> Self {
self.base_options = options;
self
}
fn coordinate_for(&self, operation: &str) -> Result<Coordinate, OperationStatusSinkError> {
let entity = operation_status_entity(operation)
.map_err(|error| OperationStatusSinkError::new(error.to_string()))?;
Coordinate::new(&entity, OPERATION_STATUS_SCOPE)
.map_err(|error| OperationStatusSinkError::new(error.to_string()))
}
}
impl OperationStatusSink for StoreOperationStatusSink {
fn record_fact(&self, fact: &OperationStatusFactV1) -> Result<(), OperationStatusSinkError> {
let coordinate = self.coordinate_for(&fact.operation)?;
self.store
.append_typed_with_options(&coordinate, fact, self.base_options.clone())
.map_err(OperationStatusSinkError::from)
.map(|_receipt| ())?;
Ok(())
}
}
impl StoreOperationStatusSink {
pub fn record_started(
&self,
operation: &str,
receipt_kind: &str,
) -> Result<(), OperationStatusSinkError> {
self.record_fact(&OperationStatusFactV1::started(operation, receipt_kind))
}
}
#[cfg(test)]
mod sink_error_and_entity_tests {
use super::{operation_status_entity, OperationStatusSinkError};
#[test]
fn sink_error_exposes_its_message_and_display() {
let err = OperationStatusSinkError::new("append refused");
assert_eq!(err.message(), "append refused");
assert_eq!(err.to_string(), "append refused");
}
#[test]
fn operation_status_entity_prefixes_the_operation_name() {
let entity = operation_status_entity("ping").expect("entity should be valid");
assert_eq!(entity, "syncbat:operation-status:ping");
}
}