Skip to main content

haematite_cli/serve/contracts/
envelopes.rs

1//! Strict stored pointers; document bodies remain in their separate data rows.
2
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use serde_json::Value;
5
6use super::{ResolvedMutation, attribution, identifier};
7use crate::serve::protocol::{Change, Version};
8use crate::serve::storage::Failure;
9
10const FORMAT: u32 = 1;
11
12/// The written version is independent of the branch from which a row is read.
13pub trait Attributed {
14    fn written_at(&self) -> Version;
15    fn request_id(&self) -> &str;
16}
17
18/// Locate an attributed envelope's author row.
19///
20/// # Errors
21/// Returns `invalid_identifier` when the envelope's request id cannot form an attribution key.
22pub fn attribution_key(envelope: &impl Attributed) -> Result<Vec<u8>, Failure> {
23    attribution::key(envelope.written_at(), envelope.request_id())
24}
25
26#[derive(Deserialize)]
27struct Header {
28    format: Option<u32>,
29    written_at: Option<Version>,
30    changed_at: Option<Version>,
31    request_id: Option<String>,
32}
33
34fn corrupt(error: impl std::fmt::Display) -> Failure {
35    Failure::new("stored_envelope_corrupt", error.to_string())
36}
37
38fn decode<T: DeserializeOwned>(bytes: &[u8], collection: bool) -> Result<T, Failure> {
39    // Inspect only metadata before allocating a payload; no old-shape fallback.
40    let header: Header = serde_json::from_slice(bytes).map_err(corrupt)?;
41    if header.format != Some(FORMAT) {
42        return Err(Failure::new(
43            "stored_envelope_format_unsupported",
44            "stored envelope requires format 1; older shapes are not readable",
45        ));
46    }
47    let version = if collection {
48        header.changed_at
49    } else {
50        header.written_at
51    };
52    let (Some(version), Some(request_id)) = (version, header.request_id) else {
53        return Err(Failure::new(
54            "stored_envelope_pointer_missing",
55            "stored envelope requires its written version and request id",
56        ));
57    };
58    validate_pointer(version, &request_id)?;
59    serde_json::from_slice(bytes).map_err(corrupt)
60}
61
62fn encode(value: &impl Serialize) -> Result<Vec<u8>, Failure> {
63    serde_json::to_vec(value).map_err(corrupt)
64}
65
66fn validate_pointer(version: Version, request_id: &str) -> Result<(), Failure> {
67    attribution::key(version, request_id)
68        .map(|_| ())
69        .map_err(|error| Failure::new("stored_envelope_pointer_invalid", error.message))
70}
71
72fn validate_name(name: &str) -> Result<(), Failure> {
73    identifier(name).map_err(|error| Failure::new("stored_envelope_payload_invalid", error.message))
74}
75
76fn wrong_kind() -> Failure {
77    Failure::new(
78        "stored_envelope_request_kind",
79        "request does not write this kind of stored envelope",
80    )
81}
82
83#[derive(Debug, Serialize)]
84#[serde(transparent)]
85pub struct Document {
86    wire: DocumentWire,
87}
88
89#[derive(Debug, Serialize, Deserialize)]
90#[serde(deny_unknown_fields)]
91struct DocumentWire {
92    format: u32,
93    collection_branch: String,
94    collection_version: Version,
95    written_in: String,
96    written_at: Version,
97    request_id: String,
98}
99
100impl Document {
101    /// Build document provenance for a document-writing request.
102    ///
103    /// # Errors
104    /// Refuses another change kind, an invalid attribution pointer or invalid branch names.
105    pub fn for_request(
106        request: &ResolvedMutation,
107        written_at: Version,
108        collection_branch: String,
109        collection_version: Version,
110    ) -> Result<Self, Failure> {
111        if !matches!(request.change(), Change::Put(_) | Change::PutMany(_)) {
112            return Err(wrong_kind());
113        }
114        Self::from_wire(DocumentWire {
115            format: FORMAT,
116            collection_branch,
117            collection_version,
118            written_in: request.branch().to_owned(),
119            written_at,
120            request_id: request.request_id().to_owned(),
121        })
122    }
123
124    fn from_wire(wire: DocumentWire) -> Result<Self, Failure> {
125        validate_pointer(wire.written_at, &wire.request_id)?;
126        validate_name(&wire.collection_branch)?;
127        validate_name(&wire.written_in)?;
128        Ok(Self { wire })
129    }
130
131    /// Decode and validate document provenance.
132    ///
133    /// # Errors
134    /// Refuses malformed JSON, unsupported formats, missing or invalid attribution pointers and invalid branch names.
135    pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
136        Self::from_wire(decode(bytes, false)?)
137    }
138
139    /// Encode document provenance.
140    ///
141    /// # Errors
142    /// Returns `stored_envelope_corrupt` if serialization fails.
143    pub fn encode(&self) -> Result<Vec<u8>, Failure> {
144        encode(self)
145    }
146
147    #[must_use]
148    pub fn collection_branch(&self) -> &str {
149        &self.wire.collection_branch
150    }
151
152    #[must_use]
153    pub const fn collection_version(&self) -> Version {
154        self.wire.collection_version
155    }
156
157    #[must_use]
158    pub fn written_in(&self) -> &str {
159        &self.wire.written_in
160    }
161}
162
163impl Attributed for Document {
164    fn written_at(&self) -> Version {
165        self.wire.written_at
166    }
167
168    fn request_id(&self) -> &str {
169        &self.wire.request_id
170    }
171}
172
173#[derive(Debug, Serialize)]
174#[serde(transparent)]
175pub struct Collection {
176    wire: CollectionWire,
177}
178
179#[derive(Debug, Serialize, Deserialize)]
180#[serde(deny_unknown_fields)]
181struct CollectionWire {
182    format: u32,
183    name: String,
184    schema: Option<Value>,
185    changed_in: String,
186    changed_at: Version,
187    request_id: String,
188}
189
190impl Collection {
191    /// Build a collection envelope from its declaring request.
192    ///
193    /// # Errors
194    /// Refuses another change kind, an invalid attribution pointer or invalid collection and branch names.
195    pub fn for_request(request: &ResolvedMutation, changed_at: Version) -> Result<Self, Failure> {
196        let Change::Collection(write) = request.change() else {
197            return Err(wrong_kind());
198        };
199        Self::from_wire(CollectionWire {
200            format: FORMAT,
201            name: write.collection.clone(),
202            schema: write.schema.clone(),
203            changed_in: request.branch().to_owned(),
204            changed_at,
205            request_id: request.request_id().to_owned(),
206        })
207    }
208
209    fn from_wire(wire: CollectionWire) -> Result<Self, Failure> {
210        validate_pointer(wire.changed_at, &wire.request_id)?;
211        validate_name(&wire.name)?;
212        validate_name(&wire.changed_in)?;
213        Ok(Self { wire })
214    }
215
216    /// Decode and validate a collection envelope.
217    ///
218    /// # Errors
219    /// Refuses malformed JSON, unsupported formats, missing or invalid attribution pointers and invalid names.
220    pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
221        Self::from_wire(decode(bytes, true)?)
222    }
223
224    /// Encode this collection envelope.
225    ///
226    /// # Errors
227    /// Returns `stored_envelope_corrupt` if serialization fails.
228    pub fn encode(&self) -> Result<Vec<u8>, Failure> {
229        encode(self)
230    }
231
232    #[must_use]
233    pub fn name(&self) -> &str {
234        &self.wire.name
235    }
236
237    #[must_use]
238    pub const fn schema(&self) -> Option<&Value> {
239        self.wire.schema.as_ref()
240    }
241
242    #[must_use]
243    pub fn changed_in(&self) -> &str {
244        &self.wire.changed_in
245    }
246
247    #[must_use]
248    pub const fn changed_at(&self) -> Version {
249        self.wire.changed_at
250    }
251}
252
253impl Attributed for Collection {
254    fn written_at(&self) -> Version {
255        self.wire.changed_at
256    }
257
258    fn request_id(&self) -> &str {
259        &self.wire.request_id
260    }
261}
262
263#[derive(Debug, Serialize)]
264#[serde(transparent)]
265pub struct Edge {
266    wire: EdgeWire,
267}
268
269#[derive(Debug, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271struct EdgeWire {
272    format: u32,
273    properties: Value,
274    written_in: String,
275    written_at: Version,
276    request_id: String,
277}
278
279impl Edge {
280    /// Build an edge envelope from its writing request.
281    ///
282    /// # Errors
283    /// Refuses another change kind, an invalid attribution pointer, an invalid branch name or properties that are not an object.
284    pub fn for_request(request: &ResolvedMutation, written_at: Version) -> Result<Self, Failure> {
285        let Change::EdgePut(edge) = request.change() else {
286            return Err(wrong_kind());
287        };
288        Self::from_wire(EdgeWire {
289            format: FORMAT,
290            properties: edge.properties.clone(),
291            written_in: request.branch().to_owned(),
292            written_at,
293            request_id: request.request_id().to_owned(),
294        })
295    }
296
297    fn from_wire(wire: EdgeWire) -> Result<Self, Failure> {
298        validate_pointer(wire.written_at, &wire.request_id)?;
299        validate_name(&wire.written_in)?;
300        if !wire.properties.is_object() {
301            return Err(Failure::new(
302                "stored_envelope_payload_invalid",
303                "edge properties must be a JSON object",
304            ));
305        }
306        Ok(Self { wire })
307    }
308
309    /// Decode and validate an edge envelope.
310    ///
311    /// # Errors
312    /// Refuses malformed JSON, unsupported formats, invalid provenance or edge properties that are not an object.
313    pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
314        Self::from_wire(decode(bytes, false)?)
315    }
316
317    /// Encode this edge envelope.
318    ///
319    /// # Errors
320    /// Returns `stored_envelope_corrupt` if serialization fails.
321    pub fn encode(&self) -> Result<Vec<u8>, Failure> {
322        encode(self)
323    }
324
325    #[must_use]
326    pub const fn properties(&self) -> &Value {
327        &self.wire.properties
328    }
329
330    #[must_use]
331    pub fn written_in(&self) -> &str {
332        &self.wire.written_in
333    }
334}
335
336impl Attributed for Edge {
337    fn written_at(&self) -> Version {
338        self.wire.written_at
339    }
340
341    fn request_id(&self) -> &str {
342        &self.wire.request_id
343    }
344}
345
346#[derive(Debug)]
347pub struct Vector {
348    header: VectorHeader,
349    vector: Vec<f32>,
350}
351
352#[derive(Debug, Serialize, Deserialize)]
353#[serde(deny_unknown_fields)]
354struct VectorHeader {
355    format: u32,
356    written_in: String,
357    written_at: Version,
358    request_id: String,
359    dimensions: usize,
360}
361
362fn vector_header_error(error: Failure) -> Failure {
363    if error.code == "stored_envelope_corrupt" {
364        Failure::new("vector_envelope_header_invalid", error.message)
365    } else {
366        error
367    }
368}
369
370fn vector_tail_length(header: &VectorHeader) -> Result<usize, Failure> {
371    validate_pointer(header.written_at, &header.request_id)?;
372    validate_name(&header.written_in)?;
373    if header.dimensions == 0 {
374        return Err(Failure::new(
375            "vector_envelope_dimensions_invalid",
376            "vector dimensions must be at least one",
377        ));
378    }
379    header.dimensions.checked_mul(4).ok_or_else(|| {
380        Failure::new(
381            "vector_envelope_dimensions_invalid",
382            "vector byte length exceeds the addressable size",
383        )
384    })
385}
386
387fn finite(member: f32) -> Result<f32, Failure> {
388    if !member.is_finite() {
389        return Err(Failure::new(
390            "vector_envelope_non_finite",
391            "vector members must be finite",
392        ));
393    }
394    Ok(member)
395}
396
397impl Vector {
398    /// Build a vector envelope from its writing request.
399    ///
400    /// # Errors
401    /// Refuses another change kind, invalid provenance, zero or unrepresentable dimensions and non-finite vector members.
402    pub fn for_request(request: &ResolvedMutation, written_at: Version) -> Result<Self, Failure> {
403        let Change::VectorPut(vector) = request.change() else {
404            return Err(wrong_kind());
405        };
406        let header = VectorHeader {
407            format: FORMAT,
408            written_in: request.branch().to_owned(),
409            written_at,
410            request_id: request.request_id().to_owned(),
411            dimensions: vector.vector.len(),
412        };
413        vector_tail_length(&header)?;
414        for member in &vector.vector {
415            finite(*member)?;
416        }
417        Ok(Self {
418            header,
419            vector: vector.vector.clone(),
420        })
421    }
422
423    /// Decode the vector's JSON header and binary tail.
424    ///
425    /// # Errors
426    /// Refuses a missing or malformed header, unsupported format, invalid provenance or dimensions, a mismatched tail length or non-finite members.
427    pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
428        let newline = bytes
429            .iter()
430            .position(|byte| *byte == b'\n')
431            .ok_or_else(|| {
432                Failure::new(
433                    "vector_envelope_header_missing",
434                    "vector requires a JSON header followed by one LF",
435                )
436            })?;
437        let (line, remainder) = bytes.split_at(newline);
438        if line.contains(&b'\r')
439            || line.iter().find(|byte| !byte.is_ascii_whitespace()) != Some(&b'{')
440        {
441            return Err(Failure::new(
442                "vector_envelope_header_invalid",
443                "vector header must be a single-line JSON object",
444            ));
445        }
446        let header: VectorHeader = decode(line, false).map_err(vector_header_error)?;
447        let tail = remainder.get(1..).ok_or_else(|| {
448            Failure::new(
449                "vector_envelope_header_missing",
450                "vector header has no LF separator",
451            )
452        })?;
453        if tail.len() != vector_tail_length(&header)? {
454            return Err(Failure::new(
455                "vector_envelope_tail_invalid",
456                "vector tail must contain exactly four bytes per dimension",
457            ));
458        }
459        let vector = tail
460            .as_chunks::<4>()
461            .0
462            .iter()
463            .map(|chunk| finite(f32::from_le_bytes(*chunk)))
464            .collect::<Result<Vec<_>, _>>()?;
465        Ok(Self { header, vector })
466    }
467
468    /// Encode the vector header, separator and binary tail.
469    ///
470    /// # Errors
471    /// Refuses header serialization failure or invalid provenance and dimensions.
472    pub fn encode(&self) -> Result<Vec<u8>, Failure> {
473        let mut bytes = encode(&self.header)?;
474        bytes.push(b'\n');
475        bytes.reserve(vector_tail_length(&self.header)?);
476        for member in &self.vector {
477            bytes.extend_from_slice(&member.to_le_bytes());
478        }
479        Ok(bytes)
480    }
481
482    #[must_use]
483    pub fn vector(&self) -> &[f32] {
484        &self.vector
485    }
486
487    #[must_use]
488    pub const fn dimension(&self) -> usize {
489        self.header.dimensions
490    }
491
492    #[must_use]
493    pub fn written_in(&self) -> &str {
494        &self.header.written_in
495    }
496}
497
498impl Attributed for Vector {
499    fn written_at(&self) -> Version {
500        self.header.written_at
501    }
502
503    fn request_id(&self) -> &str {
504        &self.header.request_id
505    }
506}
507
508#[cfg(test)]
509#[path = "envelopes_tests.rs"]
510mod tests;