Skip to main content

structfs_core_store/
conformance.rs

1//! Conformance checks for the StructFS store conventions.
2//!
3//! StructFS asserts several semantics by convention rather than by trait
4//! signature. This module makes them checkable: call these functions from
5//! a store's test suite to certify it instead of re-deriving the semantics
6//! by hand.
7//!
8//! Each check panics with a descriptive message on violation, so calling
9//! one from a `#[test]` is all a store's suite needs:
10//!
11//! ```rust
12//! use structfs_core_store::{conformance, MemoryStore};
13//!
14//! conformance::check_conventions(&mut MemoryStore::new());
15//! ```
16//!
17//! The checks write under top-level components prefixed `conformance_`;
18//! run them against a fresh store instance.
19
20use crate::{path, Path, Reader, Record, Store, Value};
21
22/// Run every convention check against a fresh store.
23pub fn check_conventions<S: Store>(store: &mut S) {
24    check_leaf_roundtrip(store);
25    check_missing_reads_none(store);
26    check_deep_write_creates_intermediates(store);
27    check_prefix_read_returns_children(store);
28    check_read_children(store);
29    check_null_write_deletes_subtree(store);
30    check_map_write_replaces_subtree(store);
31}
32
33fn read_value<S: Reader>(store: &mut S, path: &Path) -> Option<Value> {
34    store
35        .read(path)
36        .unwrap_or_else(|e| panic!("read {} failed: {}", path, e))
37        .map(|record| {
38            record
39                .as_value()
40                .unwrap_or_else(|| panic!("read {} returned a raw record", path))
41                .clone()
42        })
43}
44
45/// A leaf written at a path reads back equal.
46pub fn check_leaf_roundtrip<S: Store>(store: &mut S) {
47    let p = path!("conformance_leaf/value");
48    store
49        .write(&p, Record::parsed(Value::from("roundtrip")))
50        .expect("leaf write failed");
51    assert_eq!(
52        read_value(store, &p),
53        Some(Value::from("roundtrip")),
54        "leaf write/read did not roundtrip"
55    );
56}
57
58/// Reading a path that was never written returns `Ok(None)`.
59pub fn check_missing_reads_none<S: Store>(store: &mut S) {
60    assert_eq!(
61        read_value(store, &path!("conformance_missing/never_written")),
62        None,
63        "missing path must read as None, not an error or a value"
64    );
65}
66
67/// Writing a deep path creates intermediate maps.
68pub fn check_deep_write_creates_intermediates<S: Store>(store: &mut S) {
69    store
70        .write(
71            &path!("conformance_deep/a/b/c"),
72            Record::parsed(Value::from(1i64)),
73        )
74        .expect("deep write must create intermediate maps");
75    for prefix in [
76        "conformance_deep",
77        "conformance_deep/a",
78        "conformance_deep/a/b",
79    ] {
80        let p = Path::parse(prefix).unwrap();
81        assert!(
82            matches!(read_value(store, &p), Some(Value::Map(_))),
83            "intermediate {} must exist as a map after a deep write",
84            prefix
85        );
86    }
87}
88
89/// Reading at a prefix returns a `Value::Map` containing the children.
90pub fn check_prefix_read_returns_children<S: Store>(store: &mut S) {
91    store
92        .write(
93            &path!("conformance_tree/users/alice"),
94            Record::parsed(Value::from(1i64)),
95        )
96        .expect("write failed");
97    store
98        .write(
99            &path!("conformance_tree/users/bob"),
100            Record::parsed(Value::from(2i64)),
101        )
102        .expect("write failed");
103
104    match read_value(store, &path!("conformance_tree/users")) {
105        Some(Value::Map(map)) => {
106            assert!(
107                map.contains_key("alice") && map.contains_key("bob"),
108                "prefix read must include children as map keys, got: {:?}",
109                map.keys().collect::<Vec<_>>()
110            );
111        }
112        other => panic!(
113            "prefix read must return a map of children, got: {:?}",
114            other
115        ),
116    }
117}
118
119/// `read_children` enumerates direct children at a prefix.
120pub fn check_read_children<S: Store>(store: &mut S) {
121    store
122        .write(
123            &path!("conformance_children/x"),
124            Record::parsed(Value::from(1i64)),
125        )
126        .expect("write failed");
127    store
128        .write(
129            &path!("conformance_children/y"),
130            Record::parsed(Value::from(2i64)),
131        )
132        .expect("write failed");
133
134    let mut children = store
135        .read_children(&path!("conformance_children"))
136        .expect("read_children failed")
137        .expect("read_children must return Some at an existing prefix");
138    children.sort();
139    assert_eq!(
140        children,
141        vec!["x".to_string(), "y".to_string()],
142        "read_children must enumerate direct children"
143    );
144
145    assert_eq!(
146        store
147            .read_children(&path!("conformance_children_missing"))
148            .expect("read_children failed"),
149        None,
150        "read_children at a missing path must return None"
151    );
152}
153
154/// Writing `Value::Null` deletes the node and its entire subtree, without
155/// touching siblings whose names merely share a string prefix.
156pub fn check_null_write_deletes_subtree<S: Store>(store: &mut S) {
157    store
158        .write(
159            &path!("conformance_del/accounts/personal/key"),
160            Record::parsed(Value::from("secret")),
161        )
162        .expect("write failed");
163    store
164        .write(
165            &path!("conformance_del/accounts_other"),
166            Record::parsed(Value::from("survivor")),
167        )
168        .expect("write failed");
169
170    store
171        .write(
172            &path!("conformance_del/accounts"),
173            Record::parsed(Value::Null),
174        )
175        .expect("null write failed");
176
177    assert_eq!(
178        read_value(store, &path!("conformance_del/accounts")),
179        None,
180        "null write must delete the node"
181    );
182    assert_eq!(
183        read_value(store, &path!("conformance_del/accounts/personal/key")),
184        None,
185        "null write must delete the entire subtree"
186    );
187    assert_eq!(
188        read_value(store, &path!("conformance_del/accounts_other")),
189        Some(Value::from("survivor")),
190        "null write must be component-wise: string-prefix siblings survive"
191    );
192}
193
194/// Writing a `Value::Map` at a parent replaces the full state under that
195/// path; stale descendants do not survive.
196pub fn check_map_write_replaces_subtree<S: Store>(store: &mut S) {
197    store
198        .write(
199            &path!("conformance_replace/cfg/stale"),
200            Record::parsed(Value::from("old")),
201        )
202        .expect("write failed");
203
204    let mut fresh = std::collections::BTreeMap::new();
205    fresh.insert("fresh".to_string(), Value::from("new"));
206    store
207        .write(
208            &path!("conformance_replace/cfg"),
209            Record::parsed(Value::Map(fresh)),
210        )
211        .expect("map write failed");
212
213    assert_eq!(
214        read_value(store, &path!("conformance_replace/cfg/stale")),
215        None,
216        "map write at a parent must sweep stale descendants"
217    );
218    assert_eq!(
219        read_value(store, &path!("conformance_replace/cfg/fresh")),
220        Some(Value::from("new")),
221        "map write must install the new state"
222    );
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::MemoryStore;
229
230    #[test]
231    fn memory_store_is_conformant() {
232        check_conventions(&mut MemoryStore::new());
233    }
234}