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}