diff --git a/.doc_gen/metadata/sqs_metadata.yaml b/.doc_gen/metadata/sqs_metadata.yaml index 01da7767895..b52a35a63ff 100644 --- a/.doc_gen/metadata/sqs_metadata.yaml +++ b/.doc_gen/metadata/sqs_metadata.yaml @@ -1117,6 +1117,15 @@ sqs_Scenario_TopicsAndQueues: excerpts: - snippet_tags: - sns.kotlin.workflow.main + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sqs/scenario + sdkguide: + excerpts: + - description: + snippet_tags: + - swift.sqs.scenario.main services: sns: {CreateTopic, Subscribe, Publish, Unsubscribe, DeleteTopic} sqs: {CreateQueue, GetQueueAttributes, SetQueueAttributes, ReceiveMessage, DeleteMessageBatch, DeleteQueue} diff --git a/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift b/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift index e2eeb03e5e6..802917ad10e 100644 --- a/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift +++ b/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift @@ -10,7 +10,6 @@ import AWSClientRuntime import AWSS3 import Foundation - // snippet-end:[s3.swift.intro.imports] // snippet-start:[s3.swift.intro.getbucketnames] @@ -56,11 +55,10 @@ func getBucketNames() async throws -> [String] { } } } - // snippet-end:[s3.swift.intro.getbucketnames] -// snippet-start:[s3.swift.intro.main] /// The program's asynchronous entry point. +// snippet-start:[s3.swift.intro.main] @main struct Main { static func main() async { @@ -78,5 +76,4 @@ struct Main { } } } - // snippet-end:[s3.swift.intro.main] diff --git a/swift/example_code/sns/README.md b/swift/example_code/sns/README.md index c0d6b8ef7b3..af1a6485f8b 100644 --- a/swift/example_code/sns/README.md +++ b/swift/example_code/sns/README.md @@ -45,6 +45,13 @@ Code excerpts that show you how to call individual service functions. - [Subscribe](SubscribeEmail/Sources/entry.swift#L31) - [Unsubscribe](Unsubscribe/Sources/entry.swift#L29) +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Publish messages to queues](../sqs/scenario/Sources/entry.swift) + @@ -74,6 +81,22 @@ This example shows you how to get started using Amazon SNS. +#### Publish messages to queues + +This example shows you how to do the following: + +- Create topic (FIFO or non-FIFO). +- Subscribe several queues to the topic with an option to apply a filter. +- Publish messages to the topic. +- Poll the queues for messages received. + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/swift/example_code/sqs/README.md b/swift/example_code/sqs/README.md index c00803b6140..44b3bcd38ce 100644 --- a/swift/example_code/sqs/README.md +++ b/swift/example_code/sqs/README.md @@ -46,6 +46,13 @@ Code excerpts that show you how to call individual service functions. - [ReceiveMessage](ReceiveMessage/Sources/entry.swift#L31) - [SetQueueAttributes](SetQueueAttributes/Sources/entry.swift#L32) +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Publish messages to queues](scenario/Sources/entry.swift) + @@ -75,6 +82,22 @@ This example shows you how to get started using Amazon SQS. +#### Publish messages to queues + +This example shows you how to do the following: + +- Create topic (FIFO or non-FIFO). +- Subscribe several queues to the topic with an option to apply a filter. +- Publish messages to the topic. +- Poll the queues for messages received. + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/swift/example_code/sqs/scenario/Package.swift b/swift/example_code/sqs/scenario/Package.swift new file mode 100644 index 00000000000..b012692237c --- /dev/null +++ b/swift/example_code/sqs/scenario/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "queue-scenario", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v13), + .iOS(.v15) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0"), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products + // from dependencies. + .executableTarget( + name: "queue-scenario", + dependencies: [ + .product(name: "AWSSNS", package: "aws-sdk-swift"), + .product(name: "AWSSQS", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) diff --git a/swift/example_code/sqs/scenario/Sources/entry.swift b/swift/example_code/sqs/scenario/Sources/entry.swift new file mode 100644 index 00000000000..ed4f20c9ab0 --- /dev/null +++ b/swift/example_code/sqs/scenario/Sources/entry.swift @@ -0,0 +1,703 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// This example demonstrates how to use Amazon Simple Notification Service and +// Amazon Simple Queue Service together to create queues and publish messages +// to them through a single topic. The example demonstrates various features +// of both SNS and SQS together. + +// snippet-start:[swift.sqs.scenario.main] +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "Name of the Amazon Region to use") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "queue-scenario", + abstract: """ + This example interactively demonstrates how to use Amazon Simple + Notification Service (Amazon SNS) and Amazon Simple Queue Service + (Amazon SQS) together to publish and receive messages using queues. + """, + discussion: """ + Supports filtering using a "tone" attribute. + """ + ) + + /// Prompt for an input string. Only non-empty strings are allowed. + /// + /// - Parameter prompt: The prompt to display. + /// + /// - Returns: The string input by the user. + func stringRequest(prompt: String) -> String { + var str: String? + + while str == nil { + print(prompt, terminator: "") + str = readLine() + + if str != nil && str?.count == 0 { + str = nil + } + } + + return str! + } + + /// Ask a yes/no question. + /// + /// - Parameter prompt: A prompt string to print. + /// + /// - Returns: `true` if the user answered "Y", otherwise `false`. + func yesNoRequest(prompt: String) -> Bool { + while true { + let answer = stringRequest(prompt: prompt).lowercased() + if answer == "y" || answer == "n" { + return answer == "y" + } + } + } + + /// Display a menu of options then request a selection. + /// + /// - Parameters: + /// - prompt: A prompt string to display before the menu. + /// - options: An array of strings giving the menu options. + /// + /// - Returns: The index number of the selected option or 0 if no item was + /// selected. + func menuRequest(prompt: String, options: [String]) -> Int { + let numOptions = options.count + + if numOptions == 0 { + return 0 + } + + print(prompt) + + for (index, value) in options.enumerated() { + print("(\(index)) \(value)") + } + + repeat { + print("Enter your selection (0 - \(numOptions-1)): ", terminator: "") + if let answer = readLine() { + guard let answer = Int(answer) else { + print("Please enter the number matching your selection.") + continue + } + + if answer >= 0 && answer < numOptions { + return answer + } else { + print("Please enter the number matching your selection.") + } + } + } while true + } + + /// Ask the user too press RETURN. Accepts any input but ignores it. + /// + /// - Parameter prompt: The text prompt to display. + func returnRequest(prompt: String) { + print(prompt, terminator: "") + _ = readLine() + } + + var attrValues = [ + "", + "cheerful", + "funny", + "serious", + "sincere" + ] + + /// Ask the user to choose one of the attribute values to use as a filter. + /// + /// - Parameters: + /// - message: A message to display before the menu of values. + /// - attrValues: An array of strings giving the values to choose from. + /// + /// - Returns: The string corresponding to the selected option. + func askForFilter(message: String, attrValues: [String]) -> String? { + print(message) + for (index, value) in attrValues.enumerated() { + print(" [\(index)] \(value)") + } + + var answer: Int? + repeat { + answer = Int(stringRequest(prompt: "Select an value for the 'tone' attribute or 0 to end: ")) + } while answer == nil || answer! < 0 || answer! > attrValues.count + 1 + + if answer == 0 { + return nil + } + return attrValues[answer!] + } + + /// Prompts the user for filter terms and constructs the attribute + /// record that specifies them. + /// + /// - Returns: A mapping of "FilterPolicy" to a JSON string representing + /// the user-defined filter. + func buildFilterAttributes() -> [String:String] { + var attr: [String:String] = [:] + var filterString = "" + + var first = true + + while let ans = askForFilter(message: "Choose a value to apply to the 'tone' attribute.", + attrValues: attrValues) { + if !first { + filterString += "," + } + first = false + + filterString += "\"\(ans)\"" + } + + let filterJSON = "{ \"tone\": [\(filterString)]}" + attr["FilterPolicy"] = filterJSON + + return attr + } + /// Create a queue, returning its URL string. + /// + /// - Parameters: + /// - prompt: A prompt to ask for the queue name. + /// - isFIFO: Whether or not to create a FIFO queue. + /// + /// - Returns: The URL of the queue. + func createQueue(prompt: String, sqsClient: SQSClient, isFIFO: Bool) async throws -> String? { + repeat { + var queueName = stringRequest(prompt: prompt) + var attributes: [String: String] = [:] + + if isFIFO { + queueName += ".fifo" + attributes["FifoQueue"] = "true" + } + + do { + let output = try await sqsClient.createQueue( + input: CreateQueueInput( + attributes: attributes, + queueName: queueName + ) + ) + guard let url = output.queueUrl else { + return nil + } + + return url + } catch _ as QueueDeletedRecently { + print("You need to use a different queue name. A queue by that name was recently deleted.") + continue + } + } while true + } + + /// Return the ARN of a queue given its URL. + /// + /// - Parameter queueUrl: The URL of the queue for which to return the + /// ARN. + /// + /// - Returns: The ARN of the specified queue. + func getQueueARN(sqsClient: SQSClient, queueUrl: String) async throws -> String? { + let output = try await sqsClient.getQueueAttributes( + input: GetQueueAttributesInput( + attributeNames: [.queuearn], + queueUrl: queueUrl + ) + ) + + guard let attributes = output.attributes else { + return nil + } + + return attributes["QueueArn"] + } + + /// Applies the needed policy to the specified queue. + /// + /// - Parameters: + /// - sqsClient: The Amazon SQS client to use. + /// - queueUrl: The queue to apply the policy to. + /// - queueArn: The ARN of the queue to apply the policy to. + /// - topicArn: The topic that should have access via the policy. + /// + /// - Throws: Errors from the SQS `SetQueueAttributes` action. + func setQueuePolicy(sqsClient: SQSClient, queueUrl: String, + queueArn: String, topicArn: String) async throws { + _ = try await sqsClient.setQueueAttributes( + input: SetQueueAttributesInput( + attributes: [ + "Policy": + """ + { + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Action": "sqs:SendMessage", + "Resource": "\(queueArn)", + "Condition": { + "ArnEquals": { + "aws:SourceArn": "\(topicArn)" + } + } + } + ] + } + """ + + ], + queueUrl: queueUrl + ) + ) + } + + /// Receive the available messages on a queue, outputting them to the + /// screen. Returns a dictionary you pass to DeleteMessageBatch to delete + /// all the received messages. + /// + /// - Parameters: + /// - sqsClient: The Amazon SQS client to use. + /// - queueUrl: The SQS queue on which to receive messages. + /// + /// - Throws: Errors from `SQSClient.receiveMessage()` + /// + /// - Returns: An array of SQSClientTypes.DeleteMessageBatchRequestEntry + /// items, each describing one received message in the format needed to + /// delete it. + func receiveAndListMessages(sqsClient: SQSClient, queueUrl: String) async throws + -> [SQSClientTypes.DeleteMessageBatchRequestEntry] { + let output = try await sqsClient.receiveMessage( + input: ReceiveMessageInput( + maxNumberOfMessages: 10, + queueUrl: queueUrl + ) + ) + + guard let messages = output.messages else { + print("No messages received.") + return [] + } + + var deleteList: [SQSClientTypes.DeleteMessageBatchRequestEntry] = [] + + // Print out all the messages that were received, including their + // attributes, if any. + + for message in messages { + print("Message ID: \(message.messageId ?? "")") + print("Receipt handle: \(message.receiptHandle ?? "")") + print("Message JSON: \(message.body ?? "")") + + if message.receiptHandle != nil { + deleteList.append( + SQSClientTypes.DeleteMessageBatchRequestEntry( + id: message.messageId, + receiptHandle: message.receiptHandle + ) + ) + } + } + + return deleteList + } + + /// Delete all the messages in the specified list. + /// + /// - Parameters: + /// - sqsClient: The Amazon SQS client to use. + /// - queueUrl: The SQS queue to delete messages from. + /// - deleteList: A list of `DeleteMessageBatchRequestEntry` objects + /// describing the messages to delete. + /// + /// - Throws: Errors from `SQSClient.deleteMessageBatch()`. + func deleteMessageList(sqsClient: SQSClient, queueUrl: String, + deleteList: [SQSClientTypes.DeleteMessageBatchRequestEntry]) async throws { + let output = try await sqsClient.deleteMessageBatch( + input: DeleteMessageBatchInput(entries: deleteList, queueUrl: queueUrl) + ) + + if let failed = output.failed { + print("\(failed.count) errors occurred deleting messages from the queue.") + for message in failed { + print("---> Failed to delete message \(message.id ?? "") with error: \(message.code ?? "") (\(message.message ?? "..."))") + } + } + } + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + let rowOfStars = String(repeating: "*", count: 75) + + print(""" + \(rowOfStars) + Welcome to the cross-service messaging with topics and queues example. + In this workflow, you'll create an SNS topic, then create two SQS + queues which will be subscribed to that topic. + + You can specify several options for configuring the topic, as well as + the queue subscriptions. You can then post messages to the topic and + receive the results on the queues. + \(rowOfStars)\n + """ + ) + + // 0. Create SNS and SQS clients. + + let snsConfig = try await SNSClient.SNSClientConfiguration(region: region) + let snsClient = SNSClient(config: snsConfig) + + let sqsConfig = try await SQSClient.SQSClientConfiguration(region: region) + let sqsClient = SQSClient(config: sqsConfig) + + // 1. Ask the user whether to create a FIFO topic. If so, ask whether + // to use content-based deduplication instead of requiring a + // deduplication ID. + + let isFIFO = yesNoRequest(prompt: "Do you want to create a FIFO topic (Y/N)? ") + var isContentBasedDeduplication = false + + if isFIFO { + print(""" + \(rowOfStars) + Because you've chosen to create a FIFO topic, deduplication is + supported. + + Deduplication IDs are either set in the message or are automatically + generated from the content using a hash function. + + If a message is successfully published to an SNS FIFO topic, any + message published and found to have the same deduplication ID + (within a five-minute deduplication interval), is accepted but + not delivered. + + For more information about deduplication, see: + https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html. + """ + ) + + isContentBasedDeduplication = yesNoRequest( + prompt: "Use content-based deduplication instead of entering a deduplication ID (Y/N)? ") + print(rowOfStars) + } + + var topicName = stringRequest(prompt: "Enter the name of the topic to create: ") + + // 2. Create the topic. Append ".fifo" to the name if FIFO was + // requested, and set the "FifoTopic" attribute to "true" if so as + // well. Set the "ContentBasedDeduplication" attribute to "true" if + // content-based deduplication was requested. + + if isFIFO { + topicName += ".fifo" + } + + print("Topic name: \(topicName)") + + var attributes = [ + "FifoTopic": (isFIFO ? "true" : "false") + ] + + // If it's a FIFO topic with content-based deduplication, set the + // "ContentBasedDeduplication" attribute. + + if isContentBasedDeduplication { + attributes["ContentBasedDeduplication"] = "true" + } + + // Create the topic and retrieve the ARN. + + let output = try await snsClient.createTopic( + input: CreateTopicInput( + attributes: attributes, + name: topicName + ) + ) + + guard let topicArn = output.topicArn else { + print("No topic ARN returned!") + return + } + + print(""" + Topic '\(topicName) has been created with the + topic ARN \(topicArn)." + """ + ) + + print(rowOfStars) + + // 3. Create an SQS queue. Append ".fifo" to the name if one of the + // FIFO topic configurations was chosen, and set "FifoQueue" to + // "true" if the topic is FIFO. + + print(""" + Next, you will create two SQS queues that will be subscribed + to the topic you just created.\n + """ + ) + + let q1Url = try await createQueue(prompt: "Enter the name of the first queue: ", + sqsClient: sqsClient, isFIFO: isFIFO) + guard let q1Url else { + print("Unable to create queue 1!") + return + } + + // 4. Get the SQS queue's ARN attribute using `GetQueueAttributes`. + + let q1Arn = try await getQueueARN(sqsClient: sqsClient, queueUrl: q1Url) + + guard let q1Arn else { + print("Unable to get ARN of queue 1!") + return + } + print("Got queue 1 ARN: \(q1Arn)") + + // 5. Attach an AWS IAM policy to the queue using + // `SetQueueAttributes`. + + try await setQueuePolicy(sqsClient: sqsClient, queueUrl: q1Url, + queueArn: q1Arn, topicArn: topicArn) + + // 6. Subscribe the SQS queue to the SNS topic. Set the topic ARN in + // the request. Set the protocol to "sqs". Set the queue ARN to the + // ARN just received in step 5. For FIFO topics, give the option to + // apply a filter. A filter allows only matching messages to enter + // the queue. + + var q1Attributes: [String:String]? = nil + + if isFIFO { + print( + """ + + If you add a filter to this subscription, then only the filtered messages will + be received in the queue. For information about message filtering, see + https://docs.aws.amazon.com/sns/latest/dg/sns-message-filtering.html + For this example, you can filter messages by a 'tone' attribute. + + """ + ) + + let subPrompt = """ + Would you like to filter messages for the first queue's subscription to the + topic \(topicName) (Y/N)? + """ + if (yesNoRequest(prompt: subPrompt)) { + q1Attributes = buildFilterAttributes() + } + } + + let sub1Output = try await snsClient.subscribe( + input: SubscribeInput( + attributes: q1Attributes, + endpoint: q1Arn, + protocol: "sqs", + topicArn: topicArn + ) + ) + + guard let q1SubscriptionArn = sub1Output.subscriptionArn else { + print("Invalid subscription ARN returned for queue 1!") + return + } + + // 7. Repeat steps 3-6 for the second queue. + + let q2Url = try await createQueue(prompt: "Enter the name of the second queue: ", + sqsClient: sqsClient, isFIFO: isFIFO) + + guard let q2Url else { + print("Unable to create queue 2!") + return + } + + let q2Arn = try await getQueueARN(sqsClient: sqsClient, queueUrl: q2Url) + + guard let q2Arn else { + print("Unable to get ARN of queue 2!") + return + } + print("Got queue 2 ARN: \(q2Arn)") + + try await setQueuePolicy(sqsClient: sqsClient, queueUrl: q2Url, + queueArn: q2Arn, topicArn: topicArn) + + var q2Attributes: [String:String]? = nil + + if isFIFO { + let subPrompt = """ + Would you like to filter messages for the second queue's subscription to the + topic \(topicName) (Y/N)? + """ + if (yesNoRequest(prompt: subPrompt)) { + q2Attributes = buildFilterAttributes() + } + } + + let sub2Output = try await snsClient.subscribe( + input: SubscribeInput( + attributes: q2Attributes, + endpoint: q2Arn, + protocol: "sqs", + topicArn: topicArn + ) + ) + + guard let q2SubscriptionArn = sub2Output.subscriptionArn else { + print("Invalid subscription ARN returned for queue 1!") + return + } + + // 8. Let the user publish messages to the topic, asking for a message + // body for each message. Handle the types of topic correctly (SEE + // MVP INFORMATION AND FIX THESE COMMENTS!!! + + print("\n\(rowOfStars)\n") + + var first = true + + repeat { + var publishInput = PublishInput( + topicArn: topicArn + ) + + publishInput.message = stringRequest(prompt: "Enter message text to publish: ") + + // If using a FIFO topic, a message group ID must be set on the + // message. + + if isFIFO { + if first { + print(""" + Because you're using a FIFO topic, you must set a message + group ID. All messages within the same group will be + received in the same order in which they were published.\n + """ + ) + } + publishInput.messageGroupId = stringRequest(prompt: "Enter a message group ID for this message: ") + + if !isContentBasedDeduplication { + if first { + print(""" + Because you're not using content-based deduplication, you + must enter a deduplication ID. If other messages with the + same deduplication ID are published within the same + deduplication interval, they will not be delivered. + """ + ) + } + publishInput.messageDeduplicationId = stringRequest(prompt: "Enter a deduplication ID for this message: ") + } + } + + // Allow the user to add a value for the "tone" attribute if they + // wish to do so. + + var messageAttributes: [String:SNSClientTypes.MessageAttributeValue] = [:] + let attrValSelection = menuRequest(prompt: "Choose a tone to apply to this message.", options: attrValues) + + if attrValSelection != 0 { + let val = SNSClientTypes.MessageAttributeValue(dataType: "String", stringValue: attrValues[attrValSelection]) + messageAttributes["tone"] = val + } + + publishInput.messageAttributes = messageAttributes + + // Publish the message and display its ID. + + let publishOutput = try await snsClient.publish(input: publishInput) + + guard let messageID = publishOutput.messageId else { + print("Unable to get the published message's ID!") + return + } + + print("Message published with ID \(messageID).") + first = false + + // 9. Repeat step 8 until the user says they don't want to post + // another. + + } while (yesNoRequest(prompt: "Post another message (Y/N)? ")) + + // 10. Display a list of the messages in each queue by using + // `ReceiveMessage`. Show at least the body and the attributes. + + print(rowOfStars) + print("Contents of queue 1:") + let q1DeleteList = try await receiveAndListMessages(sqsClient: sqsClient, queueUrl: q1Url) + print("\n\nContents of queue 2:") + let q2DeleteList = try await receiveAndListMessages(sqsClient: sqsClient, queueUrl: q2Url) + print(rowOfStars) + + returnRequest(prompt: "\nPress return to clean up: ") + + // 11. Delete the received messages using `DeleteMessageBatch`. + + print("Deleting the messages from queue 1...") + try await deleteMessageList(sqsClient: sqsClient, queueUrl: q1Url, deleteList: q1DeleteList) + print("\nDeleting the messages from queue 2...") + try await deleteMessageList(sqsClient: sqsClient, queueUrl: q2Url, deleteList: q2DeleteList) + + // 12. Unsubscribe and delete both queues. + + print("\nUnsubscribing from queue 1...") + _ = try await snsClient.unsubscribe( + input: UnsubscribeInput(subscriptionArn: q1SubscriptionArn) + ) + + print("Unsubscribing from queue 2...") + _ = try await snsClient.unsubscribe( + input: UnsubscribeInput(subscriptionArn: q2SubscriptionArn) + ) + + print("Deleting queue 1...") + _ = try await sqsClient.deleteQueue( + input: DeleteQueueInput(queueUrl: q1Url) + ) + + print("Deleting queue 2...") + _ = try await sqsClient.deleteQueue( + input: DeleteQueueInput(queueUrl: q2Url) + ) + + // 13. Delete the topic. + + print("Deleting the SNS topic...") + _ = try await snsClient.deleteTopic( + input: DeleteTopicInput(topicArn: topicArn) + ) + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} +// snippet-end:[swift.sqs.scenario.main]