From 6ee4a293e3428318bcec337969cef064b65ebc7f Mon Sep 17 00:00:00 2001 From: jey Date: Wed, 23 Oct 2024 20:45:05 -0500 Subject: [PATCH 1/7] SDKS-3458 - Enabling swift6 and Concurrency check --- .../PingDavinci.xcodeproj/project.pbxproj | 4 + .../PingDavinci/collector/Collector.swift | 4 +- .../collector/CollectorFactory.swift | 74 +++++++++---------- .../collector/FieldCollector.swift | 53 +++++++++++-- .../PingDavinci/collector/FlowCollector.swift | 2 +- PingDavinci/PingDavinci/collector/Form.swift | 52 +++++++++---- .../collector/PasswordCollector.swift | 2 +- .../collector/SubmitCollector.swift | 2 +- .../PingDavinci/collector/TextCollector.swift | 2 +- PingDavinci/PingDavinci/module/Oidc.swift | 2 +- .../PingDavinci/module/Transform.swift | 13 ++-- .../PingLogger.xcodeproj/project.pbxproj | 4 + PingLogger/PingLogger/Logger.swift | 2 +- .../PingOrchestrate/CookieModule.swift | 4 +- .../PingOrchestrate/CustomHeader.swift | 2 +- .../PingOrchestrate/PingHTTPCookie.swift | 2 +- .../PingOrchestrate/WorkFlow.swift | 32 ++++---- .../PingExample/DavinciViewModel.swift | 2 +- 18 files changed, 165 insertions(+), 93 deletions(-) diff --git a/PingDavinci/PingDavinci.xcodeproj/project.pbxproj b/PingDavinci/PingDavinci.xcodeproj/project.pbxproj index a072090..d85e3b8 100644 --- a/PingDavinci/PingDavinci.xcodeproj/project.pbxproj +++ b/PingDavinci/PingDavinci.xcodeproj/project.pbxproj @@ -439,6 +439,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -497,6 +498,7 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -533,6 +535,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -567,6 +570,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PingDavinci/PingDavinci/collector/Collector.swift b/PingDavinci/PingDavinci/collector/Collector.swift index 0fd6134..d93d710 100644 --- a/PingDavinci/PingDavinci/collector/Collector.swift +++ b/PingDavinci/PingDavinci/collector/Collector.swift @@ -12,9 +12,9 @@ import PingOrchestrate import Foundation /// Protocol representing a Collector. -public protocol Collector: Action, Identifiable { +public protocol Collector: Action, Identifiable, Sendable { var id: UUID { get } - init(with json: [String: Any]) + init(with json: Field) } extension ContinueNode { diff --git a/PingDavinci/PingDavinci/collector/CollectorFactory.swift b/PingDavinci/PingDavinci/collector/CollectorFactory.swift index 3f6a6bd..edd1be3 100644 --- a/PingDavinci/PingDavinci/collector/CollectorFactory.swift +++ b/PingDavinci/PingDavinci/collector/CollectorFactory.swift @@ -14,42 +14,42 @@ import PingOrchestrate /// The CollectorFactory singleton is responsible for creating and managing Collector instances. /// It maintains a dictionary of collector creation functions, keyed by type. /// It also provides functions to register new types of collectors and to create collectors from a JSON array. -final class CollectorFactory { - // A dictionary to hold the collector creation functions. - public var collectors: [String: any Collector.Type] = [:] - - public static let shared = CollectorFactory() - - init() { - register(type: Constants.TEXT, collector: TextCollector.self) - register(type: Constants.PASSWORD, collector: PasswordCollector.self) - register(type: Constants.SUBMIT_BUTTON, collector: SubmitCollector.self) - register(type: Constants.FLOW_BUTTON, collector: FlowCollector.self) - } - - /// Registers a new type of Collector. - /// - Parameters: - /// - type: The type of the Collector. - /// - block: A function that creates a new instance of the Collector. - public func register(type: String, collector: any Collector.Type) { - collectors[type] = collector - } - - /// Creates a list of Collector instances from an array of dictionaries. - /// Each dictionary should have a "type" field that matches a registered Collector type. - /// - Parameter array: The array of dictionaries to create the Collectors from. - /// - Returns: A list of Collector instances. - func collector(from array: [[String: Any]]) -> Collectors { - var list: [any Collector] = [] - for item in array { - if let type = item[Constants.type] as? String, let collectorType = collectors[type] { - list.append(collectorType.init(with: item)) - } - } - return list - } - - func reset() { - collectors.removeAll() +public actor CollectorFactory { + // A dictionary to hold the collector creation functions. + public var collectors: [String: any Collector.Type] = [:] + + public static let shared = CollectorFactory() + + init() { + collectors[Constants.TEXT] = TextCollector.self + collectors[Constants.PASSWORD] = PasswordCollector.self + collectors[Constants.SUBMIT_BUTTON] = SubmitCollector.self + collectors[Constants.FLOW_BUTTON] = FlowCollector.self + } + + /// Registers a new type of Collector. + /// - Parameters: + /// - type: The type of the Collector. + /// - block: A function that creates a new instance of the Collector. + public func register(type: String, collector: any Collector.Type) { + collectors[type] = collector + } + + /// Creates a list of Collector instances from an array of dictionaries. + /// Each dictionary should have a "type" field that matches a registered Collector type. + /// - Parameter array: The array of dictionaries to create the Collectors from. + /// - Returns: A list of Collector instances. + func collector(from array: [Field]) -> Collectors { + var list: [any Collector] = [] + for item in array { + if let collectorType = collectors[item.type] { + list.append(collectorType.init(with: item)) + } } + return list + } + + func reset() { + collectors.removeAll() + } } diff --git a/PingDavinci/PingDavinci/collector/FieldCollector.swift b/PingDavinci/PingDavinci/collector/FieldCollector.swift index 61c7806..a83efa7 100644 --- a/PingDavinci/PingDavinci/collector/FieldCollector.swift +++ b/PingDavinci/PingDavinci/collector/FieldCollector.swift @@ -11,20 +11,59 @@ import Foundation + /// Abstract class representing a field collector. /// - property key: The key of the field collector. /// - property label The label of the field collector. /// - property value The value of the field collector. It's open for modification. -open class FieldCollector: Collector { - public var key: String = "" - public var label: String = "" - public var value: String = "" +open class FieldCollector: Collector, @unchecked Sendable { + + // Private queue for thread-safe access + private let syncQueue = DispatchQueue(label: "com.fieldCollector.syncQueue", attributes: .concurrent) + + // Private backing properties to store data + private var _key: String = "" + private var _label: String = "" + private var _value: String = "" + + // Public computed properties for thread-safe access + public var key: String { + get { + return syncQueue.sync { _key } + } + set { + syncQueue.async(flags: .barrier) { self._key = newValue } + } + } + + public var label: String { + get { + return syncQueue.sync { _label } + } + set { + syncQueue.async(flags: .barrier) { self._label = newValue } + } + } + + public var value: String { + get { + return syncQueue.sync { _value } + } + set { + syncQueue.async(flags: .barrier) { self._value = newValue } + } + } + public let id = UUID() + // Default initializer public init() {} - required public init(with json: [String: Any]) { - key = json[Constants.key] as? String ?? "" - label = json[Constants.label] as? String ?? "" + // Initializer with a JSON field + required public init(with json: Field) { + syncQueue.async(flags: .barrier) { + self._key = json.key + self._label = json.label + } } } diff --git a/PingDavinci/PingDavinci/collector/FlowCollector.swift b/PingDavinci/PingDavinci/collector/FlowCollector.swift index 325a189..cfc15d5 100644 --- a/PingDavinci/PingDavinci/collector/FlowCollector.swift +++ b/PingDavinci/PingDavinci/collector/FlowCollector.swift @@ -14,4 +14,4 @@ import Foundation /// Class representing a FlowCollector. /// This class inherits from the FieldCollector class and implements the Collector protocol. /// It is used to collect data in a flow. -public class FlowCollector: FieldCollector {} +public class FlowCollector: FieldCollector, @unchecked Sendable {} diff --git a/PingDavinci/PingDavinci/collector/Form.swift b/PingDavinci/PingDavinci/collector/Form.swift index 9fff84d..d0fe9e5 100644 --- a/PingDavinci/PingDavinci/collector/Form.swift +++ b/PingDavinci/PingDavinci/collector/Form.swift @@ -13,20 +13,44 @@ import Foundation /// Class that handles the parsing and JSON representation of collectors. /// This class provides functions to parse a JSON object into a list of collectors and to represent a list of collectors as a JSON object. -class Form { - - /// Parses a JSON object into a list of collectors. - /// This function takes a JSON object and extracts the "form" field. It then iterates over the "fields" array in the "components" object, - /// parsing each field into a collector and adding it to a list. - /// - Parameter json :The JSON object to parse. - /// - Returns: A list of collectors parsed from the JSON object. - static func parse(json: [String: Any]) -> Collectors { - var collectors = Collectors() - if let form = json[Constants.form] as? [String: Any], - let components = form[Constants.components] as? [String: Any], - let fields = components[Constants.fields] as? [[String: Any]] { - collectors = CollectorFactory().collector(from: fields) +actor Form { + + /// Parses a JSON object into a list of collectors. + /// This function takes a JSON object and extracts the "form" field. It then iterates over the "fields" array in the "components" object, + /// parsing each field into a collector and adding it to a list. + /// - Parameter json :The JSON object to parse. + /// - Returns: A list of collectors parsed from the JSON object. + static func parse(json: [String: Any]) async -> Collectors { + var collectors = Collectors() + if let form = json[Constants.form] as? [String: Any], + let components = form[Constants.components] as? [String: Any], + let fields = components[Constants.fields] as? [[String: Any]] { + + let factory = CollectorFactory.shared + + let fields: [Field] = fields.compactMap { fieldDict in + if let type = fieldDict["type"] as? String { + let value = fieldDict["value"] as? String ?? "" + let key = fieldDict[Constants.key] as? String ?? "" + let label = fieldDict[Constants.label] as? String ?? "" + + return Field(type: type, value: value, key: key, label: label) } - return collectors + return nil + } + + collectors = await factory.collector(from: fields) + return collectors + } + return collectors + } +} + + +public struct Field: Sendable { + let type: String + let value: String + let key: String + let label: String } diff --git a/PingDavinci/PingDavinci/collector/PasswordCollector.swift b/PingDavinci/PingDavinci/collector/PasswordCollector.swift index 87999cf..0c77848 100644 --- a/PingDavinci/PingDavinci/collector/PasswordCollector.swift +++ b/PingDavinci/PingDavinci/collector/PasswordCollector.swift @@ -15,7 +15,7 @@ import PingOrchestrate /// Class representing a PasswordCollector. /// This class inherits from the FieldCollector class and implements the Closeable and Collector protocols. /// It is used to collect password data. -public class PasswordCollector: FieldCollector, Closeable { +public class PasswordCollector: FieldCollector, Closeable, @unchecked Sendable { public var clearPassword: Bool = true /// Overrides the close function from the Closeable protocol. diff --git a/PingDavinci/PingDavinci/collector/SubmitCollector.swift b/PingDavinci/PingDavinci/collector/SubmitCollector.swift index 8f19b91..54844cd 100644 --- a/PingDavinci/PingDavinci/collector/SubmitCollector.swift +++ b/PingDavinci/PingDavinci/collector/SubmitCollector.swift @@ -14,4 +14,4 @@ import Foundation /// Class representing a TextCollector. /// This class inherits from the FieldCollector class and implements the Collector protocol. /// `SubmitCollector` is responsible for collecting and managing submission fields. -public class SubmitCollector: FieldCollector {} +public class SubmitCollector: FieldCollector, @unchecked Sendable {} diff --git a/PingDavinci/PingDavinci/collector/TextCollector.swift b/PingDavinci/PingDavinci/collector/TextCollector.swift index 5fc3d9e..c989b94 100644 --- a/PingDavinci/PingDavinci/collector/TextCollector.swift +++ b/PingDavinci/PingDavinci/collector/TextCollector.swift @@ -14,4 +14,4 @@ import Foundation /// Class representing a TextCollector. /// This class inherits from the FieldCollector class and implements the Collector protocol. /// It is used to collect text data. -public class TextCollector: FieldCollector {} +public class TextCollector: FieldCollector, @unchecked Sendable {} diff --git a/PingDavinci/PingDavinci/module/Oidc.swift b/PingDavinci/PingDavinci/module/Oidc.swift index fe7b5c7..7bff953 100644 --- a/PingDavinci/PingDavinci/module/Oidc.swift +++ b/PingDavinci/PingDavinci/module/Oidc.swift @@ -13,7 +13,7 @@ import Foundation import PingOidc import PingOrchestrate -public class OidcModule { +public actor OidcModule { public init() {} diff --git a/PingDavinci/PingDavinci/module/Transform.swift b/PingDavinci/PingDavinci/module/Transform.swift index 8933e00..75f16a7 100644 --- a/PingDavinci/PingDavinci/module/Transform.swift +++ b/PingDavinci/PingDavinci/module/Transform.swift @@ -14,7 +14,7 @@ import PingOidc import PingOrchestrate /// Module for transforming the response from DaVinci to `Node`. -public class NodeTransformModule { +public actor NodeTransformModule { public static let config: Module = Module.of(setup: { setup in setup.transform { flowContext, response in @@ -55,7 +55,7 @@ public class NodeTransformModule { return FailureNode(cause: ApiError.error(status, json, body)) } - return transform(context: flowContext, workflow: setup.workflow, json: json) + return await transform(context: flowContext, workflow: setup.workflow, json: json) } // 5XX errors are treated as unrecoverable failures @@ -64,7 +64,7 @@ public class NodeTransformModule { }) - private static func transform(context: FlowContext, workflow: Workflow, json: [String: Any]) -> Node { + private static func transform(context: FlowContext, workflow: Workflow, json: [String: Any]) async -> Node { // If authorizeResponse is present, return success if let _ = json[Constants.authorizeResponse] as? [String: Any] { return SuccessNode(input: json, session: SessionResponse(json: json)) @@ -72,7 +72,8 @@ public class NodeTransformModule { var collectors: Collectors = [] if let _ = json[Constants.form] { - collectors.append(contentsOf: Form.parse(json: json)) + let form = await Form.parse(json: json) + collectors.append(contentsOf: form) } return DaVinciConnector(context: context, workflow: workflow, input: json, collectors: collectors) @@ -92,6 +93,8 @@ struct SessionResponse: Session { } } -public enum ApiError: Error { + +public enum ApiError: Error, @unchecked Sendable { case error(Int, [String: Any], String) } + diff --git a/PingLogger/PingLogger.xcodeproj/project.pbxproj b/PingLogger/PingLogger.xcodeproj/project.pbxproj index e3ac4ae..ed6fe65 100644 --- a/PingLogger/PingLogger.xcodeproj/project.pbxproj +++ b/PingLogger/PingLogger.xcodeproj/project.pbxproj @@ -291,6 +291,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -349,6 +350,7 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -385,6 +387,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -419,6 +422,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PingLogger/PingLogger/Logger.swift b/PingLogger/PingLogger/Logger.swift index 1ac2710..fbd0ec4 100644 --- a/PingLogger/PingLogger/Logger.swift +++ b/PingLogger/PingLogger/Logger.swift @@ -35,7 +35,7 @@ public protocol Logger { } ///LogManager to access the global logger instances -public struct LogManager { +public actor LogManager { private static var shared: Logger = NoneLogger() ///Global logger instance. If no logger is set, it defaults to Logger.None. diff --git a/PingOrchestrate/PingOrchestrate/CookieModule.swift b/PingOrchestrate/PingOrchestrate/CookieModule.swift index 070d7cf..e512dd0 100644 --- a/PingOrchestrate/PingOrchestrate/CookieModule.swift +++ b/PingOrchestrate/PingOrchestrate/CookieModule.swift @@ -12,7 +12,7 @@ import Foundation import PingStorage -public class CookieModule { +public actor CookieModule { public init() {} @@ -153,7 +153,7 @@ extension Workflow { } } -public final class InMemoryCookieStorage: HTTPCookieStorage { +public final class InMemoryCookieStorage: HTTPCookieStorage, @unchecked Sendable { private var cookieStore: [HTTPCookie] = [] public override func setCookie(_ cookie: HTTPCookie) { diff --git a/PingOrchestrate/PingOrchestrate/CustomHeader.swift b/PingOrchestrate/PingOrchestrate/CustomHeader.swift index ec3c145..09a139c 100644 --- a/PingOrchestrate/PingOrchestrate/CustomHeader.swift +++ b/PingOrchestrate/PingOrchestrate/CustomHeader.swift @@ -26,7 +26,7 @@ public class CustomHeaderConfig { } /// Module for injecting custom headers into requests. -public class CustomHeader { +public actor CustomHeader { public init() {} diff --git a/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift b/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift index 663db40..7e7faa3 100644 --- a/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift +++ b/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift @@ -10,7 +10,7 @@ import Foundation -public struct PingHTTPCookie: Codable { +public struct PingHTTPCookie: Codable, Sendable { var version: Int var name: String? var value: String? diff --git a/PingOrchestrate/PingOrchestrate/WorkFlow.swift b/PingOrchestrate/PingOrchestrate/WorkFlow.swift index a951fa7..c203caf 100644 --- a/PingOrchestrate/PingOrchestrate/WorkFlow.swift +++ b/PingOrchestrate/PingOrchestrate/WorkFlow.swift @@ -65,26 +65,24 @@ public class Workflow { self.config.register(workflow: self) } - /// Initializes the workflow. - public func initialize() async throws { - if !started { - var tasks: [Task] = [] - // Create tasks for each handler - for handler in initHandlers { - let task = Task { - try await handler() - } - tasks.append(task) - } - - // Await all tasks to complete - for task in tasks { - try await task.value - } - started = true + /// Initializes the workflow. + public func initialize() async throws { + if !started { + try await withThrowingTaskGroup(of: Void.self) { group in + // Create tasks for each handler + for handler in initHandlers { + group.addTask { + try await handler() + } } + // All tasks run concurrently and errors are automatically propagated + try await group.waitForAll() + } + started = true } + } + /// Starts the workflow with the provided request. /// - Parameter request: The request to start the workflow with. /// - Returns: The resulting Node after processing the workflow. diff --git a/SampleApps/PingExample/PingExample/DavinciViewModel.swift b/SampleApps/PingExample/PingExample/DavinciViewModel.swift index 1990931..8c0d592 100644 --- a/SampleApps/PingExample/PingExample/DavinciViewModel.swift +++ b/SampleApps/PingExample/PingExample/DavinciViewModel.swift @@ -48,7 +48,7 @@ public let davinciProd = DaVinci.createDaVinci { config in } // Change this to Prod/Stage -public let davinci = davinciProd +public let davinci = davinciTest class DavinciViewModel: ObservableObject { From 590a11ff690fc65c873ced064af4fd25cd75fd08 Mon Sep 17 00:00:00 2001 From: jey Date: Thu, 24 Oct 2024 01:14:17 -0500 Subject: [PATCH 2/7] SDKS-3458 - updated pingstorage to support swift6 --- PingDavinci/PingDavinci.xcodeproj/project.pbxproj | 6 ++++-- PingDavinci/PingDavinci/module/Connector.swift | 2 +- PingOidc/PingOidc/Token.swift | 2 +- .../PingOrchestrate.xcodeproj/project.pbxproj | 8 ++++---- PingOrchestrate/PingOrchestrate/HttpClient.swift | 2 +- PingOrchestrate/PingOrchestrate/Module.swift | 2 +- .../PingOrchestrate/ModuleRegistry.swift | 2 +- PingOrchestrate/PingOrchestrate/Node.swift | 14 +++++++------- PingOrchestrate/PingOrchestrate/Request.swift | 2 +- .../PingOrchestrate/SharedContext.swift | 2 +- PingOrchestrate/PingOrchestrate/WorkFlow.swift | 13 +++---------- .../PingOrchestrate/WorkFlowConfig.swift | 2 +- PingStorage/PingStorage.xcodeproj/project.pbxproj | 8 ++++++-- PingStorage/PingStorage/KeychainStorage.swift | 2 +- PingStorage/PingStorage/MemoryStorage.swift | 4 ++-- PingStorage/PingStorage/StorageDelegate.swift | 2 +- .../PingStorageTests/CustomStorageTests.swift | 2 +- 17 files changed, 37 insertions(+), 38 deletions(-) diff --git a/PingDavinci/PingDavinci.xcodeproj/project.pbxproj b/PingDavinci/PingDavinci.xcodeproj/project.pbxproj index d85e3b8..77cb463 100644 --- a/PingDavinci/PingDavinci.xcodeproj/project.pbxproj +++ b/PingDavinci/PingDavinci.xcodeproj/project.pbxproj @@ -440,6 +440,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -499,6 +500,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -536,7 +538,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -571,7 +573,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/PingDavinci/PingDavinci/module/Connector.swift b/PingDavinci/PingDavinci/module/Connector.swift index c2afa1c..28eb0c3 100644 --- a/PingDavinci/PingDavinci/module/Connector.swift +++ b/PingDavinci/PingDavinci/module/Connector.swift @@ -36,7 +36,7 @@ extension ContinueNode { ///- property workflow: The Workflow of the ContinueNode. ///- property input: The input JsonObject of the ContinueNode. ///- property collectors: The collectors of the ContinueNode. -class DaVinciConnector: ContinueNode { +class DaVinciConnector: ContinueNode, @unchecked Sendable { init(context: FlowContext, workflow: Workflow, input: [String: Any], collectors: Collectors) { super.init(context: context, workflow: workflow, input: input, actions: collectors) diff --git a/PingOidc/PingOidc/Token.swift b/PingOidc/PingOidc/Token.swift index 8384c6c..f314dbc 100644 --- a/PingOidc/PingOidc/Token.swift +++ b/PingOidc/PingOidc/Token.swift @@ -11,7 +11,7 @@ import Foundation -public struct Token: Codable { +public struct Token: Codable, Sendable { public let accessToken: String public let tokenType: String? public let scope: String? diff --git a/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj b/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj index a894a01..7853002 100644 --- a/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj +++ b/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj @@ -423,7 +423,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -483,7 +483,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -518,7 +518,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -551,7 +551,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/PingOrchestrate/PingOrchestrate/HttpClient.swift b/PingOrchestrate/PingOrchestrate/HttpClient.swift index ac93c70..05a452a 100644 --- a/PingOrchestrate/PingOrchestrate/HttpClient.swift +++ b/PingOrchestrate/PingOrchestrate/HttpClient.swift @@ -13,7 +13,7 @@ import Foundation import PingLogger /// `HttpClient` is responsible for handling HTTP requests and logging the details of those requests and responses. -public class HttpClient { +public final class HttpClient { var session: URLSession /// Initializes a new instance of `HttpClient`. diff --git a/PingOrchestrate/PingOrchestrate/Module.swift b/PingOrchestrate/PingOrchestrate/Module.swift index 7193c6e..1159927 100644 --- a/PingOrchestrate/PingOrchestrate/Module.swift +++ b/PingOrchestrate/PingOrchestrate/Module.swift @@ -14,7 +14,7 @@ import Foundation /// A Module represents a unit of functionality in the application. /// - property config: A function that returns the configuration for the module. /// - property setup: A function that sets up the module. -public class Module: Equatable { +public final class Module: Equatable { public private(set) var setup: (Setup) -> (Void) public private(set) var config: () -> (ModuleConfig) public var id: UUID = UUID() diff --git a/PingOrchestrate/PingOrchestrate/ModuleRegistry.swift b/PingOrchestrate/PingOrchestrate/ModuleRegistry.swift index 0d2d8b8..068231a 100644 --- a/PingOrchestrate/PingOrchestrate/ModuleRegistry.swift +++ b/PingOrchestrate/PingOrchestrate/ModuleRegistry.swift @@ -26,7 +26,7 @@ public protocol ModuleRegistryProtocol { /// - property priority: The priority of the module in the registry. /// - property config: The configuration for the module. /// - property setup: The function that sets up the module. -public class ModuleRegistry: ModuleRegistryProtocol { +public final class ModuleRegistry: ModuleRegistryProtocol { public var id: UUID = UUID() public let priority: Int public let config: Config diff --git a/PingOrchestrate/PingOrchestrate/Node.swift b/PingOrchestrate/PingOrchestrate/Node.swift index 5bbbaf9..aaa540f 100644 --- a/PingOrchestrate/PingOrchestrate/Node.swift +++ b/PingOrchestrate/PingOrchestrate/Node.swift @@ -18,16 +18,16 @@ public protocol Closeable { } /// Protocol for Node. Represents a node in the workflow. -public protocol Node {} +public protocol Node: Sendable {} /// Represents an EmptyNode node in the workflow. -public struct EmptyNode: Node { +public struct EmptyNode: Node, @unchecked Sendable { public init() {} } /// Represents an Failure node in the workflow. /// - property cause: The cause of the error. -public struct FailureNode: Node { +public struct FailureNode: Node, @unchecked Sendable { public init(cause: any Error) { self.cause = cause } @@ -39,7 +39,7 @@ public struct FailureNode: Node { /// - property status: The status of the error. /// - property input: The input for the error. /// - property message: The message for the error. -public struct ErrorNode: Node { +public struct ErrorNode: Node, @unchecked Sendable { public init(status: Int? = nil, input: [String : Any] = [:], message: String = "") { @@ -56,7 +56,7 @@ public struct ErrorNode: Node { /// Represents a success node in the workflow. /// - property input: The input for the success. /// - property session: The session for the success. -public struct SuccessNode: Node { +public struct SuccessNode: Node, @unchecked Sendable { public let input: [String: Any] public let session: Session @@ -71,7 +71,7 @@ public struct SuccessNode: Node { /// - property workflow: The workflow for the node. /// - property input: The input for the node. /// - property actions: The actions for the node. -open class ContinueNode: Node, Closeable { +open class ContinueNode: Node, @unchecked Sendable { public let context: FlowContext public let workflow: Workflow public let input: [String: Any] @@ -105,7 +105,7 @@ public protocol Session { /// Singleton for an EmptySession. An EmptySession represents a session with no value. -public struct EmptySession: Session { +public struct EmptySession: Session, Sendable { public init() {} /// The value of the empty session as a String. diff --git a/PingOrchestrate/PingOrchestrate/Request.swift b/PingOrchestrate/PingOrchestrate/Request.swift index 069e187..a75bb3a 100644 --- a/PingOrchestrate/PingOrchestrate/Request.swift +++ b/PingOrchestrate/PingOrchestrate/Request.swift @@ -13,7 +13,7 @@ import Foundation import UIKit /// Class for a Request. A Request represents a request to be sent over the network. -public class Request { +public final class Request { public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) diff --git a/PingOrchestrate/PingOrchestrate/SharedContext.swift b/PingOrchestrate/PingOrchestrate/SharedContext.swift index 4609201..a9777d4 100644 --- a/PingOrchestrate/PingOrchestrate/SharedContext.swift +++ b/PingOrchestrate/PingOrchestrate/SharedContext.swift @@ -12,7 +12,7 @@ import Foundation /// An actor that manages a shared context using a dictionary. -public class SharedContext { +public final class SharedContext { private var map: [String: Any] = [:] private var queue = DispatchQueue(label: "shared.conext.queue", attributes: .concurrent) diff --git a/PingOrchestrate/PingOrchestrate/WorkFlow.swift b/PingOrchestrate/PingOrchestrate/WorkFlow.swift index c203caf..63227e8 100644 --- a/PingOrchestrate/PingOrchestrate/WorkFlow.swift +++ b/PingOrchestrate/PingOrchestrate/WorkFlow.swift @@ -40,7 +40,7 @@ public enum ModuleKeys: String { } /// Class representing a workflow. -public class Workflow { +public final class Workflow { /// The configuration for the workflow. public let config: WorkflowConfig /// Global SharedContext @@ -68,16 +68,9 @@ public class Workflow { /// Initializes the workflow. public func initialize() async throws { if !started { - try await withThrowingTaskGroup(of: Void.self) { group in - // Create tasks for each handler - for handler in initHandlers { - group.addTask { - try await handler() - } + for handler in initHandlers { + try await handler() } - // All tasks run concurrently and errors are automatically propagated - try await group.waitForAll() - } started = true } diff --git a/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift b/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift index e665e2e..edfec63 100644 --- a/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift +++ b/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift @@ -20,7 +20,7 @@ public enum OverrideMode { } /// Workflow configuration -public class WorkflowConfig { +public final class WorkflowConfig { // Use a list instead of a map to allow registering a module twice with different configurations public private(set) var modules: [any ModuleRegistryProtocol] = [] // Timeout for the HTTP client, default is 15 seconds diff --git a/PingStorage/PingStorage.xcodeproj/project.pbxproj b/PingStorage/PingStorage.xcodeproj/project.pbxproj index e22406f..f440b76 100644 --- a/PingStorage/PingStorage.xcodeproj/project.pbxproj +++ b/PingStorage/PingStorage.xcodeproj/project.pbxproj @@ -330,6 +330,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -388,6 +390,8 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -422,7 +426,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -455,7 +459,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/PingStorage/PingStorage/KeychainStorage.swift b/PingStorage/PingStorage/KeychainStorage.swift index 40e67cc..a95cde1 100644 --- a/PingStorage/PingStorage/KeychainStorage.swift +++ b/PingStorage/PingStorage/KeychainStorage.swift @@ -105,7 +105,7 @@ public enum KeychainError: LocalizedError { /// It is designed to store, retrieve, and manage objects of type `T`, where `T` must conform to the `Codable` protocol. This requirement ensures that the objects can be easily encoded and decoded for secure storage in the keychain. /// /// - Parameter T: The type of the objects to be stored in the keychain. Must conform to `Codable`. -public class KeychainStorage: StorageDelegate { +public class KeychainStorage: StorageDelegate, @unchecked Sendable { /// Initializes a new instance of `KeychainStorage`. /// /// This initializer configures a `KeychainStorage` instance with a specified account and security settings. diff --git a/PingStorage/PingStorage/MemoryStorage.swift b/PingStorage/PingStorage/MemoryStorage.swift index 0ce0842..765cab5 100644 --- a/PingStorage/PingStorage/MemoryStorage.swift +++ b/PingStorage/PingStorage/MemoryStorage.swift @@ -12,7 +12,7 @@ import Foundation /// A storage for storing objects in memory, where `T` is he type of the object to be stored. -public class Memory: Storage { +public class Memory: Storage { private var data: T? /// Saves the given item in memory. @@ -41,7 +41,7 @@ public class Memory: Storage { /// The generic type `T` must conform to `Codable` to ensure that objects can be encoded and decoded when written to and read from memory, respectively. /// /// - Parameter T: The type of the objects to be stored. Must conform to `Codable`. -public class MemoryStorage: StorageDelegate { +public class MemoryStorage: StorageDelegate, @unchecked Sendable { /// Initializes a new instance of `MemoryStorage`. /// /// This initializer creates a `MemoryStorage` instance that acts as a delegate for an in-memory storage diff --git a/PingStorage/PingStorage/StorageDelegate.swift b/PingStorage/PingStorage/StorageDelegate.swift index 6bca319..67d1cd8 100644 --- a/PingStorage/PingStorage/StorageDelegate.swift +++ b/PingStorage/PingStorage/StorageDelegate.swift @@ -17,7 +17,7 @@ import Foundation /// /// - Parameter T: The type of the objects being stored. Must conform to `Codable` to ensure that /// objects can be easily encoded and decoded. -open class StorageDelegate: Storage { +open class StorageDelegate: Storage, @unchecked Sendable { private let delegate: any Storage private let cacheable: Bool private var cached: T? diff --git a/PingStorage/PingStorageTests/CustomStorageTests.swift b/PingStorage/PingStorageTests/CustomStorageTests.swift index f609148..b119b85 100644 --- a/PingStorage/PingStorageTests/CustomStorageTests.swift +++ b/PingStorage/PingStorageTests/CustomStorageTests.swift @@ -67,7 +67,7 @@ public class CustomStorage: Storage { } -public class CustomStorageDelegate: StorageDelegate { +public class CustomStorageDelegate: StorageDelegate { public init(cacheable: Bool = false) { super.init(delegate: CustomStorage(), cacheable: cacheable) } From 9d0581738a0dc48962e4ce727ab9cd97abb61da1 Mon Sep 17 00:00:00 2001 From: jey Date: Thu, 24 Oct 2024 14:00:46 -0500 Subject: [PATCH 3/7] making the test pass --- .../CallbackFactoryTests.swift | 42 +++++++++---------- .../CollectorRegistryTests.swift | 39 ++++++++--------- .../FieldCollectorTests.swift | 9 +--- .../mock/MockURLProtocol.swift | 5 ++- PingOidc/PingOidcTests/mock/MockStorage.swift | 2 +- PingOrchestrate/PingOrchestrate/Request.swift | 2 +- .../PingOrchestrate/WorkFlow.swift | 3 +- .../PingOrchestrateTests/NodeTests.swift | 6 +-- .../PingStorageTests/CustomStorageTests.swift | 2 +- .../KeychainStorageTests.swift | 10 ++--- .../StorageDelegateTests.swift | 2 +- 11 files changed, 54 insertions(+), 68 deletions(-) diff --git a/PingDavinci/PingDavinciTests/CallbackFactoryTests.swift b/PingDavinci/PingDavinciTests/CallbackFactoryTests.swift index 56389eb..c770795 100644 --- a/PingDavinci/PingDavinciTests/CallbackFactoryTests.swift +++ b/PingDavinci/PingDavinciTests/CallbackFactoryTests.swift @@ -14,57 +14,53 @@ import XCTest @testable import PingDavinci class CallbackFactoryTests: XCTestCase { - override func setUp() { - CollectorFactory.shared.register(type: "type1", collector: DummyCallback.self) - CollectorFactory.shared.register(type: "type2", collector: Dummy2Callback.self) + + override func setUp() async throws { + await CollectorFactory.shared.register(type: "type1", collector: DummyCallback.self) + await CollectorFactory.shared.register(type: "type2", collector: Dummy2Callback.self) } - func testShouldReturnListOfCollectorsWhenValidTypesAreProvided() { - let jsonArray: [[String: Any]] = [ - ["type": "type1"], - ["type": "type2"] - ] - - let callbacks = CollectorFactory.shared.collector(from: jsonArray) + func testShouldReturnListOfCollectorsWhenValidTypesAreProvided() async { + let jsonArray: [Field] = [Field(type: "type1", value: "", key: "", label: ""),Field(type: "type2", value: "", key: "", label: "")] + + let callbacks = await CollectorFactory.shared.collector(from: jsonArray) XCTAssertEqual((callbacks[0] as? DummyCallback)?.value, "dummy") XCTAssertEqual((callbacks[1] as? Dummy2Callback)?.value, "dummy2") XCTAssertEqual(callbacks.count, 2) } - func testShouldReturnEmptyListWhenNoValidTypesAreProvided() { - let jsonArray: [[String: Any]] = [ - ["type": "invalidType"] - ] - - let callbacks = CollectorFactory.shared.collector(from: jsonArray) + func testShouldReturnEmptyListWhenNoValidTypesAreProvided() async { + let jsonArray: [Field] = [Field(type: "invalidType", value: "", key: "", label: "")] + + let callbacks = await CollectorFactory.shared.collector(from: jsonArray) XCTAssertTrue(callbacks.isEmpty) } - func testShouldReturnEmptyListWhenJsonArrayIsEmpty() { - let jsonArray: [[String: Any]] = [] + func testShouldReturnEmptyListWhenJsonArrayIsEmpty() async { + let jsonArray: [Field] = [] - let callbacks = CollectorFactory.shared.collector(from: jsonArray) + let callbacks = await CollectorFactory.shared.collector(from: jsonArray) XCTAssertTrue(callbacks.isEmpty) } } -class DummyCallback: Collector { +class DummyCallback: Collector, @unchecked Sendable { var id: UUID = UUID() var value: String? - required public init(with json: [String: Any]) { + required public init(with json: Field) { value = "dummy" } } -class Dummy2Callback: Collector { +class Dummy2Callback: Collector, @unchecked Sendable { var id: UUID = UUID() var value: String? - required public init(with json: [String: Any]) { + required public init(with json: Field) { value = "dummy2" } } diff --git a/PingDavinci/PingDavinciTests/CollectorRegistryTests.swift b/PingDavinci/PingDavinciTests/CollectorRegistryTests.swift index 71605c6..34d8138 100644 --- a/PingDavinci/PingDavinciTests/CollectorRegistryTests.swift +++ b/PingDavinci/PingDavinciTests/CollectorRegistryTests.swift @@ -22,36 +22,33 @@ class CollectorRegistryTests: XCTestCase { collectorFactory = CollectorFactory() } - override func tearDown() { - super.tearDown() - collectorFactory.reset() + override func tearDown() async throws { + try await super.tearDown() + await collectorFactory.reset() } - func testShouldRegisterCollector() { - let jsonArray: [[String: Any]] = [ - ["type": "TEXT"], - ["type": "PASSWORD"], - ["type": "SUBMIT_BUTTON"], - ["type": "FLOW_BUTTON"] - ] - - let collectors = collectorFactory.collector(from: jsonArray) + func testShouldRegisterCollector() async { + + let jsonArray: [Field] = [Field(type: "TEXT", value: "", key: "", label: ""), + Field(type: "PASSWORD", value: "", key: "", label: ""), + Field(type: "SUBMIT_BUTTON", value: "", key: "", label: ""), + Field(type: "FLOW_BUTTON", value: "", key: "", label: "")] + + let collectors = await collectorFactory.collector(from: jsonArray) XCTAssertTrue(collectors[0] is TextCollector) XCTAssertTrue(collectors[1] is PasswordCollector) XCTAssertTrue(collectors[2] is SubmitCollector) XCTAssertTrue(collectors[3] is FlowCollector) } - func testShouldIgnoreUnknownCollector() { - let jsonArray: [[String: Any]] = [ - ["type": "TEXT"], - ["type": "PASSWORD"], - ["type": "SUBMIT_BUTTON"], - ["type": "FLOW_BUTTON"], - ["type": "UNKNOWN"] - ] + func testShouldIgnoreUnknownCollector() async { + let jsonArray: [Field] = [Field(type: "TEXT", value: "", key: "", label: ""), + Field(type: "PASSWORD", value: "", key: "", label: ""), + Field(type: "SUBMIT_BUTTON", value: "", key: "", label: ""), + Field(type: "FLOW_BUTTON", value: "", key: "", label: ""), + Field(type: "UNKNOWN", value: "", key: "", label: "")] - let collectors = collectorFactory.collector(from: jsonArray) + let collectors = await collectorFactory.collector(from: jsonArray) XCTAssertEqual(collectors.count, 4) } } diff --git a/PingDavinci/PingDavinciTests/FieldCollectorTests.swift b/PingDavinci/PingDavinciTests/FieldCollectorTests.swift index 0a613b9..1904ac4 100644 --- a/PingDavinci/PingDavinciTests/FieldCollectorTests.swift +++ b/PingDavinci/PingDavinciTests/FieldCollectorTests.swift @@ -15,15 +15,10 @@ import XCTest class FieldCollectorTests: XCTestCase { - class MockFieldCollector: FieldCollector {} + class MockFieldCollector: FieldCollector, @unchecked Sendable {} func testShouldInitializeKeyAndLabelFromJsonObject() { - - let jsonObject: [String: String] = [ - "key": "testKey", - "label": "testLabel" - ] - + let jsonObject: Field = Field(type: "", value: "", key: "testKey", label: "testLabel") let fieldCollector = MockFieldCollector(with: jsonObject) XCTAssertEqual("testKey", fieldCollector.key) diff --git a/PingDavinci/PingDavinciTests/mock/MockURLProtocol.swift b/PingDavinci/PingDavinciTests/mock/MockURLProtocol.swift index 1a4e014..e017d63 100644 --- a/PingDavinci/PingDavinciTests/mock/MockURLProtocol.swift +++ b/PingDavinci/PingDavinciTests/mock/MockURLProtocol.swift @@ -13,9 +13,10 @@ import Foundation import XCTest class MockURLProtocol: URLProtocol { - public static var requestHistory: [URLRequest] = [URLRequest]() + + nonisolated(unsafe)public static var requestHistory: [URLRequest] = [URLRequest]() - static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + nonisolated(unsafe) static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? static func startInterceptingRequests() { URLProtocol.registerClass(MockURLProtocol.self) diff --git a/PingOidc/PingOidcTests/mock/MockStorage.swift b/PingOidc/PingOidcTests/mock/MockStorage.swift index 31e7f37..b271a45 100644 --- a/PingOidc/PingOidcTests/mock/MockStorage.swift +++ b/PingOidc/PingOidcTests/mock/MockStorage.swift @@ -28,7 +28,7 @@ public class Mock: Storage { } } -public class MockStorage: StorageDelegate { +public class MockStorage: StorageDelegate, @unchecked Sendable { public init(cacheable: Bool = false) { super.init(delegate: Mock(), cacheable: cacheable) } diff --git a/PingOrchestrate/PingOrchestrate/Request.swift b/PingOrchestrate/PingOrchestrate/Request.swift index a75bb3a..069e187 100644 --- a/PingOrchestrate/PingOrchestrate/Request.swift +++ b/PingOrchestrate/PingOrchestrate/Request.swift @@ -13,7 +13,7 @@ import Foundation import UIKit /// Class for a Request. A Request represents a request to be sent over the network. -public final class Request { +public class Request { public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) diff --git a/PingOrchestrate/PingOrchestrate/WorkFlow.swift b/PingOrchestrate/PingOrchestrate/WorkFlow.swift index 63227e8..fc90654 100644 --- a/PingOrchestrate/PingOrchestrate/WorkFlow.swift +++ b/PingOrchestrate/PingOrchestrate/WorkFlow.swift @@ -40,7 +40,7 @@ public enum ModuleKeys: String { } /// Class representing a workflow. -public final class Workflow { +public class Workflow: @unchecked Sendable { /// The configuration for the workflow. public let config: WorkflowConfig /// Global SharedContext @@ -73,7 +73,6 @@ public final class Workflow { } started = true } - } /// Starts the workflow with the provided request. diff --git a/PingOrchestrate/PingOrchestrateTests/NodeTests.swift b/PingOrchestrate/PingOrchestrateTests/NodeTests.swift index de69ecd..23fbc00 100644 --- a/PingOrchestrate/PingOrchestrateTests/NodeTests.swift +++ b/PingOrchestrate/PingOrchestrateTests/NodeTests.swift @@ -39,7 +39,7 @@ final class NodeTests: XCTestCase { } // Supporting Test Classes -class WorkflowMock: Workflow { +class WorkflowMock: Workflow, @unchecked Sendable { var nextReturnValue: Node? override func next(_ context: FlowContext, _ current: ContinueNode) async -> Node { return nextReturnValue ?? NodeMock() @@ -48,9 +48,9 @@ class WorkflowMock: Workflow { class FlowContextMock: FlowContext {} -class NodeMock: Node {} +final class NodeMock: Node, Sendable {} -class TestContinueNode: ContinueNode { +class TestContinueNode: ContinueNode, @unchecked Sendable { override func asRequest() -> Request { return RequestMock(urlString: "https://openam.example.com") } diff --git a/PingStorage/PingStorageTests/CustomStorageTests.swift b/PingStorage/PingStorageTests/CustomStorageTests.swift index b119b85..68ab23b 100644 --- a/PingStorage/PingStorageTests/CustomStorageTests.swift +++ b/PingStorage/PingStorageTests/CustomStorageTests.swift @@ -67,7 +67,7 @@ public class CustomStorage: Storage { } -public class CustomStorageDelegate: StorageDelegate { +public class CustomStorageDelegate: StorageDelegate, @unchecked Sendable { public init(cacheable: Bool = false) { super.init(delegate: CustomStorage(), cacheable: cacheable) } diff --git a/PingStorage/PingStorageTests/KeychainStorageTests.swift b/PingStorage/PingStorageTests/KeychainStorageTests.swift index 6ebe4c9..9ff921a 100644 --- a/PingStorage/PingStorageTests/KeychainStorageTests.swift +++ b/PingStorage/PingStorageTests/KeychainStorageTests.swift @@ -20,12 +20,10 @@ final class KeychainStorageTests: XCTestCase { keychainStorage = KeychainStorage(account: "testAccount", encryptor: SecuredKeyEncryptor() ?? NoEncryptor()) } - override func tearDown() { - Task { - try? await keychainStorage.delete() - keychainStorage = nil - } - super.tearDown() + override func tearDown() async throws { + try? await keychainStorage.delete() + keychainStorage = nil + try await super.tearDown() } func testSaveItem() async throws { diff --git a/PingStorage/PingStorageTests/StorageDelegateTests.swift b/PingStorage/PingStorageTests/StorageDelegateTests.swift index 4ac6867..c748b14 100644 --- a/PingStorage/PingStorageTests/StorageDelegateTests.swift +++ b/PingStorage/PingStorageTests/StorageDelegateTests.swift @@ -12,7 +12,7 @@ import XCTest @testable import PingStorage -final class StorageDelegateTests: XCTestCase { +final class StorageDelegateTests: XCTestCase, @unchecked Sendable { private var storageDelegate: StorageDelegate! private var memoryStorage: MemoryStorage! From be03b0b8032b1c8bf89c38ea1b3001390a420537 Mon Sep 17 00:00:00 2001 From: Stoyan Petrov Date: Thu, 31 Oct 2024 09:05:39 -0700 Subject: [PATCH 4/7] Update the pipeline to use MacOS 15 with Xcode 16.1.0 in order to support Swift 6.0 --- .github/workflows/build-and-test.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index e40e208..691cf2b 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-14, macos-14-large] + os: [macos-15, macos-15-large] runs-on: ${{ matrix.os }} timeout-minutes: 20 @@ -29,17 +29,17 @@ jobs: run: echo "CHIP_TYPE=$(uname -m)" >> $GITHUB_ENV # Set target Xcode version. For more details and options see: - # https://github.com/actions/virtual-environments/blob/main/images/macos/macos-14-Readme.md + # https://github.com/actions/virtual-environments/blob/main/images/macos/macos-15-Readme.md - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_15.4.app && /usr/bin/xcodebuild -version + run: sudo xcode-select -switch /Applications/Xcode_16.1.0.app && /usr/bin/xcodebuild -version # Run all tests - name: Run tests - run: xcodebuild test -scheme PingTestHost -workspace SampleApps/Ping.xcworkspace -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.5' -derivedDataPath DerivedData -enableCodeCoverage YES -resultBundlePath TestResults | xcpretty && exit ${PIPESTATUS[0]} + run: xcodebuild test -scheme PingTestHost -workspace SampleApps/Ping.xcworkspace -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 16 Pro Max,OS=18.1' -derivedDataPath DerivedData -enableCodeCoverage YES -resultBundlePath TestResults | xcpretty && exit ${PIPESTATUS[0]} # Publish test results - name: Publish test results - uses: kishikawakatsumi/xcresulttool@v1 + uses: slidoapp/xcresulttool@v3.1.0 with: title: "Test Results ${{ matrix.os }} - ${{ env.CHIP_TYPE }}" path: TestResults.xcresult From 598813da8b8d406c03178ae76d3102f0272bbde4 Mon Sep 17 00:00:00 2001 From: jey Date: Mon, 4 Nov 2024 15:29:34 -0600 Subject: [PATCH 5/7] SDKS-3458 - More improvements --- .../PingOrchestrate/HttpClient.swift | 4 +- PingOrchestrate/PingOrchestrate/Node.swift | 2 +- PingOrchestrate/PingOrchestrate/Request.swift | 2 +- .../PingOrchestrate/SharedContext.swift | 94 +++++++++---------- 4 files changed, 46 insertions(+), 56 deletions(-) diff --git a/PingOrchestrate/PingOrchestrate/HttpClient.swift b/PingOrchestrate/PingOrchestrate/HttpClient.swift index 05a452a..eb58e6f 100644 --- a/PingOrchestrate/PingOrchestrate/HttpClient.swift +++ b/PingOrchestrate/PingOrchestrate/HttpClient.swift @@ -13,8 +13,8 @@ import Foundation import PingLogger /// `HttpClient` is responsible for handling HTTP requests and logging the details of those requests and responses. -public final class HttpClient { - var session: URLSession +public final class HttpClient: Sendable { + let session: URLSession /// Initializes a new instance of `HttpClient`. /// - Parameter session: The URLSession instance to be used for HTTP requests. Defaults to a session with `RedirectPreventer` delegate. diff --git a/PingOrchestrate/PingOrchestrate/Node.swift b/PingOrchestrate/PingOrchestrate/Node.swift index aaa540f..b2a36f8 100644 --- a/PingOrchestrate/PingOrchestrate/Node.swift +++ b/PingOrchestrate/PingOrchestrate/Node.swift @@ -21,7 +21,7 @@ public protocol Closeable { public protocol Node: Sendable {} /// Represents an EmptyNode node in the workflow. -public struct EmptyNode: Node, @unchecked Sendable { +public struct EmptyNode: Node, Sendable { public init() {} } diff --git a/PingOrchestrate/PingOrchestrate/Request.swift b/PingOrchestrate/PingOrchestrate/Request.swift index 069e187..a75bb3a 100644 --- a/PingOrchestrate/PingOrchestrate/Request.swift +++ b/PingOrchestrate/PingOrchestrate/Request.swift @@ -13,7 +13,7 @@ import Foundation import UIKit /// Class for a Request. A Request represents a request to be sent over the network. -public class Request { +public final class Request { public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) diff --git a/PingOrchestrate/PingOrchestrate/SharedContext.swift b/PingOrchestrate/PingOrchestrate/SharedContext.swift index a9777d4..d2094ca 100644 --- a/PingOrchestrate/PingOrchestrate/SharedContext.swift +++ b/PingOrchestrate/PingOrchestrate/SharedContext.swift @@ -13,58 +13,48 @@ import Foundation /// An actor that manages a shared context using a dictionary. public final class SharedContext { - private var map: [String: Any] = [:] - private var queue = DispatchQueue(label: "shared.conext.queue", attributes: .concurrent) + private var map: [String: Any] + + /// Initializes the SharedContext with an empty dictionary or a pre-existing one. + /// + /// - Parameter map: A dictionary to initialize the context with. Defaults to an empty dictionary. + public init(_ map: [String: Any] = [:]) { + self.map = map + } + + /// Sets a value for the given key in the shared context. + /// + /// - Parameters: + /// - key: The key for which to set the value. + /// - value: The value to set for the given key. + public func set(key: String, value: Any) { + self.map[key] = value + } + + /// Retrieves the value for the given key from the shared context. + /// + /// - Parameter key: The key for which to get the value. + /// - Returns: The value associated with the key, or `nil` if the key does not exist. + public func get(key: String) -> Any? { + return self.map[key] - /// Initializes the SharedContext with an empty dictionary or a pre-existing one. - /// - /// - Parameter map: A dictionary to initialize the context with. Defaults to an empty dictionary. - public init(_ map: [String: Any] = [:]) { - queue.sync(flags: .barrier) { - self.map = map - } - } + } + + /// Removes the value for the given key from the shared context. + /// + /// - Parameter key: The key for which to remove the value. + /// - Returns: The removed value, or `nil` if the key does not exist. + public func removeValue(forKey key: String) -> Any? { + self.map.removeValue(forKey: key) + } + + /// A Boolean value indicating whether the shared context is empty. + public var isEmpty: Bool { + return self.map.isEmpty + } + + /// A namespace for key names to be added in an extension. + public enum Keys { - /// Sets a value for the given key in the shared context. - /// - /// - Parameters: - /// - key: The key for which to set the value. - /// - value: The value to set for the given key. - public func set(key: String, value: Any) { - queue.sync(flags: .barrier) { - self.map[key] = value - } - } - - /// Retrieves the value for the given key from the shared context. - /// - /// - Parameter key: The key for which to get the value. - /// - Returns: The value associated with the key, or `nil` if the key does not exist. - public func get(key: String) -> Any? { - queue.sync { - return self.map[key] - } - } - - /// Removes the value for the given key from the shared context. - /// - /// - Parameter key: The key for which to remove the value. - /// - Returns: The removed value, or `nil` if the key does not exist. - public func removeValue(forKey key: String) -> Any? { - queue.sync(flags: .barrier) { - self.map.removeValue(forKey: key) - } - } - - /// A Boolean value indicating whether the shared context is empty. - public var isEmpty: Bool { - queue.sync { - return self.map.isEmpty - } - } - - /// A namespace for key names to be added in an extension. - public enum Keys { - - } + } } From 79e92786195b09e17074d3e6a66042ad9829360a Mon Sep 17 00:00:00 2001 From: jey Date: Mon, 4 Nov 2024 18:59:17 -0600 Subject: [PATCH 6/7] SDKS-3458 - More improvements --- .../PingOrchestrate.xcodeproj/project.pbxproj | 4 + PingOrchestrate/PingOrchestrate/Request.swift | 2 +- .../PingOrchestrate/SharedContext.swift | 27 +- .../PingOrchestrate/ThreadSafeHelper.swift | 33 ++ .../PingOrchestrate/WorkFlow.swift | 331 +++++++++--------- 5 files changed, 226 insertions(+), 171 deletions(-) create mode 100644 PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift diff --git a/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj b/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj index 7853002..bf5bde2 100644 --- a/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj +++ b/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 3A54417F2BCDF1D900385131 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */; }; 3A5441842BCDF1D900385131 /* PingOrchestrateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441832BCDF1D900385131 /* PingOrchestrateTests.swift */; }; 3A5441852BCDF1D900385131 /* PingOrchestrate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A5441772BCDF1D900385131 /* PingOrchestrate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3A6D7F392CD9636700EFEBCC /* ThreadSafeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7F382CD9636100EFEBCC /* ThreadSafeHelper.swift */; }; 3A7575762C063F2A00891EC7 /* ModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575752C063F2A00891EC7 /* ModuleTests.swift */; }; 3A7575782C0673A100891EC7 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575772C0673A100891EC7 /* ResponseTests.swift */; }; 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575792C06947000891EC7 /* SharedContext.swift */; }; @@ -82,6 +83,7 @@ 3A5441772BCDF1D900385131 /* PingOrchestrate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PingOrchestrate.h; sourceTree = ""; }; 3A54417E2BCDF1D900385131 /* PingOrchestrateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PingOrchestrateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3A5441832BCDF1D900385131 /* PingOrchestrateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingOrchestrateTests.swift; sourceTree = ""; }; + 3A6D7F382CD9636100EFEBCC /* ThreadSafeHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeHelper.swift; sourceTree = ""; }; 3A7575752C063F2A00891EC7 /* ModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleTests.swift; sourceTree = ""; }; 3A7575772C0673A100891EC7 /* ResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseTests.swift; sourceTree = ""; }; 3A7575792C06947000891EC7 /* SharedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedContext.swift; sourceTree = ""; }; @@ -149,6 +151,7 @@ children = ( A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */, 3A5441772BCDF1D900385131 /* PingOrchestrate.h */, + 3A6D7F382CD9636100EFEBCC /* ThreadSafeHelper.swift */, 3A203DA92BE06DFF0020C995 /* CookieModule.swift */, A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */, 3A203DA12BE0312A0020C995 /* HttpClient.swift */, @@ -327,6 +330,7 @@ 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */, 3AB1C9E92BD6410A003FCE3C /* Response.swift in Sources */, 3A203D592BD9DC600020C995 /* Request.swift in Sources */, + 3A6D7F392CD9636700EFEBCC /* ThreadSafeHelper.swift in Sources */, A51D4CE32C62EB4B00FE09E0 /* CustomHeader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/PingOrchestrate/PingOrchestrate/Request.swift b/PingOrchestrate/PingOrchestrate/Request.swift index a75bb3a..069e187 100644 --- a/PingOrchestrate/PingOrchestrate/Request.swift +++ b/PingOrchestrate/PingOrchestrate/Request.swift @@ -13,7 +13,7 @@ import Foundation import UIKit /// Class for a Request. A Request represents a request to be sent over the network. -public final class Request { +public class Request { public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) diff --git a/PingOrchestrate/PingOrchestrate/SharedContext.swift b/PingOrchestrate/PingOrchestrate/SharedContext.swift index d2094ca..39c0b3e 100644 --- a/PingOrchestrate/PingOrchestrate/SharedContext.swift +++ b/PingOrchestrate/PingOrchestrate/SharedContext.swift @@ -12,14 +12,17 @@ import Foundation /// An actor that manages a shared context using a dictionary. -public final class SharedContext { - private var map: [String: Any] +public final class SharedContext: @unchecked Sendable { + private var map: [String: Any] = [:] + private var queue = DispatchQueue(label: "shared.conext.queue", attributes: .concurrent) /// Initializes the SharedContext with an empty dictionary or a pre-existing one. /// /// - Parameter map: A dictionary to initialize the context with. Defaults to an empty dictionary. public init(_ map: [String: Any] = [:]) { - self.map = map + queue.sync(flags: .barrier) { + self.map = map + } } /// Sets a value for the given key in the shared context. @@ -28,7 +31,10 @@ public final class SharedContext { /// - key: The key for which to set the value. /// - value: The value to set for the given key. public func set(key: String, value: Any) { - self.map[key] = value + let sendableValue = SendableAny(value) + queue.async(flags: .barrier) { + self.map[key] = sendableValue.value + } } /// Retrieves the value for the given key from the shared context. @@ -36,8 +42,9 @@ public final class SharedContext { /// - Parameter key: The key for which to get the value. /// - Returns: The value associated with the key, or `nil` if the key does not exist. public func get(key: String) -> Any? { - return self.map[key] - + queue.sync { + return self.map[key] + } } /// Removes the value for the given key from the shared context. @@ -45,12 +52,16 @@ public final class SharedContext { /// - Parameter key: The key for which to remove the value. /// - Returns: The removed value, or `nil` if the key does not exist. public func removeValue(forKey key: String) -> Any? { - self.map.removeValue(forKey: key) + queue.sync(flags: .barrier) { + self.map.removeValue(forKey: key) + } } /// A Boolean value indicating whether the shared context is empty. public var isEmpty: Bool { - return self.map.isEmpty + queue.sync { + return self.map.isEmpty + } } /// A namespace for key names to be added in an extension. diff --git a/PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift b/PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift new file mode 100644 index 0000000..ff64a5c --- /dev/null +++ b/PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift @@ -0,0 +1,33 @@ +// +// ThreadSafe.swift +// PingOrchestrate +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation + +public struct SendableAny: @unchecked Sendable { + public let value: Any + + public init(_ value: Any) { + self.value = value + } +} + + +public struct UncheckedSendableHandler: @unchecked Sendable { + private let handler: () async throws -> Void + + init(_ handler: @escaping () async throws -> Void) { + self.handler = handler + } + + func execute() async throws { + try await handler() + } +} + diff --git a/PingOrchestrate/PingOrchestrate/WorkFlow.swift b/PingOrchestrate/PingOrchestrate/WorkFlow.swift index fc90654..c8d6b28 100644 --- a/PingOrchestrate/PingOrchestrate/WorkFlow.swift +++ b/PingOrchestrate/PingOrchestrate/WorkFlow.swift @@ -15,193 +15,200 @@ import PingLogger /// Class representing the context of a flow. /// - property flowContext: The shared context of the flow. public class FlowContext { - public let flowContext: SharedContext - - public init(flowContext: SharedContext) { - self.flowContext = flowContext - } + public let flowContext: SharedContext + + public init(flowContext: SharedContext) { + self.flowContext = flowContext + } } extension Workflow { - /// Creates a new Workflow instance with the provided configuration block. - /// - Parameter block: The configuration block for the Workflow. - /// - Returns: A new Workflow instance. - public static func createWorkflow(_ block: (WorkflowConfig) -> Void = { _ in }) -> Workflow { - let config = WorkflowConfig() - block(config) - return Workflow(config: config) - } + /// Creates a new Workflow instance with the provided configuration block. + /// - Parameter block: The configuration block for the Workflow. + /// - Returns: A new Workflow instance. + public static func createWorkflow(_ block: (WorkflowConfig) -> Void = { _ in }) -> Workflow { + let config = WorkflowConfig() + block(config) + return Workflow(config: config) + } } public enum ModuleKeys: String { - case customHeader = "customHeader" - case nosession = "nosession" - case forceAuth = "forceAuth" + case customHeader = "customHeader" + case nosession = "nosession" + case forceAuth = "forceAuth" } /// Class representing a workflow. public class Workflow: @unchecked Sendable { - /// The configuration for the workflow. - public let config: WorkflowConfig - /// Global SharedContext - public let sharedContext = SharedContext() - - private var started = false - - internal var initHandlers = [() async throws -> Void]() - internal var startHandlers = [(FlowContext, Request) async throws -> Request]() - internal var nextHandlers = [(FlowContext, ContinueNode, Request) async throws -> Request]() - internal var responseHandlers = [(FlowContext, Response) async throws -> Void]() - internal var nodeHandlers = [(FlowContext, Node) async throws -> Node]() - internal var successHandlers = [(FlowContext, SuccessNode) async throws -> SuccessNode]() - internal var signOffHandlers = [(Request) async throws -> Request]() - // Transform response to Node, we can only have one transform - internal var transformHandler: (FlowContext, Response) async throws -> Node = { _, _ in EmptyNode() } - - /// Initializes the workflow. - /// - Parameter config: The configuration for the workflow. - public init(config: WorkflowConfig) { - self.config = config - self.config.register(workflow: self) - } - + /// The configuration for the workflow. + public let config: WorkflowConfig + /// Global SharedContext + public let sharedContext = SharedContext() + + private var started = false + + internal var initHandlers = [() async throws -> Void]() + internal var startHandlers = [(FlowContext, Request) async throws -> Request]() + internal var nextHandlers = [(FlowContext, ContinueNode, Request) async throws -> Request]() + internal var responseHandlers = [(FlowContext, Response) async throws -> Void]() + internal var nodeHandlers = [(FlowContext, Node) async throws -> Node]() + internal var successHandlers = [(FlowContext, SuccessNode) async throws -> SuccessNode]() + internal var signOffHandlers = [(Request) async throws -> Request]() + // Transform response to Node, we can only have one transform + internal var transformHandler: (FlowContext, Response) async throws -> Node = { _, _ in EmptyNode() } + + /// Initializes the workflow. + /// - Parameter config: The configuration for the workflow. + public init(config: WorkflowConfig) { + self.config = config + self.config.register(workflow: self) + } + /// Initializes the workflow. public func initialize() async throws { if !started { - for handler in initHandlers { - try await handler() + try await withThrowingTaskGroup(of: Void.self) { group in + // Create tasks for each handler + for handler in initHandlers { + let uncheckedHandler = UncheckedSendableHandler(handler) + group.addTask { + try await uncheckedHandler.execute() + } } - started = true + // All tasks run concurrently and errors are automatically propagated + try await group.waitForAll() + } } } - /// Starts the workflow with the provided request. - /// - Parameter request: The request to start the workflow with. - /// - Returns: The resulting Node after processing the workflow. - private func start(request: Request) async throws -> Node { - // Before we start, make sure all the module init has been completed - try await initialize() - config.logger.i("Starting...") - let context = FlowContext(flowContext: SharedContext()) - var currentRequest = request - for handler in startHandlers { - currentRequest = try await handler(context, currentRequest) - } - let response = try await send(context, request: currentRequest) - - let transform = try await transformHandler(context, response) - - var initialNode = transform - for handler in nodeHandlers { - initialNode = try await handler(context, initialNode) - } - - return try await next(context, initialNode) + /// Starts the workflow with the provided request. + /// - Parameter request: The request to start the workflow with. + /// - Returns: The resulting Node after processing the workflow. + private func start(request: Request) async throws -> Node { + // Before we start, make sure all the module init has been completed + try await initialize() + config.logger.i("Starting...") + let context = FlowContext(flowContext: SharedContext()) + var currentRequest = request + for handler in startHandlers { + currentRequest = try await handler(context, currentRequest) } + let response = try await send(context, request: currentRequest) - /// Starts the workflow with a default request. - /// - Returns: The resulting Node after processing the workflow. - public func start() async -> Node { - do { - return try await start(request: Request()) - } - catch { - return FailureNode(cause: error) - } - } + let transform = try await transformHandler(context, response) - /// Sends a request and returns the response. - /// - Parameters: - /// - context: The context of the flow. - /// - request: The request to be sent. - /// - Returns: The response received. - private func send(_ context: FlowContext, request: Request) async throws -> Response { - let (data, urlResponse) = try await config.httpClient.sendRequest(request: request) - let response = Response(data: data, response: urlResponse) - for handler in responseHandlers { - try await handler(context, response) - } - return response + var initialNode = transform + for handler in nodeHandlers { + initialNode = try await handler(context, initialNode) } - /// Sends a request and returns the response. - /// - Parameter request: The request to be sent. - /// - Returns: The response received. - private func send(_ request: Request) async throws -> Response { - // semaphore - let (data, urlResponse) = try await config.httpClient.sendRequest(request: request) - return Response(data: data, response: urlResponse) + return try await next(context, initialNode) + } + + /// Starts the workflow with a default request. + /// - Returns: The resulting Node after processing the workflow. + public func start() async -> Node { + do { + return try await start(request: Request()) } - - /// Processes the next node if it is a success node. - /// - Parameters: - /// - context: The context of the flow. - /// - node: The current node. - /// - Returns: The resulting Node after processing the next step. - private func next(_ context: FlowContext, _ node: Node) async throws -> Node { - if let success = node as? SuccessNode { - var result = success - for handler in successHandlers { - result = try await handler(context, result) - } - return result - } else { - return node - } + catch { + return FailureNode(cause: error) } - - /// Processes the next node in the workflow. - /// - Parameters: - /// - context: The context of the flow. - /// - current: The current ContinueNode. - /// - Returns: The resulting Node after processing the next step. - public func next(_ context: FlowContext, _ current: ContinueNode) async -> Node { - do { - config.logger.i("Next...") - let initialRequest = current.asRequest() - var request = initialRequest - for handler in nextHandlers { - request = try await handler(context, current, request) - } - current.close() - let initialNode = try await transformHandler(context, try await send(context, request: request)) - var node = initialNode - for handler in nodeHandlers { - node = try await handler(context, node) - } - return try await next(context, node) - } - catch { - return FailureNode(cause: error) - } + } + + /// Sends a request and returns the response. + /// - Parameters: + /// - context: The context of the flow. + /// - request: The request to be sent. + /// - Returns: The response received. + private func send(_ context: FlowContext, request: Request) async throws -> Response { + let (data, urlResponse) = try await config.httpClient.sendRequest(request: request) + let response = Response(data: data, response: urlResponse) + for handler in responseHandlers { + try await handler(context, response) } - - /// Signs off the workflow. - /// - Returns: A Result indicating the success or failure of the sign off. - public func signOff() async -> Result { - self.config.logger.i("SignOff...") - do { - try await initialize() - var request = Request() - for handler in signOffHandlers { - request = try await handler(request) - } - _ = try await send(request) - return .success(()) - } - catch { - config.logger.e("Error during sign off", error: error) - return .failure(error) - } + return response + } + + /// Sends a request and returns the response. + /// - Parameter request: The request to be sent. + /// - Returns: The response received. + private func send(_ request: Request) async throws -> Response { + // semaphore + let (data, urlResponse) = try await config.httpClient.sendRequest(request: request) + return Response(data: data, response: urlResponse) + } + + /// Processes the next node if it is a success node. + /// - Parameters: + /// - context: The context of the flow. + /// - node: The current node. + /// - Returns: The resulting Node after processing the next step. + private func next(_ context: FlowContext, _ node: Node) async throws -> Node { + if let success = node as? SuccessNode { + var result = success + for handler in successHandlers { + result = try await handler(context, result) + } + return result + } else { + return node } - - /// Processes the response. - /// - Parameters: - /// - context: The context of the flow. - /// - response: The response to be processed. - private func response(context: FlowContext, response: Response) async throws { - for handler in responseHandlers { - try await handler(context, response) - } + } + + /// Processes the next node in the workflow. + /// - Parameters: + /// - context: The context of the flow. + /// - current: The current ContinueNode. + /// - Returns: The resulting Node after processing the next step. + public func next(_ context: FlowContext, _ current: ContinueNode) async -> Node { + do { + config.logger.i("Next...") + let initialRequest = current.asRequest() + var request = initialRequest + for handler in nextHandlers { + request = try await handler(context, current, request) + } + current.close() + let initialNode = try await transformHandler(context, try await send(context, request: request)) + var node = initialNode + for handler in nodeHandlers { + node = try await handler(context, node) + } + return try await next(context, node) + } + catch { + return FailureNode(cause: error) + } + } + + /// Signs off the workflow. + /// - Returns: A Result indicating the success or failure of the sign off. + public func signOff() async -> Result { + self.config.logger.i("SignOff...") + do { + try await initialize() + var request = Request() + for handler in signOffHandlers { + request = try await handler(request) + } + _ = try await send(request) + return .success(()) + } + catch { + config.logger.e("Error during sign off", error: error) + return .failure(error) + } + } + + /// Processes the response. + /// - Parameters: + /// - context: The context of the flow. + /// - response: The response to be processed. + private func response(context: FlowContext, response: Response) async throws { + for handler in responseHandlers { + try await handler(context, response) } + } } From 947f55d9e169cb739fcdf0d133f8858f0b5de312 Mon Sep 17 00:00:00 2001 From: jey Date: Mon, 4 Nov 2024 22:49:26 -0600 Subject: [PATCH 7/7] SDKS-3458 - More improvements --- PingDavinci/PingDavinci/User.swift | 2 +- PingDavinci/PingDavinci/collector/Form.swift | 2 +- PingDavinci/PingDavinci/module/Oidc.swift | 2 +- .../PingDavinci/module/Transform.swift | 5 +- .../PingLogger.xcodeproj/project.pbxproj | 4 +- PingOidc/PingOidc.xcodeproj/project.pbxproj | 6 +- PingOidc/PingOidc/OidcError.swift | 2 +- PingOidc/PingOidc/OidcUser.swift | 2 +- PingOidc/PingOidc/User.swift | 2 +- PingOidc/PingOidcTests/mock/MockStorage.swift | 2 +- .../PingOidcTests/mock/MockURLProtocol.swift | 79 ++++---- .../PingOrchestrate.xcodeproj/project.pbxproj | 8 +- .../PingOrchestrate/CookieModule.swift | 3 +- PingOrchestrate/PingOrchestrate/Node.swift | 15 +- .../PingOrchestrate/PingHTTPCookie.swift | 138 ++++++------- PingOrchestrate/PingOrchestrate/Request.swift | 186 +++++++++--------- .../PingOrchestrate/Response.swift | 78 ++++---- ...dSafeHelper.swift => SendableHelper.swift} | 7 +- .../PingOrchestrate/WorkFlowConfig.swift | 116 +++++------ .../PingOrchestrateTests/NodeTests.swift | 4 +- .../PingOrchestrateTests/SessionTests.swift | 2 +- PingStorage/PingStorage/MemoryStorage.swift | 2 +- 22 files changed, 341 insertions(+), 326 deletions(-) rename PingOrchestrate/PingOrchestrate/{ThreadSafeHelper.swift => SendableHelper.swift} (73%) diff --git a/PingDavinci/PingDavinci/User.swift b/PingDavinci/PingDavinci/User.swift index fdc15cf..75c8b60 100644 --- a/PingDavinci/PingDavinci/User.swift +++ b/PingDavinci/PingDavinci/User.swift @@ -70,7 +70,7 @@ extension SuccessNode { /// - property daVinci: The DaVinci instance. /// - property user: The user. /// - property session: The session. -struct UserDelegate: User, Session { +struct UserDelegate: User, Session, Sendable { private let daVinci: DaVinci private let user: User private let session: Session diff --git a/PingDavinci/PingDavinci/collector/Form.swift b/PingDavinci/PingDavinci/collector/Form.swift index d0fe9e5..d990eac 100644 --- a/PingDavinci/PingDavinci/collector/Form.swift +++ b/PingDavinci/PingDavinci/collector/Form.swift @@ -13,7 +13,7 @@ import Foundation /// Class that handles the parsing and JSON representation of collectors. /// This class provides functions to parse a JSON object into a list of collectors and to represent a list of collectors as a JSON object. -actor Form { +struct Form { /// Parses a JSON object into a list of collectors. /// This function takes a JSON object and extracts the "form" field. It then iterates over the "fields" array in the "components" object, diff --git a/PingDavinci/PingDavinci/module/Oidc.swift b/PingDavinci/PingDavinci/module/Oidc.swift index 7bff953..bc33749 100644 --- a/PingDavinci/PingDavinci/module/Oidc.swift +++ b/PingDavinci/PingDavinci/module/Oidc.swift @@ -13,7 +13,7 @@ import Foundation import PingOidc import PingOrchestrate -public actor OidcModule { +public actor OidcModule: Sendable { public init() {} diff --git a/PingDavinci/PingDavinci/module/Transform.swift b/PingDavinci/PingDavinci/module/Transform.swift index 75f16a7..b0f47db 100644 --- a/PingDavinci/PingDavinci/module/Transform.swift +++ b/PingDavinci/PingDavinci/module/Transform.swift @@ -14,7 +14,7 @@ import PingOidc import PingOrchestrate /// Module for transforming the response from DaVinci to `Node`. -public actor NodeTransformModule { +public actor NodeTransformModule: Sendable { public static let config: Module = Module.of(setup: { setup in setup.transform { flowContext, response in @@ -80,7 +80,8 @@ public actor NodeTransformModule { } } -struct SessionResponse: Session { +struct SessionResponse: Session, Sendable { + nonisolated(unsafe) public let json: [String: Any] public init(json: [String: Any] = [:]) { diff --git a/PingLogger/PingLogger.xcodeproj/project.pbxproj b/PingLogger/PingLogger.xcodeproj/project.pbxproj index ed6fe65..4efa414 100644 --- a/PingLogger/PingLogger.xcodeproj/project.pbxproj +++ b/PingLogger/PingLogger.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -423,7 +423,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/PingOidc/PingOidc.xcodeproj/project.pbxproj b/PingOidc/PingOidc.xcodeproj/project.pbxproj index 030dd59..ab37ff0 100644 --- a/PingOidc/PingOidc.xcodeproj/project.pbxproj +++ b/PingOidc/PingOidc.xcodeproj/project.pbxproj @@ -384,6 +384,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -442,6 +443,7 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -477,7 +479,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -510,7 +512,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/PingOidc/PingOidc/OidcError.swift b/PingOidc/PingOidc/OidcError.swift index 9e9276a..521567e 100644 --- a/PingOidc/PingOidc/OidcError.swift +++ b/PingOidc/PingOidc/OidcError.swift @@ -12,7 +12,7 @@ import Foundation /// enum fopr class for OIDC errors. -public enum OidcError: LocalizedError { +public enum OidcError: LocalizedError, Sendable { case authorizeError(cause: Error? = nil, message: String? = nil) case networkError(cause: Error? = nil, message: String? = nil) case apiError(code: Int, message: String) diff --git a/PingOidc/PingOidc/OidcUser.swift b/PingOidc/PingOidc/OidcUser.swift index 8654853..66457bc 100644 --- a/PingOidc/PingOidc/OidcUser.swift +++ b/PingOidc/PingOidc/OidcUser.swift @@ -10,7 +10,7 @@ /// Class for an OIDC User -public class OidcUser: User { +public final class OidcUser: User, @unchecked Sendable { private var userinfo: UserInfo? private let oidcClient: OidcClient diff --git a/PingOidc/PingOidc/User.swift b/PingOidc/PingOidc/User.swift index 823410b..4e1ef6d 100644 --- a/PingOidc/PingOidc/User.swift +++ b/PingOidc/PingOidc/User.swift @@ -10,7 +10,7 @@ /// Provides methods for token management, user information retrieval, and logout. -public protocol User { +public protocol User: Sendable { /// Retrieves the token for the user. /// - Returns: A `Result` object containing either the `Token` or an `OidcError`. func token() async -> Result diff --git a/PingOidc/PingOidcTests/mock/MockStorage.swift b/PingOidc/PingOidcTests/mock/MockStorage.swift index b271a45..c82a6ad 100644 --- a/PingOidc/PingOidcTests/mock/MockStorage.swift +++ b/PingOidc/PingOidcTests/mock/MockStorage.swift @@ -28,7 +28,7 @@ public class Mock: Storage { } } -public class MockStorage: StorageDelegate, @unchecked Sendable { +public class MockStorage: StorageDelegate, @unchecked Sendable { public init(cacheable: Bool = false) { super.init(delegate: Mock(), cacheable: cacheable) } diff --git a/PingOidc/PingOidcTests/mock/MockURLProtocol.swift b/PingOidc/PingOidcTests/mock/MockURLProtocol.swift index d634e56..d1c7f15 100644 --- a/PingOidc/PingOidcTests/mock/MockURLProtocol.swift +++ b/PingOidc/PingOidcTests/mock/MockURLProtocol.swift @@ -13,46 +13,49 @@ import Foundation import XCTest import PingLogger -class MockURLProtocol: URLProtocol { - public static var requestHistory: [URLRequest] = [URLRequest]() +class MockURLProtocol: URLProtocol, @unchecked Sendable { + + nonisolated(unsafe) + public static var requestHistory: [URLRequest] = [URLRequest]() + + nonisolated(unsafe) + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + static func startInterceptingRequests() { + URLProtocol.registerClass(MockURLProtocol.self) + } + + static func stopInterceptingRequests() { + URLProtocol.unregisterClass(MockURLProtocol.self) + requestHistory.removeAll() + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + MockURLProtocol.requestHistory.append(request) - static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? - - static func startInterceptingRequests() { - URLProtocol.registerClass(MockURLProtocol.self) + guard let handler = MockURLProtocol.requestHandler else { + XCTFail("Received unexpected request with no handler set") + return } - - static func stopInterceptingRequests() { - URLProtocol.unregisterClass(MockURLProtocol.self) - requestHistory.removeAll() + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) } + } + + override func stopLoading() { - override class func canInit(with request: URLRequest) -> Bool { - return true - } - - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - - override func startLoading() { - MockURLProtocol.requestHistory.append(request) - - guard let handler = MockURLProtocol.requestHandler else { - XCTFail("Received unexpected request with no handler set") - return - } - do { - let (response, data) = try handler(request) - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - client?.urlProtocol(self, didLoad: data) - client?.urlProtocolDidFinishLoading(self) - } catch { - client?.urlProtocol(self, didFailWithError: error) - } - } - - override func stopLoading() { - - } + } } diff --git a/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj b/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj index bf5bde2..c30787f 100644 --- a/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj +++ b/PingOrchestrate/PingOrchestrate.xcodeproj/project.pbxproj @@ -20,7 +20,7 @@ 3A54417F2BCDF1D900385131 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */; }; 3A5441842BCDF1D900385131 /* PingOrchestrateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441832BCDF1D900385131 /* PingOrchestrateTests.swift */; }; 3A5441852BCDF1D900385131 /* PingOrchestrate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A5441772BCDF1D900385131 /* PingOrchestrate.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3A6D7F392CD9636700EFEBCC /* ThreadSafeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7F382CD9636100EFEBCC /* ThreadSafeHelper.swift */; }; + 3A6D7F392CD9636700EFEBCC /* SendableHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7F382CD9636100EFEBCC /* SendableHelper.swift */; }; 3A7575762C063F2A00891EC7 /* ModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575752C063F2A00891EC7 /* ModuleTests.swift */; }; 3A7575782C0673A100891EC7 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575772C0673A100891EC7 /* ResponseTests.swift */; }; 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575792C06947000891EC7 /* SharedContext.swift */; }; @@ -83,7 +83,7 @@ 3A5441772BCDF1D900385131 /* PingOrchestrate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PingOrchestrate.h; sourceTree = ""; }; 3A54417E2BCDF1D900385131 /* PingOrchestrateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PingOrchestrateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3A5441832BCDF1D900385131 /* PingOrchestrateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingOrchestrateTests.swift; sourceTree = ""; }; - 3A6D7F382CD9636100EFEBCC /* ThreadSafeHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeHelper.swift; sourceTree = ""; }; + 3A6D7F382CD9636100EFEBCC /* SendableHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendableHelper.swift; sourceTree = ""; }; 3A7575752C063F2A00891EC7 /* ModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleTests.swift; sourceTree = ""; }; 3A7575772C0673A100891EC7 /* ResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseTests.swift; sourceTree = ""; }; 3A7575792C06947000891EC7 /* SharedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedContext.swift; sourceTree = ""; }; @@ -151,7 +151,7 @@ children = ( A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */, 3A5441772BCDF1D900385131 /* PingOrchestrate.h */, - 3A6D7F382CD9636100EFEBCC /* ThreadSafeHelper.swift */, + 3A6D7F382CD9636100EFEBCC /* SendableHelper.swift */, 3A203DA92BE06DFF0020C995 /* CookieModule.swift */, A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */, 3A203DA12BE0312A0020C995 /* HttpClient.swift */, @@ -330,7 +330,7 @@ 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */, 3AB1C9E92BD6410A003FCE3C /* Response.swift in Sources */, 3A203D592BD9DC600020C995 /* Request.swift in Sources */, - 3A6D7F392CD9636700EFEBCC /* ThreadSafeHelper.swift in Sources */, + 3A6D7F392CD9636700EFEBCC /* SendableHelper.swift in Sources */, A51D4CE32C62EB4B00FE09E0 /* CustomHeader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/PingOrchestrate/PingOrchestrate/CookieModule.swift b/PingOrchestrate/PingOrchestrate/CookieModule.swift index e512dd0..96693dd 100644 --- a/PingOrchestrate/PingOrchestrate/CookieModule.swift +++ b/PingOrchestrate/PingOrchestrate/CookieModule.swift @@ -12,7 +12,8 @@ import Foundation import PingStorage -public actor CookieModule { +/// Module for injecting cookies into requests. +public actor CookieModule: Sendable { public init() {} diff --git a/PingOrchestrate/PingOrchestrate/Node.swift b/PingOrchestrate/PingOrchestrate/Node.swift index b2a36f8..d159552 100644 --- a/PingOrchestrate/PingOrchestrate/Node.swift +++ b/PingOrchestrate/PingOrchestrate/Node.swift @@ -10,10 +10,10 @@ /// Protocol for actions -public protocol Action {} +public protocol Action: Sendable {} /// Protocol for closeable resources -public protocol Closeable { +public protocol Closeable: Sendable { func close() } @@ -27,7 +27,7 @@ public struct EmptyNode: Node, Sendable { /// Represents an Failure node in the workflow. /// - property cause: The cause of the error. -public struct FailureNode: Node, @unchecked Sendable { +public struct FailureNode: Node, Sendable { public init(cause: any Error) { self.cause = cause } @@ -39,7 +39,7 @@ public struct FailureNode: Node, @unchecked Sendable { /// - property status: The status of the error. /// - property input: The input for the error. /// - property message: The message for the error. -public struct ErrorNode: Node, @unchecked Sendable { +public struct ErrorNode: Node, Sendable { public init(status: Int? = nil, input: [String : Any] = [:], message: String = "") { @@ -48,6 +48,7 @@ public struct ErrorNode: Node, @unchecked Sendable { self.status = status } + nonisolated(unsafe) public let input: [String: Any] public let message: String public let status: Int? @@ -56,7 +57,8 @@ public struct ErrorNode: Node, @unchecked Sendable { /// Represents a success node in the workflow. /// - property input: The input for the success. /// - property session: The session for the success. -public struct SuccessNode: Node, @unchecked Sendable { +public struct SuccessNode: Node, Sendable { + nonisolated(unsafe) public let input: [String: Any] public let session: Session @@ -75,6 +77,7 @@ open class ContinueNode: Node, @unchecked Sendable { public let context: FlowContext public let workflow: Workflow public let input: [String: Any] + public let actions: [any Action] public init(context: FlowContext, workflow: Workflow, input: [String: Any], actions: [any Action]) { @@ -98,7 +101,7 @@ open class ContinueNode: Node, @unchecked Sendable { } /// Protocol for a Session. A Session represents a user's session in the application. -public protocol Session { +public protocol Session: Sendable { /// Returns the value of the session as a String. func value() -> String } diff --git a/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift b/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift index 7e7faa3..43dcc29 100644 --- a/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift +++ b/PingOrchestrate/PingOrchestrate/PingHTTPCookie.swift @@ -11,76 +11,76 @@ import Foundation public struct PingHTTPCookie: Codable, Sendable { - var version: Int - var name: String? - var value: String? - var expiresDate: Date? - var isSessionOnly: Bool - var domain: String? - var path: String? - var isSecure: Bool - var isHTTPOnly: Bool - var comment: String? - var commentURL: URL? - var portList: [Int]? - var sameSitePolicy: String? + var version: Int + var name: String? + var value: String? + var expiresDate: Date? + var isSessionOnly: Bool + var domain: String? + var path: String? + var isSecure: Bool + var isHTTPOnly: Bool + var comment: String? + var commentURL: URL? + var portList: [Int]? + var sameSitePolicy: String? + + enum CodingKeys: String, CodingKey { + case version + case name + case value + case expiresDate + case isSessionOnly + case domain + case path + case isSecure + case isHTTPOnly + case comment + case commentURL + case portList + case sameSitePolicy + } + + public init(from cookie: HTTPCookie) { + self.version = cookie.version + self.name = cookie.name + self.value = cookie.value + self.expiresDate = cookie.expiresDate + self.isSessionOnly = cookie.isSessionOnly + self.domain = cookie.domain + self.path = cookie.path + self.isSecure = cookie.isSecure + self.isHTTPOnly = cookie.isHTTPOnly + self.comment = cookie.comment + self.commentURL = cookie.commentURL + self.portList = cookie.portList?.map { $0.intValue } + self.sameSitePolicy = cookie.sameSitePolicy?.rawValue + } + + public func toHTTPCookie() -> HTTPCookie? { + var properties = [HTTPCookiePropertyKey: Any]() + properties[.version] = self.version + properties[.name] = self.name + properties[.value] = self.value + properties[.expires] = self.expiresDate + properties[.discard] = self.isSessionOnly ? Constants.true : nil + properties[.domain] = self.domain + properties[.path] = self.path + properties[.secure] = self.isSecure ? Constants.true : nil + properties[HTTPCookiePropertyKey(Constants.httpOnly)] = self.isHTTPOnly ? Constants.true : nil + properties[.comment] = self.comment + properties[.commentURL] = self.commentURL + properties[.port] = self.portList?.map { NSNumber(value: $0) } - enum CodingKeys: String, CodingKey { - case version - case name - case value - case expiresDate - case isSessionOnly - case domain - case path - case isSecure - case isHTTPOnly - case comment - case commentURL - case portList - case sameSitePolicy + if let sameSitePolicyValue = self.sameSitePolicy { + properties[HTTPCookiePropertyKey.sameSitePolicy] = HTTPCookieStringPolicy(rawValue: sameSitePolicyValue) } - public init(from cookie: HTTPCookie) { - self.version = cookie.version - self.name = cookie.name - self.value = cookie.value - self.expiresDate = cookie.expiresDate - self.isSessionOnly = cookie.isSessionOnly - self.domain = cookie.domain - self.path = cookie.path - self.isSecure = cookie.isSecure - self.isHTTPOnly = cookie.isHTTPOnly - self.comment = cookie.comment - self.commentURL = cookie.commentURL - self.portList = cookie.portList?.map { $0.intValue } - self.sameSitePolicy = cookie.sameSitePolicy?.rawValue - } - - public func toHTTPCookie() -> HTTPCookie? { - var properties = [HTTPCookiePropertyKey: Any]() - properties[.version] = self.version - properties[.name] = self.name - properties[.value] = self.value - properties[.expires] = self.expiresDate - properties[.discard] = self.isSessionOnly ? Constants.true : nil - properties[.domain] = self.domain - properties[.path] = self.path - properties[.secure] = self.isSecure ? Constants.true : nil - properties[HTTPCookiePropertyKey(Constants.httpOnly)] = self.isHTTPOnly ? Constants.true : nil - properties[.comment] = self.comment - properties[.commentURL] = self.commentURL - properties[.port] = self.portList?.map { NSNumber(value: $0) } - - if let sameSitePolicyValue = self.sameSitePolicy { - properties[HTTPCookiePropertyKey.sameSitePolicy] = HTTPCookieStringPolicy(rawValue: sameSitePolicyValue) - } - - return HTTPCookie(properties: properties) - } - - enum Constants { - static let `true` = "TRUE" - static let httpOnly = "HttpOnly" - } + return HTTPCookie(properties: properties) + } + + enum Constants { + static let `true` = "TRUE" + static let httpOnly = "HttpOnly" + } } diff --git a/PingOrchestrate/PingOrchestrate/Request.swift b/PingOrchestrate/PingOrchestrate/Request.swift index 069e187..c6ef758 100644 --- a/PingOrchestrate/PingOrchestrate/Request.swift +++ b/PingOrchestrate/PingOrchestrate/Request.swift @@ -14,105 +14,105 @@ import UIKit /// Class for a Request. A Request represents a request to be sent over the network. public class Request { - - public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) - - /// Initializes a Request with a URL. - /// - Parameter urlString: The URL of the request. - public init(urlString: String = "https://") { - self.urlRequest.url = URL(string: urlString)! + + public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!) + + /// Initializes a Request with a URL. + /// - Parameter urlString: The URL of the request. + public init(urlString: String = "https://") { + self.urlRequest.url = URL(string: urlString)! + } + + /// Sets the URL of the request. + /// - Parameter urlString: The URL to be set. + public func url(_ urlString: String) { + if let url = URL(string: urlString) { + self.urlRequest.url = url + // keeping Default Content type + self.header(name: Constants.contentType, value: ContentType.json.rawValue) } - - /// Sets the URL of the request. - /// - Parameter urlString: The URL to be set. - public func url(_ urlString: String) { - if let url = URL(string: urlString) { - self.urlRequest.url = url - // keeping Default Content type - self.header(name: Constants.contentType, value: ContentType.json.rawValue) + } + + /// Adds a parameter to the request. + /// - Parameters: + /// - name: The name of the parameter. + /// - value: The value of the parameter. + public func parameter(name: String, value: String) { + if let url = self.urlRequest.url { + if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + if components.queryItems == nil { + components.queryItems = [] } - } - - /// Adds a parameter to the request. - /// - Parameters: - /// - name: The name of the parameter. - /// - value: The value of the parameter. - public func parameter(name: String, value: String) { - if let url = self.urlRequest.url { - if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - if components.queryItems == nil { - components.queryItems = [] - } - components.queryItems?.append(URLQueryItem(name: name, value: value)) - if let updatedURL = components.url { - self.urlRequest.url = updatedURL - } - } - } - } - - /// Adds a header to the request. - /// - Parameters: - /// - name: The name of the header. - /// - value: The value of the header. - public func header(name: String, value: String) { - self.urlRequest.setValue(value, forHTTPHeaderField: name) - } - - /// Adds cookies to the request. - /// - Parameter cookies: The cookies to be added. - public func cookies(cookies: [HTTPCookie]) { - let headers = HTTPCookie.requestHeaderFields(with: cookies) - for (key, value) in headers { - self.urlRequest.addValue(value, forHTTPHeaderField: key) + components.queryItems?.append(URLQueryItem(name: name, value: value)) + if let updatedURL = components.url { + self.urlRequest.url = updatedURL } + } } - - /// Sets the body of the request. - /// - Parameter body: The body to be set. - public func body(body: [String: Any]) { - self.urlRequest.httpMethod = HTTPMethod.post.rawValue - self.urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: Constants.contentType) - self.urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) + } + + /// Adds a header to the request. + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public func header(name: String, value: String) { + self.urlRequest.setValue(value, forHTTPHeaderField: name) + } + + /// Adds cookies to the request. + /// - Parameter cookies: The cookies to be added. + public func cookies(cookies: [HTTPCookie]) { + let headers = HTTPCookie.requestHeaderFields(with: cookies) + for (key, value) in headers { + self.urlRequest.addValue(value, forHTTPHeaderField: key) } - - /// Sets the form of the request. - /// - Parameter formData: The form to be set. - public func form(formData: [String: String]) { - var formString = "" - for (key, value) in formData { - formString += "\(key)=\(value)&" - } - if !formString.isEmpty { - formString.removeLast() // Remove the last '&' character - } - - self.urlRequest.httpMethod = HTTPMethod.post.rawValue - self.urlRequest.setValue(ContentType.urlEncoded.rawValue, forHTTPHeaderField: Constants.contentType) - self.urlRequest.httpBody = formString.data(using: .utf8) + } + + /// Sets the body of the request. + /// - Parameter body: The body to be set. + public func body(body: [String: Any]) { + self.urlRequest.httpMethod = HTTPMethod.post.rawValue + self.urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: Constants.contentType) + self.urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) + } + + /// Sets the form of the request. + /// - Parameter formData: The form to be set. + public func form(formData: [String: String]) { + var formString = "" + for (key, value) in formData { + formString += "\(key)=\(value)&" } - - public enum ContentType: String { - case plainText = "text/plain" - case json = "application/json" - case urlEncoded = "application/x-www-form-urlencoded" - } - - public enum HTTPMethod: String { - case get = "GET" - case put = "PUT" - case post = "POST" - case delete = "DELETE" + if !formString.isEmpty { + formString.removeLast() // Remove the last '&' character } - public enum Constants { - public static let contentType = "Content-Type" - public static let accept = "Accept" - public static let xRequestedWith = "x-requested-with" - public static let xRequestedPlatform = "x-requested-platform" - public static let forgerockSdk = "forgerock-sdk" - public static let ios = "ios" - public static let stCookie = "ST" - public static let stNoSsCookie = "ST-NO-SS" - } + self.urlRequest.httpMethod = HTTPMethod.post.rawValue + self.urlRequest.setValue(ContentType.urlEncoded.rawValue, forHTTPHeaderField: Constants.contentType) + self.urlRequest.httpBody = formString.data(using: .utf8) + } + + public enum ContentType: String { + case plainText = "text/plain" + case json = "application/json" + case urlEncoded = "application/x-www-form-urlencoded" + } + + public enum HTTPMethod: String { + case get = "GET" + case put = "PUT" + case post = "POST" + case delete = "DELETE" + } + + public enum Constants { + public static let contentType = "Content-Type" + public static let accept = "Accept" + public static let xRequestedWith = "x-requested-with" + public static let xRequestedPlatform = "x-requested-platform" + public static let forgerockSdk = "forgerock-sdk" + public static let ios = "ios" + public static let stCookie = "ST" + public static let stNoSsCookie = "ST-NO-SS" + } } diff --git a/PingOrchestrate/PingOrchestrate/Response.swift b/PingOrchestrate/PingOrchestrate/Response.swift index 16a213c..98f8854 100644 --- a/PingOrchestrate/PingOrchestrate/Response.swift +++ b/PingOrchestrate/PingOrchestrate/Response.swift @@ -15,44 +15,44 @@ import Foundation /// - property data: The data received from the network request. /// - response The URLResponse received from the network request. public struct Response { - public let data: Data - public let response: URLResponse - - /// Returns the body of the response. - /// - Returns: The body of the response as a String. - public func body() -> String { - return String(data: data, encoding: .utf8) ?? "" - } - - /// Returns the body of the response as a JSON object. - /// - Parameter data: The data to convert to a JSON object. - /// - Returns: The body of the response as a JSON object. - public func json(data: Data) throws -> [String: Any] { - return (try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:] - } - - /// Returns the status code of the response. - /// - Returns: The status code of the response as an Int. - public func status() -> Int { - return (response as? HTTPURLResponse)?.statusCode ?? 0 - } - - /// Returns the value of a specific header from the response. - /// - Parameter name: The name of the header. - /// - Returns: The value of the header as a String. - public func header(name: String) -> String? { - return (response as? HTTPURLResponse)?.allHeaderFields[name] as? String - } - - /// Returns the cookies from the response. - /// - Returns: The cookies from the response as an array of HTTPCookie. - public func getCookies() -> [HTTPCookie] { - if let response = (response as? HTTPURLResponse), - let allHeaders = response.allHeaderFields as? [String : String], - let url = response.url { - let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaders, for: url) - return cookies - } - return [] + public let data: Data + public let response: URLResponse + + /// Returns the body of the response. + /// - Returns: The body of the response as a String. + public func body() -> String { + return String(data: data, encoding: .utf8) ?? "" + } + + /// Returns the body of the response as a JSON object. + /// - Parameter data: The data to convert to a JSON object. + /// - Returns: The body of the response as a JSON object. + public func json(data: Data) throws -> [String: Any] { + return (try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:] + } + + /// Returns the status code of the response. + /// - Returns: The status code of the response as an Int. + public func status() -> Int { + return (response as? HTTPURLResponse)?.statusCode ?? 0 + } + + /// Returns the value of a specific header from the response. + /// - Parameter name: The name of the header. + /// - Returns: The value of the header as a String. + public func header(name: String) -> String? { + return (response as? HTTPURLResponse)?.allHeaderFields[name] as? String + } + + /// Returns the cookies from the response. + /// - Returns: The cookies from the response as an array of HTTPCookie. + public func getCookies() -> [HTTPCookie] { + if let response = (response as? HTTPURLResponse), + let allHeaders = response.allHeaderFields as? [String : String], + let url = response.url { + let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaders, for: url) + return cookies } + return [] + } } diff --git a/PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift b/PingOrchestrate/PingOrchestrate/SendableHelper.swift similarity index 73% rename from PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift rename to PingOrchestrate/PingOrchestrate/SendableHelper.swift index ff64a5c..237b68c 100644 --- a/PingOrchestrate/PingOrchestrate/ThreadSafeHelper.swift +++ b/PingOrchestrate/PingOrchestrate/SendableHelper.swift @@ -10,22 +10,27 @@ import Foundation +/// Sendable wrapper for a any value. public struct SendableAny: @unchecked Sendable { + /// The value to be wrapped. public let value: Any + /// Creates a new instance of `SendableAny`. public init(_ value: Any) { self.value = value } } - +///Sendable wrapper for a closure. public struct UncheckedSendableHandler: @unchecked Sendable { private let handler: () async throws -> Void + /// Creates a new instance of `UncheckedSendableHandler`. init(_ handler: @escaping () async throws -> Void) { self.handler = handler } + /// Executes the handler. func execute() async throws { try await handler() } diff --git a/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift b/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift index edfec63..da49050 100644 --- a/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift +++ b/PingOrchestrate/PingOrchestrate/WorkFlowConfig.swift @@ -14,69 +14,69 @@ import PingLogger /// Enum representing the mode of module override. public enum OverrideMode { - case override // Override the previous registered module - case append // Append to the list, and cannot be overridden - case ignore // Ignore if the module is already registered + case override // Override the previous registered module + case append // Append to the list, and cannot be overridden + case ignore // Ignore if the module is already registered } /// Workflow configuration public final class WorkflowConfig { - // Use a list instead of a map to allow registering a module twice with different configurations - public private(set) var modules: [any ModuleRegistryProtocol] = [] - // Timeout for the HTTP client, default is 15 seconds - public var timeout: TimeInterval = 15.0 - // Logger for the log, default is NoneLogger - public var logger: Logger = LogManager.logger { - didSet { - // Propagate the logger to Modules - LogManager.logger = logger - } + // Use a list instead of a map to allow registering a module twice with different configurations + public private(set) var modules: [any ModuleRegistryProtocol] = [] + // Timeout for the HTTP client, default is 15 seconds + public var timeout: TimeInterval = 15.0 + // Logger for the log, default is NoneLogger + public var logger: Logger = LogManager.logger { + didSet { + // Propagate the logger to Modules + LogManager.logger = logger } - // HTTP client for the engine - public internal(set) var httpClient: HttpClient = HttpClient() + } + // HTTP client for the engine + public internal(set) var httpClient: HttpClient = HttpClient() + + public init() {} + + public func module(_ module: Module, + _ priority: Int = 10, + mode: OverrideMode = .override, + _ config: @escaping (T) -> (Void) = { _ in }) { - public init() {} - - public func module(_ module: Module, - _ priority: Int = 10, - mode: OverrideMode = .override, - _ config: @escaping (T) -> (Void) = { _ in }) { - - switch mode { - case .override: - - if let index = modules.firstIndex(where: { $0.id == module.id }) { - modules[index] = ModuleRegistry(setup: module.setup, priority: modules[index].priority, id: modules[index].id, config: configValue(initalValue: module.config, nextValue: config)) - } else { - let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config)) - modules.append(registry ) - } - - case .append: - let uuid = UUID() - let moduleCopy = module - let registry = ModuleRegistry(setup: moduleCopy.setup, priority: priority, id: uuid, config: configValue(initalValue: moduleCopy.config, nextValue: config)) - modules.append(registry) - - case .ignore: - if modules.contains(where: { $0.id == module.id }) { - return - } - let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config)) - modules.append(registry) - } - } - - private func configValue(initalValue: @escaping () -> (T), nextValue: @escaping (T) -> (Void)) -> T { - let initConfig = initalValue() - nextValue(initConfig) - return initConfig + switch mode { + case .override: + + if let index = modules.firstIndex(where: { $0.id == module.id }) { + modules[index] = ModuleRegistry(setup: module.setup, priority: modules[index].priority, id: modules[index].id, config: configValue(initalValue: module.config, nextValue: config)) + } else { + let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config)) + modules.append(registry ) + } + + case .append: + let uuid = UUID() + let moduleCopy = module + let registry = ModuleRegistry(setup: moduleCopy.setup, priority: priority, id: uuid, config: configValue(initalValue: moduleCopy.config, nextValue: config)) + modules.append(registry) + + case .ignore: + if modules.contains(where: { $0.id == module.id }) { + return + } + let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config)) + modules.append(registry) } - - public func register(workflow: Workflow) { - httpClient.setTimeoutInterval(timeoutInterval: timeout) - modules.sort(by: { $0.priority < $1.priority }) - modules.forEach { $0.register(workflow: workflow) } - } - + } + + private func configValue(initalValue: @escaping () -> (T), nextValue: @escaping (T) -> (Void)) -> T { + let initConfig = initalValue() + nextValue(initConfig) + return initConfig + } + + public func register(workflow: Workflow) { + httpClient.setTimeoutInterval(timeoutInterval: timeout) + modules.sort(by: { $0.priority < $1.priority }) + modules.forEach { $0.register(workflow: workflow) } + } + } diff --git a/PingOrchestrate/PingOrchestrateTests/NodeTests.swift b/PingOrchestrate/PingOrchestrateTests/NodeTests.swift index 23fbc00..a749919 100644 --- a/PingOrchestrate/PingOrchestrateTests/NodeTests.swift +++ b/PingOrchestrate/PingOrchestrateTests/NodeTests.swift @@ -46,7 +46,7 @@ class WorkflowMock: Workflow, @unchecked Sendable { } } -class FlowContextMock: FlowContext {} +class FlowContextMock: FlowContext, @unchecked Sendable {} final class NodeMock: Node, Sendable {} @@ -56,7 +56,7 @@ class TestContinueNode: ContinueNode, @unchecked Sendable { } } -class TestAction: Action, Closeable { +class TestAction: Action, Closeable, @unchecked Sendable { var isClosed = false func close() { isClosed = true diff --git a/PingOrchestrate/PingOrchestrateTests/SessionTests.swift b/PingOrchestrate/PingOrchestrateTests/SessionTests.swift index 4e1fc1c..a3d2fba 100644 --- a/PingOrchestrate/PingOrchestrateTests/SessionTests.swift +++ b/PingOrchestrate/PingOrchestrateTests/SessionTests.swift @@ -24,7 +24,7 @@ final class SessionTests: XCTestCase { XCTAssertEqual("session_value", session.value()) } - class MockSession: Session { + final class MockSession: Session, Sendable { func value() -> String { return "session_value" } diff --git a/PingStorage/PingStorage/MemoryStorage.swift b/PingStorage/PingStorage/MemoryStorage.swift index 765cab5..1956eee 100644 --- a/PingStorage/PingStorage/MemoryStorage.swift +++ b/PingStorage/PingStorage/MemoryStorage.swift @@ -12,7 +12,7 @@ import Foundation /// A storage for storing objects in memory, where `T` is he type of the object to be stored. -public class Memory: Storage { +public class Memory: Storage { private var data: T? /// Saves the given item in memory.