⎇ f0c0a718605924227076bef7cb8b403c388261b6
crates/modules/nakui/core/src/store.rs
Historia
Quién tocó qué
crudo
feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
1use serde_json::Value;
2use sha2::{Digest, Sha256};
3use std::collections::HashMap;
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::delta::FieldOp;
8
9#[derive(Debug, Clone, Error)]
10pub enum StoreError {
11 #[error("entity {0} id {1} not found")]
12 NotFound(String, Uuid),
13 #[error("entity {0} id {1} already exists")]
14 Conflict(String, Uuid),
15 #[error("set on non-object record at {0} {1}")]
16 NotAnObject(String, Uuid),
17 /// Backend-specific transient or systemic failure (network, disk,
18 /// driver). Distinct from the data-shape errors above.
19 #[error("backend error: {0}")]
20 Backend(String),
21}
22
23pub trait Store {
24 fn load(&self, entity: &str, id: Uuid) -> Option<Value>;
25
26 /// Insert or replace a record without going through the morphism
27 /// pipeline. Represents external/boundary input — the source of
28 /// records that didn't originate from a kernel-validated event.
29 fn seed(&mut self, entity: &str, id: Uuid, data: Value);
30
31 /// Read-only check: would `apply(ops)` succeed against current state?
32 /// Does NOT mutate. The kernel runs this as the last step of `compute`
33 /// so that, by the time we log an event, the apply is contractually
34 /// guaranteed to land.
35 fn apply_dry_run(&self, ops: &[FieldOp]) -> Result<(), StoreError>;
36
37 fn apply(&mut self, ops: &[FieldOp]) -> Result<(), StoreError>;
38
39 /// Drop every record. Used by `reconcile` to wipe a stale store before
40 /// replaying the log. Must leave the store in the same state it would
41 /// be in immediately after construction. Implementors that override
42 /// `last_applied_seq` must reset that marker here too — a cleared
43 /// store has applied nothing.
44 fn clear(&mut self) -> Result<(), StoreError>;
45
46 /// The last log seq whose effects are reflected in this store, if
47 /// the store can persist that fact. Default `Ok(None)` covers
48 /// transient backends. The startup path uses this to skip the full
49 /// replay when the store is verifiably already in sync with the log.
50 fn last_applied_seq(&self) -> Result<Option<u64>, StoreError> {
51 Ok(None)
52 }
53
54 /// Persist the marker after a successful apply / seed / replay.
55 /// Best-effort: callers ignore failures here because a stale marker
56 /// only costs an extra full replay on next startup, never
57 /// correctness — full replay starts with `clear()`, so it tolerates
58 /// any prior state. Default impl is a no-op for transient backends.
59 fn set_last_applied_seq(&mut self, _seq: u64) -> Result<(), StoreError> {
60 Ok(())
61 }
62
63 /// Enumerate every record in canonical order: sorted first by entity
64 /// name, then by id bytes. The canonical order is what makes
65 /// `hash_state` reproducible — without it two stores with the same
66 /// records would hash differently depending on insertion order.
67 ///
68 /// Returns owned `Value`s. For an in-memory backend this clones; for
69 /// a remote backend it materializes a snapshot. V1 chooses simplicity
70 /// over streaming — the hash and drift-comparison use cases need to
71 /// see all records anyway, and an iterator over a Vec keeps the
72 /// trait method object-safe and free of async lifetime concerns.
73 fn iter(&self) -> Result<Box<dyn Iterator<Item = (String, Uuid, Value)> + '_>, StoreError>;
74
75 /// Deterministic SHA-256 of the store's full state. Two stores with
76 /// the same records (regardless of how they got there or which
77 /// backend they live in) produce the same hash; any drift produces
78 /// a different one. The default impl is the contract — backends
79 /// should only override it for backend-native acceleration (e.g.
80 /// server-side table digests), and an override must produce the
81 /// same bytes as the default.
82 ///
83 /// Framing per record:
84 /// entity_bytes | 0x00 | id_bytes | 0x00 | canonical_value_hash
85 /// The length prefix on entity/id prevents (entity="ab", id="c")
86 /// from colliding with (entity="a", id="bc"). The value bytes are
87 /// produced by `hash_value`, which walks the JSON tree with
88 /// type-tagged framing — that decouples the hash from
89 /// `serde_json::to_vec`'s representation choices (especially
90 /// integer-valued floats vs ints) so cross-backend comparison
91 /// works.
92 fn hash_state(&self) -> Result<[u8; 32], StoreError> {
93 let mut hasher = Sha256::new();
94 for (entity, id, value) in self.iter()? {
95 hasher.update(entity.as_bytes());
96 hasher.update([0u8]);
97 hasher.update(id.as_bytes());
98 hasher.update([0u8]);
99 hash_value(&mut hasher, &value);
100 }
101 Ok(hasher.finalize().into())
102 }
103}
104
105/// Canonical hash of a `serde_json::Value`. Type-tagged so a string
106/// "true" can't collide with the boolean `true`; length-prefixed so
107/// concatenation can't shift bytes between fields. Numbers normalize:
108/// any integer-valued number (i64, u64, or a finite f64 with no
109/// fractional part) is hashed as an i128 — that's what makes
110/// cross-backend equality work, since SurrealDB may round-trip
111/// what the caller wrote as `100_i64` back as the same numeric value
112/// without us needing to commit to a wire-format-specific
113/// representation.
114pub fn hash_value(hasher: &mut Sha256, v: &Value) {
115 match v {
116 Value::Null => hasher.update([TAG_NULL]),
117 Value::Bool(b) => {
118 hasher.update([TAG_BOOL]);
119 hasher.update([*b as u8]);
120 }
121 Value::Number(n) => {
122 if let Some(i) = n.as_i64() {
123 hash_int(hasher, i as i128);
124 } else if let Some(u) = n.as_u64() {
125 hash_int(hasher, u as i128);
126 } else if let Some(f) = n.as_f64() {
127 // Integer-valued floats canonicalize to int. Anything
128 // else (fractions, NaN, infinities) hashes as the raw
129 // f64 bit pattern — that's still deterministic, just
130 // not normalized.
131 if f.is_finite()
132 && f.fract() == 0.0
133 && f >= I128_MIN_AS_F64
134 && f <= I128_MAX_AS_F64
135 {
136 hash_int(hasher, f as i128);
137 } else {
138 hasher.update([TAG_FLOAT]);
139 hasher.update(f.to_bits().to_le_bytes());
140 }
141 } else {
142 // serde_json::Number guarantees one of the above; this
143 // branch only fires if a future variant appears.
144 hasher.update([TAG_FLOAT]);
145 hasher.update(f64::NAN.to_bits().to_le_bytes());
146 }
147 }
148 Value::String(s) => {
149 hasher.update([TAG_STRING]);
150 hasher.update((s.len() as u64).to_le_bytes());
151 hasher.update(s.as_bytes());
152 }
153 Value::Array(arr) => {
154 hasher.update([TAG_ARRAY]);
155 hasher.update((arr.len() as u64).to_le_bytes());
156 for item in arr {
157 hash_value(hasher, item);
158 }
159 }
160 Value::Object(map) => {
161 hasher.update([TAG_OBJECT]);
162 hasher.update((map.len() as u64).to_le_bytes());
163 // serde_json::Map without `preserve_order` is BTreeMap
164 // (alphabetical). We sort defensively in case the build
165 // pulls in `preserve_order` transitively from a future dep.
166 let mut keys: Vec<&String> = map.keys().collect();
167 keys.sort();
168 for k in keys {
169 hasher.update((k.len() as u64).to_le_bytes());
170 hasher.update(k.as_bytes());
171 hash_value(hasher, &map[k]);
172 }
173 }
174 }
175}
176
177fn hash_int(hasher: &mut Sha256, n: i128) {
178 hasher.update([TAG_INT]);
179 hasher.update(n.to_le_bytes());
180}
181
182const TAG_NULL: u8 = 0;
183const TAG_BOOL: u8 = 1;
184const TAG_INT: u8 = 2;
185const TAG_FLOAT: u8 = 3;
186const TAG_STRING: u8 = 4;
187const TAG_ARRAY: u8 = 5;
188const TAG_OBJECT: u8 = 6;
189
190// f64 can't represent i128::MAX exactly; the cast truncates upward to
191// the next representable f64. Use those as the comparison bounds so
192// `f as i128` stays well-defined.
193const I128_MIN_AS_F64: f64 = -1.7014118346046923e38;
194const I128_MAX_AS_F64: f64 = 1.7014118346046923e38;
195
196#[derive(Debug, Default, Clone, PartialEq)]
197pub struct MemoryStore {
198 records: HashMap<String, HashMap<Uuid, Value>>,
199 /// Last log seq whose effects are reflected here. In-process only —
200 /// resets to `None` on construction or `clear`. The skip-replay
201 /// optimization in `nakui run` benefits the persistent backends;
202 /// for `MemoryStore` it's harmless bookkeeping (process restart =
203 /// new store = `None`, which forces full replay).
204 last_applied: Option<u64>,
205}
206
207impl MemoryStore {
208 pub fn new() -> Self {
209 Self::default()
210 }
211
212 /// Borrow the internal records map. Used by `Snapshot::from_memory_store`
213 /// to capture state for snapshot persistence.
214 pub fn records(&self) -> &HashMap<String, HashMap<Uuid, Value>> {
215 &self.records
216 }
217}
218
219impl Store for MemoryStore {
220 fn load(&self, entity: &str, id: Uuid) -> Option<Value> {
221 self.records.get(entity)?.get(&id).cloned()
222 }
223
224 fn seed(&mut self, entity: &str, id: Uuid, data: Value) {
225 self.records
226 .entry(entity.to_string())
227 .or_default()
228 .insert(id, data);
229 }
230
231 fn apply_dry_run(&self, ops: &[FieldOp]) -> Result<(), StoreError> {
232 for op in ops {
233 match op {
234 FieldOp::Set { path, .. } | FieldOp::Clear { path } => {
235 // Set y Clear comparten la misma pre-condición: el
236 // record padre tiene que existir y ser un objeto.
237 // Clear de un field que no existe en el map es no-op
238 // benigno en apply (no error).
239 match self.records.get(&path.entity).and_then(|m| m.get(&path.id)) {
240 None => {
241 return Err(StoreError::NotFound(path.entity.clone(), path.id));
242 }
243 Some(Value::Object(_)) => {}
244 Some(_) => {
245 return Err(StoreError::NotAnObject(path.entity.clone(), path.id));
246 }
247 }
248 }
249 FieldOp::Create { entity, id, .. } => {
250 if self
251 .records
252 .get(entity)
253 .and_then(|m| m.get(id))
254 .is_some()
255 {
256 return Err(StoreError::Conflict(entity.clone(), *id));
257 }
258 }
259 FieldOp::Delete { entity, id } => {
260 if self
261 .records
262 .get(entity)
263 .and_then(|m| m.get(id))
264 .is_none()
265 {
266 return Err(StoreError::NotFound(entity.clone(), *id));
267 }
268 }
269 }
270 }
271 Ok(())
272 }
273
274 fn apply(&mut self, ops: &[FieldOp]) -> Result<(), StoreError> {
275 self.apply_dry_run(ops)?;
276 for op in ops {
277 match op {
278 FieldOp::Set { path, value } => {
279 let rec = self
280 .records
281 .get_mut(&path.entity)
282 .and_then(|m| m.get_mut(&path.id))
283 .expect("validated by dry_run");
284 let map = match rec {
285 Value::Object(m) => m,
286 _ => unreachable!("dry_run guards against non-object"),
287 };
288 map.insert(path.field.clone(), value.clone());
289 }
290 FieldOp::Clear { path } => {
291 let rec = self
292 .records
293 .get_mut(&path.entity)
294 .and_then(|m| m.get_mut(&path.id))
295 .expect("validated by dry_run");
296 let map = match rec {
297 Value::Object(m) => m,
298 _ => unreachable!("dry_run guards against non-object"),
299 };
300 // Clear de un field ausente: no-op silencioso. El
301 // post-state es el mismo (el field no está) y permite
302 // que el caller emita Clear sin hacer load previo.
303 map.remove(&path.field);
304 }
305 FieldOp::Create { entity, id, data } => {
306 self.records
307 .entry(entity.clone())
308 .or_default()
309 .insert(*id, data.clone());
310 }
311 FieldOp::Delete { entity, id } => {
312 self.records
313 .get_mut(entity)
314 .expect("validated by dry_run")
315 .remove(id);
316 }
317 }
318 }
319 Ok(())
320 }
321
322 fn clear(&mut self) -> Result<(), StoreError> {
323 self.records.clear();
324 self.last_applied = None;
325 Ok(())
326 }
327
328 fn last_applied_seq(&self) -> Result<Option<u64>, StoreError> {
329 Ok(self.last_applied)
330 }
331
332 fn set_last_applied_seq(&mut self, seq: u64) -> Result<(), StoreError> {
333 self.last_applied = Some(seq);
334 Ok(())
335 }
336
337 fn iter(&self) -> Result<Box<dyn Iterator<Item = (String, Uuid, Value)> + '_>, StoreError> {
338 let mut out: Vec<(String, Uuid, Value)> = self
339 .records
340 .iter()
341 .flat_map(|(entity, m)| {
342 m.iter()
343 .map(move |(id, v)| (entity.clone(), *id, v.clone()))
344 })
345 .collect();
346 out.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.as_bytes().cmp(b.1.as_bytes())));
347 Ok(Box::new(out.into_iter()))
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::delta::{FieldOp, FieldPath};
355 use serde_json::json;
356
357 #[test]
358 fn dry_run_rejects_set_on_non_object() {
359 let mut store = MemoryStore::new();
360 let id = Uuid::new_v4();
361 store.seed("Caja", id, json!(42)); // not an object
362 let op = FieldOp::Set {
363 path: FieldPath {
364 entity: "Caja".into(),
365 id,
366 field: "saldo".into(),
367 },
368 value: json!(100),
369 };
370 match store.apply_dry_run(&[op.clone()]) {
371 Err(StoreError::NotAnObject(e, i)) => {
372 assert_eq!(e, "Caja");
373 assert_eq!(i, id);
374 }
375 other => panic!("expected NotAnObject, got {:?}", other),
376 }
377 // apply must reject too without panicking.
378 assert!(matches!(
379 store.apply(&[op]),
380 Err(StoreError::NotAnObject(_, _))
381 ));
382 }
383
384 #[test]
385 fn dry_run_rejects_create_conflict() {
386 let mut store = MemoryStore::new();
387 let id = Uuid::new_v4();
388 store.seed("Caja", id, json!({"id": id.to_string()}));
389 let op = FieldOp::Create {
390 entity: "Caja".into(),
391 id,
392 data: json!({"id": id.to_string()}),
393 };
394 assert!(matches!(
395 store.apply_dry_run(&[op]),
396 Err(StoreError::Conflict(_, _))
397 ));
398 }
399
400 #[test]
401 fn dry_run_passes_for_valid_set() {
402 let mut store = MemoryStore::new();
403 let id = Uuid::new_v4();
404 store.seed("Caja", id, json!({"saldo": 100, "currency": "USD"}));
405 let op = FieldOp::Set {
406 path: FieldPath {
407 entity: "Caja".into(),
408 id,
409 field: "saldo".into(),
410 },
411 value: json!(150),
412 };
413 assert!(store.apply_dry_run(&[op]).is_ok());
414 }
415
416 #[test]
417 fn apply_clear_removes_field_key() {
418 let mut store = MemoryStore::new();
419 let id = Uuid::new_v4();
420 store.seed(
421 "Customer",
422 id,
423 json!({"id": id.to_string(), "name": "Acme", "notes": "lorem"}),
424 );
425 let op = FieldOp::Clear {
426 path: FieldPath {
427 entity: "Customer".into(),
428 id,
429 field: "notes".into(),
430 },
431 };
432 store.apply(&[op]).unwrap();
433 let after = store.load("Customer", id).unwrap();
434 let map = after.as_object().unwrap();
435 assert!(!map.contains_key("notes"), "notes debería estar borrado");
436 assert_eq!(map.get("name"), Some(&json!("Acme")), "otros fields intactos");
437 }
438
439 #[test]
440 fn apply_clear_on_absent_field_is_noop() {
441 let mut store = MemoryStore::new();
442 let id = Uuid::new_v4();
443 store.seed(
444 "Customer",
445 id,
446 json!({"id": id.to_string(), "name": "Acme"}),
447 );
448 let op = FieldOp::Clear {
449 path: FieldPath {
450 entity: "Customer".into(),
451 id,
452 field: "notes".into(),
453 },
454 };
455 // No debería errar: clear de un field ausente es benigno.
456 store.apply(&[op]).unwrap();
457 let after = store.load("Customer", id).unwrap();
458 assert_eq!(
459 after.as_object().unwrap().get("name"),
460 Some(&json!("Acme"))
461 );
462 }
463
464 #[test]
465 fn dry_run_rejects_clear_on_missing_record() {
466 let store = MemoryStore::new();
467 let id = Uuid::new_v4();
468 let op = FieldOp::Clear {
469 path: FieldPath {
470 entity: "Customer".into(),
471 id,
472 field: "notes".into(),
473 },
474 };
475 assert!(matches!(
476 store.apply_dry_run(&[op]),
477 Err(StoreError::NotFound(_, _))
478 ));
479 }
480
481 #[test]
482 fn dry_run_rejects_clear_on_non_object() {
483 let mut store = MemoryStore::new();
484 let id = Uuid::new_v4();
485 store.seed("Customer", id, json!(42)); // not an object
486 let op = FieldOp::Clear {
487 path: FieldPath {
488 entity: "Customer".into(),
489 id,
490 field: "notes".into(),
491 },
492 };
493 assert!(matches!(
494 store.apply_dry_run(&[op]),
495 Err(StoreError::NotAnObject(_, _))
496 ));
497 }
498
499 #[test]
500 fn iter_returns_canonical_order_regardless_of_insertion() {
501 let a = Uuid::new_v4();
502 let b = Uuid::new_v4();
503 let c = Uuid::new_v4();
504
505 let mut s1 = MemoryStore::new();
506 s1.seed("Caja", a, json!({"id": a.to_string(), "x": 1}));
507 s1.seed("Movimiento", c, json!({"id": c.to_string(), "y": 3}));
508 s1.seed("Caja", b, json!({"id": b.to_string(), "x": 2}));
509
510 let mut s2 = MemoryStore::new();
511 s2.seed("Movimiento", c, json!({"id": c.to_string(), "y": 3}));
512 s2.seed("Caja", b, json!({"id": b.to_string(), "x": 2}));
513 s2.seed("Caja", a, json!({"id": a.to_string(), "x": 1}));
514
515 let r1: Vec<_> = s1.iter().unwrap().collect();
516 let r2: Vec<_> = s2.iter().unwrap().collect();
517 assert_eq!(r1, r2, "iter order must be insertion-independent");
518
519 // Entities lexicographically sorted (Caja before Movimiento).
520 let entities: Vec<&str> = r1.iter().map(|(e, _, _)| e.as_str()).collect();
521 assert_eq!(entities, vec!["Caja", "Caja", "Movimiento"]);
522
523 // Within Caja, ids in byte order.
524 let caja_ids: Vec<Uuid> = r1
525 .iter()
526 .filter(|(e, _, _)| e == "Caja")
527 .map(|(_, id, _)| *id)
528 .collect();
529 let mut expected = vec![a, b];
530 expected.sort_by(|x, y| x.as_bytes().cmp(y.as_bytes()));
531 assert_eq!(caja_ids, expected);
532 }
533
534 #[test]
535 fn hash_state_is_deterministic_and_independent_of_insertion_order() {
536 let a = Uuid::new_v4();
537 let b = Uuid::new_v4();
538
539 let mut s1 = MemoryStore::new();
540 s1.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
541 s1.seed("Caja", b, json!({"id": b.to_string(), "saldo": 200}));
542
543 let mut s2 = MemoryStore::new();
544 s2.seed("Caja", b, json!({"id": b.to_string(), "saldo": 200}));
545 s2.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
546
547 assert_eq!(
548 s1.hash_state().unwrap(),
549 s2.hash_state().unwrap(),
550 "equal state must hash identically regardless of how it was built"
551 );
552 }
553
554 #[test]
555 fn hash_state_changes_when_state_changes() {
556 let a = Uuid::new_v4();
557
558 let mut s1 = MemoryStore::new();
559 s1.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
560
561 let mut s2 = MemoryStore::new();
562 s2.seed("Caja", a, json!({"id": a.to_string(), "saldo": 101}));
563
564 assert_ne!(
565 s1.hash_state().unwrap(),
566 s2.hash_state().unwrap(),
567 "off-by-one in a single field must produce a different hash"
568 );
569
570 // Adding a record changes the hash too.
571 let mut s3 = MemoryStore::new();
572 s3.seed("Caja", a, json!({"id": a.to_string(), "saldo": 100}));
573 s3.seed("Caja", Uuid::new_v4(), json!({"id": "extra", "saldo": 0}));
574 assert_ne!(s1.hash_state().unwrap(), s3.hash_state().unwrap());
575 }
576
577 #[test]
578 fn last_applied_seq_round_trips_and_resets_on_clear() {
579 let mut store = MemoryStore::new();
580 assert_eq!(
581 store.last_applied_seq().unwrap(),
582 None,
583 "fresh MemoryStore has no marker"
584 );
585 store.set_last_applied_seq(5).unwrap();
586 assert_eq!(store.last_applied_seq().unwrap(), Some(5));
587 store.set_last_applied_seq(12).unwrap();
588 assert_eq!(store.last_applied_seq().unwrap(), Some(12));
589 store.clear().unwrap();
590 assert_eq!(
591 store.last_applied_seq().unwrap(),
592 None,
593 "clear() resets the marker — a cleared store has applied nothing"
594 );
595 }
596
597 #[test]
598 fn integer_and_integer_valued_float_hash_identically() {
599 // The cross-backend property: the same numeric value, written
600 // by a backend as i64 vs read back as integer-valued f64,
601 // must hash the same.
602 let int_value = json!({"saldo": 100_i64});
603 let float_value = json!({"saldo": 100.0_f64});
604
605 let mut h_int = sha2::Sha256::new();
606 super::hash_value(&mut h_int, &int_value);
607 let mut h_float = sha2::Sha256::new();
608 super::hash_value(&mut h_float, &float_value);
609 assert_eq!(
610 h_int.finalize(),
611 h_float.finalize(),
612 "integer-valued numbers must canonicalize regardless of source representation"
613 );
614 }
615
616 #[test]
617 fn fractional_floats_do_not_canonicalize_to_int() {
618 // Floats with fractional parts must remain floats — collapsing
619 // 100.5 into 100 would hide real differences.
620 let int_value = json!({"x": 100_i64});
621 let frac_value = json!({"x": 100.5_f64});
622
623 let mut h_int = sha2::Sha256::new();
624 super::hash_value(&mut h_int, &int_value);
625 let mut h_frac = sha2::Sha256::new();
626 super::hash_value(&mut h_frac, &frac_value);
627 assert_ne!(
628 h_int.finalize(),
629 h_frac.finalize(),
630 "100 and 100.5 must hash differently"
631 );
632 }
633
634 #[test]
635 fn same_object_with_different_insertion_order_hashes_same() {
636 // serde_json::Map is BTreeMap by default but we sort defensively
637 // in case `preserve_order` is enabled by some transitive dep.
638 let mut m1 = serde_json::Map::new();
639 m1.insert("a".into(), json!(1));
640 m1.insert("b".into(), json!(2));
641 m1.insert("c".into(), json!(3));
642 let mut m2 = serde_json::Map::new();
643 m2.insert("c".into(), json!(3));
644 m2.insert("a".into(), json!(1));
645 m2.insert("b".into(), json!(2));
646
647 let mut h1 = sha2::Sha256::new();
648 super::hash_value(&mut h1, &Value::Object(m1));
649 let mut h2 = sha2::Sha256::new();
650 super::hash_value(&mut h2, &Value::Object(m2));
651 assert_eq!(h1.finalize(), h2.finalize());
652 }
653
654 #[test]
655 fn type_tagged_framing_distinguishes_string_from_number() {
656 // The string "42" must not collide with the number 42.
657 let str_v = json!("42");
658 let num_v = json!(42);
659 let mut h_str = sha2::Sha256::new();
660 super::hash_value(&mut h_str, &str_v);
661 let mut h_num = sha2::Sha256::new();
662 super::hash_value(&mut h_num, &num_v);
663 assert_ne!(h_str.finalize(), h_num.finalize());
664
665 // Bool true must not collide with the number 1.
666 let bool_v = json!(true);
667 let one_v = json!(1);
668 let mut h_bool = sha2::Sha256::new();
669 super::hash_value(&mut h_bool, &bool_v);
670 let mut h_one = sha2::Sha256::new();
671 super::hash_value(&mut h_one, &one_v);
672 assert_ne!(h_bool.finalize(), h_one.finalize());
673 }
674
675 #[test]
676 fn empty_store_has_a_well_defined_hash() {
677 let s1 = MemoryStore::new();
678 let s2 = MemoryStore::new();
679 assert_eq!(s1.hash_state().unwrap(), s2.hash_state().unwrap());
680 // The empty hash is the SHA-256 of an empty input — fix the
681 // expected bytes so an accidental framing change in `hash_state`
682 // can't silently sail through.
683 let expected = hex_decode(
684 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
685 );
686 assert_eq!(s1.hash_state().unwrap().to_vec(), expected);
687 }
688
689 fn hex_decode(s: &str) -> Vec<u8> {
690 (0..s.len())
691 .step_by(2)
692 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex"))
693 .collect()
694 }
695}