Skip to main content

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}