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            spawner: None,
149            meta: EnhanceSettingMeta::default(),
150        }
151    }
152
153    #[test]
154    fn enhance_setting_id_default_is_default_literal() {
155        assert_eq!(EnhanceSettingId::default_id().as_str(), "default");
156    }
157
158    #[test]
159    fn enhance_setting_id_display_is_inner_string() {
160        let id = EnhanceSettingId::new("foo");
161        assert_eq!(format!("{id}"), "foo");
162    }
163
164    #[tokio::test]
165    async fn inmemory_put_then_get_returns_same_setting() {
166        let store = InMemoryEnhanceSettingStore::new();
167        let id = EnhanceSettingId::new("s1");
168        let s = dummy_setting("s1", "bp-1");
169        store.put(&id, s.clone()).await.unwrap();
170        let got = store.get(&id).await.unwrap();
171        assert_eq!(got.id, "s1");
172        assert_eq!(got.blueprint_id.as_str(), "bp-1");
173    }
174
175    #[tokio::test]
176    async fn inmemory_get_missing_returns_not_found() {
177        let store = InMemoryEnhanceSettingStore::new();
178        let err = store.get(&EnhanceSettingId::new("nope")).await.unwrap_err();
179        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
180    }
181
182    #[tokio::test]
183    async fn inmemory_delete_missing_returns_not_found() {
184        let store = InMemoryEnhanceSettingStore::new();
185        let err = store
186            .delete(&EnhanceSettingId::new("nope"))
187            .await
188            .unwrap_err();
189        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
190    }
191
192    #[tokio::test]
193    async fn inmemory_put_then_delete_then_get_is_not_found() {
194        let store = InMemoryEnhanceSettingStore::new();
195        let id = EnhanceSettingId::new("s2");
196        store.put(&id, dummy_setting("s2", "bp-x")).await.unwrap();
197        store.delete(&id).await.unwrap();
198        assert!(matches!(
199            store.get(&id).await.unwrap_err(),
200            EnhanceSettingStoreError::NotFound(_)
201        ));
202    }
203
204    #[tokio::test]
205    async fn inmemory_list_returns_all_inserted_ids() {
206        let store = InMemoryEnhanceSettingStore::new();
207        store
208            .put(&EnhanceSettingId::new("a"), dummy_setting("a", "bp-a"))
209            .await
210            .unwrap();
211        store
212            .put(&EnhanceSettingId::new("b"), dummy_setting("b", "bp-b"))
213            .await
214            .unwrap();
215        let mut ids: Vec<String> = store
216            .list()
217            .await
218            .unwrap()
219            .into_iter()
220            .map(|i| i.0)
221            .collect();
222        ids.sort();
223        assert_eq!(ids, vec!["a", "b"]);
224    }
225
226    #[tokio::test]
227    async fn inmemory_put_overwrites_existing_setting() {
228        let store = InMemoryEnhanceSettingStore::new();
229        let id = EnhanceSettingId::new("s3");
230        store.put(&id, dummy_setting("s3", "bp-old")).await.unwrap();
231        store.put(&id, dummy_setting("s3", "bp-new")).await.unwrap();
232        let got = store.get(&id).await.unwrap();
233        assert_eq!(got.blueprint_id.as_str(), "bp-new");
234    }
235
236    #[tokio::test]
237    async fn inmemory_name_is_in_memory() {
238        let store = InMemoryEnhanceSettingStore::new();
239        assert_eq!(store.name(), "in-memory");
240    }
241}