Skip to main content

khive_changeset/
preimage.rs

1//! Field-scoped comparison against the record visible to an applying writer.
2
3use khive_types::{Entity, Header, Link, Note};
4
5use crate::{
6    DeleteOp, DeletePreimage, EdgePreimage, EntityPreimage, NotePreimage, UpdateOp, UpdatePreimage,
7};
8
9#[derive(Clone, Copy, Debug)]
10/// A caller-supplied record from the applying transaction, without a storage handle.
11pub enum CanonicalRecordRef<'a> {
12    Entity(&'a Entity),
13    Note(&'a Note),
14    Edge(&'a Link),
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18/// A stable field path without captured or current record contents.
19pub struct PreimageMismatch {
20    pub field_path: String,
21}
22
23impl std::fmt::Display for PreimageMismatch {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "captured preimage differs at {}", self.field_path)
26    }
27}
28
29impl std::error::Error for PreimageMismatch {}
30
31fn equal<T: PartialEq + ?Sized>(
32    field: &str,
33    captured: &T,
34    live: &T,
35) -> Result<(), PreimageMismatch> {
36    if captured == live {
37        Ok(())
38    } else {
39        Err(PreimageMismatch {
40            field_path: field.into(),
41        })
42    }
43}
44
45fn touched<T: PartialEq>(
46    field: &str,
47    captured: &Option<T>,
48    live: &T,
49) -> Result<(), PreimageMismatch> {
50    match captured {
51        Some(captured) => equal(field, captured, live),
52        None => Ok(()),
53    }
54}
55
56/// The caller must read `live` inside the same transaction that applies `op`.
57pub fn check_update(op: &UpdateOp, live: CanonicalRecordRef<'_>) -> Result<(), PreimageMismatch> {
58    match (op.preimage(), live) {
59        (UpdatePreimage::Entity(preimage), CanonicalRecordRef::Entity(live)) => {
60            equal("id", &op.target_id(), &live.header.id)?;
61            let EntityPreimage {
62                name,
63                description,
64                properties,
65                tags,
66                entity_type,
67            } = preimage;
68            touched("name", name, &live.name)?;
69            touched("description", description, &live.description)?;
70            touched("properties", properties, &live.properties)?;
71            touched("tags", tags, &live.tags)?;
72            touched("entity_type", entity_type, &live.entity_type)
73        }
74        (UpdatePreimage::Note(preimage), CanonicalRecordRef::Note(live)) => {
75            equal("id", &op.target_id(), &live.header.id)?;
76            let NotePreimage {
77                content,
78                salience,
79                decay_factor,
80                properties,
81                tags,
82            } = preimage;
83            touched("content", content, &live.content)?;
84            touched("salience", salience, &live.salience)?;
85            touched("decay_factor", decay_factor, &live.decay_factor)?;
86            touched("properties", properties, &live.properties)?;
87            touched("tags", tags, &live.tags)
88        }
89        (UpdatePreimage::Edge(preimage), CanonicalRecordRef::Edge(live)) => {
90            equal("id", &op.target_id(), &live.id)?;
91            let EdgePreimage { relation, weight } = preimage;
92            touched("relation", relation, &live.relation)?;
93            touched("weight", weight, &live.weight)
94        }
95        _ => Err(PreimageMismatch {
96            field_path: "substrate".into(),
97        }),
98    }
99}
100
101fn header(captured: &Header, live: &Header) -> Result<(), PreimageMismatch> {
102    let Header {
103        id,
104        namespace,
105        created_at,
106        updated_at: _,
107    } = captured;
108    equal("id", id, &live.id)?;
109    equal("namespace", namespace, &live.namespace)?;
110    equal("created_at", created_at, &live.created_at)
111}
112
113/// The captured record is exact except for its applier-stamped `updated_at`.
114pub fn check_delete(op: &DeleteOp, live: CanonicalRecordRef<'_>) -> Result<(), PreimageMismatch> {
115    match (&op.preimage, live) {
116        (DeletePreimage::Entity(captured), CanonicalRecordRef::Entity(live)) => {
117            equal("id", &op.target_id, &live.header.id)?;
118            let Entity {
119                header: captured_header,
120                kind,
121                entity_type,
122                name,
123                description,
124                properties,
125                tags,
126                deleted_at,
127            } = captured.as_ref();
128            header(captured_header, &live.header)?;
129            equal("kind", kind, &live.kind)?;
130            equal("entity_type", entity_type, &live.entity_type)?;
131            equal("name", name, &live.name)?;
132            equal("description", description, &live.description)?;
133            equal("properties", properties, &live.properties)?;
134            equal("tags", tags, &live.tags)?;
135            equal("deleted_at", deleted_at, &live.deleted_at)
136        }
137        (DeletePreimage::Note(captured), CanonicalRecordRef::Note(live)) => {
138            equal("id", &op.target_id, &live.header.id)?;
139            let Note {
140                header: captured_header,
141                kind,
142                status,
143                content,
144                properties,
145                tags,
146                salience,
147                decay_factor,
148                expires_at,
149                deleted_at,
150            } = captured.as_ref();
151            header(captured_header, &live.header)?;
152            equal("kind", kind, &live.kind)?;
153            equal("status", status, &live.status)?;
154            equal("content", content, &live.content)?;
155            equal("properties", properties, &live.properties)?;
156            equal("tags", tags, &live.tags)?;
157            equal("salience", salience, &live.salience)?;
158            equal("decay_factor", decay_factor, &live.decay_factor)?;
159            equal("expires_at", expires_at, &live.expires_at)?;
160            equal("deleted_at", deleted_at, &live.deleted_at)
161        }
162        (DeletePreimage::Edge(captured), CanonicalRecordRef::Edge(live)) => {
163            equal("id", &op.target_id, &live.id)?;
164            let Link {
165                id,
166                namespace,
167                source,
168                target,
169                relation,
170                properties,
171                weight,
172                created_at,
173                updated_at: _,
174                deleted_at,
175            } = captured.as_ref();
176            equal("id", id, &live.id)?;
177            equal("namespace", namespace, &live.namespace)?;
178            equal("source", source, &live.source)?;
179            equal("target", target, &live.target)?;
180            equal("relation", relation, &live.relation)?;
181            equal("properties", properties, &live.properties)?;
182            equal("weight", weight, &live.weight)?;
183            equal("created_at", created_at, &live.created_at)?;
184            equal("deleted_at", deleted_at, &live.deleted_at)
185        }
186        _ => Err(PreimageMismatch {
187            field_path: "substrate".into(),
188        }),
189    }
190}
191
192#[cfg(test)]
193#[path = "preimage_tests.rs"]
194mod tests;