From 305b28f0c526f9a9117d2f6df62f2da8f1504e19 Mon Sep 17 00:00:00 2001 From: Johannes Lund Date: Tue, 20 Sep 2016 00:58:17 +0200 Subject: [PATCH 1/2] Add ErrorFormatter.swift --- Decodable.xcodeproj/project.pbxproj | 10 ++ Sources/ErrorFormatter.swift | 165 ++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 Sources/ErrorFormatter.swift diff --git a/Decodable.xcodeproj/project.pbxproj b/Decodable.xcodeproj/project.pbxproj index d740000..7442edc 100644 --- a/Decodable.xcodeproj/project.pbxproj +++ b/Decodable.xcodeproj/project.pbxproj @@ -59,6 +59,10 @@ 8F87BCC51B592F0E00E53A8C /* DecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87BCC31B592F0E00E53A8C /* DecodingError.swift */; }; 8F956D1F1B4D6FF700243072 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F956D1E1B4D6FF700243072 /* Operators.swift */; }; 8FA733591D328D13003A90A7 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F3E45AF1D32853F00FB71FC /* Header.swift */; }; + 8FA8B0601D90A4A3008E5728 /* ErrorFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA8B05F1D90A4A3008E5728 /* ErrorFormatter.swift */; }; + 8FA8B0611D90A4A3008E5728 /* ErrorFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA8B05F1D90A4A3008E5728 /* ErrorFormatter.swift */; }; + 8FA8B0621D90A4A3008E5728 /* ErrorFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA8B05F1D90A4A3008E5728 /* ErrorFormatter.swift */; }; + 8FA8B0631D90A4A3008E5728 /* ErrorFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA8B05F1D90A4A3008E5728 /* ErrorFormatter.swift */; }; 8FB48ECA1D306C4700BC50A1 /* KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB48EC91D306C4700BC50A1 /* KeyPath.swift */; }; 8FB48ECB1D306C4700BC50A1 /* KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB48EC91D306C4700BC50A1 /* KeyPath.swift */; }; 8FB48ECC1D306C4700BC50A1 /* KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB48EC91D306C4700BC50A1 /* KeyPath.swift */; }; @@ -139,6 +143,7 @@ 8F87BCBA1B580CE200E53A8C /* ErrorPathTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPathTests.swift; sourceTree = ""; }; 8F87BCC31B592F0E00E53A8C /* DecodingError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecodingError.swift; sourceTree = ""; }; 8F956D1E1B4D6FF700243072 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; + 8FA8B05F1D90A4A3008E5728 /* ErrorFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorFormatter.swift; sourceTree = ""; }; 8FB48EC91D306C4700BC50A1 /* KeyPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPath.swift; sourceTree = ""; }; 8FD3D92E1C270A2D00D1AF4E /* MissingKeyOperatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MissingKeyOperatorTests.swift; sourceTree = ""; }; 8FE7B5621B4C9FB900837609 /* Decodable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Decodable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -261,6 +266,7 @@ 8F72DC551C3CB8C500A39E10 /* NSValueCastable.swift */, 8F3E45A31D327E4500FB71FC /* Decoders.swift */, 8FB48EC91D306C4700BC50A1 /* KeyPath.swift */, + 8FA8B05F1D90A4A3008E5728 /* ErrorFormatter.swift */, 8F3E45991D31362B00FB71FC /* OptionalKeyPath.swift */, 8F956D1E1B4D6FF700243072 /* Operators.swift */, 8F00623E1C81EF61007BCF48 /* Overloads.swift */, @@ -624,6 +630,7 @@ buildActionMask = 2147483647; files = ( 8F87BCC51B592F0E00E53A8C /* DecodingError.swift in Sources */, + 8FA8B0611D90A4A3008E5728 /* ErrorFormatter.swift in Sources */, 8FFAB8131B7CFA9500E2D724 /* Parse.swift in Sources */, 8F012EF61BB5A920007D0B5C /* Castable.swift in Sources */, 8FB48ECB1D306C4700BC50A1 /* KeyPath.swift in Sources */, @@ -664,6 +671,7 @@ buildActionMask = 2147483647; files = ( 57FCDE5B1BA283C900130C48 /* DecodingError.swift in Sources */, + 8FA8B0631D90A4A3008E5728 /* ErrorFormatter.swift in Sources */, 57FCDE5C1BA283C900130C48 /* Parse.swift in Sources */, 8F012EF81BB5A928007D0B5C /* Castable.swift in Sources */, 8FB48ECD1D306C4700BC50A1 /* KeyPath.swift in Sources */, @@ -688,6 +696,7 @@ 8FB48ECA1D306C4700BC50A1 /* KeyPath.swift in Sources */, 8FE7B57E1B4CA01400837609 /* Decodable.swift in Sources */, 8F72DC561C3CB8C500A39E10 /* NSValueCastable.swift in Sources */, + 8FA8B0601D90A4A3008E5728 /* ErrorFormatter.swift in Sources */, 8F3E459A1D31362B00FB71FC /* OptionalKeyPath.swift in Sources */, 8F00623F1C81EF61007BCF48 /* Overloads.swift in Sources */, 8F3E45B81D32884700FB71FC /* Documentation.swift in Sources */, @@ -724,6 +733,7 @@ buildActionMask = 2147483647; files = ( D0DC547A1B78150900F79CB0 /* DecodingError.swift in Sources */, + 8FA8B0621D90A4A3008E5728 /* ErrorFormatter.swift in Sources */, 8FFAB8141B7CFA9500E2D724 /* Parse.swift in Sources */, 8F012EF71BB5A920007D0B5C /* Castable.swift in Sources */, 8FB48ECC1D306C4700BC50A1 /* KeyPath.swift in Sources */, diff --git a/Sources/ErrorFormatter.swift b/Sources/ErrorFormatter.swift new file mode 100644 index 0000000..0e0911d --- /dev/null +++ b/Sources/ErrorFormatter.swift @@ -0,0 +1,165 @@ +// +// ErrorFormatter.swift +// Decodable +// +// Created by Johannes Lund on 2016-09-20. +// Copyright © 2016 anviking. All rights reserved. +// + +import Foundation + +extension String { + var indented: String { + return " " + self.replacingOccurrences(of: "\n", with: "\n ") + } + + func replaceNewline(with string: String) -> String { + return replacingOccurrences(of: "\n", with: string) + } +} + +enum DebugKey { + + /// Dictionary/Object key + case key(String) + + /// Array index + case index(Int) + + var key: String? { + switch self { + case .key(let key): + return key + default: + return nil + } + } + + var index: Int? { + switch self { + case .index(let index): + return index + default: + return nil + } + } +} + +extension DebugKey: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .key(value) + } + + public init(extendedGraphemeClusterLiteral value: String) { + self = .key(value) + } + + public init(unicodeScalarLiteral value: String) { + self = .key(value) + } +} + +extension DebugKey: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .index(value) + } + +} + +/// Sorry. /Past me +struct JSONErrorFormatter { + var path: [DebugKey] + let message: String + + mutating func format(json: Any) -> String { + var result = "" + switch json { + case let json as NSDictionary: + + for (key, value) in json { + result.append("\n") + + // Check if key matches first item in `path`. + var highlightMatch = false + var isFinalKey = false + if let a = (key as? String), let b = path.first?.key, a == b { + highlightMatch = true + path.removeFirst() + } + + if path.count == 0 && highlightMatch { + isFinalKey = true + } + + var formattedValue = format(json: value) + let formattedKey = "\(key)" + + switch (highlightMatch, formattedValue.contains("\n")) { + case (true, true): + formattedValue = formattedValue.replaceNewline(with: "\n | ") + case (false, true): + formattedValue = formattedValue.replaceNewline(with: "\n ") + default: + break + } + + if isFinalKey { + var lines = formattedValue.components(separatedBy: "\n") + if lines.count > 0 { + lines[0] = lines[0] + " <<<<---- \(message)" + } + formattedValue = lines.joined(separator: "\n") + } + + result.append("\(formattedKey): \(formattedValue)") + + + } + + case let json as [Any]: + + var items: [String] = [] + result.append("\n") + for (index, value) in json.enumerated() { + + print(index) + // Check if key matches first item in `path`. + var highlightMatch = false + var isFinalKey = false + if let i = path.first?.index, index == i { + highlightMatch = true + path.removeFirst() + } + + if path.count == 0 && highlightMatch { + isFinalKey = true + } + var formattedValue = format(json: value) + + + + if isFinalKey { + var lines = formattedValue.components(separatedBy: "\n") + if lines.count > 0 { + lines[0] = lines[0] + " <<<<---- \(message)" + } + formattedValue = lines.joined(separator: "\n") + } + + formattedValue = formattedValue.indented + if highlightMatch { + formattedValue = formattedValue.replacingOccurrences(of: "\n", with: "\n |") + } + + items.append("[\(index)] \(formattedValue)") + } + result.append(items.joined(separator: "\n")) + case let json as String: + return "\"\(json)\"" + default: + result += "\(json)" + } + + return result + } +} From f5273244cec1000e589e179a774cb9ad2e26cfd1 Mon Sep 17 00:00:00 2001 From: Johannes Lund Date: Tue, 20 Sep 2016 01:15:44 +0200 Subject: [PATCH 2/2] Use new JSONErrorFormatter for DecodingError.description --- Sources/DecodingError.swift | 24 ++++++++++++++++++++++++ Sources/ErrorFormatter.swift | 20 +++++++++++++------- Tests/ErrorPathTests.swift | 11 +++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Sources/DecodingError.swift b/Sources/DecodingError.swift index 044e200..323379b 100644 --- a/Sources/DecodingError.swift +++ b/Sources/DecodingError.swift @@ -99,6 +99,30 @@ public enum DecodingError: Error, Equatable { } +extension DecodingError: CustomStringConvertible { + + private var errorMessage: String { + switch self { + case let .typeMismatch(expected, actual, _): + return "TypeMismatch: expected \(expected), not \(actual)" + case let .missingKey(key, _): + return "MissingKey: \(key)" + case .rawRepresentableInitializationError(_, _): + return "RawRepresentableInitializationError: could not be used to initialize \("TYPE").)" // FIXME + case let .other(error, _): + return "\(error)" + } + } + + public var description: String { + var formatter = JSONErrorFormatter(path: metadata.path, message: errorMessage) + guard let rootObject = metadata.rootObject else { + return debugDescription // FIXME: Don't do this + } + return formatter.format(json: rootObject) + } +} + // Allow types to be used in pattern matching // E.g case typeMismatchError(NSNull.self, _, _) but be careful diff --git a/Sources/ErrorFormatter.swift b/Sources/ErrorFormatter.swift index 0e0911d..35fc5ae 100644 --- a/Sources/ErrorFormatter.swift +++ b/Sources/ErrorFormatter.swift @@ -18,6 +18,7 @@ extension String { } } +/* TODO: Propagate errors through arrays enum DebugKey { /// Dictionary/Object key @@ -65,10 +66,10 @@ extension DebugKey: ExpressibleByIntegerLiteral { } } - +*/ /// Sorry. /Past me struct JSONErrorFormatter { - var path: [DebugKey] + var path: [String] let message: String mutating func format(json: Any) -> String { @@ -82,7 +83,8 @@ struct JSONErrorFormatter { // Check if key matches first item in `path`. var highlightMatch = false var isFinalKey = false - if let a = (key as? String), let b = path.first?.key, a == b { + //if let a = (key as? String), let b = path.first?.key, a == b { + if let a = (key as? String), let b = path.first, a == b { highlightMatch = true path.removeFirst() } @@ -124,18 +126,21 @@ struct JSONErrorFormatter { print(index) // Check if key matches first item in `path`. + /* var highlightMatch = false var isFinalKey = false + if let i = path.first?.index, index == i { highlightMatch = true path.removeFirst() } - + if path.count == 0 && highlightMatch { isFinalKey = true } + */ var formattedValue = format(json: value) - + /* if isFinalKey { @@ -145,12 +150,13 @@ struct JSONErrorFormatter { } formattedValue = lines.joined(separator: "\n") } - + */ formattedValue = formattedValue.indented + /* if highlightMatch { formattedValue = formattedValue.replacingOccurrences(of: "\n", with: "\n |") } - + */ items.append("[\(index)] \(formattedValue)") } result.append(items.joined(separator: "\n")) diff --git a/Tests/ErrorPathTests.swift b/Tests/ErrorPathTests.swift index 499e1af..5364dcf 100644 --- a/Tests/ErrorPathTests.swift +++ b/Tests/ErrorPathTests.swift @@ -49,6 +49,17 @@ class ErrorPathTests: XCTestCase { } } + func testErrorDescription() { + // Actually there is no test here... + let dict: NSDictionary = ["object": ["repo": ["owner": ["id" : 1, "login": "anviking"]]]] + + do { + _ = try dict => "object" => "repo" => "owner" => "oops" as String + } catch { + print(error) + } + } + // FIXME: # func testNestedUnexpectedNSNull() { let dict: NSDictionary = ["id": 1, "color": ["name": NSNull()]]