Skip to main content

dynamodb_facade/schema/
attributes.rs

1use crate::{IntoTypedAttributeValue, NoId};
2
3use super::AttributeValueRef;
4
5pub(super) mod sealed_traits {
6    /// Seals [`AttributeType`](super::AttributeType) so only `StringAttribute`, `NumberAttribute`, and `BinaryAttribute` can implement it.
7    pub trait AttributeTypeSeal {}
8}
9
10crate::utils::impl_sealed_marker_types!(
11    /// Sealed marker trait for DynamoDB attribute types.
12    ///
13    /// Implemented only by [`StringAttribute`], [`NumberAttribute`], and
14    /// [`BinaryAttribute`]. This trait is sealed and cannot be implemented
15    /// outside of this crate. It is used as a bound on
16    /// [`AttributeDefinition::Type`] to restrict attribute definitions to the
17    /// three scalar DynamoDB types authorized for key schemas (S, N, B).
18    AttributeType,
19    sealed_traits::AttributeTypeSeal;
20    /// Marker type for DynamoDB String (`S`) attributes.
21    ///
22    /// Use this as the `Type` in an [`attribute_definitions!`](crate::attribute_definitions)
23    /// block to declare that an attribute stores a string value. Rust types
24    /// that implement [`IntoTypedAttributeValue<StringAttribute>`](crate::IntoTypedAttributeValue)
25    /// include [`String`], [`&str`], `&String`, and [`Cow<'_, str>`](std::borrow::Cow).
26    ///
27    /// The type system enforces this constraint at compile time: passing a
28    /// value of the wrong type (e.g. a `u32` or `Vec<u8>`) to a
29    /// `StringAttribute` attribute will not compile.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use dynamodb_facade::{attribute_definitions, has_attributes, StringAttribute};
35    ///
36    /// attribute_definitions! {
37    ///     Label { "label": StringAttribute }
38    /// }
39    ///
40    /// struct MyItem;
41    ///
42    /// // ✓ &'static str implements IntoTypedAttributeValue<StringAttribute>
43    /// has_attributes! {
44    ///     MyItem {
45    ///         Label { const VALUE: &'static str = "hello"; }
46    ///     }
47    /// }
48    ///
49    /// struct OtherItem;
50    /// // ✓ String also works
51    /// has_attributes! {
52    ///     OtherItem {
53    ///         Label { fn attribute_value(id) -> String { "world".to_owned() } }
54    ///     }
55    /// }
56    ///
57    /// // These would NOT compile — wrong attribute type:
58    /// // has_attributes! { MyItem { Label { const VALUE: u32 = 42; } } }
59    /// // has_attributes! { MyItem { Label { fn attribute_value(id) -> Vec<u8> { vec![] } } } }
60    /// ```
61    StringAttribute,
62    /// Marker type for DynamoDB Number (`N`) attributes.
63    ///
64    /// Use this as the `Type` in an [`attribute_definitions!`](crate::attribute_definitions)
65    /// block to declare that an attribute stores a numeric value. Rust types
66    /// that implement [`IntoTypedAttributeValue<NumberAttribute>`](crate::IntoTypedAttributeValue)
67    /// include all integer and floating-point primitives, as well as
68    /// [`AsNumber<T>`](crate::AsNumber) (for pre-formatted number strings).
69    ///
70    /// The type system enforces this constraint at compile time: passing a
71    /// value of the wrong type (e.g. a `&str` or `Vec<u8>`) to a
72    /// `NumberAttribute` attribute will not compile.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use dynamodb_facade::{attribute_definitions, has_attributes, NumberAttribute};
78    ///
79    /// attribute_definitions! {
80    ///     Score { "score": NumberAttribute }
81    /// }
82    ///
83    /// struct MyItem;
84    /// // ✓ u32 implements IntoTypedAttributeValue<NumberAttribute>
85    /// has_attributes! {
86    ///     MyItem {
87    ///         Score { fn attribute_value(id) -> u32 { 100 } }
88    ///     }
89    /// }
90    ///
91    /// struct OtherItem;
92    /// // ✓ i64 also works
93    /// has_attributes! {
94    ///     OtherItem {
95    ///         Score { fn attribute_value(id) -> i64 { -5 } }
96    ///     }
97    /// }
98    ///
99    /// // These would NOT compile — wrong attribute type:
100    /// // has_attributes! { MyItem { Score { const VALUE: &'static str = "hi"; } } }
101    /// // has_attributes! { MyItem { Score { fn attribute_value(id) -> Vec<u8> { vec![] } } } }
102    /// ```
103    NumberAttribute,
104    /// Marker type for DynamoDB Binary (`B`) attributes.
105    ///
106    /// Use this as the `Type` in an [`attribute_definitions!`](crate::attribute_definitions)
107    /// block to declare that an attribute stores binary data. Rust types that
108    /// implement [`IntoTypedAttributeValue<BinaryAttribute>`](crate::IntoTypedAttributeValue)
109    /// include [`Vec<u8>`] and [`&[u8]`].
110    ///
111    /// The type system enforces this constraint at compile time: passing a
112    /// value of the wrong type (e.g. a `&str` or `u32`) to a
113    /// `BinaryAttribute` attribute will not compile.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use dynamodb_facade::{attribute_definitions, has_attributes, BinaryAttribute};
119    ///
120    /// attribute_definitions! {
121    ///     Thumbnail { "thumbnail": BinaryAttribute }
122    /// }
123    ///
124    /// struct MyItem;
125    ///
126    /// // ✓ Vec<u8> implements IntoTypedAttributeValue<BinaryAttribute>
127    /// has_attributes! {
128    ///     MyItem {
129    ///         Thumbnail {
130    ///             fn attribute_value(id) -> Vec<u8> { vec![0x89, 0x50, 0x4e, 0x47] }
131    ///         }
132    ///     }
133    /// }
134    ///
135    /// // These would NOT compile — wrong attribute type:
136    /// // has_attributes! { MyItem { Thumbnail { const VALUE: &'static str = "img"; } } }
137    /// // has_attributes! { MyItem { Thumbnail { fn attribute_value(id) -> u32 { 0 } } } }
138    /// ```
139    BinaryAttribute
140);
141
142/// Defines the name and type of a single DynamoDB attribute at the type level.
143///
144/// Implementations are generated by
145/// [`attribute_definitions!`](crate::attribute_definitions). Each implementing
146/// type is a zero-sized struct that carries:
147///
148/// - `NAME` — the DynamoDB attribute name as a `&'static str`.
149/// - `Type` — one of [`StringAttribute`], [`NumberAttribute`], or
150///   [`BinaryAttribute`], indicating the DynamoDB scalar type.
151///
152/// These types serve as type-safe identifiers that connect attribute names and
153/// DynamoDB types to your key schemas, item definitions, and query builders.
154///
155/// # Examples
156///
157/// ```
158/// use dynamodb_facade::{attribute_definitions, has_attributes, AttributeDefinition, StringAttribute};
159///
160/// attribute_definitions! {
161///     CourseId { "course_id": StringAttribute }
162/// }
163///
164///
165/// // Use with the has_attributes! or dynamodb_item! macros:
166/// struct MyItem;
167/// has_attributes! {
168///     MyItem {
169///         CourseId { const VALUE: &'static str = "COURSE1234"; }
170///     }
171/// }
172///
173/// // Access the attribute name.
174/// assert_eq!(CourseId::NAME, "course_id");
175/// ```
176pub trait AttributeDefinition {
177    /// The DynamoDB attribute name (e.g. `"PK"`, `"email"`).
178    const NAME: &'static str;
179    /// The DynamoDB scalar type: [`StringAttribute`], [`NumberAttribute`], or [`BinaryAttribute`].
180    type Type: AttributeType + AttributeValueRef;
181}
182
183/// Links an item type to a dynamic DynamoDB attribute.
184///
185/// Implementing this trait for a pair `(Item, Attr)` declares that `Item`
186/// contributes the attribute `Attr` to its DynamoDB representation, where the
187/// attribute value is derived from the item at runtime.
188///
189/// The trait has two key methods:
190///
191/// - `attribute_id` — extracts an "Id" value from `&self` (e.g. a
192///   `&str` field).
193/// - `attribute_value` — converts the Id into a Rust value of type
194///   [`Self::Value`](HasAttribute::Value) (e.g. produces `"USER#{id}"` as a
195///   `String`). This is **not** an [`AttributeValue`](crate::AttributeValue)
196///   yet — the library converts it downstream using
197///   [`IntoTypedAttributeValue`].
198/// - `attribute` — convenience method that calls both in sequence to obtain
199///   the `Self::Value` from `&self`.
200///
201/// Implementations are generated by [`dynamodb_item!`](crate::dynamodb_item)
202/// and [`has_attributes!`](crate::has_attributes). Every type that implements
203/// [`HasConstAttribute<A>`] automatically gets a blanket `HasAttribute<A>`
204/// implementation.
205///
206/// # Examples
207///
208/// ```
209/// # use dynamodb_facade::test_fixtures::*;
210/// use dynamodb_facade::HasAttribute;
211///
212/// let user = sample_user();
213///
214/// // Retrieve the DynamoDB PK value for this user.
215/// let pk_value = <User as HasAttribute<PK>>::attribute(&user);
216/// assert_eq!(pk_value, "USER#user-1");
217/// ```
218pub trait HasAttribute<A: AttributeDefinition> {
219    /// The identifier extracted from `&self`, passed to
220    /// [`attribute_value`](HasAttribute::attribute_value).
221    ///
222    /// For constant attributes this is [`NoId`]. For dynamic
223    /// attributes it is typically a borrowed field (e.g. `&str`).
224    type Id<'id>;
225    /// A Rust type convertible to the DynamoDB attribute value for this
226    /// attribute.
227    ///
228    /// Bounded by [`IntoTypedAttributeValue<A::Type>`](crate::IntoTypedAttributeValue),
229    /// which guarantees that when this Rust value is converted to an
230    /// [`AttributeValue`](crate::AttributeValue) it will produce the correct
231    /// DynamoDB scalar type (`S` for [`StringAttribute`], `N` for
232    /// [`NumberAttribute`], `B` for [`BinaryAttribute`]).
233    type Value: IntoTypedAttributeValue<A::Type>;
234    /// Extracts the attribute ID from this item.
235    fn attribute_id(&self) -> Self::Id<'_>;
236    /// Converts an attribute ID into a Rust value of type [`Self::Value`](HasAttribute::Value)
237    /// which can then be converted into the correct [`AttributeValue`](crate::AttributeValue)
238    /// at serialization using the via
239    /// [`IntoTypedAttributeValue`].
240    fn attribute_value(id: Self::Id<'_>) -> Self::Value;
241    /// Convenience method: calls [`attribute_id`](HasAttribute::attribute_id)
242    /// then [`attribute_value`](HasAttribute::attribute_value), returning a
243    /// Rust value of type [`Self::Value`](HasAttribute::Value).
244    fn attribute(&self) -> Self::Value {
245        <Self as HasAttribute<A>>::attribute_value(self.attribute_id())
246    }
247}
248
249/// Links an item type to a compile-time constant DynamoDB attribute value.
250///
251/// Implementing this trait for a pair `(Item, Attr)` declares that every
252/// instance of `Item` has the same fixed value for attribute `Attr`. This is
253/// the common case for type discriminators (e.g. `ItemType` always `"USER"`).
254///
255/// Every type that implements [`HasConstAttribute<A>`] automatically gets a
256/// blanket [`HasAttribute<A>`] implementation that returns `VALUE` regardless
257/// of the instance.
258///
259/// Implementations are generated by [`dynamodb_item!`](crate::dynamodb_item)
260/// and [`has_attributes!`](crate::has_attributes).
261///
262/// # Examples
263///
264/// ```
265/// # use dynamodb_facade::test_fixtures::*;
266/// use dynamodb_facade::{HasAttribute, HasConstAttribute, NoId};
267///
268/// // PlatformConfig has a constant PK value.
269/// assert_eq!(<PlatformConfig as HasConstAttribute<PK>>::VALUE, "PLATFORM_CONFIG");
270/// // Also returned by the blanket HasAttribute<PK> implementation
271/// assert_eq!(<PlatformConfig as HasAttribute<PK>>::attribute_value(NoId), "PLATFORM_CONFIG");
272///
273/// // User has a constant SK value.
274/// assert_eq!(<User as HasConstAttribute<SK>>::VALUE, "USER");
275/// ```
276pub trait HasConstAttribute<A: AttributeDefinition> {
277    /// A Rust constant type convertible to the DynamoDB attribute value for this
278    /// attribute.
279    ///
280    /// Bounded by [`IntoTypedAttributeValue<A::Type>`](crate::IntoTypedAttributeValue),
281    /// which guarantees that when this Rust value is converted to an
282    /// [`AttributeValue`](crate::AttributeValue) it will produce the correct
283    /// DynamoDB scalar type (`S` for [`StringAttribute`], `N` for
284    /// [`NumberAttribute`], `B` for [`BinaryAttribute`]).
285    type Value: IntoTypedAttributeValue<A::Type>;
286    /// The constant Rust value shared by all instances of this item type,
287    /// later converted to the DynamoDB attribute value by the library.
288    const VALUE: Self::Value;
289}
290
291impl<A: AttributeDefinition, T: HasConstAttribute<A>> HasAttribute<A> for T {
292    type Id<'a> = NoId;
293    type Value = <Self as HasConstAttribute<A>>::Value;
294    fn attribute_id(&self) -> Self::Id<'_> {
295        NoId
296    }
297    fn attribute_value(_id: Self::Id<'_>) -> Self::Value {
298        <Self as HasConstAttribute<A>>::VALUE
299    }
300}