Skip to main content

pdk_data_storage_lib/
lib.rs

1// Copyright (c) 2026, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! Data Storage Library
6//!
7//! This library provides data storage functionality with support for:
8//!
9//! - Local and distributed data storage
10//! - Support for CAS (Compare-And-Swap) operations
11//! - Configurable storage modes (Always, Absent, CAS)
12//! - Asynchronous API for high-performance applications
13//!
14//! ## Features
15//! - `ll`: USE AT OWN RISK: low-level items that may change without notice. Exposes the underlying implementation of each storage type.
16//! - `experimental_datastorage_formats`: USE AT OWN RISK: experimental support for user-defined serialization formats.
17
18mod local;
19
20mod distributed;
21
22mod format;
23
24#[cfg(feature = "experimental_datastorage_formats")]
25pub use format::{Format, MessagePack};
26use std::marker::PhantomData;
27
28#[cfg(feature = "ll")]
29/// "Low Level" implementations of each storage type.
30pub mod ll {
31    /// In memory storage implementation.
32    pub mod local {
33        pub use crate::local::*;
34    }
35
36    /// External storage implementation.
37    pub mod distributed {
38        pub use crate::distributed::*;
39    }
40}
41
42use pdk_core::classy::extract::context::ConfigureContext;
43use pdk_core::classy::extract::{Extract, FromContext};
44use pdk_core::logger;
45use serde::{de::DeserializeOwned, Serialize};
46use std::rc::Rc;
47use thiserror::Error;
48use url::form_urlencoded;
49
50use crate::distributed::DistributedStorage;
51use crate::format::{DefaultFormat, FormatWrapper};
52use crate::local::LocalStorage;
53
54/// Defines the behavior for store operations.
55///
56/// This enum specifies how a store operation should behave when a key already exists
57/// in the storage system.
58#[derive(PartialEq, Eq, Debug, Clone)]
59pub enum StoreMode {
60    /// Indicates that the store operation should always succeed, overwriting any existing value.
61    ///
62    /// This mode is useful when you want to unconditionally update a value regardless
63    /// of its current state.
64    Always,
65    /// Indicates that the store operation should succeed only if no value was previously stored.
66    ///
67    /// This mode is useful for implementing "set if not exists" semantics, ensuring
68    /// that values are only written once.
69    Absent,
70    /// Indicates that the store operation should succeed only if the stored value matches the provided CAS.
71    ///
72    /// This mode is useful for implementing optimistic concurrency control, ensuring
73    /// that updates only succeed if the value hasn't been modified by another operation.
74    Cas(String),
75}
76
77/// Errors that can occur during data storage operations.
78///
79/// This enum represents all possible error conditions that can arise when
80/// performing data storage operations.
81#[derive(Debug, Error)]
82#[non_exhaustive]
83pub enum DataStorageError {
84    /// Indicates provided CAS value doesn't match current version of the stored value.
85    #[error("CAS mismatch.")]
86    CasMismatch,
87    #[error("Serialization error: {0}.")]
88    #[cfg(feature = "experimental_datastorage_formats")]
89    /// Indicates the requested operation failed due to a serialization error.
90    Format(Box<dyn std::error::Error>),
91    #[error("Serialization error: {0}.")]
92    /// Indicates the requested operation failed due to a serialization error.
93    Serialization(#[from] serde_fixint::Error),
94    /// Indicates the requested operation failed due to a CAS parse error.
95    #[error("CAS parse error: {0}.")]
96    CasParseError(#[from] std::num::ParseIntError),
97    /// Indicates the operation timed out.
98    #[error("Timeout.")]
99    Timeout,
100    /// Indicates an error occurred while performing an http client call.
101    #[error("HTTP Client Error.")]
102    HttpClient,
103    /// Indicates an unexpected error occurred.
104    #[error("Unexpected error: {0}.")]
105    Unexpected(String),
106}
107
108/// Errors that can occur when building data storage instances.
109///
110/// This enum represents error conditions that can arise when creating
111/// data storage instances using the builder pattern.
112#[derive(Debug, Error)]
113#[non_exhaustive]
114pub enum DataStorageBuilderError {
115    /// Local storage is required but not available in the current context.
116    #[error("Local storage not available")]
117    LocalStorageRequired,
118
119    /// Policy metadata is required but not available in the current context.
120    #[error("Policy metadata not available")]
121    MetadataRequired,
122}
123
124impl From<crate::local::LocalStorageError> for DataStorageError {
125    fn from(error: crate::local::LocalStorageError) -> Self {
126        match error {
127            crate::local::LocalStorageError::CasMismatch => DataStorageError::CasMismatch,
128            _ => DataStorageError::Unexpected(error.to_string()),
129        }
130    }
131}
132
133impl From<crate::distributed::DistributedStorageError> for DataStorageError {
134    fn from(error: crate::distributed::DistributedStorageError) -> Self {
135        match error {
136            crate::distributed::DistributedStorageError::CasMismatch => {
137                DataStorageError::CasMismatch
138            }
139            crate::distributed::DistributedStorageError::Timeout => DataStorageError::Timeout,
140            crate::distributed::DistributedStorageError::HttpClient(_) => {
141                DataStorageError::HttpClient
142            }
143            error => DataStorageError::Unexpected(error.to_string()),
144        }
145    }
146}
147
148/// A trait for data storage operations that can be implemented by different storage backends.
149///
150/// This trait defines the core interface for data storage operations, allowing
151/// implementations to use different storage backends (local, distributed) while
152/// providing a consistent API.
153#[allow(async_fn_in_trait)]
154pub trait DataStorage {
155    /// Returns all keys currently stored in the store.
156    async fn get_keys(&self) -> Result<Vec<String>, DataStorageError>;
157
158    /// Stores a serializable item for a given key using the provided [`StoreMode`].
159    async fn store<T: Serialize>(
160        &self,
161        key: &str,
162        mode: &StoreMode,
163        item: &T,
164    ) -> Result<(), DataStorageError>;
165
166    /// Retrieves and deserializes the value for a given key, returning the value and its CAS
167    /// string if present. Returns `Ok(None)` when the key does not exist.
168    async fn get<T: DeserializeOwned>(
169        &self,
170        key: &str,
171    ) -> Result<Option<(T, String)>, DataStorageError>;
172
173    /// Removes the item identified by the provided key from this storage instance.
174    async fn delete(&self, key: &str) -> Result<(), DataStorageError>;
175
176    /// Removes all items from this storage instance.
177    async fn delete_all(&self) -> Result<(), DataStorageError>;
178}
179
180/// A local data storage implementation that stores data in memory.
181///
182/// This implementation uses local shared data storage for high-performance
183/// in-memory operations within a single node.
184pub struct LocalDataStorage<F: format::Format = format::DefaultFormat> {
185    storage: crate::local::SharedData,
186    namespace: String,
187    format: FormatWrapper<F>,
188}
189
190impl<F: format::Format> LocalDataStorage<F> {
191    /// Creates a new local data storage instance using the give storage and namespace as unique identifier.
192    pub(crate) fn new(
193        storage: crate::local::SharedData,
194        namespace: String,
195        format: FormatWrapper<F>,
196    ) -> Self {
197        Self {
198            storage,
199            namespace,
200            format,
201        }
202    }
203
204    fn convert_store_mode(
205        &self,
206        mode: &StoreMode,
207    ) -> Result<crate::local::StoreMode, DataStorageError> {
208        match mode {
209            StoreMode::Always => Ok(crate::local::StoreMode::Always),
210            StoreMode::Absent => Ok(crate::local::StoreMode::Absent),
211            StoreMode::Cas(cas_str) => {
212                let cas: u32 = cas_str.parse()?;
213                Ok(crate::local::StoreMode::Cas(cas))
214            }
215        }
216    }
217
218    fn namespaced_key(&self, key: &str) -> String {
219        format!("{}:{}", self.namespace, key)
220    }
221}
222
223impl<F: format::Format> DataStorage for LocalDataStorage<F> {
224    async fn get_keys(&self) -> Result<Vec<String>, DataStorageError> {
225        let all_keys = self.storage.keys();
226        let namespace_prefix = format!("{}:", self.namespace);
227
228        // Filter keys to only include those that belong to this namespace
229        let filtered_keys: Vec<String> = all_keys
230            .into_iter()
231            .filter(|key| key.starts_with(&namespace_prefix))
232            .map(|key| {
233                // Remove the namespace prefix to return just the key name
234                key.strip_prefix(&namespace_prefix)
235                    .unwrap_or(&key)
236                    .to_string()
237            })
238            .collect();
239
240        Ok(filtered_keys)
241    }
242
243    async fn store<T: Serialize>(
244        &self,
245        key: &str,
246        mode: &StoreMode,
247        item: &T,
248    ) -> Result<(), DataStorageError> {
249        let serialized = self.format.serialize(item)?;
250        let local_mode = self.convert_store_mode(mode)?;
251        let namespaced_key = self.namespaced_key(key);
252        self.storage.set(&namespaced_key, &serialized, local_mode)?;
253        Ok(())
254    }
255
256    async fn get<T: DeserializeOwned>(
257        &self,
258        key: &str,
259    ) -> Result<Option<(T, String)>, DataStorageError> {
260        let namespaced_key = self.namespaced_key(key);
261        match self.storage.get(&namespaced_key)? {
262            Some((data, cas)) => {
263                let deserialized: T = self.format.deserialize(&data)?;
264                Ok(Some((deserialized, cas.to_string())))
265            }
266            None => Ok(None),
267        }
268    }
269
270    async fn delete(&self, key: &str) -> Result<(), DataStorageError> {
271        let namespaced_key = self.namespaced_key(key);
272        self.storage.delete(&namespaced_key)?;
273        Ok(())
274    }
275
276    async fn delete_all(&self) -> Result<(), DataStorageError> {
277        let all_keys = self.storage.keys();
278        let namespace_prefix = format!("{}:", self.namespace);
279
280        // Only delete keys that belong to this namespace
281        for key in all_keys {
282            if key.starts_with(&namespace_prefix) {
283                self.storage.delete(&key)?;
284            }
285        }
286        Ok(())
287    }
288}
289
290/// A distributed data storage implementation that stores data across multiple nodes.
291pub struct RemoteDataStorage<F: format::Format = format::DefaultFormat> {
292    storage: Rc<crate::distributed::DistributedStorageClient>,
293    sanitized_store: String,
294    sanitized_partition: String,
295    ttl_millis: u32,
296    format: FormatWrapper<F>,
297}
298
299impl<F: format::Format> RemoteDataStorage<F> {
300    /// Creates a high level data storage instance for the given `store`/`partition`,
301    /// using `storage` and a default TTL in milliseconds.
302    pub(crate) fn new(
303        storage: Rc<crate::distributed::DistributedStorageClient>,
304        store: String,
305        partition: String,
306        ttl_millis: u32,
307        format: FormatWrapper<F>,
308    ) -> Self {
309        // Sanitize store and partition names using form_urlencoded
310        let sanitized_store = form_urlencoded::byte_serialize(store.as_bytes()).collect();
311        let sanitized_partition = form_urlencoded::byte_serialize(partition.as_bytes()).collect();
312        Self {
313            storage,
314            sanitized_store,
315            sanitized_partition,
316            ttl_millis,
317            format,
318        }
319    }
320
321    fn convert_store_mode(&self, mode: &StoreMode) -> crate::distributed::StoreMode {
322        match mode {
323            StoreMode::Always => crate::distributed::StoreMode::Always,
324            StoreMode::Absent => crate::distributed::StoreMode::Absent,
325            StoreMode::Cas(cas_str) => crate::distributed::StoreMode::Cas(cas_str.clone()),
326        }
327    }
328
329    fn sanitize_key(&self, key: &str) -> String {
330        form_urlencoded::byte_serialize(key.as_bytes()).collect()
331    }
332}
333
334impl<F: format::Format> DataStorage for RemoteDataStorage<F> {
335    async fn get_keys(&self) -> Result<Vec<String>, DataStorageError> {
336        // Try to get keys, if store doesn't exist, return empty list
337        match self
338            .storage
339            .get_keys(&self.sanitized_store, &self.sanitized_partition)
340            .await
341        {
342            Ok(keys) => {
343                // Decode the keys to return them in the same format as they were stored
344                let decoded_keys: Vec<String> = keys
345                    .into_iter()
346                    .filter_map(|encoded_key| {
347                        let decoded = form_urlencoded::parse(encoded_key.as_bytes())
348                            .next()
349                            .map(|(key, _)| key.into_owned());
350
351                        if decoded.is_none() {
352                            logger::debug!("Key not URL-encoded or decode failed: {encoded_key}");
353                        }
354
355                        decoded
356                    })
357                    .collect();
358
359                Ok(decoded_keys)
360            }
361            Err(e) => {
362                logger::warn!("Error getting keys: {e}");
363                Ok(vec![])
364            }
365        }
366    }
367
368    async fn store<T: Serialize>(
369        &self,
370        key: &str,
371        mode: &StoreMode,
372        item: &T,
373    ) -> Result<(), DataStorageError> {
374        let serialized = self.format.serialize(item)?;
375        let distributed_mode = self.convert_store_mode(mode);
376        let sanitized_key = self.sanitize_key(key);
377
378        // Try to store first
379        match self
380            .storage
381            .store(
382                &self.sanitized_store,
383                &self.sanitized_partition,
384                &sanitized_key,
385                &distributed_mode,
386                &serialized,
387            )
388            .await
389        {
390            Ok(()) => Ok(()),
391            Err(crate::distributed::DistributedStorageError::StoreNotFound) => {
392                // Store doesn't exist, create it and retry
393                let store = crate::distributed::Store::new(
394                    self.sanitized_store.clone(),
395                    Some(self.ttl_millis),
396                    None,
397                );
398
399                // Try to create the store, ignore if it already exists
400                if let Err(e) = self.storage.upsert_store(&store).await {
401                    logger::warn!("Error creating store: {e}");
402                }
403
404                // Try storing again
405                self.storage
406                    .store(
407                        &self.sanitized_store,
408                        &self.sanitized_partition,
409                        &sanitized_key,
410                        &distributed_mode,
411                        &serialized,
412                    )
413                    .await?;
414                Ok(())
415            }
416            Err(e) => Err(e.into()), // Other errors, propagate them
417        }
418    }
419
420    async fn get<T: DeserializeOwned>(
421        &self,
422        key: &str,
423    ) -> Result<Option<(T, String)>, DataStorageError> {
424        let sanitized_key = self.sanitize_key(key);
425        match self
426            .storage
427            .get(
428                &self.sanitized_store,
429                &self.sanitized_partition,
430                &sanitized_key,
431            )
432            .await
433        {
434            Ok((data, cas)) => {
435                let deserialized: T = self.format.deserialize(&data)?;
436                Ok(Some((deserialized, cas)))
437            }
438            Err(crate::distributed::DistributedStorageError::StoreNotFound) => {
439                logger::debug!("Store not found for key {key}, returning None");
440                Ok(None)
441            }
442            Err(crate::distributed::DistributedStorageError::KeyNotFound) => {
443                logger::debug!("Key not found: {key}");
444                Ok(None)
445            }
446            Err(e) => {
447                logger::error!("Error getting value for key {key}: {e:?}");
448                Err(e.into())
449            }
450        }
451    }
452
453    async fn delete(&self, key: &str) -> Result<(), DataStorageError> {
454        // Try to delete, ignore if store doesn't exist
455        let sanitized_key = self.sanitize_key(key);
456        if let Err(e) = self
457            .storage
458            .delete(
459                &self.sanitized_store,
460                &self.sanitized_partition,
461                &sanitized_key,
462            )
463            .await
464        {
465            logger::warn!("Error deleting key {key}: {e}");
466        }
467        Ok(())
468    }
469
470    async fn delete_all(&self) -> Result<(), DataStorageError> {
471        // Try to delete partition, ignore if store doesn't exist
472        if let Err(e) = self
473            .storage
474            .delete_partition(&self.sanitized_store, &self.sanitized_partition)
475            .await
476        {
477            logger::warn!("Error deleting partition: {e}");
478        }
479        Ok(())
480    }
481}
482
483/// Builder for creating data storage instances.
484///
485/// Provides methods to create local and distributed storage instances with
486/// configurable settings. The builder pattern allows for flexible configuration
487/// of storage behavior.
488///
489/// # Examples
490///
491/// ```rust
492/// # use data_storage_lib::{DataStorageBuilder, DataStorage};
493/// # async fn example(builder: DataStorageBuilder) {
494/// // Create a local storage instance
495/// let local_storage = builder.local("my-local-storage");
496///
497/// // Create a distributed storage instance with 60-second TTL
498/// let remote_storage = builder.remote("my-remote-storage", 60000);
499/// # }
500/// ```
501pub struct DataStorageBuilder<F: format::Format = format::DefaultFormat> {
502    prefix: String,
503    shared_data: Rc<crate::local::SharedData>,
504    distributed_storage: Option<Rc<crate::distributed::DistributedStorageClient>>,
505    format_wrapper: FormatWrapper<F>,
506}
507
508/// DataStorageBuilder can be injected in your configuration function.
509/// ```rust
510/// #[entrypoint]
511/// async fn configure(
512///     launcher: Launcher,
513///     store_builder: DataStorageBuilder,
514///     Configuration(configuration): Configuration,
515/// ) -> anyhow::Result<()> {
516/// }
517/// ```
518impl FromContext<ConfigureContext> for DataStorageBuilder<DefaultFormat> {
519    type Error = DataStorageBuilderError;
520
521    fn from_context(context: &ConfigureContext) -> Result<Self, Self::Error> {
522        // Extract local storage (required)
523        let shared_data: crate::local::SharedData = context
524            .extract()
525            .map_err(|_| DataStorageBuilderError::LocalStorageRequired)?;
526        // Extract distributed storage (optional - will be None if not available)
527        let distributed_storage: Result<crate::distributed::DistributedStorageClient, _> =
528            context.extract();
529        // Extract metadata for policy isolation
530        let metadata: pdk_core::policy_context::api::Metadata = context
531            .extract()
532            .map_err(|_| DataStorageBuilderError::MetadataRequired)?;
533
534        let prefix = format!(
535            "isolated-storage-{}-{}",
536            metadata.policy_metadata.policy_name, metadata.policy_metadata.policy_namespace
537        );
538
539        pdk_core::logger::debug!(
540            "DataStorageBuilder: creating prefix '{}' for policy '{}' in namespace '{}'",
541            prefix,
542            metadata.policy_metadata.policy_name,
543            metadata.policy_metadata.policy_namespace
544        );
545
546        Ok(DataStorageBuilder {
547            prefix,
548            shared_data: Rc::new(shared_data),
549            distributed_storage: distributed_storage.ok().map(Rc::new),
550            format_wrapper: FormatWrapper::Default(DefaultFormat::default(), PhantomData),
551        })
552    }
553}
554
555impl<F: format::Format + Clone> DataStorageBuilder<F> {
556    /// Indicates that storage should not be isolated to a single policy.
557    /// The resulting state will be shared across policy instances that use
558    /// the same storage ID.
559    pub fn shared(mut self) -> Self {
560        self.prefix = "shared-storage".to_string();
561        self
562    }
563
564    /// Sets the serialization format.
565    #[cfg(feature = "experimental_datastorage_formats")]
566    pub fn format<H: Format + Clone>(self, format: H) -> DataStorageBuilder<H> {
567        DataStorageBuilder {
568            prefix: self.prefix,
569            shared_data: self.shared_data,
570            distributed_storage: self.distributed_storage,
571            format_wrapper: FormatWrapper::Custom(format),
572        }
573    }
574
575    /// Creates a local data storage instance identified by the key.
576    pub fn local<T: Into<String>>(&self, key: T) -> LocalDataStorage<F> {
577        let key_str = key.into();
578        // Create a truly unique namespace by combining prefix and key
579        // This ensures each policy gets its own isolated storage
580        let namespace = format!("{}-{}", self.prefix, key_str);
581
582        pdk_core::logger::debug!(
583            "DataStorageBuilder::local: creating namespace '{}' with prefix '{}' and key '{}'",
584            namespace,
585            self.prefix,
586            key_str
587        );
588
589        LocalDataStorage::new(
590            (*self.shared_data).clone(),
591            namespace,
592            self.format_wrapper.clone(),
593        )
594    }
595
596    /// Creates a distributed data storage instance identified by the provided key and ttl in milliseconds.
597    ///
598    /// **Note**: To make use of the remote mode of the data storage your Flex Gateway must have [Shared Storage](https://docs.mulesoft.com/gateway/latest/flex-conn-shared-storage-config) configured.
599    ///
600    /// # Panics
601    ///
602    /// Panics if distributed storage is not available in the current context.
603    /// Ensure that distributed storage is properly configured before calling this method.
604    pub fn remote<T: Into<String>>(&self, key: T, ttl_millis: u32) -> RemoteDataStorage<F> {
605        let key_str = key.into();
606        let storage = self
607            .distributed_storage
608            .as_ref()
609            .expect("Distributed storage not available - check if it's configured");
610        RemoteDataStorage::new(
611            Rc::clone(storage),
612            key_str.clone(),
613            key_str,
614            ttl_millis,
615            self.format_wrapper.clone(),
616        )
617    }
618}