Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/bitwarden-core/src/platform/state_client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::Arc;

use bitwarden_state::{
Key, Setting, SettingItem, SettingsError,
Key, Persist, Setting, SettingsError,
registry::StateRegistryError,
repository::{Repository, RepositoryItem},
};
Expand Down Expand Up @@ -61,8 +61,7 @@ impl StateClient {
/// # Ok(())
/// # }
/// ```
pub fn setting<T>(&self, key: Key<T>) -> Result<Setting<T>, SettingsError> {
let repository = self.client.internal.state_registry.get::<SettingItem>()?;
Ok(Setting::new(repository, key))
pub fn setting<T: Persist>(&self, key: Key<T>) -> Result<Setting<T>, SettingsError> {
Ok(self.client.internal.state_registry.setting(key)?)
}
}
7 changes: 2 additions & 5 deletions crates/bitwarden-send/src/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,14 @@ mod tests {
use bitwarden_api_api::apis::ApiClient;
use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
use bitwarden_state::SettingItem;
use bitwarden_test::MemoryRepository;
use bitwarden_test::{MemoryRepository, MemorySetting};
use uuid::uuid;

use super::*;
use crate::{AuthType, Send, SendId, SendTextView, SendType, SendView};

fn make_pending_setting() -> Setting<Vec<SendId>> {
let repository: Arc<dyn Repository<SettingItem>> =
Arc::new(MemoryRepository::<SettingItem>::default());
Setting::new(repository, PENDING_SEND_DELETIONS)
MemorySetting::create()
}

async fn make_store_with_send(
Expand Down
6 changes: 4 additions & 2 deletions crates/bitwarden-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ pub mod repository;
/// This module provides a registry for managing repositories of different types.
pub mod registry;

/// Type-safe settings repository for storing application configuration and state.
/// Type-safe settings API for storing application configuration and state.
pub mod settings;

pub(crate) mod any_map;
pub(crate) mod persist;
pub(crate) mod sdk_managed;

pub use persist::Persist;
pub use sdk_managed::{DatabaseConfiguration, DatabaseError};
pub use settings::{Key, Setting, SettingItem, SettingsError};
pub use settings::{Key, Setting, SettingItem, SettingTrait, SettingsError};
10 changes: 10 additions & 0 deletions crates/bitwarden-state/src/persist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use serde::{Serialize, de::DeserializeOwned};

/// A value that can be persisted to SDK-managed storage.
///
/// This exists purely as a shorthand for the bounds every stored value must satisfy, so they
/// don't have to be repeated at every use site. It carries no behavior and is implemented
/// automatically for any type that meets them.
pub trait Persist: Serialize + DeserializeOwned + Send + Sync + 'static {}

impl<T: Serialize + DeserializeOwned + Send + Sync + 'static> Persist for T {}
81 changes: 77 additions & 4 deletions crates/bitwarden-state/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use thiserror::Error;

use crate::{
any_map::AnyMap,
persist::Persist,
repository::{Repository, RepositoryItem, RepositoryMigrations},
sdk_managed::{Database, DatabaseConfiguration, DatabaseError, MemoryDatabase, SystemDatabase},
settings::{Key, Setting, SettingItem},
settings::{Key, Setting},
};

/// A registry that contains repositories for different types of items.
Expand Down Expand Up @@ -56,9 +57,8 @@ impl StateRegistry {
}

/// Get a handle to a setting by its type-safe key.
pub fn setting<T>(&self, key: Key<T>) -> Result<Setting<T>, StateRegistryError> {
let repo = self.get::<SettingItem>()?;
Ok(Setting::new(repo, key))
pub fn setting<T: Persist>(&self, key: Key<T>) -> Result<Setting<T>, StateRegistryError> {
Ok(Setting::new(self.database.get_setting::<T>(key.name)))
}

/// Registers a client-managed repository into the map, associating it with its type.
Expand Down Expand Up @@ -301,6 +301,79 @@ mod tests {
assert_eq!(setting.get().await.unwrap(), None::<String>);
}

#[tokio::test]
async fn test_settings_are_isolated_by_key() {
use crate::register_setting_key;
register_setting_key!(const THEME: String = "test_theme");
register_setting_key!(const LOCALE: String = "test_locale");

let registry = StateRegistry::new_with_memory_db();
let theme = registry.setting(THEME).unwrap();
let locale = registry.setting(LOCALE).unwrap();

theme.update("dark".to_string()).await.unwrap();
locale.update("en-US".to_string()).await.unwrap();

theme.delete().await.unwrap();
assert_eq!(theme.get().await.unwrap(), None::<String>);
assert_eq!(locale.get().await.unwrap(), Some("en-US".to_string()));
}

#[tokio::test]
async fn test_setting_is_stored_in_the_setting_table_as_bare_json() {
use crate::{register_setting_key, settings::SettingItem};

#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Config {
theme: String,
}
register_setting_key!(const CONFIG: Config = "test_config");

let registry = StateRegistry::new_with_memory_db();
let value = Config {
theme: "dark".to_string(),
};
let expected = serde_json::to_value(&value).unwrap();
registry
.setting(CONFIG)
.unwrap()
.update(value)
.await
.unwrap();

// Storage contract for existing databases: settings live in the `Setting` table,
// addressed by key name, holding the bare serialized value.
assert_eq!(SettingItem::NAME, "Setting");
let raw: SettingItem = registry
.database
.get::<SettingItem>("test_config")
.await
.unwrap()
.expect("setting is present");
assert_eq!(raw.0, expected);
}

#[tokio::test]
async fn test_setting_reports_closed_after_wipe() {
use crate::{register_setting_key, settings::SettingsError};
register_setting_key!(const TEST_SETTING: String = "test_wiped_setting");

let registry = StateRegistry::new_with_memory_db();
let setting = registry.setting(TEST_SETTING).unwrap();
setting.update("hello".to_string()).await.unwrap();

registry.wipe().await.unwrap();

assert!(matches!(
setting.get().await,
Err(SettingsError::Database(DatabaseError::Closed))
));
assert!(matches!(
setting.update("bye".to_string()).await,
Err(SettingsError::Database(DatabaseError::Closed))
));
}

/// The concrete implementation is erased by the coercion to `Arc<dyn Repository<T>>`, so two
/// implementations of the same item type share a slot and the later registration wins.
#[tokio::test]
Expand Down
10 changes: 4 additions & 6 deletions crates/bitwarden-state/src/repository.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use std::{any::TypeId, sync::Arc};

use serde::{Serialize, de::DeserializeOwned};

use crate::registry::StateRegistryError;
use crate::{persist::Persist, registry::StateRegistryError};

/// An error resulting from operations on a repository.
#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -72,9 +70,9 @@ pub trait Repository<V: RepositoryItem>: Send + Sync {
/// It should not be implemented manually; instead, users should
/// use the [crate::register_repository_item] macro to register their item types.
///
/// All repository items must implement `Serialize` and `DeserializeOwned` to support
/// SDK-managed repositories that persist items to storage.
pub trait RepositoryItem: Internal + Serialize + DeserializeOwned + Send + Sync + 'static {
/// The [`Persist`] bound is what makes an item storable by SDK-managed repositories. It is
/// satisfied automatically by any type that is serializable and thread-safe.
pub trait RepositoryItem: Internal + Persist {
/// The name of the type implementing this trait.
const NAME: &'static str;

Expand Down
40 changes: 39 additions & 1 deletion crates/bitwarden-state/src/sdk_managed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use std::sync::Arc;
use bitwarden_error::bitwarden_error;
use thiserror::Error;

use crate::repository::{Repository, RepositoryError, RepositoryItem, RepositoryMigrations};
use crate::{
persist::Persist,
repository::{Repository, RepositoryError, RepositoryItem, RepositoryMigrations},
settings::{SettingItem, SettingTrait, SettingsError},
};

mod configuration;
pub use configuration::DatabaseConfiguration;
Expand Down Expand Up @@ -242,11 +246,45 @@ impl<V: RepositoryItem> Repository<V> for DBRepository<V> {
}
}

/// Stores a single setting in the `Setting` table, keyed by `name`, as the bare serialized value.
struct DBSetting<T> {
database: SystemDatabase,
name: &'static str,
_marker: std::marker::PhantomData<T>,
}

#[async_trait::async_trait]
impl<T: Persist> SettingTrait<T> for DBSetting<T> {
async fn get(&self) -> Result<Option<T>, SettingsError> {
match self.database.get::<SettingItem>(self.name).await? {
Some(item) => Ok(Some(serde_json::from_value::<T>(item.0)?)),
None => Ok(None),
}
}

async fn set(&self, value: T) -> Result<(), SettingsError> {
let item = SettingItem(serde_json::to_value(&value)?);
Ok(self.database.set::<SettingItem>(self.name, item).await?)
}

async fn remove(&self) -> Result<(), SettingsError> {
Ok(self.database.remove::<SettingItem>(self.name).await?)
}
}

impl SystemDatabase {
pub(super) fn get_repository<V: RepositoryItem>(&self) -> Arc<dyn Repository<V>> {
Arc::new(DBRepository {
database: self.clone(),
_marker: std::marker::PhantomData,
})
}

pub(super) fn get_setting<T: Persist>(&self, name: &'static str) -> Arc<dyn SettingTrait<T>> {
Arc::new(DBSetting {
database: self.clone(),
name,
_marker: std::marker::PhantomData,
})
}
}
8 changes: 4 additions & 4 deletions crates/bitwarden-state/src/settings/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Type-safe settings repository for storing application configuration and state.
//! Type-safe settings API for storing application configuration and state.
//!
//! This module provides a type-safe key-value API for storing settings, backed by
//! the SDK's repository pattern.
//! This module provides a type-safe key-value API for storing settings. Each setting resolves to
//! its own backend, defaulting to the SDK-managed database.
//!
//! # Usage
//!
Expand Down Expand Up @@ -44,4 +44,4 @@ mod key;
mod setting;

pub use key::Key;
pub use setting::{Setting, SettingItem, SettingsError};
pub use setting::{Setting, SettingItem, SettingTrait, SettingsError};
62 changes: 25 additions & 37 deletions crates/bitwarden-state/src/settings/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ use std::sync::Arc;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use super::Key;
use crate::{
registry::StateRegistryError,
repository::{Repository, RepositoryError},
};
use crate::{persist::Persist, registry::StateRegistryError, sdk_managed::DatabaseError};

/// Internal setting value stored in the settings repository.
/// Internal setting value as stored in the SDK-managed database.
///
/// This type wraps a JSON value for flexible storage. Users should not work with
/// this type directly - use the [`Setting<T>`] handle via `StateClient::setting()` instead,
Expand All @@ -20,9 +16,16 @@ use crate::{
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SettingItem(pub(crate) serde_json::Value);

// Register SettingItem for repository usage
crate::register_repository_item!(String => SettingItem, "Setting");

#[doc(hidden)]
#[async_trait::async_trait]
pub trait SettingTrait<T: Persist>: Send + Sync {
async fn get(&self) -> Result<Option<T>, SettingsError>;
async fn set(&self, value: T) -> Result<(), SettingsError>;
async fn remove(&self) -> Result<(), SettingsError>;
}

/// A handle to a single setting value in storage.
///
/// This type provides async methods to get, update, and delete the setting value.
Expand All @@ -46,15 +49,16 @@ crate::register_repository_item!(String => SettingItem, "Setting");
/// setting.delete().await?;
/// ```
#[derive(Clone)]
pub struct Setting<T> {
repository: Arc<dyn Repository<SettingItem>>,
key: Key<T>,
pub struct Setting<T: Persist> {
backend: Arc<dyn SettingTrait<T>>,
}

impl<T> Setting<T> {
/// Create a new setting handle from a repository and key.
pub fn new(repository: Arc<dyn Repository<SettingItem>>, key: Key<T>) -> Self {
Self { repository, key }
impl<T: Persist> Setting<T> {
/// Create a new setting handle from a backend.
///
/// The backend is already bound to a single key, so it decides where the value is stored.
pub fn new(backend: Arc<dyn SettingTrait<T>>) -> Self {
Self { backend }
}

/// Get the current value of this setting.
Expand All @@ -67,34 +71,18 @@ impl<T> Setting<T> {
/// - Schema evolution problems (type definition changed)
/// - Data corruption
/// - Type mismatch (wrong `Key<T>` type for stored data)
pub async fn get(&self) -> Result<Option<T>, SettingsError>
where
T: for<'de> Deserialize<'de>,
{
match self.repository.get(self.key.name.to_string()).await? {
Some(item) => Ok(Some(serde_json::from_value::<T>(item.0)?)),
None => Ok(None),
}
pub async fn get(&self) -> Result<Option<T>, SettingsError> {
self.backend.get().await
}

/// Update (or create) this setting with a new value.
pub async fn update(&self, value: T) -> Result<(), SettingsError>
where
T: Serialize,
{
let json_value = serde_json::to_value(&value)?;
let item = SettingItem(json_value);

self.repository.set(self.key.name.to_string(), item).await?;

Ok(())
pub async fn update(&self, value: T) -> Result<(), SettingsError> {
self.backend.set(value).await
}

/// Delete this setting from storage.
pub async fn delete(&self) -> Result<(), SettingsError> {
self.repository.remove(self.key.name.to_string()).await?;

Ok(())
self.backend.remove().await
}
}

Expand All @@ -104,9 +92,9 @@ pub enum SettingsError {
/// Failed to serialize/deserialize setting value
#[error("Failed to serialize/deserialize setting: {0}")]
Json(#[from] serde_json::Error),
/// Repository operation failed
/// Database operation failed
#[error(transparent)]
Repository(#[from] RepositoryError),
Database(#[from] DatabaseError),
/// State registry operation failed
#[error(transparent)]
Registry(#[from] StateRegistryError),
Expand Down
Loading
Loading