diff --git a/swift/example_code/dynamodb/BatchGetItem/Sources/Movie.swift b/swift/example_code/dynamodb/BatchGetItem/Sources/Movie.swift index 835780f23d7..8f936c1720b 100644 --- a/swift/example_code/dynamodb/BatchGetItem/Sources/Movie.swift +++ b/swift/example_code/dynamodb/BatchGetItem/Sources/Movie.swift @@ -5,8 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // snippet-start:[ddb.swift.batchgetitem.movie] -import Foundation import AWSDynamoDB +import Foundation // snippet-start:[ddb.swift.batchgetitem.info] /// The optional details about a movie. @@ -16,6 +16,7 @@ public struct Info: Codable { /// The movie's plot, if available. var plot: String? } + // snippet-end:[ddb.swift.batchgetitem.info] public struct Movie: Codable { @@ -42,6 +43,7 @@ public struct Movie: Codable { self.info = Info(rating: rating, plot: plot) } + // snippet-end:[ddb.swift.batchgetitem.movie.init] // snippet-start:[ddb.swift.batchgetitem.movie.init-info] @@ -63,21 +65,23 @@ public struct Movie: Codable { self.info = Info(rating: nil, plot: nil) } } + // snippet-end:[ddb.swift.batchgetitem.movie.init-info] // snippet-start:[ddb.swift.batchgetitem.movie.init-withitem] /// /// Return a new `MovieTable` object, given an array mapping string to Amazon /// DynamoDB attribute values. - /// + /// /// - Parameter item: The item information provided in the form used by /// DynamoDB. This is an array of strings mapped to /// `DynamoDBClientTypes.AttributeValue` values. - init(withItem item: [Swift.String:DynamoDBClientTypes.AttributeValue]) throws { + init(withItem item: [Swift.String: DynamoDBClientTypes.AttributeValue]) throws { // Read the attributes. guard let titleAttr = item["title"], - let yearAttr = item["year"] else { + let yearAttr = item["year"] + else { throw MovieError.ItemNotFound } let infoAttr = item["info"] ?? nil @@ -116,6 +120,7 @@ public struct Movie: Codable { self.info = Info(rating: rating, plot: plot) } + // snippet-end:[ddb.swift.batchgetitem.movie.init-withitem] // snippet-start:[ddb.swift.batchgetitem.movie.getasitem] @@ -127,19 +132,19 @@ public struct Movie: Codable { /// - Returns: The movie item as an array of type /// `[Swift.String:DynamoDBClientTypes.AttributeValue]`. /// - func getAsItem() async throws -> [Swift.String:DynamoDBClientTypes.AttributeValue] { + func getAsItem() async throws -> [Swift.String: DynamoDBClientTypes.AttributeValue] { // Build the item record, starting with the year and title, which are // always present. - var item: [Swift.String:DynamoDBClientTypes.AttributeValue] = [ - "year": .n(String(self.year)), - "title": .s(self.title) + var item: [Swift.String: DynamoDBClientTypes.AttributeValue] = [ + "year": .n(String(year)), + "title": .s(title) ] // Add the `info` field with the rating and/or plot if they're // available. - var info: [Swift.String:DynamoDBClientTypes.AttributeValue] = [:] + var info: [Swift.String: DynamoDBClientTypes.AttributeValue] = [:] if self.info.rating != nil { info["rating"] = .n(String(self.info.rating!)) } @@ -152,4 +157,5 @@ public struct Movie: Codable { } // snippet-end:[ddb.swift.batchgetitem.movie.getasitem] } + // snippet-end:[ddb.swift.batchgetitem.movie] diff --git a/swift/example_code/dynamodb/BatchGetItem/Sources/MovieDatabase.swift b/swift/example_code/dynamodb/BatchGetItem/Sources/MovieDatabase.swift index fc1ff3289f8..030204b05dc 100644 --- a/swift/example_code/dynamodb/BatchGetItem/Sources/MovieDatabase.swift +++ b/swift/example_code/dynamodb/BatchGetItem/Sources/MovieDatabase.swift @@ -6,8 +6,8 @@ /// SPDX-License-Identifier: Apache-2.0 // snippet-start:[ddb.swift.batchgetitem.moviedatabase] -import Foundation import AWSDynamoDB +import Foundation // snippet-start:[ddb.swift.batchgetitem.movieerror] /// Errors that can be thrown by the `Movie` struct and the `MovieDatabase` @@ -30,20 +30,21 @@ enum MovieError: Error { /// valid. case InvalidResponse } + // snippet-end:[ddb.swift.batchgetitem.movieerror] /// A class used to access the movies in a DynamoDB database. public class MovieDatabase { let tableName: String - var ddbClient: DynamoDBClient? = nil + var ddbClient: DynamoDBClient? // snippet-start:[ddb.swift.batchgetitem.moviedatabase.init] /// Create a new `MovieDatabase`. This includes creating the DynamoDB - /// table and optionally preloading it with the contents of a specified + /// table and preloading it with the contents of a specified /// JSON file. /// /// - Parameters: - /// - region: The AWS Region in which to create the table. + /// - region: The optional AWS Region in which to create the table. /// - jsonPath: The path name of a JSON file containing movie data with /// which to populate the table. /// @@ -53,47 +54,57 @@ public class MovieDatabase { /// - `MovieError.CreateTableFailed` if the DynamoDB table could not /// be created. /// - Appropriate DynamoDB errors might be thrown also. - init(region: String = "us-east-2", jsonPath: String) async throws { - ddbClient = try DynamoDBClient(region: region) - - tableName = "ddb-batchgetitem-sample-\(Int.random(in: 1...Int.max))" - - let input = CreateTableInput( - attributeDefinitions: [ - DynamoDBClientTypes.AttributeDefinition(attributeName: "year", attributeType: .n), - DynamoDBClientTypes.AttributeDefinition(attributeName: "title", attributeType: .s), - ], - keySchema: [ - DynamoDBClientTypes.KeySchemaElement(attributeName: "year", keyType: .hash), - DynamoDBClientTypes.KeySchemaElement(attributeName: "title", keyType: .range) - ], - provisionedThroughput: DynamoDBClientTypes.ProvisionedThroughput( - readCapacityUnits: 10, - writeCapacityUnits: 10 - ), - tableName: self.tableName - ) - - // Get safe access to the `DynamoDBClient`. - - guard let client = self.ddbClient else { - throw MovieError.ClientUninitialized - } - - // Create the table. If the table description in the response is - // `nil`, throw an exception. - - let output = try await client.createTable(input: input) - if output.tableDescription == nil { - throw MovieError.CreateTableFailed + init(region: String? = nil, jsonPath: String) async throws { + do { + let config = try await DynamoDBClient.DynamoDBClientConfiguration() + if let region = region { + config.region = region + } + self.ddbClient = DynamoDBClient(config: config) + + self.tableName = "ddb-batchgetitem-sample-\(Int.random(in: 1 ... Int.max))" + + let input = CreateTableInput( + attributeDefinitions: [ + DynamoDBClientTypes.AttributeDefinition(attributeName: "year", attributeType: .n), + DynamoDBClientTypes.AttributeDefinition(attributeName: "title", attributeType: .s) + ], + keySchema: [ + DynamoDBClientTypes.KeySchemaElement(attributeName: "year", keyType: .hash), + DynamoDBClientTypes.KeySchemaElement(attributeName: "title", keyType: .range) + ], + provisionedThroughput: DynamoDBClientTypes.ProvisionedThroughput( + readCapacityUnits: 10, + writeCapacityUnits: 10 + ), + tableName: self.tableName + ) + + // Get safe access to the `DynamoDBClient`. + + guard let client = self.ddbClient else { + throw MovieError.ClientUninitialized + } + + // Create the table. If the table description in the response is + // `nil`, throw an exception. + + let output = try await client.createTable(input: input) + if output.tableDescription == nil { + throw MovieError.CreateTableFailed + } + + /// Wait until the table has been created and is active, then populate + /// the database from the file. + + try await self.awaitTableActive() + try await self.populate(jsonPath: jsonPath) + } catch { + print("ERROR: ", dump(error, name: "Initializing DynamoDBClient client")) + throw error } - - /// Wait until the table has been created and is active, then populate - /// the database from the file. - - try await awaitTableActive() - try await self.populate(jsonPath: jsonPath) } + // snippet-end:[ddb.swift.batchgetitem.moviedatabase.init] // snippet-start:[ddb.swift.batchgetitem.deletetable] @@ -104,15 +115,21 @@ public class MovieDatabase { /// been initialized. /// - DynamoDB errors are thrown without change. func deleteTable() async throws { - guard let client = self.ddbClient else { - throw MovieError.ClientUninitialized + do { + guard let client = self.ddbClient else { + throw MovieError.ClientUninitialized + } + + let input = DeleteTableInput( + tableName: self.tableName + ) + _ = try await client.deleteTable(input: input) + } catch { + print("ERROR: deleteTable", dump(error)) + throw error } - - let input = DeleteTableInput( - tableName: self.tableName - ) - _ = try await client.deleteTable(input: input) } + // snippet-end:[ddb.swift.batchgetitem.deletetable] // snippet-start:[ddb.swift.batchgetitem.tableexists] @@ -125,20 +142,26 @@ public class MovieDatabase { /// been initialized. /// - DynamoDB errors are thrown without change. func tableExists() async throws -> Bool { - guard let client = self.ddbClient else { - throw MovieError.ClientUninitialized - } - - let input = DescribeTableInput( - tableName: tableName - ) - let output = try await client.describeTable(input: input) - guard let description = output.table else { - return false + do { + guard let client = self.ddbClient else { + throw MovieError.ClientUninitialized + } + + let input = DescribeTableInput( + tableName: tableName + ) + let output = try await client.describeTable(input: input) + guard let description = output.table else { + return false + } + + return description.tableName == self.tableName + } catch { + print("ERROR: tableExists", dump(error)) + throw error } - - return (description.tableName == self.tableName) } + // snippet-end:[ddb.swift.batchgetitem.tableexists] // snippet-start:[ddb.swift.batchgetitem.gettablestatus] @@ -155,22 +178,28 @@ public class MovieDatabase { /// determined. /// - DynamoDB errors are thrown without change. func getTableStatus() async throws -> DynamoDBClientTypes.TableStatus { - guard let client = self.ddbClient else { - throw MovieError.ClientUninitialized - } - - let input = DescribeTableInput( - tableName: self.tableName - ) - let output = try await client.describeTable(input: input) - guard let description = output.table else { - throw MovieError.TableNotFound - } - guard let status = description.tableStatus else { - throw MovieError.StatusUnknown + do { + guard let client = self.ddbClient else { + throw MovieError.ClientUninitialized + } + + let input = DescribeTableInput( + tableName: self.tableName + ) + let output = try await client.describeTable(input: input) + guard let description = output.table else { + throw MovieError.TableNotFound + } + guard let status = description.tableStatus else { + throw MovieError.StatusUnknown + } + return status + } catch { + print("ERROR: getTableStatus", dump(error)) + throw error } - return status } + // snippet-end:[ddb.swift.batchgetitem.gettablestatus] // snippet-start:[ddb.swift.batchgetitem.awaittableactive] @@ -183,14 +212,25 @@ public class MovieDatabase { /// - `MovieError.StatusUnknown` if the table status couldn't be determined. /// - DynamoDB errors are thrown without change. func awaitTableActive() async throws { - while (try await tableExists() == false) { - Thread.sleep(forTimeInterval: 0.25) + while try (await self.tableExists() == false) { + do { + let duration = UInt64(0.25 * 1_000_000_000) // Convert .25 seconds to nanoseconds. + try await Task.sleep(nanoseconds: duration) + } catch { + print("Sleep error:", dump(error)) + } } - while (try await getTableStatus() != .active) { - Thread.sleep(forTimeInterval: 0.25) + while try (await self.getTableStatus() != .active) { + do { + let duration = UInt64(0.25 * 1_000_000_000) // Convert .25 seconds to nanoseconds. + try await Task.sleep(nanoseconds: duration) + } catch { + print("Sleep error:", dump(error)) + } } } + // snippet-end:[ddb.swift.batchgetitem.awaittableactive] // snippet-start:[ddb.swift.batchgetitem.populate] @@ -205,54 +245,60 @@ public class MovieDatabase { /// been initialized. /// - DynamoDB errors are thrown without change. fileprivate func populate(jsonPath: String) async throws { - guard let client = self.ddbClient else { - throw MovieError.ClientUninitialized - } - - // Create a Swift `URL` and use it to load the file into a `Data` - // object. Then decode the JSON into an array of `Movie` objects. - - let fileUrl = URL(fileURLWithPath: jsonPath) - let jsonData = try Data(contentsOf: fileUrl) - - var movieList = try JSONDecoder().decode([Movie].self, from: jsonData) - - // Truncate the list to the first 200 entries or so for this example. - - if movieList.count > 200 { - movieList = Array(movieList[...199]) - } - - // Before sending records to the database, break the movie list into - // 25-entry chunks, which is the maximum size of a batch item request. - - let count = movieList.count - let chunks = stride(from: 0, to: count, by: 25).map { - Array(movieList[$0 ..< Swift.min($0 + 25, count)]) - } - - // For each chunk, create a list of write request records and populate - // them with `PutRequest` requests, each specifying one movie from the - // chunk. After the chunk's items are all in the `PutRequest` list, - // send them to Amazon DynamoDB using the - // `DynamoDBClient.batchWriteItem()` function. - - for chunk in chunks { - var requestList: [DynamoDBClientTypes.WriteRequest] = [] - for movie in chunk { - let item: [String : DynamoDBClientTypes.AttributeValue] = try await movie.getAsItem() - let request = DynamoDBClientTypes.WriteRequest( - putRequest: .init( - item: item + do { + guard let client = self.ddbClient else { + throw MovieError.ClientUninitialized + } + + // Create a Swift `URL` and use it to load the file into a `Data` + // object. Then decode the JSON into an array of `Movie` objects. + + let fileUrl = URL(fileURLWithPath: jsonPath) + let jsonData = try Data(contentsOf: fileUrl) + + var movieList = try JSONDecoder().decode([Movie].self, from: jsonData) + + // Truncate the list to the first 200 entries or so for this example. + + if movieList.count > 200 { + movieList = Array(movieList[...199]) + } + + // Before sending records to the database, break the movie list into + // 25-entry chunks, which is the maximum size of a batch item request. + + let count = movieList.count + let chunks = stride(from: 0, to: count, by: 25).map { + Array(movieList[$0 ..< Swift.min($0 + 25, count)]) + } + + // For each chunk, create a list of write request records and populate + // them with `PutRequest` requests, each specifying one movie from the + // chunk. After the chunk's items are all in the `PutRequest` list, + // send them to Amazon DynamoDB using the + // `DynamoDBClient.batchWriteItem()` function. + + for chunk in chunks { + var requestList: [DynamoDBClientTypes.WriteRequest] = [] + for movie in chunk { + let item: [String: DynamoDBClientTypes.AttributeValue] = try await movie.getAsItem() + let request = DynamoDBClientTypes.WriteRequest( + putRequest: .init( + item: item + ) ) - ) - requestList.append(request) + requestList.append(request) + } + + let input = BatchWriteItemInput(requestItems: [self.tableName: requestList]) + _ = try await client.batchWriteItem(input: input) } - - let input = BatchWriteItemInput(requestItems: [self.tableName: requestList]) - _ = try await client.batchWriteItem(input: input) + } catch { + print("ERROR: populate", dump(error)) + throw error } } + // snippet-end:[ddb.swift.batchgetitem.populate] // snippet-start:[ddb.swift.batchgetitem.batchget] @@ -273,68 +319,74 @@ public class MovieDatabase { /// been initialized. /// - DynamoDB errors are thrown without change. func batchGet(keys: [(title: String, year: Int)]) async throws -> [Movie] { - guard let client = self.ddbClient else { - throw MovieError.ClientUninitialized - } - - var movieList: [Movie] = [] - var keyItems: [[Swift.String:DynamoDBClientTypes.AttributeValue]] = [] - - // Convert the list of keys into the form used by DynamoDB. - - for key in keys { - let item: [Swift.String:DynamoDBClientTypes.AttributeValue] = [ - "title": .s(key.title), - "year": .n(String(key.year)) - ] - keyItems.append(item) - } - - // Create the input record for `batchGetItem()`. The list of requested - // items is in the `requestItems` property. This array contains one - // entry for each table from which items are to be fetched. In this - // example, there's only one table containing the movie data. - // - // If we wanted this program to also support searching for matches - // in a table of book data, we could add a second `requestItem` - // mapping the name of the book table to the list of items we want to - // find in it. - let input = BatchGetItemInput( - requestItems: [ - self.tableName: .init( - consistentRead: true, - keys: keyItems - ) - ] - ) - - // Fetch the matching movies from the table. - - let output = try await client.batchGetItem(input: input) - - // Get the set of responses. If there aren't any, return the empty - // movie list. - - guard let responses = output.responses else { - return movieList - } - - // Get the list of matching items for the table with the name - // `tableName`. - - guard let responseList = responses[self.tableName] else { + do { + guard let client = self.ddbClient else { + throw MovieError.ClientUninitialized + } + + var movieList: [Movie] = [] + var keyItems: [[Swift.String: DynamoDBClientTypes.AttributeValue]] = [] + + // Convert the list of keys into the form used by DynamoDB. + + for key in keys { + let item: [Swift.String: DynamoDBClientTypes.AttributeValue] = [ + "title": .s(key.title), + "year": .n(String(key.year)) + ] + keyItems.append(item) + } + + // Create the input record for `batchGetItem()`. The list of requested + // items is in the `requestItems` property. This array contains one + // entry for each table from which items are to be fetched. In this + // example, there's only one table containing the movie data. + // + // If we wanted this program to also support searching for matches + // in a table of book data, we could add a second `requestItem` + // mapping the name of the book table to the list of items we want to + // find in it. + let input = BatchGetItemInput( + requestItems: [ + self.tableName: .init( + consistentRead: true, + keys: keyItems + ) + ] + ) + + // Fetch the matching movies from the table. + + let output = try await client.batchGetItem(input: input) + + // Get the set of responses. If there aren't any, return the empty + // movie list. + + guard let responses = output.responses else { + return movieList + } + + // Get the list of matching items for the table with the name + // `tableName`. + + guard let responseList = responses[self.tableName] else { + return movieList + } + + // Create `Movie` items for each of the matching movies in the table + // and add them to the `MovieList` array. + + for response in responseList { + try movieList.append(Movie(withItem: response)) + } + return movieList + } catch { + print("ERROR: batchGet", dump(error)) + throw error } - - // Create `Movie` items for each of the matching movies in the table - // and add them to the `MovieList` array. - - for response in responseList { - movieList.append(try Movie(withItem: response)) - } - - return movieList } // snippet-end:[ddb.swift.batchgetitem.batchget] } + // snippet-end:[ddb.swift.batchgetitem.moviedatabase] diff --git a/swift/example_code/dynamodb/BatchGetItem/Sources/batchgetitem.swift b/swift/example_code/dynamodb/BatchGetItem/Sources/batchgetitem.swift index 651e7447e01..ddb6146359b 100644 --- a/swift/example_code/dynamodb/BatchGetItem/Sources/batchgetitem.swift +++ b/swift/example_code/dynamodb/BatchGetItem/Sources/batchgetitem.swift @@ -7,17 +7,28 @@ /// SPDX-License-Identifier: Apache-2.0 // snippet-start:[ddb.swift.batchgetitem] -import Foundation import ArgumentParser import AWSDynamoDB import ClientRuntime +import Foundation + +extension String { + // Get the directory if the string is a file path. + func directory() -> String { + guard let lastIndex = lastIndex(of: "/") else { + print("Error: String directory separator not found.") + return "" + } + return String(self[...lastIndex]) + } +} struct ExampleCommand: ParsableCommand { @Argument(help: "The path of the sample movie data JSON file.") - var jsonPath: String = "../../../../resources/sample_files/movies.json" + var jsonPath: String = #file.directory() + "../../../../../resources/sample_files/movies.json" @Option(help: "The AWS Region to run AWS API calls in.") - var awsRegion = "us-east-2" + var awsRegion: String? @Option( help: ArgumentHelp("The level of logging for the Swift SDK to perform."), @@ -53,7 +64,7 @@ struct ExampleCommand: ParsableCommand { print("Please wait while the database is installed and searched...\n") let database = try await MovieDatabase(jsonPath: jsonPath) - + let movies = try await database.batchGet(keys: [ (title: "Titanic", year: 1997), (title: "The Shawshank Redemption", year: 1994), @@ -82,4 +93,5 @@ struct Main { } } } + // snippet-end:[ddb.swift.batchgetitem] diff --git a/swift/example_code/dynamodb/BatchGetItem/Tests/MovieDatabaseTests.swift b/swift/example_code/dynamodb/BatchGetItem/Tests/MovieDatabaseTests.swift index 55f5573385a..f461b11dbc2 100644 --- a/swift/example_code/dynamodb/BatchGetItem/Tests/MovieDatabaseTests.swift +++ b/swift/example_code/dynamodb/BatchGetItem/Tests/MovieDatabaseTests.swift @@ -13,8 +13,7 @@ import ClientRuntime /// Perform tests on the `MovieDatabase` class. final class MovieDatabaseTests: XCTestCase { - let region = "us-east-2" - let jsonPath: String = "../../../../resources/sample_files/movies.json" + let jsonPath: String = #file.directory() + "../../../../../resources/sample_files/movies.json" /// Class-wide setup function for the test case, which is run *once* /// before any tests are run. @@ -31,7 +30,7 @@ final class MovieDatabaseTests: XCTestCase { do { _ = try await MovieDatabase(jsonPath: "path/that/does/not/exist.json") } catch { - XCTAssertEqual(2, error._code, "Unexpected error returned when creating database.") + XCTAssertEqual(260, error._code, "Unexpected error returned when creating database.") return } XCTFail("Attempt to initialize the database succeeded with invalid JSON path specified.") @@ -240,4 +239,4 @@ final class MovieDatabaseTests: XCTestCase { XCTFail("Unexpected error deleting table: \(error.localizedDescription)") } } -} \ No newline at end of file +} diff --git a/swift/example_code/dynamodb/ListTables/Sources/DatabaseManager.swift b/swift/example_code/dynamodb/ListTables/Sources/DatabaseManager.swift index 440e9df1ff4..3202b57a9e5 100644 --- a/swift/example_code/dynamodb/ListTables/Sources/DatabaseManager.swift +++ b/swift/example_code/dynamodb/ListTables/Sources/DatabaseManager.swift @@ -6,9 +6,9 @@ /// SPDX-License-Identifier: Apache-2.0 // snippet-start:[ddb.swift.databasemanager-all] -import Foundation import AWSDynamoDB import ClientRuntime +import Foundation /// A protocol describing the implementation of functions that allow either /// calling through to Amazon DynamoDB or mocking DynamoDB functions. @@ -22,15 +22,14 @@ public protocol DatabaseSession { /// - Parameter input: A `ListTablesInput` object specifying the input /// parameters for the call to `listTables()`. /// - /// - Returns: A `ListTablesOutput` structure with the results. - func listTables(input: ListTablesInput) async throws -> ListTablesOutput + /// - Returns: A `[String]` list of table names.. + func listTables(input: ListTablesInput) async throws -> [String] } // snippet-start:[ddb.swift.dynamodbsession] /// An implementation of the `DatabaseSession` protocol that calls through to /// DynamoDB for its operations. public struct DynamoDBSession: DatabaseSession { - let awsRegion: String let client: DynamoDBClient // snippet-start:[ddb.swift.dynamodbsession.init] @@ -38,10 +37,17 @@ public struct DynamoDBSession: DatabaseSession { /// /// - Parameter region: The AWS Region to use for DynamoDB. /// - init(region: String = "us-east-2") throws { - self.awsRegion = region - self.client = try DynamoDBClient(region: awsRegion) + init(region: String? = nil) async throws { + do { + let config = try await DynamoDBClient.DynamoDBClientConfiguration() + if let region = region { + config.region = region + } + + self.client = DynamoDBClient(config: config) + } } + // snippet-end:[ddb.swift.dynamodbsession.init] // snippet-start:[ddb.swift.dynamodbsession.listtables] @@ -52,13 +58,31 @@ public struct DynamoDBSession: DatabaseSession { /// `ListTablesInput` object. /// /// - Returns: The `ListTablesOutput` returned by `listTables()`. - /// + /// /// - Throws: Errors from DynamoDB are thrown as usual. - public func listTables(input: ListTablesInput) async throws -> ListTablesOutput { - return try await client.listTables(input: input) + public func listTables(input: ListTablesInput) async throws -> [String] { + do { + // Use "Paginated" to get all the tables. + // This lets the SDK handle the 'lastEvaluatedTableName' property in "ListTablesOutput". + let pages = client.listTablesPaginated(input: input) + + var allTableNames: [String] = [] + for try await page in pages { + guard let tableNames = page.tableNames else { + print("Error: no table names returned.") + continue + } + allTableNames += tableNames + } + return allTableNames + } catch { + print("ERROR: listTables:", dump(error)) + throw error + } } // snippet-end:[ddb.swift.dynamodbsession.listtables] } + // snippet-end:[ddb.swift.dynamodbsession] // snippet-start:[ddb.swift.databasemanager] @@ -77,6 +101,7 @@ public class DatabaseManager { init(session: DatabaseSession) { self.session = session } + // snippet-end:[ddb.swift.databasemanager.init] // snippet-start:[ddb.swift.databasemanager.gettablelist] @@ -85,29 +110,12 @@ public class DatabaseManager { /// - Returns: An array of strings listing all of the tables available /// in the Region specified when the session was created. public func getTableList() async throws -> [String] { - var tableList: [String] = [] - var lastEvaluated: String? = nil - - // Iterate over the list of tables, 25 at a time, until we have the - // names of every table. Add each group to the `tableList` array. - // Iteration is complete when `output.lastEvaluatedTableName` is `nil`. - - repeat { - let input = ListTablesInput( - exclusiveStartTableName: lastEvaluated, - limit: 25 - ) - let output = try await self.session.listTables(input: input) - guard let tableNames = output.tableNames else { - return tableList - } - tableList.append(contentsOf: tableNames) - lastEvaluated = output.lastEvaluatedTableName - } while lastEvaluated != nil - - return tableList + let input = ListTablesInput( + ) + return try await session.listTables(input: input) } // snippet-end:[ddb.swift.databasemanager.gettablelist] } + // snippet-end:[ddb.swift.databasemanager] // snippet-end:[ddb.swift.databasemanager-all] diff --git a/swift/example_code/dynamodb/ListTables/Sources/listtables.swift b/swift/example_code/dynamodb/ListTables/Sources/listtables.swift index 04d7d633dcc..1af3b5b371e 100644 --- a/swift/example_code/dynamodb/ListTables/Sources/listtables.swift +++ b/swift/example_code/dynamodb/ListTables/Sources/listtables.swift @@ -7,14 +7,14 @@ /// SPDX-License-Identifier: Apache-2.0 // snippet-start:[ddb.swift.listtables] -import Foundation import ArgumentParser import AWSDynamoDB import ClientRuntime +import Foundation struct ExampleCommand: ParsableCommand { @Option(help: "The AWS Region to run AWS API calls in.") - var awsRegion = "us-east-2" + var awsRegion: String? @Option( help: ArgumentHelp("The level of logging for the Swift SDK to perform."), @@ -45,7 +45,7 @@ struct ExampleCommand: ParsableCommand { /// Called by ``main()`` to asynchronously run the AWS example. func runAsync() async throws { - let session = try DynamoDBSession(region: awsRegion) + let session = try await DynamoDBSession(region: awsRegion) let dbManager = DatabaseManager(session: session) let tableList = try await dbManager.getTableList() @@ -72,4 +72,5 @@ struct Main { } } } + // snippet-end:[ddb.swift.listtables] diff --git a/swift/example_code/dynamodb/ListTables/Tests/ListTablesTests.swift b/swift/example_code/dynamodb/ListTables/Tests/ListTablesTests.swift index 14c949af7f6..8a3b27e3498 100644 --- a/swift/example_code/dynamodb/ListTables/Tests/ListTablesTests.swift +++ b/swift/example_code/dynamodb/ListTables/Tests/ListTablesTests.swift @@ -80,54 +80,19 @@ public struct MockDBSession: DatabaseSession { /// Mock version of the DynamoDB client's `listTables()` function. Returns /// values from the string array `fakeTableNames` or the one specified as /// an optional input when creating the `MockDBSession`. - public func listTables(input: ListTablesInput) async throws -> ListTablesOutput { - var output = ListTablesOutput( - lastEvaluatedTableName: nil, - tableNames: nil - ) - - // Stop at once if there are no table names in the list. - - if testTableNames.count == 0 { - return output + public func listTables(input: ListTablesInput) async throws -> [String] { + var maxTables = testTableNames.count + if let limit = input.limit { + maxTables = Swift.min(limit, maxTables) } - var startIndex: Int - - // Get the starting point in the list using the input's - // `exclusiveStartTableName` property. If it's `nil` or the string - // isn't found in the list, use 0 for the index. - - if input.exclusiveStartTableName != nil { - startIndex = testTableNames.firstIndex(of: input.exclusiveStartTableName!) ?? 0 - } else { - startIndex = 0 - } - - // Split the full list of table names into the number of parts - // specified by the `limit` parameter, or 100 parts if `limit` is not - // specified. - - let chunkSize = input.limit ?? 100 - let chunks: [[String]] = stride(from: startIndex, to: testTableNames.count, by: chunkSize).map { - Array(testTableNames[$0 ..< Swift.min($0 + chunkSize, testTableNames.count)]) - } - - output.tableNames = chunks[0] - if chunks.count == 1 { - output.lastEvaluatedTableName = nil - } else { - output.lastEvaluatedTableName = chunks[1].first - } - - return output + return Array(testTableNames[0.. diff --git a/swift/example_code/dynamodb/basics/MovieList/MovieTable.swift b/swift/example_code/dynamodb/basics/MovieList/MovieTable.swift index bd663f97326..18a52de0a05 100644 --- a/swift/example_code/dynamodb/basics/MovieList/MovieTable.swift +++ b/swift/example_code/dynamodb/basics/MovieList/MovieTable.swift @@ -5,8 +5,8 @@ // using a Swift class. // snippet-start:[ddb.swift.basics.movietable] -import Foundation import AWSDynamoDB +import Foundation /// An enumeration of error codes representing issues that can arise when using /// the `MovieTable` class. @@ -23,18 +23,17 @@ enum MoviesError: Error { case InvalidAttributes } - /// A class representing an Amazon DynamoDB table containing movie /// information. public class MovieTable { - var ddbClient: DynamoDBClient? = nil + var ddbClient: DynamoDBClient? let tableName: String /// Create an object representing a movie table in an Amazon DynamoDB /// database. /// /// - Parameters: - /// - region: The Amazon Region to create the database in. + /// - region: The optional Amazon Region to create the database in. /// - tableName: The name to assign to the table. If not specified, a /// random table name is generated automatically. /// @@ -43,11 +42,21 @@ public class MovieTable { /// `awaitTableActive()` to wait until the table's status is reported as /// ready to use by Amazon DynamoDB. /// - init(region: String = "us-east-2", tableName: String) async throws { - ddbClient = try DynamoDBClient(region: region) - self.tableName = tableName + init(region: String? = nil, tableName: String) async throws { + do { + let config = try await DynamoDBClient.DynamoDBClientConfiguration() + if let region = region { + config.region = region + } + + self.ddbClient = DynamoDBClient(config: config) + self.tableName = tableName - try await self.createTable() + try await self.createTable() + } catch { + print("ERROR: ", dump(error, name: "Initializing Amazon DynamoDBClient client")) + throw error + } } // snippet-start:[ddb.swift.basics.createtable] @@ -55,30 +64,36 @@ public class MovieTable { /// Create a movie table in the Amazon DynamoDB data store. /// private func createTable() async throws { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - let input = CreateTableInput( - attributeDefinitions: [ - DynamoDBClientTypes.AttributeDefinition(attributeName: "year", attributeType: .n), - DynamoDBClientTypes.AttributeDefinition(attributeName: "title", attributeType: .s), - ], - keySchema: [ - DynamoDBClientTypes.KeySchemaElement(attributeName: "year", keyType: .hash), - DynamoDBClientTypes.KeySchemaElement(attributeName: "title", keyType: .range) - ], - provisionedThroughput: DynamoDBClientTypes.ProvisionedThroughput( - readCapacityUnits: 10, - writeCapacityUnits: 10 - ), - tableName: self.tableName - ) - let output = try await client.createTable(input: input) - if output.tableDescription == nil { - throw MoviesError.TableNotFound + let input = CreateTableInput( + attributeDefinitions: [ + DynamoDBClientTypes.AttributeDefinition(attributeName: "year", attributeType: .n), + DynamoDBClientTypes.AttributeDefinition(attributeName: "title", attributeType: .s) + ], + keySchema: [ + DynamoDBClientTypes.KeySchemaElement(attributeName: "year", keyType: .hash), + DynamoDBClientTypes.KeySchemaElement(attributeName: "title", keyType: .range) + ], + provisionedThroughput: DynamoDBClientTypes.ProvisionedThroughput( + readCapacityUnits: 10, + writeCapacityUnits: 10 + ), + tableName: self.tableName + ) + let output = try await client.createTable(input: input) + if output.tableDescription == nil { + throw MoviesError.TableNotFound + } + } catch { + print("ERROR: createTable:", dump(error)) + throw error } } + // snippet-end:[ddb.swift.basics.createtable] // snippet-start:[ddb.swift.basics.tableexists] @@ -87,20 +102,26 @@ public class MovieTable { /// - Returns: `true` if the table exists, or `false` if not. /// func tableExists() async throws -> Bool { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } + + let input = DescribeTableInput( + tableName: tableName + ) + let output = try await client.describeTable(input: input) + guard let description = output.table else { + throw MoviesError.TableNotFound + } - let input = DescribeTableInput( - tableName: tableName - ) - let output = try await client.describeTable(input: input) - guard let description = output.table else { - throw MoviesError.TableNotFound + return description.tableName == self.tableName + } catch { + print("ERROR: tableExists:", dump(error)) + throw error } - - return (description.tableName == self.tableName) } + // snippet-end:[ddb.swift.basics.tableexists] // snippet-start:[ddb.swift.basics.awaittableactive] @@ -108,14 +129,25 @@ public class MovieTable { /// Waits for the table to exist and for its status to be active. /// func awaitTableActive() async throws { - while (try await tableExists() == false) { - Thread.sleep(forTimeInterval: 0.25) + while try (await self.tableExists() == false) { + do { + let duration = UInt64(0.25 * 1_000_000_000) // Convert .25 seconds to nanoseconds. + try await Task.sleep(nanoseconds: duration) + } catch { + print("Sleep error:", dump(error)) + } } - while (try await getTableStatus() != .active) { - Thread.sleep(forTimeInterval: 0.25) + while try (await self.getTableStatus() != .active) { + do { + let duration = UInt64(0.25 * 1_000_000_000) // Convert .25 seconds to nanoseconds. + try await Task.sleep(nanoseconds: duration) + } catch { + print("Sleep error:", dump(error)) + } } } + // snippet-end:[ddb.swift.basics.awaittableactive] // snippet-start:[ddb.swift.basics.deletetable] @@ -123,15 +155,21 @@ public class MovieTable { /// Deletes the table from Amazon DynamoDB. /// func deleteTable() async throws { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } + + let input = DeleteTableInput( + tableName: self.tableName + ) + _ = try await client.deleteTable(input: input) + } catch { + print("ERROR: deleteTable:", dump(error)) + throw error } - - let input = DeleteTableInput( - tableName: self.tableName - ) - _ = try await client.deleteTable(input: input) } + // snippet-end:[ddb.swift.basics.deletetable] // snippet-start:[ddb.swift.basics.gettablestatus] @@ -141,22 +179,28 @@ public class MovieTable { /// `DynamoDBClientTypes.TableStatus` enum. /// func getTableStatus() async throws -> DynamoDBClientTypes.TableStatus { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - let input = DescribeTableInput( - tableName: self.tableName - ) - let output = try await client.describeTable(input: input) - guard let description = output.table else { - throw MoviesError.TableNotFound - } - guard let status = description.tableStatus else { - throw MoviesError.StatusUnknown + let input = DescribeTableInput( + tableName: self.tableName + ) + let output = try await client.describeTable(input: input) + guard let description = output.table else { + throw MoviesError.TableNotFound + } + guard let status = description.tableStatus else { + throw MoviesError.StatusUnknown + } + return status + } catch { + print("ERROR: getTableStatus:", dump(error)) + throw error } - return status } + // snippet-end:[ddb.swift.basics.gettablestatus] // snippet-start:[ddb.swift.basics.populate] @@ -165,84 +209,96 @@ public class MovieTable { /// - Parameter jsonPath: Path to a JSON file containing movie data. /// func populate(jsonPath: String) async throws { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - // Create a Swift `URL` and use it to load the file into a `Data` - // object. Then decode the JSON into an array of `Movie` objects. + // Create a Swift `URL` and use it to load the file into a `Data` + // object. Then decode the JSON into an array of `Movie` objects. - let fileUrl = URL(fileURLWithPath: jsonPath) - let jsonData = try Data(contentsOf: fileUrl) + let fileUrl = URL(fileURLWithPath: jsonPath) + let jsonData = try Data(contentsOf: fileUrl) - var movieList = try JSONDecoder().decode([Movie].self, from: jsonData) + var movieList = try JSONDecoder().decode([Movie].self, from: jsonData) - // Truncate the list to the first 200 entries or so for this example. + // Truncate the list to the first 200 entries or so for this example. - if movieList.count > 200 { - movieList = Array(movieList[...199]) - } + if movieList.count > 200 { + movieList = Array(movieList[...199]) + } - // Before sending records to the database, break the movie list into - // 25-entry chunks, which is the maximum size of a batch item request. + // Before sending records to the database, break the movie list into + // 25-entry chunks, which is the maximum size of a batch item request. - let count = movieList.count - let chunks = stride(from: 0, to: count, by: 25).map { - Array(movieList[$0 ..< Swift.min($0 + 25, count)]) - } + let count = movieList.count + let chunks = stride(from: 0, to: count, by: 25).map { + Array(movieList[$0 ..< Swift.min($0 + 25, count)]) + } - // For each chunk, create a list of write request records and populate - // them with `PutRequest` requests, each specifying one movie from the - // chunk. Once the chunk's items are all in the `PutRequest` list, - // send them to Amazon DynamoDB using the - // `DynamoDBClient.batchWriteItem()` function. - - for chunk in chunks { - var requestList: [DynamoDBClientTypes.WriteRequest] = [] - - for movie in chunk { - let item = try await movie.getAsItem() - let request = DynamoDBClientTypes.WriteRequest( - putRequest: .init( - item: item + // For each chunk, create a list of write request records and populate + // them with `PutRequest` requests, each specifying one movie from the + // chunk. Once the chunk's items are all in the `PutRequest` list, + // send them to Amazon DynamoDB using the + // `DynamoDBClient.batchWriteItem()` function. + + for chunk in chunks { + var requestList: [DynamoDBClientTypes.WriteRequest] = [] + + for movie in chunk { + let item = try await movie.getAsItem() + let request = DynamoDBClientTypes.WriteRequest( + putRequest: .init( + item: item + ) ) - ) - requestList.append(request) - } + requestList.append(request) + } - let input = BatchWriteItemInput(requestItems: [tableName: requestList]) - _ = try await client.batchWriteItem(input: input) + let input = BatchWriteItemInput(requestItems: [tableName: requestList]) + _ = try await client.batchWriteItem(input: input) + } + } catch { + print("ERROR: populate:", dump(error)) + throw error } } + // snippet-end:[ddb.swift.basics.populate] // snippet-start:[ddb.swift.basics.add-movie] /// Add a movie specified as a `Movie` structure to the Amazon DynamoDB /// table. - /// + /// /// - Parameter movie: The `Movie` to add to the table. /// func add(movie: Movie) async throws { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - // Get a DynamoDB item containing the movie data. - let item = try await movie.getAsItem() + // Get a DynamoDB item containing the movie data. + let item = try await movie.getAsItem() - // Send the `PutItem` request to Amazon DynamoDB. + // Send the `PutItem` request to Amazon DynamoDB. - let input = PutItemInput( - item: item, - tableName: self.tableName - ) - _ = try await client.putItem(input: input) + let input = PutItemInput( + item: item, + tableName: self.tableName + ) + _ = try await client.putItem(input: input) + } catch { + print("ERROR: add movie:", dump(error)) + throw error + } } + // snippet-end:[ddb.swift.basics.add-movie] // snippet-start:[ddb.swift.basics.add-args] /// Given a movie's details, add a movie to the Amazon DynamoDB table. - /// + /// /// - Parameters: /// - title: The movie's title as a `String`. /// - year: The release year of the movie (`Int`). @@ -252,10 +308,17 @@ public class MovieTable { /// indicating no plot summary is available). /// func add(title: String, year: Int, rating: Double? = nil, - plot: String? = nil) async throws { - let movie = Movie(title: title, year: year, rating: rating, plot: plot) - try await self.add(movie: movie) + plot: String? = nil) async throws + { + do { + let movie = Movie(title: title, year: year, rating: rating, plot: plot) + try await self.add(movie: movie) + } catch { + print("ERROR: add with fields:", dump(error)) + throw error + } } + // snippet-end:[ddb.swift.basics.add-args] // snippet-start:[ddb.swift.basics.get] @@ -270,25 +333,31 @@ public class MovieTable { /// /// - Returns: A `Movie` record with the movie's details. func get(title: String, year: Int) async throws -> Movie { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - let input = GetItemInput( - key: [ - "year": .n(String(year)), - "title": .s(title) - ], - tableName: self.tableName - ) - let output = try await client.getItem(input: input) - guard let item = output.item else { - throw MoviesError.ItemNotFound - } + let input = GetItemInput( + key: [ + "year": .n(String(year)), + "title": .s(title) + ], + tableName: self.tableName + ) + let output = try await client.getItem(input: input) + guard let item = output.item else { + throw MoviesError.ItemNotFound + } - let movie = try Movie(withItem: item) - return movie + let movie = try Movie(withItem: item) + return movie + } catch { + print("ERROR: get:", dump(error)) + throw error + } } + // snippet-end:[ddb.swift.basics.get] // snippet-start:[ddb.swift.basics.getMovies-year] @@ -299,36 +368,48 @@ public class MovieTable { /// - Returns: An array of `Movie` objects describing each matching movie. /// func getMovies(fromYear year: Int) async throws -> [Movie] { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } - - let input = QueryInput( - expressionAttributeNames: [ - "#y": "year" - ], - expressionAttributeValues: [ - ":y": .n(String(year)) - ], - keyConditionExpression: "#y = :y", - tableName: self.tableName - ) - let output = try await client.query(input: input) - - guard let items = output.items else { - throw MoviesError.ItemNotFound - } - - // Convert the found movies into `Movie` objects and return an array - // of them. + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - var movieList: [Movie] = [] - for item in items { - let movie = try Movie(withItem: item) - movieList.append(movie) + let input = QueryInput( + expressionAttributeNames: [ + "#y": "year" + ], + expressionAttributeValues: [ + ":y": .n(String(year)) + ], + keyConditionExpression: "#y = :y", + tableName: self.tableName + ) + // Use "Paginated" to get all the movies. + // This lets the SDK handle the 'lastEvaluatedKey' property in "QueryOutput". + + let pages = client.queryPaginated(input: input) + + var movieList: [Movie] = [] + for try await page in pages { + guard let items = page.items else { + print("Error: no items returned.") + continue + } + + // Convert the found movies into `Movie` objects and return an array + // of them. + + for item in items { + let movie = try Movie(withItem: item) + movieList.append(movie) + } + } + return movieList + } catch { + print("ERROR: getMovies:", dump(error)) + throw error } - return movieList } + // snippet-end:[ddb.swift.basics.getMovies-year] // snippet-start:[ddb.swift.basics.getmovies-range] @@ -347,56 +428,58 @@ public class MovieTable { /// directly. /// func getMovies(firstYear: Int, lastYear: Int, - startKey: [Swift.String:DynamoDBClientTypes.AttributeValue]? = nil) - async throws -> [Movie] { - var movieList: [Movie] = [] - - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + startKey: [Swift.String: DynamoDBClientTypes.AttributeValue]? = nil) + async throws -> [Movie] + { + do { + var movieList: [Movie] = [] + + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - let input = ScanInput( - consistentRead: true, - exclusiveStartKey: startKey, - expressionAttributeNames: [ - "#y": "year" // `year` is a reserved word, so use `#y` instead. - ], - expressionAttributeValues: [ - ":y1": .n(String(firstYear)), - ":y2": .n(String(lastYear)) - ], - filterExpression: "#y BETWEEN :y1 AND :y2", - tableName: self.tableName - ) - - let output = try await client.scan(input: input) - - guard let items = output.items else { + let input = ScanInput( + consistentRead: true, + exclusiveStartKey: startKey, + expressionAttributeNames: [ + "#y": "year" // `year` is a reserved word, so use `#y` instead. + ], + expressionAttributeValues: [ + ":y1": .n(String(firstYear)), + ":y2": .n(String(lastYear)) + ], + filterExpression: "#y BETWEEN :y1 AND :y2", + tableName: self.tableName + ) + + let pages = client.scanPaginated(input: input) + + for try await page in pages { + guard let items = page.items else { + print("Error: no items returned.") + continue + } + + // Build an array of `Movie` objects for the returned items. + + for item in items { + let movie = try Movie(withItem: item) + movieList.append(movie) + } + } return movieList - } - - // Build an array of `Movie` objects for the returned items. - for item in items { - let movie = try Movie(withItem: item) - movieList.append(movie) + } catch { + print("ERROR: getMovies with scan:", dump(error)) + throw error } - - // Call this function recursively to continue collecting matching - // movies, if necessary. - - if output.lastEvaluatedKey != nil { - let movies = try await self.getMovies(firstYear: firstYear, lastYear: lastYear, - startKey: output.lastEvaluatedKey) - movieList += movies - } - return movieList } + // snippet-end:[ddb.swift.basics.getmovies-range] // snippet-start:[ddb.swift.basics.update] /// Update the specified movie with new `rating` and `plot` information. - /// + /// /// - Parameters: /// - title: The title of the movie to update. /// - year: The release year of the movie to update. @@ -408,48 +491,55 @@ public class MovieTable { /// aren't included in this list. `nil` if no changes were made. /// func update(title: String, year: Int, rating: Double? = nil, plot: String? = nil) async throws - -> [Swift.String:DynamoDBClientTypes.AttributeValue]? { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + -> [Swift.String: DynamoDBClientTypes.AttributeValue]? + { + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - // Build the update expression and the list of expression attribute - // values. Include only the information that's changed. + // Build the update expression and the list of expression attribute + // values. Include only the information that's changed. - var expressionParts: [String] = [] - var attrValues: [Swift.String:DynamoDBClientTypes.AttributeValue] = [:] + var expressionParts: [String] = [] + var attrValues: [Swift.String: DynamoDBClientTypes.AttributeValue] = [:] - if rating != nil { - expressionParts.append("info.rating=:r") - attrValues[":r"] = .n(String(rating!)) - } - if plot != nil { - expressionParts.append("info.plot=:p") - attrValues[":p"] = .s(plot!) - } - let expression: String = "set \(expressionParts.joined(separator: ", "))" - - let input = UpdateItemInput( - // Create substitution tokens for the attribute values, to ensure - // no conflicts in expression syntax. - expressionAttributeValues: attrValues, - // The key identifying the movie to update consists of the release - // year and title. - key: [ - "year": .n(String(year)), - "title": .s(title) - ], - returnValues: .updatedNew, - tableName: self.tableName, - updateExpression: expression - ) - let output = try await client.updateItem(input: input) - - guard let attributes: [Swift.String:DynamoDBClientTypes.AttributeValue] = output.attributes else { - throw MoviesError.InvalidAttributes + if rating != nil { + expressionParts.append("info.rating=:r") + attrValues[":r"] = .n(String(rating!)) + } + if plot != nil { + expressionParts.append("info.plot=:p") + attrValues[":p"] = .s(plot!) + } + let expression = "set \(expressionParts.joined(separator: ", "))" + + let input = UpdateItemInput( + // Create substitution tokens for the attribute values, to ensure + // no conflicts in expression syntax. + expressionAttributeValues: attrValues, + // The key identifying the movie to update consists of the release + // year and title. + key: [ + "year": .n(String(year)), + "title": .s(title) + ], + returnValues: .updatedNew, + tableName: self.tableName, + updateExpression: expression + ) + let output = try await client.updateItem(input: input) + + guard let attributes: [Swift.String: DynamoDBClientTypes.AttributeValue] = output.attributes else { + throw MoviesError.InvalidAttributes + } + return attributes + } catch { + print("ERROR: update:", dump(error)) + throw error } - return attributes } + // snippet-end:[ddb.swift.basics.update] // snippet-start:[ddb.swift.basics.delete] @@ -460,19 +550,25 @@ public class MovieTable { /// - year: The movie's release year. /// func delete(title: String, year: Int) async throws { - guard let client = self.ddbClient else { - throw MoviesError.UninitializedClient - } + do { + guard let client = self.ddbClient else { + throw MoviesError.UninitializedClient + } - let input = DeleteItemInput( - key: [ - "year": .n(String(year)), - "title": .s(title) - ], - tableName: self.tableName - ) - _ = try await client.deleteItem(input: input) + let input = DeleteItemInput( + key: [ + "year": .n(String(year)), + "title": .s(title) + ], + tableName: self.tableName + ) + _ = try await client.deleteItem(input: input) + } catch { + print("ERROR: delete:", dump(error)) + throw error + } } // snippet-end:[ddb.swift.basics.delete] } + // snippet-end:[ddb.swift.basics.movietable] diff --git a/swift/example_code/dynamodb/basics/Sources/basics.swift b/swift/example_code/dynamodb/basics/Sources/basics.swift index e10190ec3a4..59034671a0d 100644 --- a/swift/example_code/dynamodb/basics/Sources/basics.swift +++ b/swift/example_code/dynamodb/basics/Sources/basics.swift @@ -28,21 +28,34 @@ /// SPDX-License-Identifier: Apache-2.0 // snippet-start:[ddb.swift.basics] -import Foundation import ArgumentParser import ClientRuntime +import Foundation + // snippet-start:[ddb.swift.import] import AWSDynamoDB + // snippet-end:[ddb.swift.import] @testable import MovieList +extension String { + // Get the directory if the string is a file path. + func directory() -> String { + guard let lastIndex = lastIndex(of: "/") else { + print("Error: String directory separator not found.") + return "" + } + return String(self[...lastIndex]) + } +} + struct ExampleCommand: ParsableCommand { @Argument(help: "The path of the sample movie data JSON file.") - var jsonPath: String = "../../../../resources/sample_files/movies.json" + var jsonPath: String = #file.directory() + "../../../../../resources/sample_files/movies.json" @Option(help: "The AWS Region to run AWS API calls in.") - var awsRegion = "us-east-2" + var awsRegion: String? @Option( help: ArgumentHelp("The level of logging for the Swift SDK to perform."), @@ -77,13 +90,12 @@ struct ExampleCommand: ParsableCommand { // the `MovieTable` class. //===================================================================== - let tableName = "ddb-movies-sample-\(Int.random(in: 1...Int.max))" - //let tableName = String.uniqueName(withPrefix: "ddb-movies-sample", maxDigits: 8) + let tableName = "ddb-movies-sample-\(Int.random(in: 1 ... Int.max))" print("Creating table \"\(tableName)\"...") let movieDatabase = try await MovieTable(region: awsRegion, - tableName: tableName) + tableName: tableName) print("\nWaiting for table to be ready to use...") try await movieDatabase.awaitTableActive() @@ -103,7 +115,7 @@ struct ExampleCommand: ParsableCommand { print("\nAdding details to the added movie...") _ = try await movieDatabase.update(title: "Avatar: The Way of Water", year: 2022, - rating: 9.2, plot: "It's a sequel.") + rating: 9.2, plot: "It's a sequel.") //===================================================================== // 4. Populate the table from the JSON file. @@ -174,4 +186,5 @@ struct Main { } } } + // snippet-end:[ddb.swift.basics]