Skip to main content

dynamodb_facade/values/
mod.rs

1mod impls;
2mod typed;
3
4pub use typed::*;
5
6use super::AttributeValue;
7
8/// Converts a Rust value into a DynamoDB [`AttributeValue`].
9///
10/// This trait is the bridge between Rust types and the DynamoDB wire format.
11/// It is implemented for all common scalar types, collections, and the
12/// [`AsSet`] and [`AsNumber<T>`] newtypes:
13///
14/// | Rust type | DynamoDB type |
15/// |---|---|
16/// | [`String`], [`&str`], `&String` | `S` |
17/// | [`bool`] | `BOOL` |
18/// | Integer and float primitives | `N` |
19/// | [`Vec<u8>`], [`&[u8]`] | `B` |
20/// | [`Vec<T>`], [`&[T]`] (where `T` is a scalar) | `L` |
21/// | [`HashSet<String>`](std::collections::HashSet), [`BTreeSet<String>`](std::collections::BTreeSet) | `SS` |
22/// | [`HashSet<N>`](std::collections::HashSet), [`BTreeSet<N>`](std::collections::BTreeSet) (numeric) | `NS` |
23/// | [`HashSet<Vec<u8>>`](std::collections::HashSet), [`BTreeSet<Vec<u8>>`](std::collections::BTreeSet) | `BS` |
24/// | [`AsSet<String>`] | `SS` |
25/// | [`AsSet<N>`] (numeric) | `NS` |
26/// | [`AsSet<Vec<u8>>`] | `BS` |
27/// | [`AsNumber<T>`] | `N` |
28/// | [`AttributeValue`] | identity |
29///
30/// Implement this trait for your own domain types to use them directly in
31/// expression builders (e.g. `Update::set("field", my_value)`).
32///
33/// # Examples
34///
35/// ```
36/// use dynamodb_facade::{AttributeValue, IntoAttributeValue};
37///
38/// // Strings become AttributeValue::S
39/// let av = "alice@example.com".into_attribute_value();
40/// assert_eq!(av, AttributeValue::S("alice@example.com".to_owned()));
41///
42/// // Numbers become AttributeValue::N
43/// let av = 42.into_attribute_value();
44/// assert_eq!(av, AttributeValue::N("42".to_owned()));
45///
46/// // Custom domain type
47/// struct UserId(String);
48/// impl IntoAttributeValue for UserId {
49///     fn into_attribute_value(self) -> AttributeValue {
50///         self.0.into_attribute_value()
51///     }
52/// }
53///
54/// let av = UserId("user-1".to_owned()).into_attribute_value();
55/// assert_eq!(av, AttributeValue::S("user-1".to_owned()));
56/// ```
57pub trait IntoAttributeValue {
58    /// Converts `self` into a DynamoDB [`AttributeValue`].
59    fn into_attribute_value(self) -> AttributeValue;
60}
61
62/// Converts a [`serde::Serialize`] value into a DynamoDB [`AttributeValue`]
63/// using [`serde_dynamo`].
64///
65/// This is a convenience wrapper around [`try_to_attribute_value`] that panics
66/// on failure. Use it when you are confident the serialization cannot fail
67/// (e.g. for well-known types like `&[&str]` or simple structs).
68///
69/// Prefer [`try_to_attribute_value`] in contexts where you want to propagate
70/// errors rather than panic.
71///
72/// # Panics
73///
74/// Panics if [`serde_dynamo::to_attribute_value`] returns an error. This
75/// should be rare in practice — it can happen for types that serialize to
76/// formats unsupported by DynamoDB (e.g. maps with non-string keys).
77///
78/// # Examples
79///
80/// Updating the `platform_config` field of a `MainPlatformConfig` item via
81/// [`Update::set`](crate::Update::set):
82///
83/// ```no_run
84/// # use dynamodb_facade::test_fixtures::*;
85/// use serde::{Serialize, Deserialize};
86/// use dynamodb_facade::{
87///     to_attribute_value, DynamoDBItemOp, KeyId, Update,
88///     StringAttribute, dynamodb_item,
89/// };
90///
91/// #[derive(Debug, Clone, Serialize, Deserialize)]
92/// struct MainPlatformConfig {
93///     platform_config: PlatformConfig,
94///     main_since_ts: u64,
95/// }
96///
97/// dynamodb_item! {
98///     #[table = PlatformTable]
99///     MainPlatformConfig {
100///         #[partition_key]
101///         PK { const VALUE: &'static str = "MAIN_PLATFORM_CONFIG"; }
102///         #[sort_key]
103///         SK { const VALUE: &'static str = "MAIN_PLATFORM_CONFIG"; }
104///     }
105/// }
106///
107/// # async fn example(client: dynamodb_facade::Client) -> dynamodb_facade::Result<()> {
108/// let new_config = PlatformConfig {
109///     max_enrollments: 50,
110///     maintenance_mode: false,
111/// };
112///
113/// // PlatformConfig is a Serialize type — to_attribute_value bridges it
114/// // to an AttributeValue for use in Update::set.
115/// MainPlatformConfig::update_by_id(
116///     client,
117///     KeyId::NONE,
118///     Update::set("platform_config", to_attribute_value(&new_config)),
119/// )
120/// .await?;
121/// # Ok(())
122/// # }
123/// ```
124pub fn to_attribute_value<T: serde::Serialize>(value: T) -> AttributeValue {
125    try_to_attribute_value(value)
126        .expect("should be infallible, use `try_to_attribute_value` instead")
127}
128
129/// Converts a [`serde::Serialize`] value into a DynamoDB [`AttributeValue`]
130/// using [`serde_dynamo`], returning a [`Result`](crate::Result) on failure.
131///
132/// Use this when you need to handle serialization errors gracefully. For
133/// infallible cases, [`to_attribute_value`] is more ergonomic.
134///
135/// # Errors
136///
137/// Returns [`Error::Serde`](crate::Error::Serde) if [`serde_dynamo`] cannot
138/// convert the value — for example, if the type serializes to a map with
139/// non-string keys, which DynamoDB does not support.
140///
141/// # Examples
142///
143/// Updating the `platform_config` field of a `MainPlatformConfig` item via
144/// [`Update::set`](crate::Update::set), propagating any serialization error with `?`:
145///
146/// ```no_run
147/// # use dynamodb_facade::test_fixtures::*;
148/// use serde::{Serialize, Deserialize};
149/// use dynamodb_facade::{
150///     try_to_attribute_value, DynamoDBItemOp, KeyId, Update,
151///     StringAttribute, dynamodb_item,
152/// };
153///
154/// #[derive(Debug, Clone, Serialize, Deserialize)]
155/// struct MainPlatformConfig {
156///     platform_config: PlatformConfig,
157///     main_since_ts: u64,
158/// }
159///
160/// dynamodb_item! {
161///     #[table = PlatformTable]
162///     MainPlatformConfig {
163///         #[partition_key]
164///         PK { const VALUE: &'static str = "MAIN_PLATFORM_CONFIG"; }
165///         #[sort_key]
166///         SK { const VALUE: &'static str = "MAIN_PLATFORM_CONFIG"; }
167///     }
168/// }
169///
170/// # async fn example(client: dynamodb_facade::Client) -> dynamodb_facade::Result<()> {
171/// let new_config = PlatformConfig {
172///     max_enrollments: 50,
173///     maintenance_mode: false,
174/// };
175///
176/// // PlatformConfig is a Serialize type — try_to_attribute_value bridges it
177/// // to an AttributeValue for use in Update::set.
178/// MainPlatformConfig::update_by_id(
179///     client,
180///     KeyId::NONE,
181///     Update::set("platform_config", try_to_attribute_value(&new_config)?),
182/// )
183/// .await?;
184/// # Ok(())
185/// # }
186/// ```
187pub fn try_to_attribute_value<T: serde::Serialize>(value: T) -> crate::Result<AttributeValue> {
188    Ok(serde_dynamo::to_attribute_value(value)?)
189}
190
191/// A newtype wrapper around [`Vec<T>`] that serializes as a DynamoDB Set type
192/// (`SS`, `NS`, or `BS`) instead of a List (`L`).
193///
194/// DynamoDB distinguishes between ordered lists (`L`) and unordered sets
195/// (`SS`/`NS`/`BS`). A plain `Vec<String>` converts to `L`, but
196/// `AsSet(vec)` converts to `SS`. This matters for `ADD` and `DELETE` update
197/// expressions, which operate on Set types.
198///
199/// [`IntoAttributeValue`] is implemented for:
200/// - `AsSet<String>` → `SS`
201/// - `AsSet<N>` (any numeric primitive) → `NS`
202///
203/// `AsSet<T>` derefs to `&Vec<T>` and implements [`IntoIterator`], so you can
204/// use it anywhere a `Vec<T>` is expected for reading.
205///
206/// # Examples
207///
208/// ```
209/// use dynamodb_facade::{AsSet, AttributeValue, IntoAttributeValue};
210///
211/// // Vec<String> → L (list)
212/// let list_av = vec!["rust".to_owned(), "dynamodb".to_owned()].into_attribute_value();
213/// assert!(matches!(list_av, AttributeValue::L(_)));
214///
215/// // AsSet<String> → SS (string set)
216/// let set_av = AsSet(vec!["rust".to_owned(), "dynamodb".to_owned()]).into_attribute_value();
217/// assert!(matches!(set_av, AttributeValue::Ss(_)));
218///
219/// // AsSet<u32> → NS (number set)
220/// let num_set_av = AsSet(vec![1, 2, 3]).into_attribute_value();
221/// assert!(matches!(num_set_av, AttributeValue::Ns(_)));
222/// ```
223///
224/// Using `AsSet` with the `add` update expression to atomically add tags:
225///
226/// ```no_run
227/// // Requires a live DynamoDB connection
228/// use dynamodb_facade::{AsSet, IntoAttributeValue};
229/// // Update::add("tags", AsSet(vec!["rust".to_owned()]).into_attribute_value())
230/// ```
231#[derive(Debug)]
232// New type wrapper for a Vec that will cause it to be serialized as a HashSet
233// See impls.rs.
234pub struct AsSet<T>(pub Vec<T>);
235impl<T> core::ops::Deref for AsSet<T> {
236    type Target = Vec<T>;
237
238    fn deref(&self) -> &Self::Target {
239        &self.0
240    }
241}
242impl<T> IntoIterator for AsSet<T> {
243    type Item = T;
244    type IntoIter = <Vec<T> as IntoIterator>::IntoIter;
245
246    fn into_iter(self) -> Self::IntoIter {
247        self.0.into_iter()
248    }
249}
250
251/// A generic newtype wrapper that converts any `T: Into<String>` directly to
252/// a DynamoDB `N` (number) attribute value without parsing.
253///
254/// Use `AsNumber` when you already have a correctly-formatted numeric string
255/// and want to pass it to DynamoDB as-is — for example, a high-precision
256/// decimal from an external API, a value from a financial system that must
257/// not be rounded through an `f64`, or a number string received from another
258/// DynamoDB client. `T` can be `&str`, [`String`], [`Cow<str>`](std::borrow::Cow),
259/// or any other type that implements `Into<String>`.
260///
261/// `AsNumber` implements [`IntoAttributeValue`] (producing
262/// [`AttributeValue::N`]) and
263/// [`IntoTypedAttributeValue<NumberAttribute>`](crate::IntoTypedAttributeValue),
264/// so it can be used anywhere a `NumberAttribute` value is expected (e.g. in
265/// [`has_attributes!`](crate::has_attributes) blocks or expression builders).
266///
267/// `AsNumber<T>` also implements [`Deref<Target = T>`](core::ops::Deref),
268/// so you can use it anywhere a `&T` is accepted.
269///
270/// ## DynamoDB number constraints
271///
272/// DynamoDB numbers can be positive, negative, or zero, with up to 38 digits
273/// of precision (exceeding this causes a runtime error). The supported ranges
274/// are:
275///
276/// - Positive: `1E-130` to `9.9999999999999999999999999999999999999E+125`
277/// - Negative: `-9.9999999999999999999999999999999999999E+125` to `-1E-130`
278///
279/// Leading and trailing zeroes are trimmed by DynamoDB. Numbers are
280/// transmitted as strings over the wire but treated as numeric types for
281/// mathematical operations.
282///
283/// > **Warning:** No validation is performed on the wrapped value. An
284/// > invalid number string (e.g. `"not-a-number"`) will be accepted by
285/// > `AsNumber` but rejected by DynamoDB at runtime.
286///
287/// # Examples
288///
289/// Basic usage — converting a pre-formatted decimal string to `AttributeValue::N`:
290///
291/// ```
292/// use dynamodb_facade::{AsNumber, AttributeValue, IntoAttributeValue};
293///
294/// // A high-precision decimal that would lose precision as f64
295/// let price = AsNumber("12345678.90123456789099");
296/// let av = price.into_attribute_value();
297/// assert_eq!(av, AttributeValue::N("12345678.90123456789099".to_owned()));
298/// ```
299///
300/// Using `AsNumber` where a [`NumberAttribute`](crate::NumberAttribute) value
301/// is required — the type system accepts it just like any numeric primitive:
302///
303/// ```
304/// use dynamodb_facade::{AsNumber, IntoTypedAttributeValue, NumberAttribute};
305///
306/// fn store_score<V: IntoTypedAttributeValue<NumberAttribute>>(_v: V) {}
307///
308/// // AsNumber satisfies the NumberAttribute bound
309/// store_score(AsNumber("99.5"));
310/// // So do ordinary numeric primitives
311/// store_score(42);
312/// ```
313pub struct AsNumber<T: Into<String>>(pub T);
314
315impl<T: Into<String>> core::ops::Deref for AsNumber<T> {
316    type Target = T;
317
318    fn deref(&self) -> &Self::Target {
319        &self.0
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use std::collections::HashMap;
326
327    use super::*;
328
329    // -- to_attribute_value ---------------------------------------------------
330
331    #[derive(serde::Serialize)]
332    struct Thing {
333        a: u32,
334        b: String,
335    }
336
337    #[test]
338    fn test_to_attribute_value_happy_path() {
339        // Scalar: u32 → N
340        let av = to_attribute_value(42u32);
341        assert_eq!(av, AttributeValue::N("42".to_owned()));
342
343        // Struct → M with expected keys
344        let thing = Thing {
345            a: 7,
346            b: "hello".to_owned(),
347        };
348        let av = to_attribute_value(thing);
349        if let AttributeValue::M(map) = av {
350            assert_eq!(map.get("a"), Some(&AttributeValue::N("7".to_owned())));
351            assert_eq!(map.get("b"), Some(&AttributeValue::S("hello".to_owned())));
352        } else {
353            panic!("expected AttributeValue::M, got something else");
354        }
355    }
356
357    #[test]
358    #[should_panic]
359    fn test_to_attribute_value_panics_on_unserializable() {
360        // serde_dynamo rejects HashMap<Option<&str>, &str> — None cannot be represented DynamoDB map.
361        let wrong = HashMap::from([(Some("key"), "v1"), (None, "v2")]);
362        to_attribute_value(wrong);
363    }
364
365    // -- try_to_attribute_value -----------------------------------------------
366
367    #[test]
368    fn test_try_to_attribute_value_happy_path() {
369        let av = try_to_attribute_value("hello").unwrap();
370        assert_eq!(av, AttributeValue::S("hello".to_owned()));
371    }
372
373    #[test]
374    fn test_try_to_attribute_value_error_path() {
375        // serde_dynamo rejects HashMap<Option<&str>, &str> — None cannot be represented DynamoDB map.
376        let wrong = HashMap::from([(Some("key"), "v1"), (None, "v2")]);
377        let result = try_to_attribute_value(wrong);
378        assert!(result.is_err());
379    }
380
381    // -- AsSet vs Vec ---------------------------------------------------------
382
383    #[test]
384    fn test_as_set_vs_vec_produce_different_variants() {
385        // Vec<String> → L (ordered list)
386        let list_av = vec!["a".to_owned(), "b".to_owned()].into_attribute_value();
387        if let AttributeValue::L(items) = &list_av {
388            assert_eq!(items.len(), 2);
389            assert!(items.iter().all(|v| matches!(v, AttributeValue::S(_))));
390        } else {
391            panic!("expected AttributeValue::L for Vec<String>");
392        }
393
394        // AsSet<String> → Ss (string set)
395        let set_av = AsSet(vec!["a".to_owned(), "b".to_owned()]).into_attribute_value();
396        if let AttributeValue::Ss(strings) = set_av {
397            assert_eq!(strings.len(), 2);
398            assert!(strings.contains(&"a".to_owned()));
399            assert!(strings.contains(&"b".to_owned()));
400        } else {
401            panic!("expected AttributeValue::Ss for AsSet<String>");
402        }
403    }
404
405    // -- AsNumber -------------------------------------------------------------
406
407    #[test]
408    fn test_as_number_preserves_exact_string() {
409        // The string "1e10" must survive round-trip without reformatting.
410        let av = AsNumber("1e10").into_attribute_value();
411        assert_eq!(av, AttributeValue::N("1e10".to_owned()));
412
413        // High-precision decimal that would lose precision through f64
414        let av = AsNumber("12345678.90123456789099").into_attribute_value();
415        assert_eq!(av, AttributeValue::N("12345678.90123456789099".to_owned()));
416    }
417}