From 3d8b55be9891520b8a1d3eccc9b9ae3e2efbb351 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 20 Mar 2025 17:04:15 +0000 Subject: [PATCH 1/5] New scenario for SNS and SQS for Swift Added the scenario example for SNS and SQS. This isn't done yet. The existing filtering code is not correct and needs to come out and be replaced. --- swift/example_code/sqs/scenario/Package.swift | 41 ++ .../sqs/scenario/Sources/entry.swift | 607 ++++++++++++++++++ 2 files changed, 648 insertions(+) create mode 100644 swift/example_code/sqs/scenario/Package.swift create mode 100644 swift/example_code/sqs/scenario/Sources/entry.swift 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..1609f075761 --- /dev/null +++ b/swift/example_code/sqs/scenario/Sources/entry.swift @@ -0,0 +1,607 @@ +// 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. + +import ArgumentParser +import AWSClientRuntime +import AWSSNS +import AWSSQS +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + 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: """ + """ + ) + + /// 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 { + var answer: String? + + while answer == nil { + answer = stringRequest(prompt: prompt) + + if answer != nil { + answer = answer!.lowercased() + + if answer != "y" && answer != "n" { + print("Please answer 'Y' or 'N'. ", terminator: "") + answer = nil + } + } + } + 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) + + var index = 1 + for option in options { + print("(\(index)) \(option)") + index += 1 + } + print("") + + var answerNum = 0 + + while answerNum < 1 || answerNum > numOptions { + print("Enter your selection (1 - \(numOptions)): ", terminator: "") + if let answer = readLine() { + let answerConvert = Int(answer) + if answerConvert == nil { + answerNum = 0 + } else { + answerNum = Int(answerConvert!) + } + + } else { + return 0 + } + } + + return answerNum + } + + /// 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() + } + + /// 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? { + var queueName = stringRequest(prompt: prompt) + var attributes: [String:String] = [:] + + if isFIFO { + queueName += ".fifo" + attributes["FifoQueue"] = "true" + } + + + let output = try await sqsClient.createQueue( + input: CreateQueueInput( + attributes: attributes, + queueName: queueName + ) + ) + + guard let url = output.queueUrl else { + return nil + } + + return url + } + + /// 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"] + } + + 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( + //messageAttributeNames: [String]?, + //messageSystemAttributeNames: + //[SQSClientTypes.MessageSystemAttributeName]?, + 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 + ) + ) + } + +/* + // If there are any attributes, output a table of them. + + if message.messageAttributes != nil { + print("Attributes:") + for attribute: (key: String, value: SQSClientTypes.MessageAttributeValue) in message.messageAttributes! { + print(String(format: "%-30s %s", attribute.key, attribute.value.stringValue ?? "")) + } + } + print(" ---") +*/ + } + + 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) + ) + + let failed = output.failed + if failed != nil { + 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 (1) a Non-FIFO topic, (2) a FIFO + // topic with content-based deduplication, or (3) a FIFO topic + // without deduplication. + + 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: ") + + if isFIFO { + topicName += ".fifo" + } + + print("Topic name: \(topicName)") + + // 2. Create the topic. Append ".fifo" to the name if either of the + // FIFO topic types were selected. Set the "FifoTopic" attribute to + // "true" if appropriate. Set the "ContentBasedDeduplication" + // attribute to "true" if deduplication was requested. + + var attributes = [ + "FifoTopic": (isFIFO ? "true" : "false") + ] + + // If it's a FIFO topic with deduplication, set the appropriate + // attribute. + + if isContentBasedDeduplication { + attributes["ContentBasedDeduplication"] = "true" + } + + 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. + + // ADD FILTER OPTION HERE!!! ADD FILTER OPTION HERE!!! + + _ = try await snsClient.subscribe( + input: SubscribeInput( + endpoint: q1Arn, + protocol: "sqs", + topicArn: topicArn + ) + ) + + // 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) + + // ADD FILTER OPTION HERE!!! ADD FILTER OPTION HERE!!! + + _ = try await snsClient.subscribe( + input: SubscribeInput( + endpoint: q2Arn, + protocol: "sqs", + topicArn: topicArn + ) + ) + + // 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 attributes to the message. In this + // example, only string attributes are supported. + + var messageAttributes: [String:SNSClientTypes.MessageAttributeValue] = [:] + + while yesNoRequest(prompt: "\nAdd an attribute to this message (Y/N)? ") { + let attrName = stringRequest(prompt: " Enter the attribute's name: ") + let attrValue = stringRequest(prompt: " Enter the value of attribute '\(attrName)': ") + + let val = SNSClientTypes.MessageAttributeValue(dataType: "String", stringValue: attrValue) + messageAttributes[attrName] = 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 from the queue then delete both queues. + + print("\nDeleting 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) + } + } +} From 6ef7ac74bc027c516c350b9aa852c7d3140ad55f Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Tue, 8 Apr 2025 19:30:03 +0000 Subject: [PATCH 2/5] Finish example --- .../sqs/scenario/Sources/entry.swift | 261 +++++++++++++----- 1 file changed, 185 insertions(+), 76 deletions(-) diff --git a/swift/example_code/sqs/scenario/Sources/entry.swift b/swift/example_code/sqs/scenario/Sources/entry.swift index 1609f075761..c4b61eef798 100644 --- a/swift/example_code/sqs/scenario/Sources/entry.swift +++ b/swift/example_code/sqs/scenario/Sources/entry.swift @@ -13,7 +13,7 @@ import AWSSQS import Foundation struct ExampleCommand: ParsableCommand { - @Option(help: "Name of the Amazon Region to use (default: us-east-1)") + @Option(help: "Name of the Amazon Region to use") var region = "us-east-1" static var configuration = CommandConfiguration( @@ -24,6 +24,7 @@ struct ExampleCommand: ParsableCommand { (Amazon SQS) together to publish and receive messages using queues. """, discussion: """ + Supports filtering using a "tone" attribute. """ ) @@ -87,31 +88,28 @@ struct ExampleCommand: ParsableCommand { print(prompt) - var index = 1 + var index = 0 for option in options { print("(\(index)) \(option)") index += 1 } print("") - var answerNum = 0 - - while answerNum < 1 || answerNum > numOptions { - print("Enter your selection (1 - \(numOptions)): ", terminator: "") + repeat { + print("Enter your selection (0 - \(numOptions-1)): ", terminator: "") if let answer = readLine() { - let answerConvert = Int(answer) - if answerConvert == nil { - answerNum = 0 - } else { - answerNum = Int(answerConvert!) + guard let answer = Int(answer) else { + print("Please enter the number matching your selection.") + continue } - } else { - return 0 + if answer >= 0 && answer < numOptions { + return answer + } else { + print("Please enter the number matching your selection.") + } } - } - - return answerNum + } while true } /// Ask the user too press RETURN. Accepts any input but ignores it. @@ -122,6 +120,66 @@ struct ExampleCommand: ParsableCommand { _ = 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: @@ -130,27 +188,32 @@ struct ExampleCommand: ParsableCommand { /// /// - Returns: The URL of the queue. func createQueue(prompt: String, sqsClient: SQSClient, isFIFO: Bool) async throws -> String? { - var queueName = stringRequest(prompt: prompt) - var attributes: [String:String] = [:] - - if isFIFO { - queueName += ".fifo" - attributes["FifoQueue"] = "true" - } - + repeat { + var queueName = stringRequest(prompt: prompt) + var attributes: [String:String] = [:] - let output = try await sqsClient.createQueue( - input: CreateQueueInput( - attributes: attributes, - queueName: queueName - ) - ) + if isFIFO { + queueName += ".fifo" + attributes["FifoQueue"] = "true" + } - guard let url = output.queueUrl else { - return nil - } + do { + let output = try await sqsClient.createQueue( + input: CreateQueueInput( + attributes: attributes, + queueName: queueName + ) + ) + guard let url = output.queueUrl else { + return nil + } - return url + 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. @@ -174,6 +237,15 @@ struct ExampleCommand: ParsableCommand { 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( @@ -223,9 +295,6 @@ struct ExampleCommand: ParsableCommand { -> [SQSClientTypes.DeleteMessageBatchRequestEntry] { let output = try await sqsClient.receiveMessage( input: ReceiveMessageInput( - //messageAttributeNames: [String]?, - //messageSystemAttributeNames: - //[SQSClientTypes.MessageSystemAttributeName]?, maxNumberOfMessages: 10, queueUrl: queueUrl ) @@ -254,18 +323,6 @@ struct ExampleCommand: ParsableCommand { ) ) } - -/* - // If there are any attributes, output a table of them. - - if message.messageAttributes != nil { - print("Attributes:") - for attribute: (key: String, value: SQSClientTypes.MessageAttributeValue) in message.messageAttributes! { - print(String(format: "%-30s %s", attribute.key, attribute.value.stringValue ?? "")) - } - } - print(" ---") -*/ } return deleteList @@ -320,9 +377,9 @@ struct ExampleCommand: ParsableCommand { let sqsConfig = try await SQSClient.SQSClientConfiguration(region: region) let sqsClient = SQSClient(config: sqsConfig) - // 1. Ask the user whether to create (1) a Non-FIFO topic, (2) a FIFO - // topic with content-based deduplication, or (3) a FIFO topic - // without deduplication. + // 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 @@ -352,29 +409,31 @@ struct ExampleCommand: ParsableCommand { } 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)") - - // 2. Create the topic. Append ".fifo" to the name if either of the - // FIFO topic types were selected. Set the "FifoTopic" attribute to - // "true" if appropriate. Set the "ContentBasedDeduplication" - // attribute to "true" if deduplication was requested. var attributes = [ "FifoTopic": (isFIFO ? "true" : "false") ] - // If it's a FIFO topic with deduplication, set the appropriate - // attribute. + // 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, @@ -406,13 +465,12 @@ struct ExampleCommand: ParsableCommand { ) let q1Url = try await createQueue(prompt: "Enter the name of the first queue: ", - sqsClient: sqsClient, isFIFO: isFIFO) - + 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) @@ -435,16 +493,43 @@ struct ExampleCommand: ParsableCommand { // apply a filter. A filter allows only matching messages to enter // the queue. - // ADD FILTER OPTION HERE!!! ADD FILTER OPTION HERE!!! + 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() + } + } - _ = try await snsClient.subscribe( + 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: ", @@ -466,16 +551,32 @@ struct ExampleCommand: ParsableCommand { try await setQueuePolicy(sqsClient: sqsClient, queueUrl: q2Url, queueArn: q2Arn, topicArn: topicArn) - // ADD FILTER OPTION HERE!!! ADD FILTER OPTION HERE!!! + var q2Attributes: [String:String]? = nil - _ = try await snsClient.subscribe( + 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!!! @@ -519,17 +620,15 @@ struct ExampleCommand: ParsableCommand { } } - // Allow the user to add attributes to the message. In this - // example, only string attributes are supported. + // 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) - while yesNoRequest(prompt: "\nAdd an attribute to this message (Y/N)? ") { - let attrName = stringRequest(prompt: " Enter the attribute's name: ") - let attrValue = stringRequest(prompt: " Enter the value of attribute '\(attrName)': ") - - let val = SNSClientTypes.MessageAttributeValue(dataType: "String", stringValue: attrValue) - messageAttributes[attrName] = val + if attrValSelection != 0 { + let val = SNSClientTypes.MessageAttributeValue(dataType: "String", stringValue: attrValues[attrValSelection]) + messageAttributes["tone"] = val } publishInput.messageAttributes = messageAttributes @@ -570,9 +669,19 @@ struct ExampleCommand: ParsableCommand { print("\nDeleting the messages from queue 2...") try await deleteMessageList(sqsClient: sqsClient, queueUrl: q2Url, deleteList: q2DeleteList) - // 12. Unsubscribe from the queue then delete both queues. + // 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("\nDeleting queue 1...") + print("Deleting queue 1...") _ = try await sqsClient.deleteQueue( input: DeleteQueueInput(queueUrl: q1Url) ) From 9eef2fae7d8a2573cddaa074d1a6a3df2087aa8e Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Wed, 9 Apr 2025 14:33:03 +0000 Subject: [PATCH 3/5] Remove stray whitespace --- swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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] From 7f9363a866ab2d1e2f0e5ef101cd9ab3a1f73b1b Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 10 Apr 2025 18:34:48 +0000 Subject: [PATCH 4/5] Review feedback --- .../sqs/scenario/Sources/entry.swift | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/swift/example_code/sqs/scenario/Sources/entry.swift b/swift/example_code/sqs/scenario/Sources/entry.swift index c4b61eef798..32adc4ece19 100644 --- a/swift/example_code/sqs/scenario/Sources/entry.swift +++ b/swift/example_code/sqs/scenario/Sources/entry.swift @@ -54,21 +54,12 @@ struct ExampleCommand: ParsableCommand { /// /// - Returns: `true` if the user answered "Y", otherwise `false`. func yesNoRequest(prompt: String) -> Bool { - var answer: String? - - while answer == nil { - answer = stringRequest(prompt: prompt) - - if answer != nil { - answer = answer!.lowercased() - - if answer != "y" && answer != "n" { - print("Please answer 'Y' or 'N'. ", terminator: "") - answer = nil - } + while true { + let answer = stringRequest(prompt: prompt).lowercased() + if answer == "y" || answer == "n" { + return answer == "y" } } - return answer == "y" } /// Display a menu of options then request a selection. @@ -88,12 +79,9 @@ struct ExampleCommand: ParsableCommand { print(prompt) - var index = 0 - for option in options { - print("(\(index)) \(option)") - index += 1 + for (index, value) in options.enumerated() { + print("(\(index)) \(value)") } - print("") repeat { print("Enter your selection (0 - \(numOptions-1)): ", terminator: "") @@ -173,9 +161,7 @@ struct ExampleCommand: ParsableCommand { filterString += "\"\(ans)\"" } - let filterJSON = """ - { "tone": [\(filterString)]} - """ + let filterJSON = "{ \"tone\": [\(filterString)]}" attr["FilterPolicy"] = filterJSON return attr @@ -190,7 +176,7 @@ struct ExampleCommand: ParsableCommand { func createQueue(prompt: String, sqsClient: SQSClient, isFIFO: Bool) async throws -> String? { repeat { var queueName = stringRequest(prompt: prompt) - var attributes: [String:String] = [:] + var attributes: [String: String] = [:] if isFIFO { queueName += ".fifo" @@ -343,10 +329,9 @@ struct ExampleCommand: ParsableCommand { input: DeleteMessageBatchInput(entries: deleteList, queueUrl: queueUrl) ) - let failed = output.failed - if failed != nil { - print("\(failed!.count) errors occurred deleting messages from the queue.") - for message in failed! { + 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 ?? "..."))") } } From 6bb1e655c8e18d8b516c32a3243f74f719e70c0a Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Fri, 11 Apr 2025 17:31:00 +0000 Subject: [PATCH 5/5] Add scenario to metadata --- .doc_gen/metadata/sqs_metadata.yaml | 9 ++++++++ swift/example_code/sns/README.md | 23 +++++++++++++++++++ swift/example_code/sqs/README.md | 23 +++++++++++++++++++ .../sqs/scenario/Sources/entry.swift | 2 ++ 4 files changed, 57 insertions(+) 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/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/Sources/entry.swift b/swift/example_code/sqs/scenario/Sources/entry.swift index 32adc4ece19..ed4f20c9ab0 100644 --- a/swift/example_code/sqs/scenario/Sources/entry.swift +++ b/swift/example_code/sqs/scenario/Sources/entry.swift @@ -6,6 +6,7 @@ // 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 @@ -699,3 +700,4 @@ struct Main { } } } +// snippet-end:[swift.sqs.scenario.main]