Skip to main content

gglib_core/ports/
settings_repository.rs

1//! Settings repository trait definition.
2//!
3//! This port defines the interface for application settings persistence.
4//! Implementations handle all storage details internally.
5
6use async_trait::async_trait;
7
8use super::{CoreError, RepositoryError};
9use crate::settings::{Settings, SettingsError};
10
11/// A change to stored settings: given them as they stand, alter them in
12/// place or refuse. What [`SettingsRepository::modify`] applies, and free to
13/// borrow what it needs for `'a`.
14///
15/// A named alias rather than the type written out at each use, because a
16/// `&mut Settings` elided inside `#[async_trait]` is given a named lifetime,
17/// and the closure then no longer accepts a borrow of any lifetime.
18pub type SettingsChange<'a> = dyn Fn(&mut Settings) -> Result<(), SettingsError> + Send + Sync + 'a;
19
20/// Repository for application settings persistence.
21///
22/// This trait defines operations for storing and retrieving the application
23/// settings as a whole. The implementation handles serialization.
24///
25/// # Design Rules
26///
27/// - No `sqlx` types in signatures
28/// - Works with domain `Settings` type directly
29/// - Implementation handles JSON serialization internally
30#[async_trait]
31pub trait SettingsRepository: Send + Sync {
32    /// Load application settings.
33    ///
34    /// Returns default settings if none are stored.
35    async fn load(&self) -> Result<Settings, RepositoryError>;
36
37    /// Save application settings.
38    async fn save(&self, settings: &Settings) -> Result<(), RepositoryError>;
39
40    /// Read the stored settings, apply `change` to them, and store the
41    /// result, with no other write landing in between.
42    ///
43    /// A partial update needs this. Read, change and save as three calls,
44    /// and a write that lands between the read and the save is overwritten
45    /// by a record read before it. Settings are written by more than one
46    /// process — the daemon, and `gglib config settings set` in a terminal —
47    /// so no lock held inside one of them can close that window.
48    ///
49    /// The default makes exactly those three calls and holds nothing between
50    /// them. It is right only for a store no other writer shares, such as an
51    /// in-memory test double; a store another process writes overrides it.
52    ///
53    /// # Errors
54    ///
55    /// [`CoreError::Settings`] with whatever `change` refused, in which case
56    /// nothing is stored, or [`CoreError::Repository`] when the store fails.
57    async fn modify(&self, change: &SettingsChange<'_>) -> Result<Settings, CoreError> {
58        let mut settings = self.load().await?;
59        change(&mut settings)?;
60        self.save(&settings).await?;
61        Ok(settings)
62    }
63}