use std::collections::BTreeMap;
use crate::error::{ReceiptSinkHandlerCause, RuntimeError};
use crate::receipt::{ReceiptHashPolicy, ReceiptOutcome, RecordedReceipt};
use crate::{handler, operation, receipt};
type BoxedHandler = Box<dyn handler::Handler + 'static>;
type BoxedReceiptSink = Box<dyn receipt::ReceiptSink + 'static>;
pub struct Core {
pub(crate) descriptors: BTreeMap<String, operation::OperationDescriptor>,
pub(crate) handlers: BTreeMap<String, BoxedHandler>,
pub(crate) receipt_sink: Option<BoxedReceiptSink>,
pub(crate) receipt_hash_policy: ReceiptHashPolicy,
}
impl Core {
#[must_use]
pub fn builder() -> crate::builder::CoreBuilder {
crate::builder::CoreBuilder::new()
}
#[must_use]
pub fn contains_operation(&self, name: impl AsRef<str>) -> bool {
self.descriptors.contains_key(name.as_ref())
}
#[must_use]
pub fn descriptor(&self, name: impl AsRef<str>) -> Option<&operation::OperationDescriptor> {
self.descriptors.get(name.as_ref())
}
pub fn invoke(
&mut self,
name: impl AsRef<str>,
input: operation::OperationInput,
) -> Result<CheckoutResult, RuntimeError> {
self.checkout_frame(CheckoutFrame::new(name.as_ref(), input))
}
pub fn checkout_frame(&mut self, frame: CheckoutFrame) -> Result<CheckoutResult, RuntimeError> {
let descriptor = self
.descriptors
.get(frame.name())
.cloned()
.ok_or_else(|| RuntimeError::unknown_operation(frame.name()))?;
self.checkout(Checkout::from_frame(descriptor, frame))
}
#[tracing::instrument(
name = "syncbat.checkout",
skip_all,
fields(
operation = %checkout.descriptor.name(),
input_bytes = checkout.input.len(),
output_bytes = tracing::field::Empty,
outcome = tracing::field::Empty,
),
)]
pub fn checkout(&mut self, checkout: Checkout) -> Result<CheckoutResult, RuntimeError> {
let name = checkout.descriptor.name();
let descriptor = self.descriptors.get(name).cloned().ok_or_else(|| {
tracing::warn!(operation = %name, outcome = "unknown_operation", "checkout rejected");
RuntimeError::unknown_operation(name)
})?;
let handler = self.handlers.get_mut(name).ok_or_else(|| {
tracing::error!(operation = %name, outcome = "missing_handler", "checkout rejected");
RuntimeError::missing_handler(name)
})?;
let input = checkout.input;
let handler_result = {
let mut ctx = Ctx::new(&descriptor);
handler.handle(&input, &mut ctx)
};
let output = match handler_result {
Ok(output) => output,
Err(error) => {
let cause = ReceiptSinkHandlerCause::new(error.class(), error.message());
let outcome = ReceiptOutcome::failed(cause.code(), cause.message());
tracing::warn!(
operation = %name,
code = %cause.code(),
message = %cause.message(),
outcome = "handler_failed",
"checkout failed in handler",
);
self.record_runtime_receipt(
&descriptor,
&input,
None,
outcome,
Some(cause.clone()),
)?;
return Err(RuntimeError::handler(name, cause.code(), cause.message()));
}
};
let recorded_receipt = self.record_runtime_receipt(
&descriptor,
&input,
Some(output.as_slice()),
ReceiptOutcome::Completed,
None,
)?;
let span = tracing::Span::current();
span.record("output_bytes", output.len());
span.record("outcome", "completed");
Ok(CheckoutResult {
descriptor,
output,
recorded_receipt,
})
}
fn record_runtime_receipt(
&self,
descriptor: &operation::OperationDescriptor,
input: &[u8],
output: Option<&[u8]>,
outcome: ReceiptOutcome,
handler_cause: Option<ReceiptSinkHandlerCause>,
) -> Result<Option<RecordedReceipt>, RuntimeError> {
let Some(sink) = self.receipt_sink.as_deref() else {
return Ok(None);
};
let mut envelope = receipt::ReceiptEnvelope::new(descriptor, outcome);
if let Some(hash) = self.receipt_hash_policy.hash(input) {
envelope = envelope.with_input_hash(hash);
}
if let Some(output) = output {
if let Some(hash) = self.receipt_hash_policy.hash(output) {
envelope = envelope.with_output_hash(hash);
}
}
sink.record_receipt(&envelope).map(Some).map_err(|error| {
let message = error.to_string();
if let Some(cause) = handler_cause {
RuntimeError::receipt_sink_after_handler_failure(descriptor.name(), message, cause)
} else {
RuntimeError::receipt_sink(descriptor.name(), message)
}
})
}
}
pub struct CheckoutFrame {
name: String,
input: operation::OperationInput,
}
impl CheckoutFrame {
#[must_use]
pub fn new(name: impl Into<String>, input: operation::OperationInput) -> Self {
Self {
name: name.into(),
input,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn input(&self) -> &[u8] {
&self.input
}
#[must_use]
pub fn into_parts(self) -> (String, operation::OperationInput) {
(self.name, self.input)
}
}
pub struct Checkout {
descriptor: operation::OperationDescriptor,
input: operation::OperationInput,
}
impl Checkout {
#[must_use]
pub fn new(
descriptor: operation::OperationDescriptor,
input: operation::OperationInput,
) -> Self {
Self { descriptor, input }
}
fn from_frame(descriptor: operation::OperationDescriptor, frame: CheckoutFrame) -> Self {
Self {
descriptor,
input: frame.input,
}
}
#[must_use]
pub fn descriptor(&self) -> &operation::OperationDescriptor {
&self.descriptor
}
#[must_use]
pub fn input(&self) -> &[u8] {
&self.input
}
#[must_use]
pub fn into_parts(self) -> (operation::OperationDescriptor, operation::OperationInput) {
(self.descriptor, self.input)
}
}
pub struct Ctx<'a> {
descriptor: &'a operation::OperationDescriptor,
}
impl<'a> Ctx<'a> {
pub(crate) fn new(descriptor: &'a operation::OperationDescriptor) -> Self {
Self { descriptor }
}
#[must_use]
pub fn descriptor(&self) -> &'a operation::OperationDescriptor {
self.descriptor
}
}
pub struct CheckoutResult {
descriptor: operation::OperationDescriptor,
output: operation::OperationOutput,
recorded_receipt: Option<RecordedReceipt>,
}
impl CheckoutResult {
#[must_use]
pub fn descriptor(&self) -> &operation::OperationDescriptor {
&self.descriptor
}
#[must_use]
pub fn output(&self) -> &operation::OperationOutput {
&self.output
}
#[must_use]
pub fn recorded_receipt(&self) -> Option<&RecordedReceipt> {
self.recorded_receipt.as_ref()
}
#[must_use]
pub fn into_output(self) -> operation::OperationOutput {
self.output
}
}