Skip to main content

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