Skip to main content

mlua_swarm/store/
enhance_setting.rs

1//! `EnhanceSettingStore` — a key-value store for `EnhanceSetting`.
2//!
3//! v0.10.0 replaced the old versioned `EnhanceConfigStore` (with
4//! `read_head` / `write_new` / `history`) with a plain CRUD shape.
5//! `EnhanceSetting` no longer carries a version of its own — Blueprint
6//! version management runs on a separate path that commits the embedded
7//! `EnhanceSetting.blueprint` to `BlueprintStore` (carry).
8//!
9//! Only an in-memory implementation ships today; a Git2 backend is a
10//! carry for a future turn.
11
12use crate::enhance::setting::EnhanceSetting;
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::sync::Mutex;
17use thiserror::Error;
18
19/// Identifier — `the server` is expected to use `"default"`.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21pub struct EnhanceSettingId(pub String);
22
23impl EnhanceSettingId {
24    /// Wrap an arbitrary string as an id.
25    pub fn new(s: impl Into<String>) -> Self {
26        Self(s.into())
27    }
28
29    /// The id used by the server's single default setting: `"default"`.
30    pub fn default_id() -> Self {
31        Self("default".into())
32    }
33
34    /// Borrow the inner string.
35    pub fn as_str(&self) -> &str {
36        &self.0
37    }
38}
39
40impl std::fmt::Display for EnhanceSettingId {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.write_str(&self.0)
43    }
44}
45
46/// Errors surfaced by an [`EnhanceSettingStore`] implementation.
47#[derive(Debug, Error)]
48pub enum EnhanceSettingStoreError {
49    /// No setting exists for the given id.
50    #[error("not found: {0}")]
51    NotFound(EnhanceSettingId),
52}
53
54/// CRUD persistence interface for [`EnhanceSetting`].
55#[async_trait]
56pub trait EnhanceSettingStore: Send + Sync {
57    /// Backend name — for diagnostics/logging.
58    fn name(&self) -> &str;
59
60    /// Fetch a setting by id.
61    async fn get(&self, id: &EnhanceSettingId) -> Result<EnhanceSetting, EnhanceSettingStoreError>;
62
63    /// Insert or overwrite the setting for `id`.
64    async fn put(
65        &self,
66        id: &EnhanceSettingId,
67        setting: EnhanceSetting,
68    ) -> Result<(), EnhanceSettingStoreError>;
69
70    /// Remove the setting for `id`. Returns `NotFound` if absent.
71    async fn delete(&self, id: &EnhanceSettingId) -> Result<(), EnhanceSettingStoreError>;
72
73    /// List every stored setting id.
74    async fn list(&self) -> Result<Vec<EnhanceSettingId>, EnhanceSettingStoreError>;
75}
76
77/// Process-volatile [`EnhanceSettingStore`] backed by a `HashMap`. The
78/// only backend that ships today; a Git2 backend is a future carry.
79#[derive(Default)]
80pub struct InMemoryEnhanceSettingStore {
81    inner: Mutex<HashMap<EnhanceSettingId, EnhanceSetting>>,
82}
83
84impl InMemoryEnhanceSettingStore {
85    /// Create an empty store.
86    pub fn new() -> Self {
87        Self::default()
88    }
89}
90
91#[async_trait]
92impl EnhanceSettingStore for InMemoryEnhanceSettingStore {
93    fn name(&self) -> &str {
94        "in-memory"
95    }
96
97    async fn get(&self, id: &EnhanceSettingId) -> Result<EnhanceSetting, EnhanceSettingStoreError> {
98        self.inner
99            .lock()
100            .unwrap()
101            .get(id)
102            .cloned()
103            .ok_or_else(|| EnhanceSettingStoreError::NotFound(id.clone()))
104    }
105
106    async fn put(
107        &self,
108        id: &EnhanceSettingId,
109        setting: EnhanceSetting,
110    ) -> Result<(), EnhanceSettingStoreError> {
111        self.inner.lock().unwrap().insert(id.clone(), setting);
112        Ok(())
113    }
114
115    async fn delete(&self, id: &EnhanceSettingId) -> Result<(), EnhanceSettingStoreError> {
116        if self.inner.lock().unwrap().remove(id).is_none() {
117            return Err(EnhanceSettingStoreError::NotFound(id.clone()));
118        }
119        Ok(())
120    }
121
122    async fn list(&self) -> Result<Vec<EnhanceSettingId>, EnhanceSettingStoreError> {
123        Ok(self.inner.lock().unwrap().keys().cloned().collect())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::application::VersionSelector;
131    use crate::blueprint::store::BlueprintId;
132    use crate::enhance::setting::EnhanceSettingMeta;
133
134    fn dummy_setting(id: &str, bp: &str) -> EnhanceSetting {
135        EnhanceSetting {
136            id: id.into(),
137            blueprint_id: BlueprintId::new(bp.to_string()),
138            ttl_secs: 10,
139            version: VersionSelector::default(),
140            verifier_axes: vec!["des".into()],
141            meta: EnhanceSettingMeta::default(),
142        }
143    }
144
145    #[test]
146    fn enhance_setting_id_default_is_default_literal() {
147        assert_eq!(EnhanceSettingId::default_id().as_str(), "default");
148    }
149
150    #[test]
151    fn enhance_setting_id_display_is_inner_string() {
152        let id = EnhanceSettingId::new("foo");
153        assert_eq!(format!("{id}"), "foo");
154    }
155
156    #[tokio::test]
157    async fn inmemory_put_then_get_returns_same_setting() {
158        let store = InMemoryEnhanceSettingStore::new();
159        let id = EnhanceSettingId::new("s1");
160        let s = dummy_setting("s1", "bp-1");
161        store.put(&id, s.clone()).await.unwrap();
162        let got = store.get(&id).await.unwrap();
163        assert_eq!(got.id, "s1");
164        assert_eq!(got.blueprint_id.as_str(), "bp-1");
165    }
166
167    #[tokio::test]
168    async fn inmemory_get_missing_returns_not_found() {
169        let store = InMemoryEnhanceSettingStore::new();
170        let err = store.get(&EnhanceSettingId::new("nope")).await.unwrap_err();
171        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
172    }
173
174    #[tokio::test]
175    async fn inmemory_delete_missing_returns_not_found() {
176        let store = InMemoryEnhanceSettingStore::new();
177        let err = store
178            .delete(&EnhanceSettingId::new("nope"))
179            .await
180            .unwrap_err();
181        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
182    }
183
184    #[tokio::test]
185    async fn inmemory_put_then_delete_then_get_is_not_found() {
186        let store = InMemoryEnhanceSettingStore::new();
187        let id = EnhanceSettingId::new("s2");
188        store.put(&id, dummy_setting("s2", "bp-x")).await.unwrap();
189        store.delete(&id).await.unwrap();
190        assert!(matches!(
191            store.get(&id).await.unwrap_err(),
192            EnhanceSettingStoreError::NotFound(_)
193        ));
194    }
195
196    #[tokio::test]
197    async fn inmemory_list_returns_all_inserted_ids() {
198        let store = InMemoryEnhanceSettingStore::new();
199        store
200            .put(&EnhanceSettingId::new("a"), dummy_setting("a", "bp-a"))
201            .await
202            .unwrap();
203        store
204            .put(&EnhanceSettingId::new("b"), dummy_setting("b", "bp-b"))
205            .await
206            .unwrap();
207        let mut ids: Vec<String> = store
208            .list()
209            .await
210            .unwrap()
211            .into_iter()
212            .map(|i| i.0)
213            .collect();
214        ids.sort();
215        assert_eq!(ids, vec!["a", "b"]);
216    }
217
218    #[tokio::test]
219    async fn inmemory_put_overwrites_existing_setting() {
220        let store = InMemoryEnhanceSettingStore::new();
221        let id = EnhanceSettingId::new("s3");
222        store.put(&id, dummy_setting("s3", "bp-old")).await.unwrap();
223        store.put(&id, dummy_setting("s3", "bp-new")).await.unwrap();
224        let got = store.get(&id).await.unwrap();
225        assert_eq!(got.blueprint_id.as_str(), "bp-new");
226    }
227
228    #[tokio::test]
229    async fn inmemory_name_is_in_memory() {
230        let store = InMemoryEnhanceSettingStore::new();
231        assert_eq!(store.name(), "in-memory");
232    }
233}