haematite_cli/serve/contracts/
envelopes.rs1use 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
12pub trait Attributed {
14 fn written_at(&self) -> Version;
15 fn request_id(&self) -> &str;
16}
17
18pub 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 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 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 pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
136 Self::from_wire(decode(bytes, false)?)
137 }
138
139 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 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 pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
221 Self::from_wire(decode(bytes, true)?)
222 }
223
224 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 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 pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
314 Self::from_wire(decode(bytes, false)?)
315 }
316
317 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 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 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 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;