dynamodb_facade/operations/put.rs
1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use super::*;
5
6use aws_sdk_dynamodb::operation::put_item::builders::PutItemFluentBuilder;
7
8/// Builder for a DynamoDB `PutItem` request.
9///
10/// Constructed via [`DynamoDBItemOp::put`] (typed, with a concrete `T`) or
11/// [`PutItemRequest::new`] (stand-alone, raw output). The builder provides:
12///
13/// - **Output format** — the result can be deserialized into `T`.
14/// Call [`.raw()`][PutItemRequest::raw] to receive an untyped [`Item<TD>`]
15/// instead (one-way).
16/// - **Return value** — by default nothing is returned. Call
17/// [`.return_old()`][PutItemRequest::return_old] to request the previous
18/// item, or [`.return_none()`][PutItemRequest::return_none] to revert.
19/// - **Condition** — optionally add a guard expression via
20/// [`.condition()`][PutItemRequest::condition],
21/// [`.exists()`][PutItemRequest::exists], or
22/// [`.not_exists()`][PutItemRequest::not_exists]. DynamoDB accepts a
23/// single condition expression per request, so this can only be called once.
24///
25/// The builder implements [`IntoFuture`], so it can
26/// be `.await`ed directly.
27///
28/// # Errors
29///
30/// Returns [`Err`] if the DynamoDB request fails, if a condition expression
31/// is set and the condition check fails
32/// (`ConditionalCheckFailedException`), or if serialization of `self` fails.
33///
34/// # Examples
35///
36/// ```no_run
37/// # use dynamodb_facade::test_fixtures::*;
38/// use dynamodb_facade::{DynamoDBItemOp, Condition};
39///
40/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
41/// let user = sample_user();
42///
43/// # let client = cclient.clone();
44/// // Simple put
45/// user.put(client).await?;
46///
47/// # let client = cclient.clone();
48/// // Create-only: fails if item already exists
49/// user.put(client).not_exists().await?;
50///
51/// # let client = cclient.clone();
52/// // Custom condition
53/// user.put(client)
54/// .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
55/// .await?;
56///
57/// # let client = cclient.clone();
58/// // Put and return the old item
59/// let old /* : Option<User> */ = user.put(client).return_old().await?;
60/// # Ok(())
61/// # }
62/// ```
63#[must_use = "builder does nothing until awaited or executed"]
64pub struct PutItemRequest<
65 TD: TableDefinition,
66 T = (),
67 O: OutputFormat = Raw,
68 R: ReturnValue = ReturnNothing,
69 C: ConditionState = NoCondition,
70> {
71 builder: PutItemFluentBuilder,
72 _marker: PhantomData<(TD, T, O, R, C)>,
73}
74
75// -- Common methods (all states) --------------------------------------------
76
77impl<TD: TableDefinition, T, R: ReturnValue, O: OutputFormat, C: ConditionState>
78 PutItemRequest<TD, T, O, R, C>
79{
80 /// Consumes the builder and returns the underlying SDK
81 /// [`PutItemFluentBuilder`].
82 ///
83 /// Use this escape hatch when you need to set options not exposed by this
84 /// facade, or when integrating with code that expects the raw SDK builder.
85 ///
86 /// # Examples
87 ///
88 /// ```no_run
89 /// # use dynamodb_facade::test_fixtures::*;
90 /// use dynamodb_facade::DynamoDBItemOp;
91 ///
92 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
93 /// let sdk_builder = sample_user().put(client).into_inner();
94 /// // configure sdk_builder further, then call .send().await
95 /// # Ok(())
96 /// # }
97 /// ```
98 pub fn into_inner(self) -> PutItemFluentBuilder {
99 self.builder
100 }
101}
102
103// -- Stand-alone constructor (ReturnNothing, NoCondition, T = (), O = Raw)
104
105impl<TD: TableDefinition> PutItemRequest<TD, (), Raw> {
106 /// Creates a stand-alone `PutItemRequest` with raw output (`T = ()`, `O = Raw`).
107 ///
108 /// Use this when you already have an [`Item<TD>`] and do not need typed
109 /// deserialization of the old value. For typed access, prefer
110 /// [`DynamoDBItemOp::put`] instead.
111 ///
112 /// # Examples
113 ///
114 /// ```no_run
115 /// # use dynamodb_facade::test_fixtures::*;
116 /// use dynamodb_facade::PutItemRequest;
117 ///
118 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
119 /// let item = sample_user_item();
120 /// PutItemRequest::<PlatformTable>::new(client, item).await?;
121 /// # Ok(())
122 /// # }
123 /// ```
124 pub fn new(client: aws_sdk_dynamodb::Client, item: Item<TD>) -> Self {
125 Self::_new(client, item)
126 }
127}
128
129// -- Constructor (any R, any O, any C) ------------------------------
130
131impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue, C: ConditionState>
132 PutItemRequest<TD, T, O, R, C>
133{
134 /// Creates a new `PutItemRequest` with the given item.
135 pub(super) fn _new(client: aws_sdk_dynamodb::Client, item: Item<TD>) -> Self {
136 let table_name = TD::table_name();
137 tracing::debug!(table_name, "PutItem");
138 Self {
139 builder: client
140 .put_item()
141 .table_name(table_name)
142 .set_item(Some(item.into_inner())),
143 _marker: PhantomData,
144 }
145 }
146}
147
148// -- Return-value transitions (preserve O, C) -------------------------------
149
150impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
151 PutItemRequest<TD, T, O, ReturnNothing, C>
152{
153 /// Requests that DynamoDB return the item's previous attributes after the put.
154 ///
155 /// When executed, [`execute`][PutItemRequest::execute] returns
156 /// `Option<T>` (typed) or `Option<Item<TD>>` (raw) — `None` if no item
157 /// previously existed at that key.
158 ///
159 /// # Examples
160 ///
161 /// ```no_run
162 /// # use dynamodb_facade::test_fixtures::*;
163 /// use dynamodb_facade::DynamoDBItemOp;
164 ///
165 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
166 /// let user = sample_user();
167 /// let old /* : Option<User> */ = user.put(client).return_old().await?;
168 /// // old is None if this was the first put, Some(prev_user) otherwise
169 /// # Ok(())
170 /// # }
171 /// ```
172 pub fn return_old(self) -> PutItemRequest<TD, T, O, Return<Old>, C> {
173 tracing::debug!("PutItem return_old");
174 PutItemRequest {
175 builder: self.builder,
176 _marker: PhantomData,
177 }
178 }
179}
180
181impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
182 PutItemRequest<TD, T, O, Return<Old>, C>
183{
184 /// Reverts the return-value setting so that nothing is returned.
185 ///
186 /// After this call, [`execute`][PutItemRequest::execute] returns `()`
187 /// instead of the old item.
188 ///
189 /// # Examples
190 ///
191 /// ```no_run
192 /// # use dynamodb_facade::test_fixtures::*;
193 /// use dynamodb_facade::DynamoDBItemOp;
194 ///
195 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
196 /// let user = sample_user();
197 /// // Start with return_old, then decide we don't need the old value
198 /// user.put(client).return_old().return_none().await?;
199 /// # Ok(())
200 /// # }
201 /// ```
202 pub fn return_none(self) -> PutItemRequest<TD, T, O, ReturnNothing, C> {
203 tracing::debug!("PutItem return_none");
204 PutItemRequest {
205 builder: self.builder,
206 _marker: PhantomData,
207 }
208 }
209}
210
211// -- Condition (NoCondition only) -------------------------------------------
212
213impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue>
214 PutItemRequest<TD, T, O, R, NoCondition>
215{
216 /// Adds a condition expression that must be satisfied for the put to succeed.
217 ///
218 /// DynamoDB accepts a single condition expression per request, so this
219 /// method can only be called once. If the condition fails at runtime,
220 /// DynamoDB returns a `ConditionalCheckFailedException`.
221 ///
222 /// For the common item exists/not_exists cases, prefer
223 /// the [`.exists()`][PutItemRequest::exists] and
224 /// [`.not_exists()`][PutItemRequest::not_exists] shorthands.
225 ///
226 /// # Examples
227 ///
228 /// ```no_run
229 /// # use dynamodb_facade::test_fixtures::*;
230 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
231 ///
232 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
233 /// let user = sample_user();
234 /// // Put only if the item does not exist OR its TTL has expired
235 /// user.put(client)
236 /// .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
237 /// .await?;
238 /// # Ok(())
239 /// # }
240 /// ```
241 pub fn condition(
242 mut self,
243 condition: Condition<'_>,
244 ) -> PutItemRequest<TD, T, O, R, AlreadyHasCondition> {
245 tracing::debug!(%condition, "PutItem condition");
246 self.builder = condition.apply(self.builder);
247 PutItemRequest {
248 builder: self.builder,
249 _marker: PhantomData,
250 }
251 }
252}
253
254impl<TD: TableDefinition, T: DynamoDBItem<TD>, O: OutputFormat, R: ReturnValue>
255 PutItemRequest<TD, T, O, R, NoCondition>
256{
257 /// Adds an `attribute_exists(<PK>)` condition, requiring the item to already exist.
258 ///
259 /// # Examples
260 ///
261 /// ```no_run
262 /// # use dynamodb_facade::test_fixtures::*;
263 /// use dynamodb_facade::DynamoDBItemOp;
264 ///
265 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
266 /// // Overwrite only if the item already exists
267 /// sample_user().put(client).exists().await?;
268 /// # Ok(())
269 /// # }
270 /// ```
271 pub fn exists(self) -> PutItemRequest<TD, T, O, R, AlreadyHasCondition> {
272 self.condition(T::exists())
273 }
274
275 /// Adds an `attribute_not_exists(<PK>)` condition, requiring the item to not yet exist.
276 ///
277 /// Use this to implement create-only (insert-if-absent) semantics.
278 ///
279 /// # Examples
280 ///
281 /// ```no_run
282 /// # use dynamodb_facade::test_fixtures::*;
283 /// use dynamodb_facade::DynamoDBItemOp;
284 ///
285 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
286 /// // Create-only: fails if user already exists
287 /// sample_user().put(client).not_exists().await?;
288 /// # Ok(())
289 /// # }
290 /// ```
291 pub fn not_exists(self) -> PutItemRequest<TD, T, O, R, AlreadyHasCondition> {
292 self.condition(T::not_exists())
293 }
294}
295
296// -- Output format transition (preserve R, C) -------------------------------
297
298impl<TD: TableDefinition, T, R: ReturnValue, C: ConditionState> PutItemRequest<TD, T, Typed, R, C> {
299 /// Switches the output format from `Typed` to `Raw`.
300 ///
301 /// After calling `.raw()`, [`execute`][PutItemRequest::execute] returns
302 /// `Option<Item<TD>>` instead of `Option<T>` when `Return<Old>` is active.
303 /// This transition is one-way.
304 ///
305 /// # Examples
306 ///
307 /// ```no_run
308 /// # use dynamodb_facade::test_fixtures::*;
309 /// use dynamodb_facade::DynamoDBItemOp;
310 ///
311 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
312 /// let old_raw = sample_user()
313 /// .put(client)
314 /// .return_old()
315 /// .raw()
316 /// .await?;
317 /// // old_raw: Option<Item<PlatformTable>>
318 /// # Ok(())
319 /// # }
320 /// ```
321 pub fn raw(self) -> PutItemRequest<TD, T, Raw, R, C> {
322 PutItemRequest {
323 builder: self.builder,
324 _marker: PhantomData,
325 }
326 }
327}
328
329// -- Terminal: ReturnNothing (any O, any C) ---------------------------------
330
331impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
332 PutItemRequest<TD, T, O, ReturnNothing, C>
333{
334 /// Sends the `PutItem` request, returning nothing on success.
335 ///
336 /// This method is also available implicitly via `.await`.
337 ///
338 /// # Errors
339 ///
340 /// Returns [`Err`] if the DynamoDB request fails or if a condition
341 /// expression is set and the check fails
342 /// (`ConditionalCheckFailedException`).
343 ///
344 /// # Examples
345 ///
346 /// ```no_run
347 /// # use dynamodb_facade::test_fixtures::*;
348 /// use dynamodb_facade::DynamoDBItemOp;
349 ///
350 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
351 /// sample_user().put(client).not_exists().execute().await?;
352 /// # Ok(())
353 /// # }
354 /// ```
355 #[tracing::instrument(level = "debug", skip(self), name = "put_execute")]
356 pub fn execute(self) -> impl Future<Output = Result<()>> + Send + 'static {
357 let builder = self.builder;
358 async move {
359 builder.return_values(SDKReturnValue::None).send().await?;
360 Ok(())
361 }
362 }
363}
364
365impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState> IntoFuture
366 for PutItemRequest<TD, T, O, ReturnNothing, C>
367{
368 type Output = Result<()>;
369 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
370
371 fn into_future(self) -> Self::IntoFuture {
372 Box::pin(self.execute())
373 }
374}
375
376// -- Terminal: ReturnItem<Old> + Typed (any C) ------------------------------
377
378impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState>
379 PutItemRequest<TD, T, Typed, Return<Old>, C>
380{
381 /// Sends the `PutItem` request and returns the previous item deserialized as `T`.
382 ///
383 /// Returns `Ok(None)` if no item previously existed at the key.
384 ///
385 /// This method is also available implicitly via `.await`.
386 ///
387 /// # Errors
388 ///
389 /// Returns [`Err`] if the DynamoDB request fails, if a condition check
390 /// fails, or if deserialization of the returned attributes fails.
391 ///
392 /// # Examples
393 ///
394 /// ```no_run
395 /// # use dynamodb_facade::test_fixtures::*;
396 /// use dynamodb_facade::DynamoDBItemOp;
397 ///
398 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
399 /// let old /* : Option<User> */ = sample_user().put(client).return_old().execute().await?;
400 /// // old is None on first put, Some(previous_user) on subsequent puts
401 /// # Ok(())
402 /// # }
403 /// ```
404 #[tracing::instrument(level = "debug", skip(self), name = "put_execute_old")]
405 pub fn execute(self) -> impl Future<Output = Result<Option<T>>> + Send + 'static {
406 let builder = self.builder;
407 async move {
408 builder
409 .return_values(SDKReturnValue::AllOld)
410 .send()
411 .await?
412 .attributes
413 .map(Item::from_dynamodb_response)
414 .map(T::try_from_item)
415 .transpose()
416 }
417 }
418}
419
420impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState> IntoFuture
421 for PutItemRequest<TD, T, Typed, Return<Old>, C>
422{
423 type Output = Result<Option<T>>;
424 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
425
426 fn into_future(self) -> Self::IntoFuture {
427 Box::pin(self.execute())
428 }
429}
430
431// -- Terminal: ReturnItem<Old> + Raw (any C) --------------------------------
432
433impl<TD: TableDefinition, T, C: ConditionState> PutItemRequest<TD, T, Raw, Return<Old>, C> {
434 /// Sends the `PutItem` request and returns the previous raw item map.
435 ///
436 /// Returns `Ok(None)` if no item previously existed at the key.
437 ///
438 /// This method is also available implicitly via `.await`.
439 ///
440 /// # Errors
441 ///
442 /// Returns [`Err`] if the DynamoDB request fails or if a condition check
443 /// fails.
444 ///
445 /// # Examples
446 ///
447 /// ```no_run
448 /// # use dynamodb_facade::test_fixtures::*;
449 /// use dynamodb_facade::DynamoDBItemOp;
450 ///
451 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
452 /// let old_raw = sample_user()
453 /// .put(client)
454 /// .return_old()
455 /// .raw()
456 /// .execute()
457 /// .await?;
458 /// // old_raw: Option<Item<PlatformTable>>
459 /// # Ok(())
460 /// # }
461 /// ```
462 #[tracing::instrument(level = "debug", skip(self), name = "put_execute_old_raw")]
463 pub fn execute(self) -> impl Future<Output = Result<Option<Item<TD>>>> + Send + 'static {
464 let builder = self.builder;
465 async move {
466 Ok(builder
467 .return_values(SDKReturnValue::AllOld)
468 .send()
469 .await
470 .map(|out| out.attributes.map(Item::from_dynamodb_response))?)
471 }
472 }
473}
474
475impl<TD: TableDefinition, T, C: ConditionState> IntoFuture
476 for PutItemRequest<TD, T, Raw, Return<Old>, C>
477{
478 type Output = Result<Option<Item<TD>>>;
479 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
480
481 fn into_future(self) -> Self::IntoFuture {
482 Box::pin(self.execute())
483 }
484}