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}