diff --git a/MVMCore/MVMCore/Models/Model/ModelRegistry.swift b/MVMCore/MVMCore/Models/Model/ModelRegistry.swift index 097868e..78d8657 100644 --- a/MVMCore/MVMCore/Models/Model/ModelRegistry.swift +++ b/MVMCore/MVMCore/Models/Model/ModelRegistry.swift @@ -10,93 +10,192 @@ public struct ModelRegistry { + /// Error object for the Model Registry. public enum Error: Swift.Error { case keyNotFound case encoderError case decoderError case decoderOther(message: String) - case decoderErrorObjectNotPresent(codingKey: CodingKey, codingPath: [CodingKey]) + case decoderErrorObjectNotPresent(codingKey: CodingKey, codingPath: [CodingKey]) case decoderErrorModelNotMapped(identifer: String? = nil, codingKey: CodingKey? = nil, codingPath: [CodingKey]? = nil) + case duplicateRegistration(message: String) case other(message: String) case handlerNotMapped(identifer: String? = nil, categoryName: String? = nil) } - private struct Category { + public struct Category { var name: String var codingKey: String var instanceTypes: [String: ModelProtocol.Type] = [:] var handlerTypes: [String: ModelHandlerProtocol.Type] = [:] } - private static var categories: [String: Category] = [:] - - /// Registers models for Atomic use. - public static func register(handler: H.Type, for model: M.Type) throws { - //register the type - try self.register(model) + public static var categories: [String: Category] = [:] + + //-------------------------------------------------- + // MARK: - Convenient Registration + //-------------------------------------------------- + + /// A convenience wrapping function where error handling is managed within the class. + /// - Parameters: + /// - handler: The handling class taking an object of ModelHandlerProtocol.self which is used to register + /// - model: The data object of ModelProtocol.self which is used to register + public static func register(handler: H.Type, for model: M.Type) { + do { + try throwable_register(handler: handler, model: model) + } catch { + handleError(error) + } + } + + /// A convenience wrapping function where error handling is managed within the class. + /// - Parameter type: Takes an object of ModelProtocol.self which is used to register + public static func register(_ type: M.Type) { + do { + try throwable_register(type: type) + } catch { + handleError(error) + } + } + + //-------------------------------------------------- + // MARK: - Error Handling + //-------------------------------------------------- + + /// Handles an error if thrown when registering a ModelProtocol object. + /// - Parameter error: The error object produced by a thrown exception. + public static func handleError(_ error: Swift.Error) { - //get the key for the handler + guard let errorObject = MVMCoreErrorObject(title: nil, + messageToLog: nil, + code: (error as NSError).code, + domain: ErrorDomainNative, + location: #file) else { return } + + switch error as? ModelRegistry.Error { + case .keyNotFound: + errorObject.title = "Key Not Found" + case .encoderError: + errorObject.title = "Encoder Error" + case .decoderError: + errorObject.title = "Decoder Error" + case .decoderOther(let message): + errorObject.title = "Decoder Error: Other" + errorObject.messageToLog = message + case .decoderErrorObjectNotPresent(let codingKey, let codingPath): + errorObject.title = "Decoder Error: Object Not Present" + let codingPathConcat = codingPath.compactMap { $0.stringValue }.joined(separator: " ") + errorObject.messageToLog = codingKey.stringValue + codingPathConcat + case .decoderErrorModelNotMapped(let identifer, let codingKey, let codingPath): + errorObject.title = "Decoder Error: Model Not Mapped" + let codingPathConcat = codingPath?.compactMap { $0.stringValue }.joined(separator: " ") ?? "" + errorObject.messageToLog = (identifer ?? "") + (codingKey?.stringValue ?? "") + codingPathConcat + case .handlerNotMapped(let identifer, let categoryName): + errorObject.title = "Handler Not Mapped" + errorObject.messageToLog = (identifer ?? "") + (categoryName ?? "") + case .other(let message): + errorObject.title = "Other Registry Error" + errorObject.messageToLog = message + case .duplicateRegistration(let message): + errorObject.title = "Duplicate Registration" + errorObject.messageToLog = message + default: + errorObject.title = "Unknown Model Registry Error" + } + + MVMCoreLoggingHandler.addError(toLog: errorObject) + #if DEBUG + triggerCrashInDebug() + #endif + } + + #if DEBUG + private static func triggerCrashInDebug() { + fatalError() + } + #endif + + //-------------------------------------------------- + // MARK: - Throwable Registration + //-------------------------------------------------- + + /// Registers models for Atomic use. + /// - Parameters: + /// - handler: The handling class taking an object of ModelHandlerProtocol.self which is used to register + /// - model: The data object of ModelProtocol.self which is used to register + /// - Throws: An Error object of `ModelRegistry.Error` + public static func throwable_register(handler: H.Type, model: M.Type) throws { + // Register the type + try self.throwable_register(type: model) + + // Get the key for the handler let key = model.identifier - //get the category for the ModelProtocol + // Get the category for the ModelProtocol var category = getCategory(for: model) // Check to ensure the Category/Container combination doesn't exist. if category.handlerTypes[key] != nil { - throw ModelRegistry.Error.other(message: "ModelHandlerProtocol: \(String(describing: handler)) already exists in Category: \(category.name)") + throw ModelRegistry.Error.duplicateRegistration(message: "ModelHandlerProtocol: \(String(describing: handler)) already exists in Category: \(category.name)") } else { category.handlerTypes[key] = handler } + categories[category.name] = category } /// Registers models for Atomic use. - public static func register(_ type: M.Type) throws { - //get the category for the ModelProtocol + /// - Parameter type: Takes an object of ModelProtocol.self which is used to register + /// - Throws: An Error object of `ModelRegistry.Error` + public static func throwable_register(type: M.Type) throws { + // Get the category for the ModelProtocol var category = getCategory(for: type) // Check to ensure the Category/Type combination doesn't exist. if category.instanceTypes[M.identifier] != nil { - throw ModelRegistry.Error.other(message: "ModelProtocol: \(M.identifier) already exists in Category: \(M.categoryName)") + throw ModelRegistry.Error.duplicateRegistration(message: "ModelProtocol: \(M.identifier) already exists in Category: \(M.categoryName)") } + category.instanceTypes[M.identifier] = type categories[M.categoryName] = category } + //-------------------------------------------------- + // MARK: - Functions + //-------------------------------------------------- + public static func getHandler(_ model: ModelProtocol) throws -> ModelHandlerProtocol.Type { - //get the modelType + // Get the modelType let modelType = type(of: model) - - //get the category for the ModelProtocol + + // Get the category for the ModelProtocol guard let category = categories[modelType.categoryName] else { throw ModelRegistry.Error.other(message: "Category not found: \(modelType.categoryName)") } - - //get the containerProtocol for this ModelProtocol you had registered + + // Get the containerProtocol for this ModelProtocol you had registered guard let handlerType = category.handlerTypes[modelType.identifier] else { throw ModelRegistry.Error.handlerNotMapped(identifer: modelType.identifier, categoryName: category.name) } + return handlerType } - - private static func getCategory(for type: M.Type) -> Category { - return categories[type.categoryName] ?? Category(name: type.categoryName, codingKey: type.categoryCodingKey) + + public static func getCategory(for type: M.Type) -> Category { + categories[type.categoryName] ?? Category(name: type.categoryName, codingKey: type.categoryCodingKey) } - - private static func getCategory(for type: T.Type) -> Category? { + + public static func getCategory(for type: T.Type) -> Category? { // Temporary code till we find a better solution. - //if this is a protocol composotion, loop through each protocol for a category lookup - let protocols = String(describing: T.self).components(separatedBy: "&").compactMap{$0.trimmingCharacters(in: .whitespaces)} + // if this is a protocol composotion, loop through each protocol for a category lookup + let protocols = String(describing: T.self).components(separatedBy: "&").compactMap{ $0.trimmingCharacters(in: .whitespaces)} return protocols.compactMap({ (p) -> Category? in - guard let c = categories[p] else { - return nil - } - return c + categories[p] ?? nil }).first } public static func getType(for name: String, with type: T.Type) -> ModelProtocol.Type? { - return getCategory(for: type)?.instanceTypes[name] + getCategory(for: type)?.instanceTypes[name] } public static func getCodingKey(for type: T.Type) throws -> AnyCodingKey { @@ -108,12 +207,13 @@ public struct ModelRegistry { } } -extension KeyedDecodingContainer where Key: CodingKey { - +public extension KeyedDecodingContainer where Key: CodingKey { + //-------------------------------------------------- // MARK: - Decode + //-------------------------------------------------- /// Decodes to a registered model based on the identifier - public func decodeModel(codingKey: KeyedDecodingContainer.Key) throws -> T { + func decodeModel(codingKey: KeyedDecodingContainer.Key) throws -> T { guard let model: T = try decodeModelIfPresent(codingKey: codingKey) else { MVMCoreLoggingHandler.logDebugMessage(withDelegate: "ModelRegistry Error decoderErrorObjectNotPresent: \(codingKey.stringValue)") throw ModelRegistry.Error.decoderErrorObjectNotPresent(codingKey: codingKey, codingPath: codingPath) @@ -122,7 +222,7 @@ extension KeyedDecodingContainer where Key: CodingKey { } /// Decodes an array of registered model based on the identifiers. - public func decodeModels(codingKey: KeyedDecodingContainer.Key) throws -> [T] { + func decodeModels(codingKey: KeyedDecodingContainer.Key) throws -> [T] { guard let model: [T] = try decodeModelsIfPresent(codingKey: codingKey) else { MVMCoreLoggingHandler.logDebugMessage(withDelegate: "ModelRegistry Error decoderErrorObjectNotPresent: \(codingKey.stringValue)") throw ModelRegistry.Error.decoderErrorObjectNotPresent(codingKey: codingKey, codingPath: codingPath) @@ -131,7 +231,7 @@ extension KeyedDecodingContainer where Key: CodingKey { } /// Decodes an array with arrays of models based on the identifiers. - public func decodeModels2D(codingKey: KeyedDecodingContainer.Key) throws -> [[T]] { + func decodeModels2D(codingKey: KeyedDecodingContainer.Key) throws -> [[T]] { guard let models: [[T]] = try decodeModels2DIfPresent(codingKey: codingKey) else { MVMCoreLoggingHandler.logDebugMessage(withDelegate: "ModelRegistry Error decoderErrorObjectNotPresent: \(codingKey.stringValue)") throw ModelRegistry.Error.decoderErrorObjectNotPresent(codingKey: codingKey, codingPath: codingPath) @@ -139,25 +239,27 @@ extension KeyedDecodingContainer where Key: CodingKey { return models } + //-------------------------------------------------- // MARK: - DecodeIfPresent + //-------------------------------------------------- /// Decodes to a registered model based on the identifier, optional. - public func decodeModelIfPresent(codingKey: KeyedDecodingContainer.Key) throws -> T? { - //create coding key + func decodeModelIfPresent(codingKey: KeyedDecodingContainer.Key) throws -> T? { + // Create coding key let typeCodingKey = try ModelRegistry.getCodingKey(for: T.self) - //get the container that holds the identifier value + // Get the container that holds the identifier value guard contains(codingKey), - let container = try? self.nestedContainer(keyedBy: AnyCodingKey.self, forKey: codingKey), - let identifier = try container.decodeIfPresent(String.self, forKey: typeCodingKey) else { return nil } + let container = try? self.nestedContainer(keyedBy: AnyCodingKey.self, forKey: codingKey), + let identifier = try container.decodeIfPresent(String.self, forKey: typeCodingKey) else { return nil } - //get the type from the identifier value in the Registry + // Get the type from the identifier value in the Registry guard let type = ModelRegistry.getType(for: identifier, with: T.self) else { MVMCoreLoggingHandler.logDebugMessage(withDelegate: "ModelProtocol not mapped: \(identifier)") throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: identifier, codingKey: typeCodingKey, codingPath: container.codingPath) } - //decode the type using the decoder + // Decode the type using the decoder let model = try type.decode(keyedContainer: self, codingKey: codingKey) if let model = model as? T { @@ -169,19 +271,19 @@ extension KeyedDecodingContainer where Key: CodingKey { } /// Decodes an array of registered model based on the identifiers, optional. - public func decodeModelsIfPresent(codingKey: KeyedDecodingContainer.Key) throws -> [T]? { + func decodeModelsIfPresent(codingKey: KeyedDecodingContainer.Key) throws -> [T]? { guard contains(codingKey), - var container = try? self.nestedUnkeyedContainer(forKey: codingKey) - else { return nil } + var container = try? self.nestedUnkeyedContainer(forKey: codingKey) + else { return nil } return try container.decodeModelsIfPresent() } /// Decodes an array with arrays of models based on the identifiers, optional. - public func decodeModels2DIfPresent(codingKey: KeyedDecodingContainer.Key) throws -> [[T]]? { + func decodeModels2DIfPresent(codingKey: KeyedDecodingContainer.Key) throws -> [[T]]? { guard contains(codingKey), - var container = try? nestedUnkeyedContainer(forKey: codingKey) - else { return nil } + var container = try? nestedUnkeyedContainer(forKey: codingKey) + else { return nil } return try container.decodeModels2DIfPresent() } @@ -259,6 +361,7 @@ public extension UnkeyedDecodingContainer { throw ModelRegistry.Error.decoderError } } + return models } @@ -277,6 +380,7 @@ public extension UnkeyedDecodingContainer { } modelLists.append(models) } + return modelLists } } diff --git a/MVMCore/MVMCore/Models/ModelMapping.swift b/MVMCore/MVMCore/Models/ModelMapping.swift index 26cb696..d5e602d 100644 --- a/MVMCore/MVMCore/Models/ModelMapping.swift +++ b/MVMCore/MVMCore/Models/ModelMapping.swift @@ -8,22 +8,21 @@ @objcMembers open class ModelMapping: NSObject { - open class func registerObjects() {} + open class func registerObjects() { } open class func registerActions() { - try? ModelRegistry.register(ActionOpenPageModel.self) - try? ModelRegistry.register(ActionOpenUrlModel.self) - try? ModelRegistry.register(ActionCallModel.self) - try? ModelRegistry.register(ActionBackModel.self) - try? ModelRegistry.register(ActionShareModel.self) - try? ModelRegistry.register(ActionRestartModel.self) - try? ModelRegistry.register(ActionPreviousSubmitModel.self) - try? ModelRegistry.register(ActionCancelModel.self) - try? ModelRegistry.register(ActionSettingModel.self) - try? ModelRegistry.register(ActionNoopModel.self) - try? ModelRegistry.register(ActionActionsModel.self) - try? ModelRegistry.register(ActionOpenSMSModel.self) - try? ModelRegistry.register(ActionContactModel.self) + ModelRegistry.register(ActionOpenPageModel.self) + ModelRegistry.register(ActionOpenUrlModel.self) + ModelRegistry.register(ActionCallModel.self) + ModelRegistry.register(ActionBackModel.self) + ModelRegistry.register(ActionShareModel.self) + ModelRegistry.register(ActionRestartModel.self) + ModelRegistry.register(ActionPreviousSubmitModel.self) + ModelRegistry.register(ActionCancelModel.self) + ModelRegistry.register(ActionSettingModel.self) + ModelRegistry.register(ActionNoopModel.self) + ModelRegistry.register(ActionActionsModel.self) + ModelRegistry.register(ActionOpenSMSModel.self) + ModelRegistry.register(ActionContactModel.self) } } -