dynamodb_facade/operations/get.rs
1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use super::*;
5
6use aws_sdk_dynamodb::operation::get_item::builders::GetItemFluentBuilder;
7
8/// Builder for a DynamoDB `GetItem` request.
9///
10/// Constructed via [`DynamoDBItemOp::get`] (typed, with a concrete `T`) or
11/// [`GetItemRequest::new`] (stand-alone, raw output). The builder provides:
12///
13/// - **Output format** — the result can be deserialized into `T`.
14/// Call [`.raw()`][GetItemRequest::raw] to receive an untyped [`Item<TD>`]
15/// instead (one-way).
16/// - **Projection** — call [`.project()`][GetItemRequest::project] to limit
17/// which attributes are returned. This can only be called once and
18/// automatically switches to raw output, since the projected result may
19/// not contain all fields required for deserialization.
20///
21/// The builder implements [`IntoFuture`], so it can
22/// be `.await`ed directly without calling `.execute()` explicitly.
23///
24/// # Errors
25///
26/// Returns [`Err`] if the DynamoDB request fails or if deserialization of
27/// the returned attributes fails.
28///
29/// # Examples
30///
31/// ```no_run
32/// # use dynamodb_facade::test_fixtures::*;
33/// use dynamodb_facade::{DynamoDBItemOp, KeyId};
34///
35/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
36/// # let client = cclient.clone();
37/// // Simple get
38/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1")).await?;
39///
40/// # let client = cclient.clone();
41/// // Consistent read
42/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
43/// .consistent_read()
44/// .await?;
45/// # Ok(())
46/// # }
47/// ```
48#[must_use = "builder does nothing until awaited or executed"]
49pub struct GetItemRequest<
50 TD: TableDefinition,
51 T = (),
52 O: OutputFormat = Raw,
53 P: ProjectionState = NoProjection,
54> {
55 builder: GetItemFluentBuilder,
56 _marker: PhantomData<(TD, T, O, P)>,
57}
58
59// -- Stand-alone constructor (T = (), O = Raw)
60
61impl<TD: TableDefinition> GetItemRequest<TD> {
62 /// Creates a stand-alone `GetItemRequest` with raw output (`T = ()`, `O = Raw`).
63 ///
64 /// Use this when you do not have a concrete item type and want to work with
65 /// the raw [`Item<TD>`] map directly. For typed access, prefer
66 /// [`DynamoDBItemOp::get`] instead.
67 ///
68 /// # Examples
69 ///
70 /// ```no_run
71 /// # use dynamodb_facade::test_fixtures::*;
72 /// use dynamodb_facade::{GetItemRequest, Key};
73 ///
74 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
75 /// # let key: Key<PlatformTable> = sample_user_item().into_key_only();
76 /// let raw_item = GetItemRequest::<PlatformTable>::new(client, key).await?;
77 /// # Ok(())
78 /// # }
79 /// ```
80 pub fn new(client: aws_sdk_dynamodb::Client, key: Key<TD>) -> Self {
81 Self::_new(client, key)
82 }
83}
84
85// -- Common methods (all states) --------------------------------------------
86
87impl<TD: TableDefinition, T, O: OutputFormat, P: ProjectionState> GetItemRequest<TD, T, O, P> {
88 /// Creates a new `GetItemRequest` targeting the given key.
89 pub(super) fn _new(client: aws_sdk_dynamodb::Client, key: Key<TD>) -> Self {
90 let table_name = TD::table_name();
91 tracing::debug!(table_name, ?key, "GetItem");
92 Self {
93 builder: client
94 .get_item()
95 .table_name(table_name)
96 .set_key(Some(key.into_inner())),
97 _marker: PhantomData,
98 }
99 }
100
101 /// Enables strongly consistent reads for this request.
102 ///
103 /// By default DynamoDB uses eventually consistent reads. Enabling consistent
104 /// reads guarantees the most up-to-date data but consumes twice the read
105 /// capacity units and is not supported on Global Secondary Indexes.
106 ///
107 /// # Examples
108 ///
109 /// ```no_run
110 /// # use dynamodb_facade::test_fixtures::*;
111 /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
112 ///
113 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
114 /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
115 /// .consistent_read()
116 /// .await?;
117 /// # Ok(())
118 /// # }
119 /// ```
120 pub fn consistent_read(mut self) -> Self {
121 tracing::debug!("GetItem consistent_read");
122 self.builder = self.builder.consistent_read(true);
123 self
124 }
125
126 /// Consumes the builder and returns the underlying SDK
127 /// [`GetItemFluentBuilder`].
128 ///
129 /// Use this escape hatch when you need to set options not exposed by this
130 /// facade, such as `expression_attribute_names` for a manual projection, or
131 /// when integrating with code that expects the raw SDK builder.
132 ///
133 /// # Examples
134 ///
135 /// ```no_run
136 /// # use dynamodb_facade::test_fixtures::*;
137 /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
138 ///
139 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
140 /// let sdk_builder = User::get(client, KeyId::pk("user-1")).into_inner();
141 /// // configure sdk_builder further, then call .send().await
142 /// # Ok(())
143 /// # }
144 /// ```
145 pub fn into_inner(self) -> GetItemFluentBuilder {
146 self.builder
147 }
148}
149
150// -- Projection (NoProjection only) -----------------------------------------
151
152impl<TD: TableDefinition, T, O: OutputFormat> GetItemRequest<TD, T, O, NoProjection> {
153 /// Applies a projection expression, limiting the attributes returned.
154 ///
155 /// This method can only be called once. It forces the output to raw
156 /// [`Item<TD>`] because projected results may not contain all fields
157 /// required for deserialization into `T`.
158 ///
159 /// # Examples
160 ///
161 /// ```no_run
162 /// # use dynamodb_facade::test_fixtures::*;
163 /// use dynamodb_facade::{AttributeDefinition, DynamoDBItemOp, KeyId, Projection};
164 ///
165 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
166 /// // Fetch only the "name" and "email" attributes
167 /// let partial /* : Option<Item<PlatformTable>> */ =
168 /// User::get(client, KeyId::pk("user-1"))
169 /// .project(Projection::new(["name", Email::NAME]))
170 /// .await?;
171 /// // partial: contains only "PK", "SK", "name" and "email"
172 /// # Ok(())
173 /// # }
174 /// ```
175 pub fn project(
176 mut self,
177 projection: Projection<'_, TD>,
178 ) -> GetItemRequest<TD, T, Raw, AlreadyHasProjection> {
179 tracing::debug!(%projection, "GetItem project");
180 self.builder = projection.apply_projection(self.builder);
181 GetItemRequest {
182 builder: self.builder,
183 _marker: PhantomData,
184 }
185 }
186}
187
188// -- Output format transition (preserve P) ----------------------------------
189
190impl<TD: TableDefinition, T, P: ProjectionState> GetItemRequest<TD, T, Typed, P> {
191 /// Switches the output format from `Typed` to `Raw`.
192 ///
193 /// After calling `.raw()`, [`execute`][GetItemRequest::execute] returns
194 /// `Option<Item<TD>>` instead of `Option<T>`. This transition is one-way —
195 /// you cannot switch back to `Typed`.
196 ///
197 /// # Examples
198 ///
199 /// ```no_run
200 /// # use dynamodb_facade::test_fixtures::*;
201 /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
202 ///
203 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
204 /// let raw_item = User::get(client, KeyId::pk("user-1"))
205 /// .raw()
206 /// .await?;
207 /// // raw_item: Option<Item<PlatformTable>>
208 /// if let Some(item) = raw_item {
209 /// let name = item.get("name");
210 /// }
211 /// # Ok(())
212 /// # }
213 /// ```
214 pub fn raw(self) -> GetItemRequest<TD, T, Raw, P> {
215 GetItemRequest {
216 builder: self.builder,
217 _marker: PhantomData,
218 }
219 }
220}
221
222// -- Terminal: Typed (any P) ------------------------------------------------
223
224impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, P: ProjectionState>
225 GetItemRequest<TD, T, Typed, P>
226{
227 /// Sends the `GetItem` request and returns the item deserialized as `T`.
228 ///
229 /// Returns `Ok(None)` if no item exists for the given key.
230 ///
231 /// This method is also available implicitly via `.await` because
232 /// [`GetItemRequest`] implements [`IntoFuture`].
233 ///
234 /// # Errors
235 ///
236 /// Returns [`Err`] if the DynamoDB request fails or if deserialization of
237 /// the returned attributes fails.
238 ///
239 /// # Examples
240 ///
241 /// ```no_run
242 /// # use dynamodb_facade::test_fixtures::*;
243 /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
244 ///
245 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
246 /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
247 /// .consistent_read()
248 /// .execute()
249 /// .await?;
250 /// # Ok(())
251 /// # }
252 /// ```
253 #[tracing::instrument(level = "debug", skip(self), name = "get_execute")]
254 pub fn execute(self) -> impl Future<Output = Result<Option<T>>> + Send + 'static {
255 let builder = self.builder;
256 async move {
257 builder
258 .send()
259 .await?
260 .item
261 .map(Item::from_dynamodb_response)
262 .map(T::try_from_item)
263 .transpose()
264 }
265 }
266}
267
268impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, P: ProjectionState> IntoFuture
269 for GetItemRequest<TD, T, Typed, P>
270{
271 type Output = Result<Option<T>>;
272 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
273
274 fn into_future(self) -> Self::IntoFuture {
275 Box::pin(self.execute())
276 }
277}
278
279// -- Terminal: Raw (any P) --------------------------------------------------
280
281impl<TD: TableDefinition, T, P: ProjectionState> GetItemRequest<TD, T, Raw, P> {
282 /// Sends the `GetItem` request and returns the raw item map.
283 ///
284 /// Returns `Ok(None)` if no item exists for the given key.
285 ///
286 /// This method is also available implicitly via `.await` because
287 /// [`GetItemRequest`] implements [`IntoFuture`].
288 ///
289 /// # Errors
290 ///
291 /// Returns [`Err`] if the DynamoDB request fails.
292 ///
293 /// # Examples
294 ///
295 /// ```no_run
296 /// # use dynamodb_facade::test_fixtures::*;
297 /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
298 ///
299 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
300 /// let raw = User::get(client, KeyId::pk("user-1"))
301 /// .raw()
302 /// .execute()
303 /// .await?;
304 /// // raw: Option<Item<PlatformTable>>
305 /// if let Some(item) = raw {
306 /// let name = item.get("name");
307 /// }
308 /// # Ok(())
309 /// # }
310 /// ```
311 #[tracing::instrument(level = "debug", skip(self), name = "get_execute_raw")]
312 pub fn execute(self) -> impl Future<Output = Result<Option<Item<TD>>>> + Send + 'static {
313 let builder = self.builder;
314 async move { Ok(builder.send().await?.item.map(Item::from_dynamodb_response)) }
315 }
316}
317
318impl<TD: TableDefinition, T, P: ProjectionState> IntoFuture for GetItemRequest<TD, T, Raw, P> {
319 type Output = Result<Option<Item<TD>>>;
320 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
321
322 fn into_future(self) -> Self::IntoFuture {
323 Box::pin(self.execute())
324 }
325}