arora_simple_data_store/namespaced.rs
1//! A [`DataStore`] wrapper that namespaces every key under a common prefix.
2//!
3//! One mutualized store can be shared across many runtimes (a device's HAL,
4//! bridge, and behavior writes), each runtime seeing a *device-relative* view of
5//! the keys while the storage lives under a per-device prefix in the single
6//! shared store. [`NamespacedStore`] is that view: it transparently rewrites a
7//! device-relative key `joint1.position` to `<namespace>/joint1.position` before
8//! delegating to the inner store, so two namespaces over one shared inner store
9//! never collide.
10//!
11//! It does NOT own the storage: it wraps a shared inner store (e.g. a cloned
12//! [`SimpleDataStore`], whose clones share storage) and is `Send + Sync`, so it
13//! can be handed to a device via `Arora::builder().with_data_store(..)` as
14//! `Arc<dyn DataStore>`.
15
16use std::sync::Arc;
17
18use arora_types::data::{DataError, DataStore, Key, Slot, State, StateChange, Subscription};
19use arora_types::value::Value;
20
21/// A [`DataStore`] view that prefixes every key with `<namespace>/` before
22/// delegating to an inner, shared store.
23///
24/// The inner store is held as `Arc<dyn DataStore>`, so the same underlying
25/// storage can back several differently-namespaced views (and other,
26/// un-namespaced holders) at once.
27#[derive(Clone)]
28pub struct NamespacedStore {
29 inner: Arc<dyn DataStore>,
30 namespace: String,
31}
32
33impl NamespacedStore {
34 /// Wrap `inner`, prefixing every key with `<namespace>/`.
35 pub fn new(inner: Arc<dyn DataStore>, namespace: impl Into<String>) -> Self {
36 Self {
37 inner,
38 namespace: namespace.into(),
39 }
40 }
41
42 /// The namespace this view prefixes keys with.
43 pub fn namespace(&self) -> &str {
44 &self.namespace
45 }
46
47 /// Rewrite a device-relative key into its namespaced form
48 /// (`<namespace>/<key_path>`).
49 fn prefixed(&self, key: &Key) -> Key {
50 Key::from(format!("{}/{}", self.namespace, key.path))
51 }
52}
53
54impl DataStore for NamespacedStore {
55 fn read(&self, keys: &[Key]) -> Vec<Option<Value>> {
56 let prefixed: Vec<Key> = keys.iter().map(|k| self.prefixed(k)).collect();
57 // The inner store preserves order, so values line up with `keys`.
58 self.inner.read(&prefixed)
59 }
60
61 fn write(&self, changes: StateChange) -> Result<(), DataError> {
62 let set = changes
63 .set
64 .iter()
65 .map(|(k, v)| (self.prefixed(k), v.clone()))
66 .collect();
67 let unset = changes.unset.iter().map(|k| self.prefixed(k)).collect();
68 self.inner.write(StateChange { set, unset })
69 }
70
71 fn slot(&self, key: &Key) -> Box<dyn Slot> {
72 self.inner.slot(&self.prefixed(key))
73 }
74
75 /// Delegates to the inner store as-is — the snapshot carries the **full,
76 /// namespaced** keys, not the device-relative ones.
77 ///
78 /// NOTE: not yet prefix-filtered or stripped. A namespaced view's snapshot
79 /// returns the entire shared store (every namespace), with full keys. This
80 /// is a later refinement; for now it is fine because the runtime only drains
81 /// [`subscribe`](DataStore::subscribe) to flush changes outward and the
82 /// device's HAL/bridge are fakes.
83 fn snapshot(&self) -> State {
84 self.inner.snapshot()
85 }
86
87 /// Delegates to the inner store as-is — the feed carries the **full,
88 /// namespaced** keys, not the device-relative ones, and is **not**
89 /// filtered to this namespace.
90 ///
91 /// NOTE: not yet prefix-filtered or stripped (see [`snapshot`](Self::snapshot)).
92 /// A later refinement; fine for now because the runtime only drains this to
93 /// flush changes outward (already-namespaced keys are what the shared feed
94 /// wants) and the device's HAL/bridge are fakes.
95 fn subscribe(&self) -> Subscription {
96 self.inner.subscribe()
97 }
98
99 fn clone_box(&self) -> Box<dyn DataStore> {
100 Box::new(self.clone())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::SimpleDataStore;
108
109 #[test]
110 fn write_lands_under_namespace_in_inner_store() {
111 let shared = SimpleDataStore::new();
112 let view = NamespacedStore::new(Arc::new(shared.clone()), "robotA");
113
114 // A device-relative write through the view…
115 view.write(StateChange::set("joint1.position", Value::Boolean(true)))
116 .unwrap();
117
118 // …lands prefixed in the inner store.
119 assert_eq!(
120 shared.read(&[Key::from("robotA/joint1.position")]),
121 vec![Some(Value::Boolean(true))]
122 );
123 // The un-prefixed key is NOT present in the inner store.
124 assert_eq!(shared.read(&[Key::from("joint1.position")]), vec![None]);
125 }
126
127 #[test]
128 fn read_round_trips_device_relative_keys() {
129 let shared = SimpleDataStore::new();
130 let view = NamespacedStore::new(Arc::new(shared.clone()), "robotA");
131
132 view.write(StateChange::set("battery_level", Value::Boolean(false)))
133 .unwrap();
134
135 // Reading the device-relative key through the view returns the value.
136 assert_eq!(
137 view.read(&[Key::from("battery_level")]),
138 vec![Some(Value::Boolean(false))]
139 );
140 }
141
142 #[test]
143 fn slot_round_trips_and_coincides_with_inner() {
144 let shared = SimpleDataStore::new();
145 let view = NamespacedStore::new(Arc::new(shared.clone()), "robotA");
146
147 // Write through a device-relative slot…
148 let slot = view.slot(&Key::from("joint1.position"));
149 slot.set(Some(Value::Boolean(true))).unwrap();
150
151 // …reads back through the view's slot…
152 assert_eq!(slot.get(), Some(Value::Boolean(true)));
153 // …and lands prefixed in the inner store.
154 assert_eq!(
155 shared.read(&[Key::from("robotA/joint1.position")]),
156 vec![Some(Value::Boolean(true))]
157 );
158 }
159
160 #[test]
161 fn two_namespaces_over_one_inner_store_do_not_collide() {
162 let shared = SimpleDataStore::new();
163 let a = NamespacedStore::new(Arc::new(shared.clone()), "robotA");
164 let b = NamespacedStore::new(Arc::new(shared.clone()), "robotB");
165
166 // Both write the SAME device-relative key…
167 a.write(StateChange::set("joint1.position", Value::Boolean(true)))
168 .unwrap();
169 b.write(StateChange::set("joint1.position", Value::Boolean(false)))
170 .unwrap();
171
172 // …but each lands under its own prefix, so they don't clobber.
173 assert_eq!(
174 a.read(&[Key::from("joint1.position")]),
175 vec![Some(Value::Boolean(true))]
176 );
177 assert_eq!(
178 b.read(&[Key::from("joint1.position")]),
179 vec![Some(Value::Boolean(false))]
180 );
181 // And both prefixed keys coexist in the shared inner store.
182 assert_eq!(
183 shared.read(&[
184 Key::from("robotA/joint1.position"),
185 Key::from("robotB/joint1.position"),
186 ]),
187 vec![Some(Value::Boolean(true)), Some(Value::Boolean(false))]
188 );
189 }
190}