diff --git a/Sources/Dictionary+Unbox.swift b/Sources/Dictionary+Unbox.swift index dfef766..38869ab 100644 --- a/Sources/Dictionary+Unbox.swift +++ b/Sources/Dictionary+Unbox.swift @@ -70,6 +70,9 @@ private extension Dictionary { } catch { if !allowInvalidElements { throw error + } else if let unboxError = error as? UnboxError { + let warning = UnboxWarning.invalidElement(error: unboxError) + Unboxer.warningLogger?.log(warning: warning) } } } diff --git a/Sources/Sequence+Unbox.swift b/Sources/Sequence+Unbox.swift index 69b0451..dd1e3ef 100644 --- a/Sources/Sequence+Unbox.swift +++ b/Sources/Sequence+Unbox.swift @@ -13,7 +13,17 @@ internal extension Sequence { } return self.flatMap { - return try? transform($0) + + do { + let unboxed = try transform($0) + return unboxed + } catch { + if let error = error as? UnboxError { + let warning = UnboxWarning.invalidElement(error: error) + Unboxer.warningLogger?.log(warning: warning) + } + return nil + } } } } diff --git a/Sources/UnboxWarning.swift b/Sources/UnboxWarning.swift new file mode 100644 index 0000000..656b3ee --- /dev/null +++ b/Sources/UnboxWarning.swift @@ -0,0 +1,18 @@ +// +// UnboxWarningLogger.swift +// Unbox +// +// Created by Nicolas Jakubowski on 6/6/17. +// Copyright © 2017 John Sundell. All rights reserved. +// + +import Foundation + +/// Warnings are things that went wrong during the unbox operation +/// These aren't thrown, instead they are sent to a logger where you'll be able to keep record of them and research why you're getting them +public enum UnboxWarning { + + /// An invalid element was found in an array + case invalidElement(error: UnboxError) + +} diff --git a/Sources/UnboxWarningLogger.swift b/Sources/UnboxWarningLogger.swift new file mode 100644 index 0000000..145d443 --- /dev/null +++ b/Sources/UnboxWarningLogger.swift @@ -0,0 +1,16 @@ +// +// UnboxWarningLogger.swift +// Unbox +// +// Created by Nicolas Jakubowski on 6/6/17. +// Copyright © 2017 John Sundell. All rights reserved. +// + +import Foundation + +/// Takes care of dealing with warnings +public protocol UnboxWarningLogger { + + /// Called whenever a warning is found when Unboxing + func log(warning: UnboxWarning) +} diff --git a/Sources/Unboxer.swift b/Sources/Unboxer.swift index 9e920f1..b8cc5bd 100644 --- a/Sources/Unboxer.swift +++ b/Sources/Unboxer.swift @@ -15,6 +15,12 @@ import Foundation * - and the correct type will be returned. If a required (non-optional) value couldn't be unboxed `UnboxError` will be thrown. */ public final class Unboxer { + + /// Takes care of logging warnings found during unbox operation. + /// Warnings are not fatal as errors, they just indicate that something is wrong. + /// An example of a warning is when you use the `allowInvalidElements` for parsing a collection. If an element in that collection fails to unbox, you'll receive a warning in this logger. + public static var warningLogger: UnboxWarningLogger? + /// The underlying JSON dictionary that is being unboxed public let dictionary: UnboxableDictionary diff --git a/Tests/UnboxTests/UnboxTests.swift b/Tests/UnboxTests/UnboxTests.swift index 0d0e97f..4c08a3f 100644 --- a/Tests/UnboxTests/UnboxTests.swift +++ b/Tests/UnboxTests/UnboxTests.swift @@ -1744,6 +1744,154 @@ class UnboxTests: XCTestCase { XCTFail("\(error)") } } + + func testWarningsAreLoggedForInvalidElementsInArray() { + + let logger = UnboxWarningLoggerMock() + Unboxer.warningLogger = logger + + struct Model: Unboxable { + let string: String + + init(unboxer: Unboxer) throws { + self.string = try unboxer.unbox(key: "string") + } + } + + let dictionaries: [UnboxableDictionary] = [ + ["string" : "one"], + ["invalid" : "element"], + ["string" : "two"] + ] + + do { + let unboxed: [Model] = try unbox(dictionaries: dictionaries, allowInvalidElements: true) + XCTAssertEqual(unboxed.first?.string, "one") + XCTAssertEqual(unboxed.last?.string, "two") + } catch { + XCTFail("\(error)") + } + + XCTAssertEqual(logger.receivedWarnings.count, 1, "Expected only one warning call") + + guard let warning = logger.receivedWarnings.first else { + XCTFail("Expected to find a warning but there wasn't any") + return + } + + switch warning { + case .invalidElement: + break + default: + XCTFail("Expected logged warning to be .invalidElement but instead got \(warning)") + } + } + + func testWarningsareLoggedForInvalidElementsInNestedArrayOfDictionaries() { + + let logger = UnboxWarningLoggerMock() + Unboxer.warningLogger = logger + + struct Model: Unboxable { + let nestedModels: [NestedModel] + + init(unboxer: Unboxer) throws { + self.nestedModels = try unboxer.unbox(key: "nested", allowInvalidElements: true) + } + } + + struct NestedModel: Unboxable { + let string: String + + init(unboxer: Unboxer) throws { + self.string = try unboxer.unbox(key: "string") + } + } + + let dictionary: UnboxableDictionary = [ + "nested" : [ + ["string" : "one"], + ["invalid" : "element"], + ["string" : "two"] + ] + ] + + do { + let unboxed: Model = try unbox(dictionary: dictionary) + XCTAssertEqual(unboxed.nestedModels.first?.string, "one") + XCTAssertEqual(unboxed.nestedModels.last?.string, "two") + } catch { + XCTFail("\(error)") + } + + XCTAssertEqual(logger.receivedWarnings.count, 1, "Expected only one warning call") + + guard let warning = logger.receivedWarnings.first else { + XCTFail("Expected to find a warning but there wasn't any") + return + } + + switch warning { + case .invalidElement: + break + default: + XCTFail("Expected logged warning to be .invalidElement but instead got \(warning)") + } + } + + func testWarningsAreLoggedForInvalidElementsInNestedDictionary() { + + let logger = UnboxWarningLoggerMock() + Unboxer.warningLogger = logger + + struct Model: Unboxable { + let nestedModels: [String : NestedModel] + + init(unboxer: Unboxer) throws { + self.nestedModels = try unboxer.unbox(key: "nested", allowInvalidElements: true) + } + } + + struct NestedModel: Unboxable { + let string: String + + init(unboxer: Unboxer) throws { + self.string = try unboxer.unbox(key: "string") + } + } + + let dictionary: UnboxableDictionary = [ + "nested" : [ + "one" : ["string" : "one"], + "two" : ["invalid" : "element"], + "three" : ["string" : "two"] + ] + ] + + do { + let unboxed: Model = try unbox(dictionary: dictionary) + XCTAssertEqual(unboxed.nestedModels.count, 2) + XCTAssertEqual(unboxed.nestedModels["one"]?.string, "one") + XCTAssertEqual(unboxed.nestedModels["three"]?.string, "two") + } catch { + XCTFail("\(error)") + } + + XCTAssertEqual(logger.receivedWarnings.count, 1, "Expected only one warning call") + + guard let warning = logger.receivedWarnings.first else { + XCTFail("Expected to find a warning but there wasn't any") + return + } + + switch warning { + case .invalidElement: + break + default: + XCTFail("Expected logged warning to be .invalidElement but instead got \(warning)") + } + } + } private func UnboxTestDictionaryWithAllRequiredKeysWithValidValues(nested: Bool) -> UnboxableDictionary { @@ -2024,6 +2172,15 @@ private final class UnboxTestContextMock: UnboxableWithContext { } } +private final class UnboxWarningLoggerMock: UnboxWarningLogger { + + private(set) var receivedWarnings: [UnboxWarning] = [] + func log(warning: UnboxWarning) { + receivedWarnings.append(warning) + } + +} + private struct UnboxTestSimpleMock: Unboxable, Equatable { let int: Int diff --git a/Unbox.xcodeproj/project.pbxproj b/Unbox.xcodeproj/project.pbxproj index f9a425f..414deff 100644 --- a/Unbox.xcodeproj/project.pbxproj +++ b/Unbox.xcodeproj/project.pbxproj @@ -181,6 +181,14 @@ 52D6D9871BEFF229002C0205 /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* Unbox.framework */; }; DD7502881C68FEDE006590AF /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Unbox.framework */; }; DD7502921C690C7A006590AF /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Unbox.framework */; }; + FE63CED71EE70829000239C9 /* UnboxWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED61EE70829000239C9 /* UnboxWarning.swift */; }; + FE63CED91EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED81EE709DF000239C9 /* UnboxWarningLogger.swift */; }; + FE63CEDA1EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED81EE709DF000239C9 /* UnboxWarningLogger.swift */; }; + FE63CEDB1EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED81EE709DF000239C9 /* UnboxWarningLogger.swift */; }; + FE63CEDC1EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED81EE709DF000239C9 /* UnboxWarningLogger.swift */; }; + FE63CEDD1EE709F0000239C9 /* UnboxWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED61EE70829000239C9 /* UnboxWarning.swift */; }; + FE63CEDE1EE709F0000239C9 /* UnboxWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED61EE70829000239C9 /* UnboxWarning.swift */; }; + FE63CEDF1EE709F1000239C9 /* UnboxWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63CED61EE70829000239C9 /* UnboxWarning.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -260,6 +268,8 @@ AD2FAA281CD0B6E100659CF4 /* UnboxTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = UnboxTests.plist; sourceTree = ""; }; DD75027A1C68FCFC006590AF /* Unbox-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Unbox-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; DD75028D1C690C7A006590AF /* Unbox-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Unbox-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + FE63CED61EE70829000239C9 /* UnboxWarning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnboxWarning.swift; sourceTree = ""; }; + FE63CED81EE709DF000239C9 /* UnboxWarningLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnboxWarningLogger.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -363,6 +373,8 @@ 524F1E041E89B0A3000AC6FE /* UnboxPathError.swift */, 524F1E9F1E89BF6C000AC6FE /* UnboxPathNode.swift */, 524F1E901E89BDFC000AC6FE /* URL+Unbox.swift */, + FE63CED61EE70829000239C9 /* UnboxWarning.swift */, + FE63CED81EE709DF000239C9 /* UnboxWarningLogger.swift */, ); path = Sources; sourceTree = ""; @@ -736,7 +748,9 @@ 524F1E5F1E89BA9E000AC6FE /* UInt64+Unbox.swift in Sources */, 524F1EBE1E89C1C6000AC6FE /* Data+Unbox.swift in Sources */, 524F1E371E89B42E000AC6FE /* UnboxFormatter.swift in Sources */, + FE63CED71EE70829000239C9 /* UnboxWarning.swift in Sources */, 524F1E501E89BA26000AC6FE /* Int32+Unbox.swift in Sources */, + FE63CED91EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */, 524F1ECD1E89C466000AC6FE /* UnboxArrayContainer.swift in Sources */, 524F1EB91E89C16B000AC6FE /* JSONSerialization+Unbox.swift in Sources */, 524F1EAA1E89C01B000AC6FE /* NSArray+Unbox.swift in Sources */, @@ -766,12 +780,14 @@ 524F1E891E89BD1C000AC6FE /* CGFloat+Unbox.swift in Sources */, 524F1E341E89B409000AC6FE /* UnboxableByTransform.swift in Sources */, 524F1E5C1E89BA78000AC6FE /* UInt32+Unbox.swift in Sources */, + FE63CEDB1EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */, 524F1ECA1E89C41C000AC6FE /* UnboxContainer.swift in Sources */, 524F1E841E89BC99000AC6FE /* Dictionary+Unbox.swift in Sources */, 524F1E201E89B345000AC6FE /* UnboxableCollection.swift in Sources */, 524F1E931E89BDFC000AC6FE /* URL+Unbox.swift in Sources */, 524F1E6B1E89BAE7000AC6FE /* Float+Unbox.swift in Sources */, 524F1E3E1E89B884000AC6FE /* Optional+Unbox.swift in Sources */, + FE63CEDE1EE709F0000239C9 /* UnboxWarning.swift in Sources */, 524F1E1B1E89B2A9000AC6FE /* UnboxableRawType.swift in Sources */, 524F1E111E89B168000AC6FE /* UnboxableWithContext.swift in Sources */, 524F1E251E89B381000AC6FE /* UnboxCollectionElementTransformer.swift in Sources */, @@ -815,12 +831,14 @@ 524F1E8A1E89BD1C000AC6FE /* CGFloat+Unbox.swift in Sources */, 524F1E351E89B409000AC6FE /* UnboxableByTransform.swift in Sources */, 524F1E5D1E89BA78000AC6FE /* UInt32+Unbox.swift in Sources */, + FE63CEDC1EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */, 524F1ECB1E89C41C000AC6FE /* UnboxContainer.swift in Sources */, 524F1E851E89BC99000AC6FE /* Dictionary+Unbox.swift in Sources */, 524F1E211E89B345000AC6FE /* UnboxableCollection.swift in Sources */, 524F1E941E89BDFC000AC6FE /* URL+Unbox.swift in Sources */, 524F1E6C1E89BAE7000AC6FE /* Float+Unbox.swift in Sources */, 524F1E3F1E89B884000AC6FE /* Optional+Unbox.swift in Sources */, + FE63CEDF1EE709F1000239C9 /* UnboxWarning.swift in Sources */, 524F1E1C1E89B2A9000AC6FE /* UnboxableRawType.swift in Sources */, 524F1E121E89B168000AC6FE /* UnboxableWithContext.swift in Sources */, 524F1E261E89B381000AC6FE /* UnboxCollectionElementTransformer.swift in Sources */, @@ -864,12 +882,14 @@ 524F1E881E89BD1C000AC6FE /* CGFloat+Unbox.swift in Sources */, 524F1E331E89B409000AC6FE /* UnboxableByTransform.swift in Sources */, 524F1E5B1E89BA78000AC6FE /* UInt32+Unbox.swift in Sources */, + FE63CEDA1EE709DF000239C9 /* UnboxWarningLogger.swift in Sources */, 524F1EC91E89C41C000AC6FE /* UnboxContainer.swift in Sources */, 524F1E831E89BC99000AC6FE /* Dictionary+Unbox.swift in Sources */, 524F1E1F1E89B345000AC6FE /* UnboxableCollection.swift in Sources */, 524F1E921E89BDFC000AC6FE /* URL+Unbox.swift in Sources */, 524F1E6A1E89BAE7000AC6FE /* Float+Unbox.swift in Sources */, 524F1E3D1E89B884000AC6FE /* Optional+Unbox.swift in Sources */, + FE63CEDD1EE709F0000239C9 /* UnboxWarning.swift in Sources */, 524F1E1A1E89B2A9000AC6FE /* UnboxableRawType.swift in Sources */, 524F1E101E89B168000AC6FE /* UnboxableWithContext.swift in Sources */, 524F1E241E89B381000AC6FE /* UnboxCollectionElementTransformer.swift in Sources */,