Skip to main content

gglib_core/services/
settings_service.rs

1//! Settings service - orchestrates settings operations.
2
3use crate::ports::{CoreError, SettingsRepository};
4use crate::settings::{Settings, SettingsUpdate, validate_settings};
5use std::sync::Arc;
6
7/// Service for settings operations.
8pub struct SettingsService {
9    repo: Arc<dyn SettingsRepository>,
10}
11
12impl SettingsService {
13    /// Create a new settings service.
14    pub fn new(repo: Arc<dyn SettingsRepository>) -> Self {
15        Self { repo }
16    }
17
18    /// Return the underlying settings repository.
19    pub fn repo(&self) -> Arc<dyn SettingsRepository> {
20        Arc::clone(&self.repo)
21    }
22
23    /// Get current settings.
24    pub async fn get(&self) -> Result<Settings, CoreError> {
25        self.repo.load().await.map_err(CoreError::from)
26    }
27
28    /// Update settings with partial changes.
29    ///
30    /// The merge and the validation run on the settings as they stand when
31    /// the update is written, not as they stood at some earlier read: the
32    /// repository reads, applies and stores in one step
33    /// ([`SettingsRepository::modify`]). A field this update does not set is
34    /// left as its last writer left it, whichever process that was.
35    pub async fn update(&self, update: SettingsUpdate) -> Result<Settings, CoreError> {
36        self.repo
37            .modify(&|settings: &mut Settings| {
38                settings.merge(&update);
39                validate_settings(settings)
40            })
41            .await
42    }
43
44    /// Save complete settings (validates first).
45    pub async fn save(&self, settings: &Settings) -> Result<(), CoreError> {
46        validate_settings(settings)?;
47        self.repo.save(settings).await.map_err(CoreError::from)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::ports::RepositoryError;
55    use async_trait::async_trait;
56    use std::sync::Mutex;
57
58    struct MockSettingsRepo {
59        settings: Mutex<Settings>,
60    }
61
62    impl MockSettingsRepo {
63        fn new() -> Self {
64            Self {
65                settings: Mutex::new(Settings::with_defaults()),
66            }
67        }
68    }
69
70    #[async_trait]
71    impl SettingsRepository for MockSettingsRepo {
72        async fn load(&self) -> Result<Settings, RepositoryError> {
73            Ok(self.settings.lock().unwrap().clone())
74        }
75
76        async fn save(&self, settings: &Settings) -> Result<(), RepositoryError> {
77            *self.settings.lock().unwrap() = settings.clone();
78            Ok(())
79        }
80    }
81
82    #[tokio::test]
83    async fn test_get_default_settings() {
84        let repo = Arc::new(MockSettingsRepo::new());
85        let service = SettingsService::new(repo);
86
87        let settings = service.get().await.unwrap();
88        // Unset, not the floor: nothing has chosen a global default here.
89        assert_eq!(settings.default_context_size, None);
90    }
91
92    #[tokio::test]
93    async fn test_update_settings() {
94        let repo = Arc::new(MockSettingsRepo::new());
95        let service = SettingsService::new(repo);
96
97        let update = SettingsUpdate {
98            default_context_size: Some(Some(8192)),
99            ..Default::default()
100        };
101
102        let updated = service.update(update).await.unwrap();
103        assert_eq!(updated.default_context_size, Some(8192));
104
105        // Verify persisted
106        let fetched = service.get().await.unwrap();
107        assert_eq!(fetched.default_context_size, Some(8192));
108    }
109
110    /// An update that fails validation stores nothing, not even the part of
111    /// it that was valid.
112    #[tokio::test]
113    async fn an_update_that_fails_validation_stores_nothing() {
114        let repo = Arc::new(MockSettingsRepo::new());
115        let service = SettingsService::new(repo);
116
117        let refused = service
118            .update(SettingsUpdate {
119                proxy_port: Some(Some(9191)),
120                default_context_size: Some(Some(1)),
121                ..Default::default()
122            })
123            .await;
124
125        assert!(
126            matches!(refused, Err(CoreError::Settings(_))),
127            "{refused:?}"
128        );
129        assert_eq!(
130            service.get().await.unwrap().proxy_port,
131            Settings::with_defaults().proxy_port,
132            "the valid half of a refused update was not stored"
133        );
134    }
135}