ubiquisync_core/codec/op.rs
1use std::io::BufRead;
2
3use crate::codec::{error::CodecError, reader::EntryBufferReader, writer::EntryBufferWriter};
4
5/// An op vocabulary that can be encoded to / decoded from the log wire format.
6///
7/// Each data domain implements this for its own op type (e.g. the table op
8/// enum in `ubiquisync-tables`, which is also named `Op`). The generic
9/// [`Encoder`](crate::codec::Encoder) and [`Decoder`](crate::codec::Decoder)
10/// drive it: the framing reads the entry tag and hands it to [`decode`], which
11/// decodes the body; [`encode`] is responsible for the **whole** op — it writes
12/// the tag *and* the body. The framing supplies everything else (timestamp,
13/// attribution, integrity hash).
14///
15/// # Reserved tag
16///
17/// Tag `255` (`0xFF`, [`TAG_EXPUNGED`](crate::codec::TAG_EXPUNGED)) is reserved
18/// by the framing for expunged-entry markers and is **not a valid op tag**. An
19/// implementation must never write `0xFF` as its tag, and [`decode`] is never
20/// called with `tag == 0xFF` — the framing intercepts that value before
21/// dispatching. Emitting `0xFF` from [`encode`] would make the entry decode as
22/// an expunged marker, silently corrupting the log.
23///
24/// [`decode`]: Op::decode
25/// [`encode`]: Op::encode
26pub trait Op: Sized {
27 /// Decode an op body for the given entry `tag`, which the framing has
28 /// already read. `tag` is never `0xFF`
29 /// ([`TAG_EXPUNGED`](crate::codec::TAG_EXPUNGED)) — the framing handles that
30 /// reserved value itself.
31 fn decode<R: BufRead>(tag: u8, r: &mut EntryBufferReader<R>) -> Result<Self, CodecError>;
32 /// Encode the op as a tag byte followed by its body. The tag must never be
33 /// `0xFF` ([`TAG_EXPUNGED`](crate::codec::TAG_EXPUNGED)), which is reserved.
34 fn encode(&self, w: &mut EntryBufferWriter) -> Result<(), CodecError>;
35}
36
37/// An [`Op`] that can also be split into an indexable `(tag, key, value)`
38/// triple for the SQL op-log. `key` is the indexable identity (such as table + primary key).
39/// `value` is the op's remaining payload.
40///
41/// # Round-trip
42///
43/// [`from_index_parts`](IndexableOp::from_index_parts) must invert
44/// [`to_index_entry`](IndexableOp::to_index_entry), so the full op can be
45/// reconstructed from its stored parts.
46pub trait IndexableOp: Op {
47 /// Split `self` into its `(tag, key, value)` triple.
48 fn to_index_entry(&self) -> Result<OpIndexEntry, CodecError>;
49 /// Reconstruct an op from a stored triple. The inverse of
50 /// [`to_index_entry`](IndexableOp::to_index_entry).
51 fn from_index_parts(tag: u8, key: &[u8], value: &[u8]) -> Result<Self, CodecError>;
52}
53
54/// The indexable form of an op: its tag plus the `key`/`value` split of its
55/// body. See [`IndexableOp`].
56pub struct OpIndexEntry {
57 /// The op's tag byte (the same one [`Op::encode`] writes).
58 pub tag: u8,
59 /// The op's key part, for efficient querying by the affected row/entity.
60 pub key: Vec<u8>,
61 /// The op-specific payload after `key`; may be empty.
62 pub value: Vec<u8>,
63}