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}