Skip to main content

bitwarden_state/persisted_value/
handle.rs

1//! Persisted value types for type-safe access to individual persisted values.
2
3use std::{marker::PhantomData, sync::Arc};
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use super::PersistedValueKey;
9use crate::{persist::Persist, sdk_managed::DatabaseError};
10
11/// Internal persisted value as stored in the SDK-managed database.
12///
13/// This type wraps a JSON value for flexible storage. Users should not work with
14/// this type directly - use the [`PersistedValue<K>`] handle via `StateClient::value()` instead,
15/// which provides type-safe access.
16#[doc(hidden)]
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct PersistedValueItem(pub(crate) serde_json::Value);
19
20crate::register_repository_item!(String => PersistedValueItem, "PersistedValue");
21
22#[doc(hidden)]
23#[async_trait::async_trait]
24pub trait PersistedValueTrait<T: Persist>: Send + Sync {
25    async fn get(&self) -> Result<Option<T>, PersistedValueError>;
26    async fn set(&self, value: T) -> Result<(), PersistedValueError>;
27    async fn remove(&self) -> Result<(), PersistedValueError>;
28}
29
30// A macro so `tracing` resolves `file!`/`line!` per call site; a function would collapse every
31// event onto its own line. Records the value name only, never the stored value.
32macro_rules! trace_failure {
33    ($operation:literal, $name:expr, $error:expr) => {
34        tracing::error!(
35            persisted_value = $name,
36            operation = $operation,
37            error = %$error,
38            "persisted value operation failed"
39        )
40    };
41}
42
43/// A handle to a single persisted value in storage.
44///
45/// This type provides async methods to get, update, and delete the persisted value.
46/// Obtained via `StateClient::value()`, or injected with `#[derive(FromClient)]`.
47///
48/// Every failed operation is logged with `tracing::error!`, so callers that discard an error
49/// don't need to log it themselves. `try_get` finding no value is not a failure.
50///
51/// # Example
52/// ```rust,ignore
53/// use bitwarden_state::register_persisted_value;
54///
55/// register_persisted_value!(THEME: String = "theme");
56///
57/// let value = client.platform().state().value::<THEME>();
58///
59/// // Get the current value
60/// let current: Option<String> = value.try_get().await?;
61///
62/// // Update the value
63/// value.set("dark".to_string()).await?;
64///
65/// // Delete the value
66/// value.remove().await?;
67/// ```
68pub struct PersistedValue<K: PersistedValueKey> {
69    backend: Arc<dyn PersistedValueTrait<K::Value>>,
70    _marker: PhantomData<K>,
71}
72
73// Implemented by hand because `#[derive(Clone)]` would require `K: Clone`.
74impl<K: PersistedValueKey> Clone for PersistedValue<K> {
75    fn clone(&self) -> Self {
76        Self {
77            backend: self.backend.clone(),
78            _marker: PhantomData,
79        }
80    }
81}
82
83impl<K: PersistedValueKey> PersistedValue<K> {
84    /// Create a new persisted value handle from a backend.
85    ///
86    /// The backend is already bound to a single key, so it decides where the value is stored.
87    pub fn new(backend: Arc<dyn PersistedValueTrait<K::Value>>) -> Self {
88        Self {
89            backend,
90            _marker: PhantomData,
91        }
92    }
93
94    /// Get the current value, failing with [`PersistedValueError::ValueNotFound`] if it doesn't
95    /// exist in storage. Use [`Self::try_get`] where absence is an expected outcome.
96    pub async fn get(&self) -> Result<K::Value, PersistedValueError> {
97        self.backend
98            .get()
99            .await
100            .and_then(|value| value.ok_or(PersistedValueError::ValueNotFound(K::NAME)))
101            .inspect_err(|e| trace_failure!("get", K::NAME, e))
102    }
103
104    /// Get the current value.
105    ///
106    /// Returns `None` if the value doesn't exist in storage.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if deserialization fails, which may indicate:
111    /// - Schema evolution problems (type definition changed)
112    /// - Data corruption
113    /// - Type mismatch (two keys sharing a key name)
114    pub async fn try_get(&self) -> Result<Option<K::Value>, PersistedValueError> {
115        self.backend
116            .get()
117            .await
118            .inspect_err(|e| trace_failure!("try_get", K::NAME, e))
119    }
120
121    /// Update (or create) this persisted value with a new value.
122    pub async fn set(&self, value: K::Value) -> Result<(), PersistedValueError> {
123        self.backend
124            .set(value)
125            .await
126            .inspect_err(|e| trace_failure!("set", K::NAME, e))
127    }
128
129    /// Delete this persisted value from storage.
130    pub async fn remove(&self) -> Result<(), PersistedValueError> {
131        self.backend
132            .remove()
133            .await
134            .inspect_err(|e| trace_failure!("remove", K::NAME, e))
135    }
136}
137
138/// Errors that can occur when working with persisted values.
139#[derive(Debug, Error)]
140pub enum PersistedValueError {
141    /// The value has never been written.
142    #[error("Persisted value '{0}' not found")]
143    ValueNotFound(&'static str),
144    /// The database was closed by `StateRegistry::wipe()`.
145    #[error("Persisted value storage is closed")]
146    Closed,
147    /// Any other failure. Never contains the stored value.
148    #[error("{0}")]
149    Internal(String),
150}
151
152impl From<DatabaseError> for PersistedValueError {
153    fn from(e: DatabaseError) -> Self {
154        match e {
155            DatabaseError::Closed => Self::Closed,
156            other => Self::Internal(other.to_string()),
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::{register_persisted_value, registry::StateRegistry};
165
166    register_persisted_value!(THEME: String = "persisted_value_test_theme");
167
168    #[tokio::test]
169    async fn test_get_fails_when_absent() {
170        let value = StateRegistry::new_with_memory_db().value::<THEME>();
171
172        assert!(matches!(
173            value.get().await,
174            Err(PersistedValueError::ValueNotFound(
175                "persisted_value_test_theme"
176            ))
177        ));
178        assert_eq!(value.try_get().await.unwrap(), None);
179    }
180
181    #[tokio::test]
182    async fn test_deserialization_error_does_not_contain_stored_value() {
183        register_persisted_value!(AS_STRING: String = "persisted_value_test_aliased");
184        register_persisted_value!(AS_NUMBER: u32 = "persisted_value_test_aliased");
185
186        let registry = StateRegistry::new_with_memory_db();
187        registry
188            .value::<AS_STRING>()
189            .set("secret-token".to_string())
190            .await
191            .unwrap();
192
193        let error = registry.value::<AS_NUMBER>().get().await.unwrap_err();
194
195        assert!(matches!(error, PersistedValueError::Internal(_)));
196        assert!(!error.to_string().contains("secret-token"));
197    }
198}