diff --git a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift index 2f79085..a889da1 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift @@ -99,10 +99,6 @@ public extension ClientParameterHandler { MVMCoreLoadingOverlayHandler.sharedLoadingOverlay()?.stopLoading(true) } } - return try await withCheckedThrowingContinuation({ continuation in - getParameters(with: model, requestParameters: requestParameters, actionId: actionId) { parameters in - continuation.resume(returning: parameters) - } - }) + return await getParameters(with: model, requestParameters: requestParameters, actionId: actionId) } } diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 6e6b01e..21149cf 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -6,14 +6,39 @@ // Copyright © 2021 myverizon. All rights reserved. // -@objcMembers open class ClientParameterHandler: NSObject { +/// Sample clientParameters +///{ +/// "timeout": 30, +/// "list": [ +/// { +/// "type": "currentLocation", +/// "inputParameters": { +/// "accuracy": 10, +/// "timeThreshold": 600 +/// } +/// } +/// ] +///} +/// completionHandler can return flat dictinary or a map. It depends on the paramters handler + + +@objc open class ClientParameterHandler: NSObject { public static let DefaultTimeout = 30.0 - - open func createParametersHandler(_ clientParameterModel: ClientParameterModelProtocol) -> ClientParameterProtocol? { + + open func createParametersHandler(_ clientParameterModel: ClientParameterModelProtocol) -> (any ClientParameterProtocol)? { do { - let parameterType = try ModelRegistry.getHandler(clientParameterModel) as! ClientParameterProtocol.Type - return parameterType.init(clientParameterModel) + //Ensure the handlerType return will be initable using ClientParameterHandler + guard let handlerType = try ModelRegistry.getHandler(clientParameterModel) as? AnyClientParameterProtocol.Type else { throw ClientParameterError.castingError } + + //init the handler + let handler = try handlerType.init(clientParameterModel: clientParameterModel) + + //Cast from AnyClientParameterProtocol to AnyClientParameterProtocol, if not throw + guard let castedHandler = handler as? any ClientParameterProtocol else { throw ClientParameterError.castingError } + + //Return the any ClientParameterProtocol + return castedHandler } catch { if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) @@ -21,26 +46,13 @@ return nil } } - - func getClientParameterModel(_ clientParameters: [String: Any]) throws -> ClientParameterModel? { + + open func getClientParameterModel(_ clientParameters: [String: Any]) throws -> ClientParameterModel? { let data = try JSONSerialization.data(withJSONObject: clientParameters) return try JSONDecoder.create().decode(ClientParameterModel.self, from: data) } - /// Sample clientParameters - ///{ - /// "timeout": 30, - /// "list": [ - /// { - /// "type": "currentLocation", - /// "inputParameters": { - /// "accuracy": 10, - /// "timeThreshold": 600 - /// } - /// } - /// ] - ///} - /// completionHandler can return flat dictinary or a map. It depends on the paramters handler + @objc open func getParameters(with clientParameters: [String: Any], requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) throws { guard let clientParameterModel = try getClientParameterModel(clientParameters) else { completionHandler(nil) @@ -49,7 +61,15 @@ getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId, completionHandler: completionHandler) } - open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) { + open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { + await withCheckedContinuation { continuation in + getParameters(with: model, requestParameters: requestParameters, actionId: actionId) { parameters in + continuation.resume(returning: parameters) + } + } + } + + private func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: AnyHashable]) -> ()) { let parametersWorkQueue = DispatchQueue(label: "com.mva.clientparameter") let group = DispatchGroup() @@ -66,11 +86,12 @@ var mergedParametersList: [String: AnyHashable] { return returnedList.enumerated().map { (index, element) -> [String: AnyHashable] in guard let parameter = element else { + let handler = parameterHandlerList[index] MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( - name: model.list[index].type, + name: handler.clientParameterModel.type, uuid: requestUUID[index], actionId: actionId)) - return parameterHandlerList[index].valueOnTimeout() + return handler.valueOnTimeout() } return parameter }.reduce(into: [String: AnyHashable]()) { partialResult, next in @@ -81,13 +102,15 @@ // Setup completion handlers. Barriered to ensure one happens after the other. var complete = false let timeoutWorkItem = DispatchWorkItem(qos: .userInitiated) { - completionHandler(mergedParametersList); + let params = mergedParametersList + completionHandler(params); complete = true } let completionWorkItem = DispatchWorkItem(qos: .userInitiated) { timeoutWorkItem.cancel() if !complete { // In the case of firing after timeout. - completionHandler(mergedParametersList); + let params = mergedParametersList + completionHandler(params); complete = true } } @@ -100,7 +123,7 @@ let parameterType = parameterHandler.clientParameterModel.type MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch(name: parameterType, uuid: requestUUID[index], actionId: actionId)) group.enter() - parameterHandler.fetchClientParameters(requestParameters: requestParameters, + parameterHandler.fetchClientParametersBridge(requestParameters: requestParameters, timingOutIn: timeout) { (receivedParameter) in // Queue the results for merge. parametersWorkQueue.async { @@ -124,3 +147,12 @@ } } } + +fileprivate extension ClientParameterProtocol { + func fetchClientParametersBridge(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completion: @escaping ([String: AnyHashable]?) -> ()) { + Task { + let parameters = await fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) + completion(parameters) + } + } +} diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift index b742170..bbde12c 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift @@ -8,8 +8,13 @@ import Foundation +/// A protocol defining models for client parameter fetching handlers. public protocol ClientParameterModelProtocol: ModelProtocol { + /// The type of client parameter. var type: String { get } + + /// A unique identifier for this instance. + var id: String { get } } public extension ClientParameterModelProtocol { diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift index e55fd15..6f9eacc 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift @@ -8,32 +8,50 @@ import Foundation -public protocol ClientParameterProtocol: ModelHandlerProtocol { - static var name: String { get } +/// Errors used for ClientParamter +public enum ClientParameterError: Error { + case castingError + case modelMismatch(message: String) +} - init(_ clientParameterModel: ClientParameterModelProtocol) +public protocol AnyClientParameterProtocol { + /// Original init using a ClientParameterModelProtocol to get around the self - associatedType issues with ClientParameterProtocol + /// - Parameter clientParameterModel: any Model using the ClientParameterModelProtocol + init(clientParameterModel: ClientParameterModelProtocol) throws +} + +/// A protocol defining client parameter fetching handlers. +public protocol ClientParameterProtocol: ModelHandlerProtocol, AnyClientParameterProtocol { - var clientParameterModel: ClientParameterModelProtocol { get set } + associatedtype Model: ClientParameterModelProtocol + + var clientParameterModel: Model { get set } + init(clientParameterModel: Model) - func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completionHandler:@escaping ([String: AnyHashable]?) -> ()) + func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval) async -> [String: AnyHashable] /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. func valueOnTimeout() -> [String: AnyHashable] } public extension ClientParameterProtocol { - - func valueOnTimeout() -> [String: AnyHashable] { - return [Self.name: "failed_to_collect"] + /// Helper init to convert protocol to specific model. Default Implementation should never be overridden + init(clientParameterModel: ClientParameterModelProtocol) throws { + // Cast the clientParameterModel to ensure it matches the asscociatedType Model assigned to this Handler. + guard let castedModel = clientParameterModel as? Model else { + let message = "Expected \(Model.self) but received \(clientParameterModel.type)" + throw ClientParameterError.modelMismatch(message: message) + } + // Call init for the Handler Class and pass in the casted Model. + self.init(clientParameterModel: castedModel) } - /// The handler should call this method to pass the parameter back to the caller. - /// If using isFlatMap, you must provide at least 1 element in parameters or it will result in triggering a timeout. - func returnParameters(_ isFlatMap: Bool, _ parameter: [String: AnyHashable], completionHandler: @escaping ([String: AnyHashable]?) -> ()) { - if isFlatMap { - completionHandler(parameter) - } else { - completionHandler([Self.name: parameter]) - } + func valueOnTimeout() -> [String: AnyHashable] { + return [clientParameterModel.type: "failed_to_collect"] + } + + /// Convenience function to format the return parameters consistently. + func formatReturnParameters(_ parameter: AnyHashable) -> [String: AnyHashable] { + return [clientParameterModel.type: parameter] } }