Skip to main content

vantage_dataset/traits/
valueset.rs

1use std::pin::Pin;
2
3use crate::record::ActiveRecord;
4
5use super::Result;
6use async_trait::async_trait;
7use futures_core::Stream;
8use indexmap::IndexMap;
9use vantage_types::Record;
10
11/// Foundation trait for all dataset operations, defining the basic types used for storage.
12/// Typically you would implement ValueSet in combination with:
13///
14///  - [`ReadableValueSet`]
15///  - [`WritableValueSet`]
16///  - [`InsertableValueSet`]
17///
18/// `ValueSet` establishes the contract for working with raw storage values, providing
19/// the building blocks that higher-level [`DataSet`](super::DataSet) traits build upon.
20/// This separation allows the same storage backend to work with both typed entities
21/// and raw values efficiently.
22///
23/// # Type Parameters
24///
25/// - `Id`: Unique identifier type chosen by the storage implementation
26/// - `Value`: Raw storage value type, typically JSON-like structures
27///
28/// # Example
29///
30/// ```rust,ignore
31/// use vantage_dataset::{ReadableValueSet, ValueSet, prelude::*};
32/// use vantage_types::Record;
33/// use serde_json::Value;
34///
35/// struct CsvFile {
36///     filename: String,
37/// }
38///
39/// impl ValueSet for CsvFile {
40///     type Id = String;
41///     type Value = serde_json::Value;
42/// }
43///
44/// #[async_trait]
45/// impl ReadableValueSet for CsvFile {
46///     async fn list_values(&self) -> Result<IndexMap<Self::Id, Record<Self::Value>>> {
47///         // Parse CSV and return as JSON values
48///         // Implementation details...
49///     }
50///
51///     async fn get_value(&self, id: &Self::Id) -> Result<Option<Record<Self::Value>>> {
52///         // Find specific record by ID
53///         // Implementation details...
54///     }
55///
56///     async fn get_some_value(&self) -> Result<Option<(Self::Id, Record<Self::Value>)>> {
57///         // Return first record if any exists
58///         // Implementation details...
59///     }
60/// }
61/// ```
62#[async_trait]
63pub trait ValueSet {
64    /// Unique identifier type for records in this storage backend.
65    ///
66    /// Common choices:
67    /// - `String` for most databases and APIs
68    /// - `uuid::Uuid` if database does not support other types of IDs.
69    /// - Database-specific types like `surrealdb::sql::Thing`
70    type Id: Send + Sync + Clone;
71
72    /// Raw storage value type, representing data as stored in the backend, like
73    /// serde_json::Value or cborium::Value. Can also be a custom type.
74    type Value: Send + Sync + Clone;
75}
76
77/// Read-only access to raw storage values without entity deserialization.
78///
79/// See documentation for [`ValueSet`] for implementation example.
80#[async_trait]
81pub trait ReadableValueSet: ValueSet {
82    /// Retrieve all records as raw storage values preserving insertion order where supported.
83    ///
84    /// # Performance
85    /// In Vantage you can't retrieve values of a Set partially. Instead you should
86    /// create a sub-set of your existing set, then list values of that set instead.
87    async fn list_values(&self) -> Result<IndexMap<Self::Id, Record<Self::Value>>>;
88
89    /// Retrieve a specific record by ID as a structured record.
90    ///
91    /// Returns `Ok(None)` when no record exists with the given ID. Returns an
92    /// error only if the lookup itself fails.
93    async fn get_value(
94        &self,
95        id: impl Into<Self::Id> + Send,
96    ) -> Result<Option<Record<Self::Value>>>;
97
98    /// Retrieve one single record from the set. If records are ordered - return first record.
99    /// will return Ok(None).
100    ///
101    /// Useful when you operate with a very specific subset of data.
102    async fn get_some_value(&self) -> Result<Option<(Self::Id, Record<Self::Value>)>>;
103
104    /// Stream all records as (Id, Record) pairs.
105    ///
106    /// Default wraps `list_values()`. Backends with native streaming
107    /// (e.g. paginated REST APIs) can override for incremental fetching.
108    #[allow(clippy::type_complexity)]
109    fn stream_values(
110        &self,
111    ) -> Pin<Box<dyn Stream<Item = Result<(Self::Id, Record<Self::Value>)>> + Send + '_>>
112    where
113        Self: Sync,
114    {
115        Box::pin(async_stream::stream! {
116            let records = self.list_values().await;
117            match records {
118                Ok(map) => {
119                    for item in map {
120                        yield Ok(item);
121                    }
122                }
123                Err(e) => yield Err(e),
124            }
125        })
126    }
127}
128
129/// Write operations on raw storage values with idempotent behavior.
130///
131/// See documentation for [`ValueSet`] for implementation example.
132#[async_trait]
133pub trait WritableValueSet: ValueSet {
134    /// Insert value with a specific ID (often generated) (HTTP POST with ID)
135    ///
136    /// **Idempotent**: Succeeds if no record exists with the given ID. If
137    /// record already exists, must return success without overwriting
138    /// data, returning original data.
139    ///
140    /// **Returns**: Record as it was stored.
141    ///
142    /// # Use Case
143    /// Generate unique ID and store record while avoiding duplicates.
144    async fn insert_value(
145        &self,
146        id: impl Into<Self::Id> + Send,
147        record: &Record<Self::Value>,
148    ) -> Result<Record<Self::Value>>;
149
150    /// Replace the entire record at the specified ID (HTTP PUT)
151    ///
152    /// **Idempotent**: Always succeeds, completely overwrites existing data
153    /// if present. If possible, will remove/recreate record; therefore if
154    /// `record` doesn't contain certain attributes which were present in the
155    /// database, those will be removed. If record does not exist, will
156    /// create it.
157    ///
158    /// **Returns**: Record as it was stored.
159    ///
160    /// # Use Case
161    /// Replace with a new version of a record.
162    async fn replace_value(
163        &self,
164        id: impl Into<Self::Id> + Send,
165        record: &Record<Self::Value>,
166    ) -> Result<Record<Self::Value>>;
167
168    /// Partially update a record by merging with the provided value (HTTP PATCH)
169    ///
170    /// **Fails if record doesn't exist**. The exact merge behavior depends on
171    /// the storage implementation - typically merges object fields for JSON-like values.
172    ///
173    /// **Returns**: Record as it was stored (not only the partial change).
174    ///
175    /// # Use Case
176    /// Update only the modified fields of a record.
177    async fn patch_value(
178        &self,
179        id: impl Into<Self::Id> + Send,
180        partial: &Record<Self::Value>,
181    ) -> Result<Record<Self::Value>>;
182
183    /// Delete a record by ID (HTTP DELETE)
184    ///
185    /// **Idempotent**: Always succeeds, even if the record doesn't exist.
186    /// This allows safe cleanup operations without checking existence first.
187    async fn delete(&self, id: impl Into<Self::Id> + Send) -> Result<()>;
188
189    /// Delete all records in the set (HTTP DELETE without ID)
190    ///
191    /// **Idempotent**: All records in the set will be deleted.
192    /// Executing several times is OK.
193    ///
194    /// Execute on a subset of your entire database.
195    async fn delete_all(&self) -> Result<()>;
196}
197
198/// Append-only operations on raw storage values with automatic ID generation.
199///
200/// See documentation for [`ValueSet`] for implementation example.
201#[async_trait]
202pub trait InsertableValueSet: ValueSet {
203    /// Insert a value and return the generated ID (Similar to HTTP POST without ID)
204    ///
205    /// The storage backend generates a unique identifier for the new record.
206    ///
207    /// # Warning
208    ///
209    /// This method is **not idempotent** - each call creates a new record with
210    /// a new ID, even if the value data is identical.
211    async fn insert_return_id_value(&self, record: &Record<Self::Value>) -> Result<Self::Id>;
212}
213
214/// Change tracking for raw storage values with automatic persistence.
215///
216/// See documentation for [`ValueSet`] for implementation example.
217#[async_trait]
218pub trait ActiveRecordSet: ReadableValueSet + WritableValueSet {
219    /// Retrieve a record wrapped for change tracking and deferred persistence.
220    ///
221    /// The returned `RecordValue` can be modified in-place and will track all
222    /// changes for efficient persistence when `save()` is called.
223    ///
224    /// # Returns
225    ///
226    /// - `Ok(Some(RecordValue))`: Record wrapper with change tracking
227    /// - `Ok(None)`: No record with this ID
228    /// - `Err`: If the lookup itself fails
229    async fn get_value_record(
230        &self,
231        id: impl Into<Self::Id> + Send,
232    ) -> Result<Option<ActiveRecord<'_, Self>>>;
233
234    /// Retrieve all records wrapped for change tracking.
235    ///
236    /// Each returned `RecordValue` operates independently - modifications to one
237    /// record don't affect others, and each must be saved separately.
238    ///
239    /// # Performance Note
240    ///
241    /// This loads all records into memory. Consider pagination or streaming
242    /// approaches for large datasets.
243    async fn list_value_records(&self) -> Result<Vec<ActiveRecord<'_, Self>>>;
244}
245
246// Auto-implement for any type that has both readable and writable traits
247#[async_trait]
248impl<T> ActiveRecordSet for T
249where
250    T: ReadableValueSet + WritableValueSet + Sync,
251{
252    async fn get_value_record(
253        &self,
254        id: impl Into<Self::Id> + Send,
255    ) -> Result<Option<ActiveRecord<'_, Self>>> {
256        let id = id.into();
257        Ok(self
258            .get_value(id.clone())
259            .await?
260            .map(|record| ActiveRecord::new(id, record, self)))
261    }
262
263    async fn list_value_records(&self) -> Result<Vec<ActiveRecord<'_, Self>>> {
264        let items = self.list_values().await?;
265
266        Ok(items
267            .into_iter()
268            .map(|(id, record)| ActiveRecord::new(id, record, self))
269            .collect::<Vec<_>>())
270    }
271}