From dab6c11752d0e7f26b31e9c8075842835f037d4b Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Fri, 22 Aug 2025 17:56:35 +0000 Subject: [PATCH 1/7] Add SFN Hello example --- swift/example_code/sfn/hello/Package.swift | 47 ++++++++++++ .../sfn/hello/Sources/entry.swift | 72 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 swift/example_code/sfn/hello/Package.swift create mode 100644 swift/example_code/sfn/hello/Sources/entry.swift diff --git a/swift/example_code/sfn/hello/Package.swift b/swift/example_code/sfn/hello/Package.swift new file mode 100644 index 00000000000..c2013d449a3 --- /dev/null +++ b/swift/example_code/sfn/hello/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// (swift-tools-version has two lines here because it needs to be the first +// line in the file, but it should also appear in the snippet below) +// +// snippet-start:[swift.sfn.hello.package] +// swift-tools-version: 5.9 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "hello-sfn", + // 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: "hello-sfn", + dependencies: [ + .product(name: "AWSSFN", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) +// snippet-end:[swift.sfn.hello.package] diff --git a/swift/example_code/sfn/hello/Sources/entry.swift b/swift/example_code/sfn/hello/Sources/entry.swift new file mode 100644 index 00000000000..a2a11a7424c --- /dev/null +++ b/swift/example_code/sfn/hello/Sources/entry.swift @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// snippet-start:[swift.sfn.hello] +// An example that shows how to use the AWS SDK for Swift to perform a simple +// operation using Amazon Elastic Compute Cloud (EC2). +// + +import ArgumentParser +import Foundation + +// snippet-start:[swift.sfn.import] +import AWSSFN +// snippet-end:[swift.sfn.import] + +struct ExampleCommand: ParsableCommand { + @Option(help: "The AWS Region to run AWS API calls in.") + var awsRegion = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "hello-sfn", + abstract: """ + Demonstrates a simple operation using AWS Step Functions. + """, + discussion: """ + An example showing how to make a call to AWS Step Functions using the + AWS SDK for Swift. + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + let sfnConfig = try await SFNClient.SFNClientConfiguration(region: awsRegion) + let sfnClient = SFNClient(config: sfnConfig) + + do { + let output = try await sfnClient.listStateMachines( + input: ListStateMachinesInput( + maxResults: 10 + ) + ) + + guard let stateMachines = output.stateMachines else { + print("*** No state machines found.") + return + } + + print("Found \(stateMachines.count) state machines (capped to 10)...") + for machine in stateMachines { + print(" \(machine.name ?? ""): \(machine.stateMachineArn ?? "")") + } + } catch { + print("*** Error fetching state machine list: \(error.localizedDescription)") + } + } +} + +/// 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.sfn.hello] From c5ca3bf0eed78fc575c3443f895dd8d243f42c0b Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 28 Aug 2025 16:30:45 +0000 Subject: [PATCH 2/7] Finished scenario --- .../sfn/hello/Sources/entry.swift | 2 + swift/example_code/sfn/scenario/Package.swift | 48 +++ .../sfn/scenario/Sources/Activity.swift | 125 +++++++ .../sfn/scenario/Sources/Example.swift | 252 +++++++++++++ .../sfn/scenario/Sources/StateMachine.swift | 353 ++++++++++++++++++ .../sfn/scenario/Sources/entry.swift | 62 +++ 6 files changed, 842 insertions(+) create mode 100644 swift/example_code/sfn/scenario/Package.swift create mode 100644 swift/example_code/sfn/scenario/Sources/Activity.swift create mode 100644 swift/example_code/sfn/scenario/Sources/Example.swift create mode 100644 swift/example_code/sfn/scenario/Sources/StateMachine.swift create mode 100644 swift/example_code/sfn/scenario/Sources/entry.swift diff --git a/swift/example_code/sfn/hello/Sources/entry.swift b/swift/example_code/sfn/hello/Sources/entry.swift index a2a11a7424c..c753654c391 100644 --- a/swift/example_code/sfn/hello/Sources/entry.swift +++ b/swift/example_code/sfn/hello/Sources/entry.swift @@ -33,6 +33,7 @@ struct ExampleCommand: ParsableCommand { let sfnConfig = try await SFNClient.SFNClientConfiguration(region: awsRegion) let sfnClient = SFNClient(config: sfnConfig) + // snippet-start:[swift.sfn.hello.ListStateMachines] do { let output = try await sfnClient.listStateMachines( input: ListStateMachinesInput( @@ -52,6 +53,7 @@ struct ExampleCommand: ParsableCommand { } catch { print("*** Error fetching state machine list: \(error.localizedDescription)") } + // snippet-end:[swift.sfn.hello.ListStateMachines] } } diff --git a/swift/example_code/sfn/scenario/Package.swift b/swift/example_code/sfn/scenario/Package.swift new file mode 100644 index 00000000000..ef21eeb8a90 --- /dev/null +++ b/swift/example_code/sfn/scenario/Package.swift @@ -0,0 +1,48 @@ +// swift-tools-version: 5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// (swift-tools-version has two lines here because it needs to be the first +// line in the file, but it should also appear in the snippet below) +// +// snippet-start:[swift.sfn.hello.package] +// swift-tools-version: 5.9 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "sfn-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: "sfn-scenario", + dependencies: [ + .product(name: "AWSIAM", package: "aws-sdk-swift"), + .product(name: "AWSSFN", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + path: "Sources") + + ] +) +// snippet-end:[swift.sfn.hello.package] diff --git a/swift/example_code/sfn/scenario/Sources/Activity.swift b/swift/example_code/sfn/scenario/Sources/Activity.swift new file mode 100644 index 00000000000..e870e9df5b0 --- /dev/null +++ b/swift/example_code/sfn/scenario/Sources/Activity.swift @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import AWSSFN + +/// Describes errors that occur on Step Functions activities. +enum ActivityError: Error { + /// The ARN is missing from the returned activity. + case missingArnError + /// The activity list is missing from the response. + case missingActivityListError + /// No matching activity was found. + case activityNotFoundError + + var errorDescription: String { + switch self { + case .missingArnError: + return "The ARN is missing from the returned activity" + case .missingActivityListError: + return "The activity list is missing from the response" + case .activityNotFoundError: + return "No activity with the specified name was found" + } + } +} + +/// Manage a Step Functions activity. +class Activity { + let sfnClient: SFNClient + let activityName: String + var activityArn = "" + + init(client: SFNClient, name: String) async throws { + sfnClient = client + self.activityName = name + + try await self.findOrCreateActivity() + } + + /// Create a new Step Functions activity. + /// + /// - Throws: `ActivityError` and appropriate AWS errors. + private func createActivity() async throws { + let output = try await sfnClient.createActivity( + input: CreateActivityInput(name: activityName) + ) + + guard let arn = output.activityArn else { + throw ActivityError.missingArnError + } + + activityArn = arn + } + + /// Find an activity with the name specified when initializing the + /// `Activity` object. + /// + /// - Throws: `ActivityError` and appropriate AWS errors. + private func findActivity() async throws { + let pages = sfnClient.listActivitiesPaginated( + input: ListActivitiesInput() + ) + + for try await page in pages { + guard let activities = page.activities else { + throw ActivityError.missingActivityListError + } + + for activity in activities { + if activity.name == activityName { + guard let arn = activity.activityArn else { + throw ActivityError.missingArnError + } + self.activityArn = arn + } + } + } + + throw ActivityError.activityNotFoundError + } + + /// Finds an existing activity with the name given when initializing + /// the `Activity`. If one isn't found, a new one is created. + /// + /// - Throws: `ActivityError` and appropriate AWS errors. + private func findOrCreateActivity() async throws { + do { + try await findActivity() + } catch { + try await createActivity() + } + } + + /// Delete the activity described by this object. + public func delete() async { + do { + _ = try await sfnClient.deleteActivity( + input: DeleteActivityInput(activityArn: activityArn) + ) + } catch { + print("*** Error deleting the activity: \(error.localizedDescription)") + } + } + + /// Sends a task success notification to the activity. + /// + /// - Parameters: + /// - taskToken: The task's token. + /// - response: The task response. + /// + /// - Returns: `ActivityError` and appropriate AWS errors. + public func sendTaskSuccess(taskToken: String, response: String) async -> Bool { + do { + _ = try await sfnClient.sendTaskSuccess( + input: SendTaskSuccessInput(output: response, taskToken: taskToken) + ) + + return true + } catch { + print("*** Error sending task success: \(error.localizedDescription)") + return false + } + } +} \ No newline at end of file diff --git a/swift/example_code/sfn/scenario/Sources/Example.swift b/swift/example_code/sfn/scenario/Sources/Example.swift new file mode 100644 index 00000000000..43113be53e1 --- /dev/null +++ b/swift/example_code/sfn/scenario/Sources/Example.swift @@ -0,0 +1,252 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import AWSIAM +import AWSSFN + +class Example { + let sfnClient: SFNClient + let iamClient: IAMClient + + let username: String + let activityName: String + var activity: Activity? + var stateMachineName: String + var stateMachinei: StateMachine? + var stateMachine: StateMachine? + var definitionPath: String + var runArn: String? = nil + var iamRole: IAMClientTypes.Role? + + init(region: String, username: String, activityName: String, stateMachineName: String, + definitionPath: String) async throws { + let sfnConfig = try await SFNClient.SFNClientConfiguration(region: region) + sfnClient = SFNClient(config: sfnConfig) + + let iamConfig = try await IAMClient.IAMClientConfiguration(region: region) + iamClient = IAMClient(config: iamConfig) + + self.username = username + self.activityName = activityName + self.stateMachineName = stateMachineName + self.definitionPath = definitionPath + } + + /// Clean up artifacts created by the program. + func cleanUp() async { + if iamRole != nil { + print("Deleting the IAM role: \(iamRole?.roleName ?? "")...") + do { + _ = try await iamClient.deleteRole( + input: DeleteRoleInput(roleName: iamRole?.roleName) + ) + } catch { + print("*** Unable to delete the IAM role: \(error.localizedDescription)") + } + } + + if activity != nil { + await activity?.delete() + } + + if stateMachine != nil { + print("Deleting the State Machine...") + await stateMachine?.delete() + } + } + + /// Create a new IAM role. + /// + /// - Returns: The `IAMClientTypes.Role` that was created, or `nil` if it + /// couldn't be created. + func createIAMRole() async -> IAMClientTypes.Role? { + let trustPolicy = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": {"Service": "states.amazonaws.com"}, + "Action": "sts:AssumeRole" + } + ] + } + """ + + do { + let output = try await iamClient.createRole( + input: CreateRoleInput( + assumeRolePolicyDocument: trustPolicy, + roleName: tempName(prefix: "state-machine-demo-role") + ) + ) + + return output.role + } catch { + print("*** Error creating the IAM role: \(error.localizedDescription)") + return nil + } + } + + /// Delete the IAM role. + /// + /// - Throws: The AWS error, if any. + func deleteIAMRole() async throws { + guard let iamRole = self.iamRole else { + return + } + + print("Deleting the IAM role: \(iamRole.roleName ?? "")") + + _ = try await iamClient.deleteRole( + input: DeleteRoleInput(roleName: iamRole.roleName) + ) + } + + /// Generate and return a unique file name that begins with the specified + /// string. + /// + /// - Parameters: + /// - prefix: Text to use at the beginning of the returned name. + /// + /// - Returns: A string containing a unique filename that begins with the + /// specified `prefix`. + /// + /// The returned name uses a random number between 1 million and 1 billion to + /// provide reasonable certainty of uniqueness for the purposes of this + /// example. + func tempName(prefix: String) -> String { + return "\(prefix)-\(Int.random(in: 1000000..<1000000000))" + } + + /// Run the example. + func run() async { + print("Creating the IAM role...") + iamRole = await createIAMRole() + + if iamRole == nil { + print("Unable to create the IAM role. Exiting.") + return + } + + print("Created role: \(iamRole?.roleName ?? "")") + + // Find or create a Step Functions activity. + + print("Finding or creating a Step Functions activity...") + + do { + activity = try await Activity(client: sfnClient, name: activityName) + } catch let error as ActivityError { + print("Unable to create the activity. \(error.errorDescription)") + await cleanUp() + return + } catch { + print("An AWS error occurred: \(error.localizedDescription)") + await cleanUp() + return + } + + guard let activity = activity else { + print("No activity available.") + await cleanUp() + return + } + + print("Created Step Functions activity with ARN \(activity.activityArn).") + + // Find or create a State Machine. + + print("Finding or creating a State Machine...") + do { + stateMachine = try await StateMachine( + sfnClient: sfnClient, + name: stateMachineName, + iamRole: iamRole!, + definitionPath: definitionPath, + activity: activity + ) + } catch let error as StateMachineError { + print("Unable to create the state machine: \(error.errorDescription)") + await cleanUp() + return + } catch { + print("An AWS error occurred while creating the state machine: \(error.localizedDescription)") + await cleanUp() + return + } + + guard let stateMachine = stateMachine else { + print("No state machine available.") + await cleanUp() + return + } + + // Display information about the State Machine. + + do { + try await stateMachine.describe() + } catch let error as StateMachineError { + print("Unable to describe the state machine: \(error.errorDescription)") + await cleanUp() + return + } catch { + print("An AWS error occurred getting state machine details: \(error.localizedDescription)") + await cleanUp() + return + } + + // Run the state machine. + + do { + runArn = try await stateMachine.start(username: username) + } catch let error as StateMachineError { + print("Unable to start the state machine: \(error.errorDescription)") + await cleanUp() + return + } catch { + print("An AWS error occurred while starting the state machine: \(error.localizedDescription)") + await cleanUp() + return + } + + guard let runArn else { + print("Unable to run the state machine. Exiting.") + await cleanUp() + return + } + + // Step through the state machine. This function runs until the state + // machine enters its "done" state. + + do { + try await stateMachine.execute() + } catch let error as StateMachineError { + print("Error executing the state machine: \(error.errorDescription)") + await cleanUp() + return + } catch { + print("AWS error while executing the state machine: \(error.localizedDescription)") + await cleanUp() + return + } + + // Finish running the state machine. + + do { + try await stateMachine.finishExecution(arn: runArn) + } catch let error as StateMachineError { + print("Error while stopping the state machine: \(error.errorDescription)") + await cleanUp() + return + } catch { + print("AWS error while stopping the state machine: \(error.localizedDescription)") + await cleanUp() + return + } + + await cleanUp() + } +} diff --git a/swift/example_code/sfn/scenario/Sources/StateMachine.swift b/swift/example_code/sfn/scenario/Sources/StateMachine.swift new file mode 100644 index 00000000000..4a8e3d14496 --- /dev/null +++ b/swift/example_code/sfn/scenario/Sources/StateMachine.swift @@ -0,0 +1,353 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import AWSSFN +import AWSIAM + +/// Describes an error the occurred while managing the state machine. +enum StateMachineError: Error { + /// No matching role was found. + case roleNotFoundError + /// The ARN is missing from the returned state machine. + case missingArnError + /// The state machine list is missing from the response. + case missingStateMachineListError + /// No matching state machine was found. + case stateMachineNotFoundError + /// Unable to read the state machine definition file. + case definitionFileReadError + /// A state machine's details are missing + case stateMachineDetailsMissingError + /// The task token is missing from the activity task. + case taskTokenMissingError + /// The input is missing from the activity task. + case inputMissingError + /// The state machine's output is missing. + case outputMissingError + /// The state machine's execution has been aborted. + case executionAborted + /// The state machine's execution failed. + case executionFailed + /// The state machine timed out. + case executionTimedOut + /// The state machine's status is unrecognized. + case executionStatusUnknown + + var errorDescription: String { + switch self { + case .roleNotFoundError: + return "The specified role was not found or could not be created" + case .missingArnError: + return "The ARN is missing from the returned activity" + case .missingStateMachineListError: + return "The state machine list is missing from the response" + case .stateMachineNotFoundError: + return "No state machine with the specified name was found" + case .definitionFileReadError: + return "Unable to read the state machine definition file" + case .stateMachineDetailsMissingError: + return "The state machine's details are missing" + case .taskTokenMissingError: + return "The task token is missing from the activity task." + case .inputMissingError: + return "The input is missing from the activity task." + case .outputMissingError: + return "The state machine's output is missing." + case .executionAborted: + return "The state machine's execution was aborted." + case .executionFailed: + return "The state machine's execution failed." + case .executionTimedOut: + return "The state machine's execution timed out." + case .executionStatusUnknown: + return "The state machine has entered an unknown status." + } + } +} + +/// Describes a message and a list of actions that can be taken in response +/// to that message. +struct ActionList: Decodable { + let message: String + let actions: [String] +} + +/// Describes a message returned by an action. +struct Output: Decodable { + let message: String +} + +/// Encapsulates an AWS Step Functions state machine. +class StateMachine { + let sfnClient: SFNClient + let iamRole: IAMClientTypes.Role + let activity: Activity + let stateMachineName: String + let definitionPath: String + var stateMachineArn = "" + + init(sfnClient: SFNClient, name: String, + iamRole: IAMClientTypes.Role, definitionPath: String, + activity: Activity) async throws { + + self.sfnClient = sfnClient + self.iamRole = iamRole + self.stateMachineName = name + self.definitionPath = definitionPath + self.activity = activity + + try await findOrCreateStateMachine() + } + + /// Finds a state machine matching the name specified when initializing the `StateMachine`. + /// - Throws: `StateMachineError` and appropriate AWS errors. + private func findStateMachine() async throws { + let pages = sfnClient.listStateMachinesPaginated( + input: ListStateMachinesInput() + ) + + for try await page in pages { + guard let stateMachines = page.stateMachines else { + throw StateMachineError.missingStateMachineListError + } + + for stateMachine in stateMachines { + if stateMachine.name == stateMachineName { + guard let arn = stateMachine.stateMachineArn else { + throw StateMachineError.missingArnError + } + stateMachineArn = arn + } + } + } + + throw StateMachineError.stateMachineNotFoundError + } + + /// Create a new state machine with the name given when initializing the + /// `StateMachine` object. + /// + /// - Throws: `StateMachineError` and appropriate AWS errors. + private func createStateMachine() async throws { + var definition: String + + print("Reading the state machine file from \(definitionPath)...") + do { + definition = try String(contentsOfFile: definitionPath, encoding: .utf8) + } catch { + throw StateMachineError.definitionFileReadError + } + + // Swap in the activity's ARN into the definition string. + + definition.replace("{{DOC_EXAMPLE_ACTIVITY_ARN}}", with: activity.activityArn) + + let output = try await sfnClient.createStateMachine( + input: CreateStateMachineInput( + definition: definition, + name: stateMachineName, + roleArn: iamRole.arn + ) + ) + + guard let arn = output.stateMachineArn else { + throw StateMachineError.missingArnError + } + + stateMachineArn = arn + } + + /// Finds a state machine matching the name given when initializing the + /// `StateMachine` object. If it doesn't exist, a new one is created. + /// + /// - Throws: `StateMachineError` and appropriate AWS errors. + private func findOrCreateStateMachine() async throws { + do { + try await findStateMachine() + } catch { + try await createStateMachine() + } + } + + /// Outputs a description of the state machine. + /// + /// - Throws: `StateMachineError` and appropriate AWS errors. + func describe() async throws { + let output = try await sfnClient.describeStateMachine( + input: DescribeStateMachineInput( + stateMachineArn: stateMachineArn + ) + ) + + guard let name = output.name, + let status = output.status else { + throw StateMachineError.stateMachineDetailsMissingError + } + + print() + print("State machine details: ") + print(" Name: \(name)") + print(" ARN: \(stateMachineArn)") + print(" Status: \(status)") + print() + } + + /// Start up the state machine. + /// + /// - Parameter username: The username to use for the conversation. + /// + /// - Throws: `StateMachineError` and appropriate AWS errors. + /// - Returns: The execution ARN of the running state machine. + func start(username: String) async throws -> String? { + let runInput = """ + { "name": "\(username)" } + """ + + let output = try await sfnClient.startExecution( + input: StartExecutionInput( + input: runInput, + stateMachineArn: stateMachineArn + ) + ) + + return output.executionArn + } + + /// Execute the steps of the state machine until it exits. + /// + /// - Throws: `StateMachineError` and appropriate AWS errors. + func execute() async throws { + var action: String = "" + + while action != "done" { + let getTaskOutput = try await sfnClient.getActivityTask( + input: GetActivityTaskInput( + activityArn: activity.activityArn + ) + ) + + guard let token = getTaskOutput.taskToken else { + throw StateMachineError.taskTokenMissingError + } + guard let input = getTaskOutput.input else { + throw StateMachineError.inputMissingError + } + + let inputData = input.data(using: .utf8)! + let inputObject = try! JSONDecoder().decode(ActionList.self, from: inputData) + + print("Task message: \(inputObject.message)") + + action = menuRequest(prompt: "Choose an action:", options: inputObject.actions) + _ = await activity.sendTaskSuccess(taskToken: token, response: """ + { "action": "\(action)" } + """ + ) + } + } + + /// Wait for the execution to end, then output its final message. + /// + /// - Parameter arn: The execution ARN to finish. + /// + /// - Throws: `StateMachineError` and appropriate AWS errors. + func finishExecution(arn: String) async throws { + var status: SFNClientTypes.ExecutionStatus = .running + + while status == .running { + let output = try await sfnClient.describeExecution( + input: DescribeExecutionInput( + executionArn: arn + ) + ) + + status = output.status ?? .aborted + + switch status { + case .running: + print("The state machine is still running. Waiting for it to finish.") + await sleep(forSeconds: 1) + case .succeeded: + guard let outputString = output.output else { + throw StateMachineError.outputMissingError + } + + let outputData = outputString.data(using: .utf8)! + let outputObject = try! JSONDecoder().decode(Output.self, from: outputData) + print(""" + Execution completed with final message: \(outputObject.message) + """) + case .aborted: + throw StateMachineError.executionAborted + case .failed: + throw StateMachineError.executionFailed + case .timedOut: + throw StateMachineError.executionTimedOut + default: + throw StateMachineError.executionStatusUnknown + } + } + } + + /// Delete the state machine. + func delete() async { + do { + _ = try await sfnClient.deleteStateMachine( + input: DeleteStateMachineInput(stateMachineArn: stateMachineArn) + ) + } catch { + print("*** Error deleting the state machine: \(error.localizedDescription)") + } + } + + /// Sleep for the specified number of seconds. + /// + /// - Parameter seconds: The number of seconds to sleep, as a floating + /// point value. + func sleep(forSeconds seconds: Double) async { + do { + try await Task.sleep(for: .seconds(seconds)) + } catch { + return + } + + } + + /// 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 string value of the selected option. + func menuRequest(prompt: String, options: [String]) -> String { + let numOptions = options.count + + if numOptions == 0 { + return "done" + } + + print(prompt) + + for (index, value) in options.enumerated() { + print("(\(index+1)) \(value)") + } + + repeat { + print("Enter your selection (1 - \(numOptions)): ", 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 options[answer-1] + } else { + print("Please enter the number matching your selection.") + } + } + } while true + } +} \ No newline at end of file diff --git a/swift/example_code/sfn/scenario/Sources/entry.swift b/swift/example_code/sfn/scenario/Sources/entry.swift new file mode 100644 index 00000000000..97aadc36113 --- /dev/null +++ b/swift/example_code/sfn/scenario/Sources/entry.swift @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// snippet-start:[swift.sfn.scenario] +// An example that shows how to use the AWS SDK for Swift to perform a simple +// operation using Amazon Elastic Compute Cloud (EC2). +// + +import ArgumentParser +import Foundation + +struct ExampleCommand: ParsableCommand { + @Option(help: "The AWS Region to run AWS API calls in.") + var awsRegion = "us-east-1" + + @Option(help: "The user's name.") + var username = "Johanna Doe" + + @Option(help: "The name of the activity to find or create.") + var activityName = "scenario-example-activity" + + @Option(help: "The name of the state machine to find or create.") + var stateMachineName = "scenario-example-state-machine" + + @Option(help: "Path of the State Machine definition file.") + var definitionPath = "../../../../../../../resources/sample_files/chat_sfn_state_machine.json" + + static var configuration = CommandConfiguration( + commandName: "sfn-scenario", + abstract: """ + Demonstrates a variety of AWS Step Function features. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + let example = try await Example(region: awsRegion, username: username, + activityName: activityName, + stateMachineName: stateMachineName, + definitionPath: definitionPath) + + await example.run() + } +} + +/// 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.sfn.scenario] From 53774a48d9a73581fbc27850b15f0670dfc7e9c0 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 28 Aug 2025 18:02:12 +0000 Subject: [PATCH 3/7] Add SoS metadata --- .doc_gen/metadata/sfn_metadata.yaml | 103 +++++++++++++++ swift/example_code/sfn/README.md | 122 ++++++++++++++++++ .../sfn/scenario/Sources/Activity.swift | 14 +- .../sfn/scenario/Sources/StateMachine.swift | 20 ++- .../sfn/scenario/Sources/entry.swift | 2 +- 5 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 swift/example_code/sfn/README.md diff --git a/.doc_gen/metadata/sfn_metadata.yaml b/.doc_gen/metadata/sfn_metadata.yaml index 75a96e309fc..be3cbc837df 100644 --- a/.doc_gen/metadata/sfn_metadata.yaml +++ b/.doc_gen/metadata/sfn_metadata.yaml @@ -81,6 +81,14 @@ sfn_CreateStateMachine: snippet_tags: - python.example_code.sfn.StateMachine_decl - python.example_code.sfn.CreateStateMachine + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.CreateStateMachine services: sfn: {CreateStateMachine} sfn_DeleteStateMachine: @@ -122,6 +130,14 @@ sfn_DeleteStateMachine: snippet_tags: - python.example_code.sfn.StateMachine_decl - python.example_code.sfn.DeleteStateMachine + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.DeleteStateMachine services: sfn: {DeleteStateMachine} sfn_ListStateMachines: @@ -163,6 +179,14 @@ sfn_ListStateMachines: snippet_tags: - python.example_code.sfn.StateMachine_decl - python.example_code.sfn.ListStateMachines + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.ListStateMachines services: sfn: {ListStateMachines} sfn_DescribeStateMachine: @@ -204,6 +228,14 @@ sfn_DescribeStateMachine: snippet_tags: - python.example_code.sfn.StateMachine_decl - python.example_code.sfn.DescribeStateMachine + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.DescribeStateMachine services: sfn: {DescribeStateMachine} sfn_StartExecution: @@ -254,6 +286,14 @@ sfn_StartExecution: - description: snippet_files: - javascriptv3/example_code/sfn/actions/start-execution.js + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.StartExecution services: sfn: {StartExecution} sfn_ListExecutions: @@ -325,6 +365,14 @@ sfn_DescribeExecution: - description: snippet_tags: - python.example_code.sfn.DescribeExecution + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.DescribeExecution services: sfn: {DescribeExecution} sfn_CreateActivity: @@ -366,6 +414,14 @@ sfn_CreateActivity: snippet_tags: - python.example_code.sfn.Activity_decl - python.example_code.sfn.CreateActivity + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.CreateActivity services: sfn: {CreateActivity} sfn_ListActivities: @@ -407,6 +463,14 @@ sfn_ListActivities: snippet_tags: - python.example_code.sfn.Activity_decl - python.example_code.sfn.ListActivities + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.CreateActivity services: sfn: {ListActivities} sfn_GetActivityTask: @@ -447,6 +511,14 @@ sfn_GetActivityTask: snippet_tags: - python.example_code.sfn.Activity_decl - python.example_code.sfn.GetActivityTask + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.GetActivityTask services: sfn: {GetActivityTask} sfn_SendTaskSuccess: @@ -488,6 +560,14 @@ sfn_SendTaskSuccess: snippet_tags: - python.example_code.sfn.Activity_decl - python.example_code.sfn.SendTaskSuccess + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.SendTaskSuccess services: sfn: {SendTaskSuccess} sfn_DeleteActivity: @@ -529,6 +609,14 @@ sfn_DeleteActivity: snippet_tags: - python.example_code.sfn.Activity_decl - python.example_code.sfn.DeleteActivity + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - snippet_tags: + - swift.sfn.import + - swift.sfn.DeleteActivity services: sfn: {DeleteActivity} sfn_Scenario_GetStartedStateMachines: @@ -586,6 +674,21 @@ sfn_Scenario_GetStartedStateMachines: - description: Define a class that wraps activity actions. snippet_tags: - python.example_code.sfn.Activity_full + Swift: + versions: + - sdk_version: 1 + github: swift/example_code/sfn/scenario + excerpts: + - description: The main program, entry.swift. + snippet_tags: + - swift.sfn.scenario + - description: The activity manager, Activity.swift + snippet_tags: + - swift.sfn.scenario.activity + - description: The state machine manager, StateMachine.swift + snippet_tags: + - swift.sfn.scenario.statemachine + services: sfn: {CreateActivity, CreateStateMachine, DeleteActivity, DeleteStateMachine, DescribeExecution, DescribeStateMachine, GetActivityTask, ListActivities, ListStateMachines, SendTaskSuccess, StartExecution, StopExecution} diff --git a/swift/example_code/sfn/README.md b/swift/example_code/sfn/README.md new file mode 100644 index 00000000000..ecdac41c071 --- /dev/null +++ b/swift/example_code/sfn/README.md @@ -0,0 +1,122 @@ +# Step Functions code examples for the SDK for Swift + +## Overview + +Shows how to use the AWS SDK for Swift to work with AWS Step Functions. + + + + +_Step Functions makes it easy to coordinate the components of distributed applications as a series of steps in a visual workflow._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `swift` folder. + + + + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](scenario/Sources/entry.swift) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreateActivity](scenario/Sources/Activity.swift#L42) +- [CreateStateMachine](scenario/Sources/StateMachine.swift#L133) +- [DeleteActivity](scenario/Sources/Activity.swift#L102) +- [DeleteStateMachine](scenario/Sources/StateMachine.swift#L308) +- [DescribeExecution](scenario/Sources/StateMachine.swift#L263) +- [DescribeStateMachine](scenario/Sources/StateMachine.swift#L180) +- [GetActivityTask](scenario/Sources/StateMachine.swift#L228) +- [ListActivities](scenario/Sources/Activity.swift#L42) +- [ListStateMachines](scenario/Sources/StateMachine.swift#L105) +- [SendTaskSuccess](scenario/Sources/Activity.swift#L115) +- [StartExecution](scenario/Sources/StateMachine.swift#L205) + + + + + +## Run the examples + +### Instructions + +To build any of these examples from a terminal window, navigate into its +directory, then use the following command: + +``` +$ swift build +``` + +To build one of these examples in Xcode, navigate to the example's directory +(such as the `ListUsers` directory, to build that example). Then type `xed.` +to open the example directory in Xcode. You can then use standard Xcode build +and run commands. + + + + + +#### Learn the basics + +This example shows you how to do the following: + +- Create an activity. +- Create a state machine from an Amazon States Language definition that contains the previously created activity as a step. +- Run the state machine and respond to the activity with user input. +- Get the final status and output after the run completes, then clean up resources. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `swift` folder. + + + + + + +## Additional resources + +- [Step Functions Developer Guide](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html) +- [Step Functions API Reference](https://docs.aws.amazon.com/step-functions/latest/apireference/Welcome.html) +- [SDK for Swift Step Functions reference](https://sdk.amazonaws.com/swift/api/awssfn/latest/documentation/awssfn) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/swift/example_code/sfn/scenario/Sources/Activity.swift b/swift/example_code/sfn/scenario/Sources/Activity.swift index e870e9df5b0..375a0b0100c 100644 --- a/swift/example_code/sfn/scenario/Sources/Activity.swift +++ b/swift/example_code/sfn/scenario/Sources/Activity.swift @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// snippet-start:[swift.sfn.scenario.activity] import Foundation import AWSSFN @@ -38,6 +39,7 @@ class Activity { try await self.findOrCreateActivity() } + // snippet-start:[swift.sfn.CreateActivity] /// Create a new Step Functions activity. /// /// - Throws: `ActivityError` and appropriate AWS errors. @@ -52,7 +54,10 @@ class Activity { activityArn = arn } + // snippet-end:[swift.sfn.CreateActivity] + // snippet-start:[swift.sfn.ListActivitiesPaginated] + // snippet-start:[swift.sfn.ListActivities] /// Find an activity with the name specified when initializing the /// `Activity` object. /// @@ -79,6 +84,8 @@ class Activity { throw ActivityError.activityNotFoundError } + // snippet-end:[swift.sfn.ListActivities] + // snippet-end:[swift.sfn.ListActivitiesPaginated] /// Finds an existing activity with the name given when initializing /// the `Activity`. If one isn't found, a new one is created. @@ -92,6 +99,7 @@ class Activity { } } + // snippet-start:[swift.sfn.DeleteActivity] /// Delete the activity described by this object. public func delete() async { do { @@ -102,7 +110,9 @@ class Activity { print("*** Error deleting the activity: \(error.localizedDescription)") } } + // snippet-end:[swift.sfn.DeleteActivity] + // snippet-start:[swift.sfn.SendTaskSuccess] /// Sends a task success notification to the activity. /// /// - Parameters: @@ -122,4 +132,6 @@ class Activity { return false } } -} \ No newline at end of file + // snippet-end:[swift.sfn.SendTaskSuccess] +} +// snippet-end:[swift.sfn.scenario.activity] diff --git a/swift/example_code/sfn/scenario/Sources/StateMachine.swift b/swift/example_code/sfn/scenario/Sources/StateMachine.swift index 4a8e3d14496..b18bf61c2ec 100644 --- a/swift/example_code/sfn/scenario/Sources/StateMachine.swift +++ b/swift/example_code/sfn/scenario/Sources/StateMachine.swift @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// snippet-start:[swift.sfn.scenario.statemachine] import Foundation import AWSSFN import AWSIAM @@ -100,6 +101,8 @@ class StateMachine { try await findOrCreateStateMachine() } + // snippet-start:[swift.sfn.ListStateMachinesPaginated] + // snippet-start:[swift.sfn.ListStateMachines] /// Finds a state machine matching the name specified when initializing the `StateMachine`. /// - Throws: `StateMachineError` and appropriate AWS errors. private func findStateMachine() async throws { @@ -124,7 +127,10 @@ class StateMachine { throw StateMachineError.stateMachineNotFoundError } + // snippet-end:[swift.sfn.ListStateMachines] + // snippet-end:[swift.sfn.ListStateMachinesPaginated] + // snippet-start:[swift.sfn.CreateStateMachine] /// Create a new state machine with the name given when initializing the /// `StateMachine` object. /// @@ -157,6 +163,7 @@ class StateMachine { stateMachineArn = arn } + // snippet-end:[swift.sfn.CreateStateMachine] /// Finds a state machine matching the name given when initializing the /// `StateMachine` object. If it doesn't exist, a new one is created. @@ -170,6 +177,7 @@ class StateMachine { } } + // snippet-start:[swift.sfn.DescribeStateMachine] /// Outputs a description of the state machine. /// /// - Throws: `StateMachineError` and appropriate AWS errors. @@ -192,7 +200,9 @@ class StateMachine { print(" Status: \(status)") print() } + // snippet-end:[swift.sfn.DescribeStateMachine] + // snippet-start:[swift.sfn.StartExecution] /// Start up the state machine. /// /// - Parameter username: The username to use for the conversation. @@ -213,7 +223,9 @@ class StateMachine { return output.executionArn } + // snippet-end:[swift.sfn.StartExecution] + // snippet-start:[swift.sfn.GetActivityTask] /// Execute the steps of the state machine until it exits. /// /// - Throws: `StateMachineError` and appropriate AWS errors. @@ -246,7 +258,9 @@ class StateMachine { ) } } + // snippet-end:[swift.sfn.GetActivityTask] + // snippet-start:[swift.sfn.DescribeExecution] /// Wait for the execution to end, then output its final message. /// /// - Parameter arn: The execution ARN to finish. @@ -289,7 +303,9 @@ class StateMachine { } } } + // snippet-end:[swift.sfn.DescribeExecution] + // snippet-start:[swift.sfn.DeleteStateMachine] /// Delete the state machine. func delete() async { do { @@ -300,6 +316,7 @@ class StateMachine { print("*** Error deleting the state machine: \(error.localizedDescription)") } } + // snippet-end:[swift.sfn.DeleteStateMachine] /// Sleep for the specified number of seconds. /// @@ -350,4 +367,5 @@ class StateMachine { } } while true } -} \ No newline at end of file +} +// snippet-end:[swift.sfn.scenario.statemachine] diff --git a/swift/example_code/sfn/scenario/Sources/entry.swift b/swift/example_code/sfn/scenario/Sources/entry.swift index 97aadc36113..c9ce063ae97 100644 --- a/swift/example_code/sfn/scenario/Sources/entry.swift +++ b/swift/example_code/sfn/scenario/Sources/entry.swift @@ -23,7 +23,7 @@ struct ExampleCommand: ParsableCommand { var stateMachineName = "scenario-example-state-machine" @Option(help: "Path of the State Machine definition file.") - var definitionPath = "../../../../../../../resources/sample_files/chat_sfn_state_machine.json" + var definitionPath: String static var configuration = CommandConfiguration( commandName: "sfn-scenario", From 19ad169f72a6516305e8f4a74d0a34a385c7696e Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Fri, 29 Aug 2025 15:00:30 +0000 Subject: [PATCH 4/7] Fix metadata bug --- .doc_gen/metadata/sfn_metadata.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.doc_gen/metadata/sfn_metadata.yaml b/.doc_gen/metadata/sfn_metadata.yaml index be3cbc837df..f79a68b4985 100644 --- a/.doc_gen/metadata/sfn_metadata.yaml +++ b/.doc_gen/metadata/sfn_metadata.yaml @@ -682,10 +682,10 @@ sfn_Scenario_GetStartedStateMachines: - description: The main program, entry.swift. snippet_tags: - swift.sfn.scenario - - description: The activity manager, Activity.swift + - description: The activity manager, Activity.swift. snippet_tags: - swift.sfn.scenario.activity - - description: The state machine manager, StateMachine.swift + - description: The state machine manager, StateMachine.swift. snippet_tags: - swift.sfn.scenario.statemachine From 77dc42b60f0bf009127ee8451146c0df50af88c1 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 4 Sep 2025 16:39:40 +0000 Subject: [PATCH 5/7] Add CognitoAWSCredentialIdentityResolver example --- .../cognito-resolver/Package.swift | 42 ++++ .../cognito-resolver/Sources/Example.swift | 232 ++++++++++++++++++ .../cognito-resolver/Sources/entry.swift | 57 +++++ 3 files changed, 331 insertions(+) create mode 100644 swift/example_code/identity-resolvers/cognito-resolver/Package.swift create mode 100644 swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift create mode 100644 swift/example_code/identity-resolvers/cognito-resolver/Sources/entry.swift diff --git a/swift/example_code/identity-resolvers/cognito-resolver/Package.swift b/swift/example_code/identity-resolvers/cognito-resolver/Package.swift new file mode 100644 index 00000000000..458af4b323c --- /dev/null +++ b/swift/example_code/identity-resolvers/cognito-resolver/Package.swift @@ -0,0 +1,42 @@ +// 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: "cognito-resolver", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v11), + .iOS(.v13) + ], + 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: "cognito-resolver", + dependencies: [ + .product(name: "AWSCognitoIdentity", package: "aws-sdk-swift"), + .product(name: "AWSIAM", package: "aws-sdk-swift"), + .product(name: "AWSSTS", package: "aws-sdk-swift"), + .product(name: "AWSS3", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources"), + ] +) diff --git a/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift b/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift new file mode 100644 index 00000000000..0f55083a61b --- /dev/null +++ b/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift @@ -0,0 +1,232 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[swift.identity.cognito.imports] +import AWSCognitoIdentity +import AWSSDKIdentity +import AWSSTS +// snippet-end:[swift.identity.cognito.imports] +import AWSIAM +import AWSS3 +import Foundation + +/// Contains the data and code for the main body of the example. +class Example { + let region: String + + var cognitoIdentityClient: CognitoIdentityClient! + var iamClient: IAMClient! + + let identityPoolName: String + var identityPoolID: String! + var roleName: String + + let managedPolicyName: String + var managedPolicyArn: String? + + /// Initialize the example. + /// + /// - Parameter region: The AWS Region to operate in. + /// + /// - Throws: Any AWS errors thrown by IAM or Cognito. + /// + /// ^ Note: IAM must always use `us-east-1`, so it doesn't use the value + /// of the `region` parameter. + init(region: String) throws { + self.region = region + + self.identityPoolName = "cognito-resolver-example-\(UUID().uuidString.split(separator: "-").first!.lowercased())" + cognitoIdentityClient = try CognitoIdentityClient(region: region) + + self.roleName = "cognito-unauth-\(identityPoolName)" + iamClient = try IAMClient(region: "us-east-1") + + self.managedPolicyName = "cognito-policy-\(identityPoolName)" + } + + /// The body of the example. + /// + /// - Throws: Errors from IAM, STS, or Cognito. + func run() async throws { + // Create an identity pool to use for this example. + + print("Creating a Cognito identity pool named \(identityPoolName)...") + identityPoolID = try await cognitoIdentityClient.createIdentityPool( + input: CreateIdentityPoolInput( + allowUnauthenticatedIdentities: true, + identityPoolName: identityPoolName + ) + ).identityPoolId + + // Create an IAM role for unauthenticated users. + + let trustPolicy = """ + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Federated": "cognito-identity.amazonaws.com"}, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": {"cognito-identity.amazonaws.com:aud": "\(identityPoolID!)"}, + "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "unauthenticated"} + } + }] + } + """ + + print("Creating an IAM role named \(roleName)...") + let createRoleInput = CreateRoleInput( + assumeRolePolicyDocument: trustPolicy, + roleName: roleName + ) + let createRoleOutput = try await iamClient.createRole(input: createRoleInput) + + guard let role = createRoleOutput.role else { + print("*** No role returned by CreateRole!") + await cleanup() + return + } + + // Wait for the role to be available. + + print("Waiting for the role to be available...") + try await Task.sleep(nanoseconds: 10_000_000_000) // Wait 10 seconds + + // Assign the role to the identity pool. + + print("Setting the identity pool's roles...") + _ = try await cognitoIdentityClient.setIdentityPoolRoles( + input: SetIdentityPoolRolesInput( + identityPoolId: identityPoolID, + roles: ["unauthenticated": role.arn!] + ) + ) + + //====================================================================== + // Resolve an identity using the Cognito credential identity resolver + // with the AWS STS function getCallerIdentity(input:). This is done + // by configuring the STS client to use the Cognito credentials + // resolver. + //====================================================================== + + // snippet-start:[swift.identity.cognito.resolve] + let cognitoCredentialResolver = try CognitoAWSCredentialIdentityResolver( + identityPoolId: identityPoolID, + identityPoolRegion: region + ) + + let cognitoSTSConfig = try await STSClient.STSClientConfiguration( + awsCredentialIdentityResolver: cognitoCredentialResolver, + region: "us-east-1" + ) + let cognitoSTSClient = STSClient(config: cognitoSTSConfig) + + let output = try await cognitoSTSClient.getCallerIdentity( + input: GetCallerIdentityInput() + ) + + print("Authenticated with AWS using Cognito!") + print(" ARN: \(output.arn ?? "")") + print(" Account ID: \(output.account ?? "")") + print(" User ID: \(output.userId ?? "")") + // snippet-end:[swift.identity.cognito.resolve] + + //====================================================================== + // Add a managed policy to the role to allow access to the AWS S3 + // function ListBuckets. + //====================================================================== + + print("Creating a managed policy to allow listing S3 buckets...") + let createPolicyOutput = try await iamClient.createPolicy( + input: CreatePolicyInput( + policyDocument: """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "arn:aws:s3:::*" + } + ] + } + """, + policyName: managedPolicyName + ) + ) + + guard let managedPolicy = createPolicyOutput.policy else { + print("No policy returned by CreatePolicy!") + await cleanup() + return + } + + managedPolicyArn = managedPolicy.arn + + print("Attaching the policy to the IAM role...") + _ = try await iamClient.attachRolePolicy( + input: AttachRolePolicyInput( + policyArn: managedPolicy.arn, + roleName: roleName + ) + ) + + // Wait for the policy to attach. + + print("Waiting for the policy to attach to the role...") + try await Task.sleep(nanoseconds: 10_000_000_000) // Wait 10 seconds + + //====================================================================== + // This is where you can do tasks using the returned AWS credentials. + // In this example, we list S3 buckets. + //====================================================================== + + let s3Config = try await S3Client.S3ClientConfiguration( + awsCredentialIdentityResolver: cognitoCredentialResolver, + region: region + ) + let s3Client = S3Client(config: s3Config) + + let listBucketsOutput = try await s3Client.listBuckets( + input: ListBucketsInput() + ) + guard let buckets = listBucketsOutput.buckets else { + print("No buckets returned by S3!") + await cleanup() + return + } + + print("Found \(buckets.count) S3 buckets:") + for bucket in buckets { + print(" \(bucket.name ?? "")") + } + + //====================================================================== + // Clean up before exiting. + //====================================================================== + + await cleanup() + } + + /// Clean up by deleting AWS assets created by the example. Ignores + /// errors since this is just simple cleanup work. + func cleanup() async { + print("Deleting the identity pool...") + _ = try? await cognitoIdentityClient.deleteIdentityPool( + input: DeleteIdentityPoolInput(identityPoolId: identityPoolID) + ) + + print("Deleting the policy...") + if managedPolicyArn != nil { + _ = try? await iamClient.deletePolicy( + input: DeletePolicyInput(policyArn: managedPolicyArn) + ) + } + + print ("Deleting the IAM role...") + _ = try? await iamClient.deleteRole( + input: DeleteRoleInput(roleName: roleName) + ) + } +} \ No newline at end of file diff --git a/swift/example_code/identity-resolvers/cognito-resolver/Sources/entry.swift b/swift/example_code/identity-resolvers/cognito-resolver/Sources/entry.swift new file mode 100644 index 00000000000..917861d533e --- /dev/null +++ b/swift/example_code/identity-resolvers/cognito-resolver/Sources/entry.swift @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +/// A simple example that shows how to use the AWS SDK for Swift to +/// authenticate using Amazon Cognito. + +// snippet-start:[swift.identity.cognito.imports] +import ArgumentParser +import AWSCognitoIdentity +import AWSIAM +import AWSS3 +import AWSSDKIdentity +import AWSSTS +import Foundation +import SmithyIdentity +// snippet-end:[swift.identity.cognito.imports] + +struct ExampleCommand: ParsableCommand { + @Option(help: "AWS Region name") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "cognito-resolver", + abstract: """ + Demonstrates how to use a Cognito credential identity resolver with the + AWS SDK for Swift. + """, + discussion: """ + """ + ) + + /// Called by ``main()`` to do the actual running of the AWS + /// example. + func runAsync() async throws { + let example = try Example(region: region) + + try await example.run() + } +} + +/// The program's asynchronous entry point. +@main +struct Main { + /// The function that serves as the main asynchronous entry point for the + /// example. It parses the command line using the Swift Argument Parser, + /// then calls the `runAsync()` function to run the example itself. + 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 7c65cb8494f67b24cf8726ecb587d32d8ed19aa1 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 4 Sep 2025 17:04:03 +0000 Subject: [PATCH 6/7] Use defer to cleanup --- .../cognito-resolver/Sources/Example.swift | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift b/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift index 0f55083a61b..848f8d2d8a6 100644 --- a/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift +++ b/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift @@ -16,12 +16,16 @@ class Example { var cognitoIdentityClient: CognitoIdentityClient! var iamClient: IAMClient! + var roleName: String + /// The name of the AWS Cognito Identity Pool to use. let identityPoolName: String + /// The ID of the Identity Pool. var identityPoolID: String! - var roleName: String + /// The name of the managed policy granting Amazon S3 permissions. let managedPolicyName: String + /// The ARN of the managed policy granting S3 permissions. var managedPolicyArn: String? /// Initialize the example. @@ -48,6 +52,16 @@ class Example { /// /// - Throws: Errors from IAM, STS, or Cognito. func run() async throws { + // Set up the cleanup function to run automatically when this object + // is discarded. This way, we clean up AWS artifacts whether the run + // is successful or an error occurs. + + defer { + blocking { + await self.cleanup() + } + } + // Create an identity pool to use for this example. print("Creating a Cognito identity pool named \(identityPoolName)...") @@ -84,7 +98,6 @@ class Example { guard let role = createRoleOutput.role else { print("*** No role returned by CreateRole!") - await cleanup() return } @@ -158,7 +171,6 @@ class Example { guard let managedPolicy = createPolicyOutput.policy else { print("No policy returned by CreatePolicy!") - await cleanup() return } @@ -193,7 +205,6 @@ class Example { ) guard let buckets = listBucketsOutput.buckets else { print("No buckets returned by S3!") - await cleanup() return } @@ -201,12 +212,6 @@ class Example { for bucket in buckets { print(" \(bucket.name ?? "")") } - - //====================================================================== - // Clean up before exiting. - //====================================================================== - - await cleanup() } /// Clean up by deleting AWS assets created by the example. Ignores @@ -229,4 +234,16 @@ class Example { input: DeleteRoleInput(roleName: roleName) ) } + + /// Create a function that blocks the caller until execution is complete. + /// + /// - Parameter block: The function to call and wait for its return. + private func blocking(_ block: @escaping @Sendable () async -> Void) { + let semaphore = DispatchSemaphore(value: 0) + Task { + await block() + semaphore.signal() + } + semaphore.wait() + } } \ No newline at end of file From 0e27c68452d9290da32833a45c800ce5c72d3d73 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Thu, 4 Sep 2025 17:31:08 +0000 Subject: [PATCH 7/7] Add another SOS tag --- .../cognito-resolver/Sources/Example.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift b/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift index 848f8d2d8a6..ea3f95c0e16 100644 --- a/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift +++ b/swift/example_code/identity-resolvers/cognito-resolver/Sources/Example.swift @@ -124,11 +124,15 @@ class Example { //====================================================================== // snippet-start:[swift.identity.cognito.resolve] + // Create a Cognito credential resolver that uses the Cognito Identity + // Pool created above. let cognitoCredentialResolver = try CognitoAWSCredentialIdentityResolver( identityPoolId: identityPoolID, identityPoolRegion: region ) + // Create an AWS STS client that uses the new Cognito credential + // resolver to do credential identity resolution. let cognitoSTSConfig = try await STSClient.STSClientConfiguration( awsCredentialIdentityResolver: cognitoCredentialResolver, region: "us-east-1" @@ -194,6 +198,7 @@ class Example { // In this example, we list S3 buckets. //====================================================================== + // snippet-start:[swift.identity.cognito.s3] let s3Config = try await S3Client.S3ClientConfiguration( awsCredentialIdentityResolver: cognitoCredentialResolver, region: region @@ -203,6 +208,7 @@ class Example { let listBucketsOutput = try await s3Client.listBuckets( input: ListBucketsInput() ) + // snippet-end:[swift.identity.cognito.s3] guard let buckets = listBucketsOutput.buckets else { print("No buckets returned by S3!") return