1use std::collections::BTreeMap;
4use std::sync::Arc;
5use std::{error::Error, fmt};
6
7use batpak::event::{EventKind, EventPayload};
8use batpak::store::{EncodedBytes, ExtensionKey};
9use serde::{Deserialize, Serialize};
10
11use crate::operation::OperationDescriptor;
12
13pub const SYNCBAT_RECEIPT_EVENT_KIND: EventKind = EventKind::custom(0xC, 0x5B7);
19
20pub type ReceiptHash = [u8; 32];
22
23pub trait ReceiptHasher {
25 fn hash(&self, bytes: &[u8]) -> ReceiptHash;
27}
28
29impl<F> ReceiptHasher for F
30where
31 F: Fn(&[u8]) -> ReceiptHash,
32{
33 fn hash(&self, bytes: &[u8]) -> ReceiptHash {
34 self(bytes)
35 }
36}
37
38#[derive(Clone, Default)]
40#[non_exhaustive]
41pub enum ReceiptHashPolicy {
42 #[default]
45 Deferred,
46 RawBytes(Arc<dyn ReceiptHasher>),
48}
49
50impl ReceiptHashPolicy {
51 #[must_use]
53 pub fn raw_bytes(hasher: impl ReceiptHasher + 'static) -> Self {
54 Self::RawBytes(Arc::new(hasher))
55 }
56
57 #[must_use]
59 pub fn hash(&self, bytes: &[u8]) -> Option<ReceiptHash> {
60 match self {
61 Self::Deferred => None,
62 Self::RawBytes(hasher) => Some(hasher.hash(bytes)),
63 }
64 }
65}
66
67pub type ReceiptExtensionDrawer = BTreeMap<String, Vec<u8>>;
72
73#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[non_exhaustive]
76pub enum ReceiptOutcome {
77 Completed,
79 Failed {
81 code: String,
83 message: String,
85 },
86 Denied {
93 code: String,
95 message: String,
97 },
98}
99
100impl ReceiptOutcome {
101 #[must_use]
103 pub fn failed(code: impl Into<String>, message: impl Into<String>) -> Self {
104 Self::Failed {
105 code: code.into(),
106 message: message.into(),
107 }
108 }
109
110 #[must_use]
112 pub fn denied(code: impl Into<String>, message: impl Into<String>) -> Self {
113 Self::Denied {
114 code: code.into(),
115 message: message.into(),
116 }
117 }
118
119 #[must_use]
121 pub const fn class(&self) -> &'static str {
122 match self {
123 Self::Completed => "completed",
124 Self::Failed { .. } => "failed",
125 Self::Denied { .. } => "denied",
126 }
127 }
128}
129
130#[derive(Clone, Debug, Eq, PartialEq)]
132#[non_exhaustive]
133pub struct BatpakReceiptFields {
134 pub event_id: batpak::id::EventId,
136 pub sequence: u64,
138 pub content_hash: ReceiptHash,
140 pub key_id: ReceiptHash,
142 pub signature: Option<[u8; 64]>,
144 pub extensions: BTreeMap<ExtensionKey, EncodedBytes>,
146}
147
148impl From<batpak::store::AppendReceipt> for BatpakReceiptFields {
149 fn from(receipt: batpak::store::AppendReceipt) -> Self {
150 Self {
151 event_id: receipt.event_id,
152 sequence: receipt.sequence,
153 content_hash: receipt.content_hash,
154 key_id: receipt.key_id,
155 signature: receipt.signature,
156 extensions: receipt.extensions,
157 }
158 }
159}
160
161#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
168#[non_exhaustive]
169pub struct ReceiptEnvelope {
170 pub descriptor_name: String,
172 pub receipt_kind: String,
174 pub input_hash: Option<ReceiptHash>,
176 pub output_hash: Option<ReceiptHash>,
178 pub outcome: ReceiptOutcome,
180 pub signed_extensions: ReceiptExtensionDrawer,
182 pub local_extensions: ReceiptExtensionDrawer,
184}
185
186impl ReceiptEnvelope {
187 #[must_use]
189 pub fn new(descriptor: &OperationDescriptor, outcome: ReceiptOutcome) -> Self {
190 Self::from_descriptor(descriptor.name(), descriptor.receipt_kind(), outcome)
191 }
192
193 #[must_use]
195 pub fn from_descriptor(
196 descriptor_name: impl Into<String>,
197 receipt_kind: impl Into<String>,
198 outcome: ReceiptOutcome,
199 ) -> Self {
200 Self {
201 descriptor_name: descriptor_name.into(),
202 receipt_kind: receipt_kind.into(),
203 input_hash: None,
204 output_hash: None,
205 outcome,
206 signed_extensions: BTreeMap::new(),
207 local_extensions: BTreeMap::new(),
208 }
209 }
210
211 #[must_use]
213 pub fn with_input_hash(mut self, hash: ReceiptHash) -> Self {
214 self.input_hash = Some(hash);
215 self
216 }
217
218 #[must_use]
220 pub fn with_output_hash(mut self, hash: ReceiptHash) -> Self {
221 self.output_hash = Some(hash);
222 self
223 }
224
225 #[must_use]
227 pub fn with_signed_extension(
228 mut self,
229 key: impl Into<String>,
230 value: impl Into<Vec<u8>>,
231 ) -> Self {
232 self.signed_extensions.insert(key.into(), value.into());
233 self
234 }
235
236 #[must_use]
238 pub fn with_local_extension(
239 mut self,
240 key: impl Into<String>,
241 value: impl Into<Vec<u8>>,
242 ) -> Self {
243 self.local_extensions.insert(key.into(), value.into());
244 self
245 }
246}
247
248impl EventPayload for ReceiptEnvelope {
249 const KIND: EventKind = SYNCBAT_RECEIPT_EVENT_KIND;
250}
251
252#[derive(Clone, Debug, Eq, PartialEq)]
258#[non_exhaustive]
259pub struct RecordedReceipt {
260 pub envelope: ReceiptEnvelope,
262 pub batpak_receipt: Option<BatpakReceiptFields>,
264}
265
266impl RecordedReceipt {
267 #[must_use]
269 pub fn new(envelope: ReceiptEnvelope) -> Self {
270 Self {
271 envelope,
272 batpak_receipt: None,
273 }
274 }
275
276 #[must_use]
278 pub fn with_batpak_receipt(mut self, receipt: impl Into<BatpakReceiptFields>) -> Self {
279 self.batpak_receipt = Some(receipt.into());
280 self
281 }
282}
283
284#[derive(Debug, Clone, Eq, PartialEq)]
286pub struct ReceiptSinkError {
287 message: String,
288}
289
290impl ReceiptSinkError {
291 #[must_use]
293 pub fn new(message: impl Into<String>) -> Self {
294 Self {
295 message: message.into(),
296 }
297 }
298
299 #[must_use]
301 pub fn message(&self) -> &str {
302 &self.message
303 }
304}
305
306impl fmt::Display for ReceiptSinkError {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 f.write_str(&self.message)
309 }
310}
311
312impl Error for ReceiptSinkError {}
313
314pub trait ReceiptSink {
316 fn record_receipt(
321 &self,
322 envelope: &ReceiptEnvelope,
323 ) -> Result<RecordedReceipt, ReceiptSinkError>;
324}