Skip to content
This repository has been archived by the owner on Jun 18, 2019. It is now read-only.

Implement better path and type reporting in error messages #219

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Sources/Array+Unbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@ extension Array: UnboxableCollection {
}

return try array.enumerated().map(allowInvalidElements: allowInvalidElements) { index, element in
let unboxedElement = try transformer.unbox(element: element, allowInvalidCollectionElements: allowInvalidElements)
return try unboxedElement.orThrow(UnboxPathError.invalidArrayElement(element, index))
let unboxedElement: Element?
do {
unboxedElement = try transformer.unbox(element: element, allowInvalidCollectionElements: allowInvalidElements)
} catch let error as UnboxPathError {
throw UnboxError.pathError(error, "\(index)")
} catch UnboxError.pathError(let pathError, let path) {
throw UnboxError.pathError(pathError, "\(index).\(path)")
}
return try unboxedElement.orThrow(UnboxPathError.invalidArrayElement(element, index, T.UnboxedElement.self))
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/CGFloat+Unbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* Licensed under the MIT license, see LICENSE file
*/

import Foundation

#if !os(Linux)
import CoreGraphics
import CoreGraphics
#else
import Foundation
#endif

/// Extension making `CGFloat` an Unboxable raw type
extension CGFloat: UnboxableByTransform {
Expand All @@ -17,4 +18,3 @@ extension CGFloat: UnboxableByTransform {
return CGFloat(unboxedValue)
}
}
#endif
13 changes: 11 additions & 2 deletions Sources/Dictionary+Unbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,17 @@ extension Dictionary: UnboxableCollection {
throw UnboxPathError.invalidDictionaryKey(key)
}

guard let unboxedValue = try transformer.unbox(element: value, allowInvalidCollectionElements: allowInvalidElements) else {
throw UnboxPathError.invalidDictionaryValue(value, key)
let transformedValue: Value?
do {
transformedValue = try transformer.unbox(element: value, allowInvalidCollectionElements: allowInvalidElements)
} catch let error as UnboxPathError {
throw UnboxError.pathError(error, "\(key)")
} catch UnboxError.pathError(let pathError, let path) {
throw UnboxError.pathError(pathError, "\(key).\(path)")
}

guard let unboxedValue = transformedValue else {
throw UnboxPathError.invalidDictionaryValue(value, key, T.UnboxedElement.self)
}

return (unboxedKey, unboxedValue)
Expand Down
22 changes: 11 additions & 11 deletions Sources/UnboxPathError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ public enum UnboxPathError: Error {
case emptyKeyPath
/// A required key was missing (contains the key)
case missingKey(String)
/// An invalid value was found (contains the value, and its key)
case invalidValue(Any, String)
/// An invalid value was found (contains the value, its key, and the expected type)
case invalidValue(Any, String, Any.Type)
/// An invalid collection element type was found (contains the type)
case invalidCollectionElementType(Any)
/// An invalid array element was found (contains the element, and its index)
case invalidArrayElement(Any, Int)
case invalidArrayElement(Any, Int, Any.Type)
/// An invalid dictionary key type was found (contains the type)
case invalidDictionaryKeyType(Any)
/// An invalid dictionary key was found (contains the key)
case invalidDictionaryKey(Any)
/// An invalid dictionary value was found (contains the value, and its key)
case invalidDictionaryValue(Any, String)
/// An invalid dictionary value was found (contains the value, its key, and the expected type)
case invalidDictionaryValue(Any, String, Any.Type)
}

extension UnboxPathError: CustomStringConvertible {
Expand All @@ -33,18 +33,18 @@ extension UnboxPathError: CustomStringConvertible {
return "Key path can't be empty."
case .missingKey(let key):
return "The key \"\(key)\" is missing."
case .invalidValue(let value, let key):
return "Invalid value (\(value)) for key \"\(key)\"."
case .invalidValue(let value, let key, let expectedType):
return "Invalid value (\(value)) for key \"\(key)\", JSON type \(type(of: value)) cannot be unboxed as \(expectedType)"
case .invalidCollectionElementType(let type):
return "Invalid collection element type: \(type). Must be UnboxCompatible or Unboxable."
case .invalidArrayElement(let element, let index):
return "Invalid array element (\(element)) at index \(index)."
case .invalidArrayElement(let element, let index, let expectedType):
return "Invalid array element (\(element)) at index \(index), JSON type \(type(of: element)) cannot be unboxed as \(expectedType)"
case .invalidDictionaryKeyType(let type):
return "Invalid dictionary key type: \(type). Must be either String or UnboxableKey."
case .invalidDictionaryKey(let key):
return "Invalid dictionary key: \(key)."
case .invalidDictionaryValue(let value, let key):
return "Invalid dictionary value (\(value)) for key \"\(key)\"."
case .invalidDictionaryValue(let value, let key, let expectedType):
return "Invalid dictionary value (\(value)) for key \"\(key)\", JSON type \(type(of: value)) cannot be unboxed as \(expectedType)"
}
}
}
28 changes: 18 additions & 10 deletions Sources/Unboxer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private extension Unboxer {
switch path {
case .key(let key):
let value = try self.dictionary[key].orThrow(UnboxPathError.missingKey(key))
return try transform(value).orThrow(UnboxPathError.invalidValue(value, key))
return try transform(value).orThrow(UnboxPathError.invalidValue(value, key, R.self))
case .keyPath(let keyPath):
var node: UnboxPathNode = self.dictionary
let components = keyPath.components(separatedBy: ".")
Expand All @@ -231,26 +231,34 @@ private extension Unboxer {
}

if index == components.index(before: components.endIndex) {
return try transform(nextValue).orThrow(UnboxPathError.invalidValue(nextValue, key))
return try transform(nextValue).orThrow(UnboxPathError.invalidValue(nextValue, key, R.self))
}

guard let nextNode = nextValue as? UnboxPathNode else {
throw UnboxPathError.invalidValue(nextValue, key)
throw UnboxPathError.invalidValue(nextValue, key, R.self)
}

node = nextNode
}

throw UnboxPathError.emptyKeyPath
}
} catch {
if let publicError = error as? UnboxError {
throw publicError
} else if let pathError = error as? UnboxPathError {
throw UnboxError.pathError(pathError, path.description)
} catch UnboxError.pathError(let pathError, let partialPath) {
switch pathError {
case .emptyKeyPath,
.invalidCollectionElementType,
.invalidDictionaryKey:
throw UnboxError.pathError(pathError, partialPath)
case .missingKey,
.invalidValue,
.invalidDictionaryKeyType,
.invalidDictionaryValue:
throw UnboxError.pathError(pathError, "\(path).\(partialPath)")
case let .invalidArrayElement(_, index, _):
throw UnboxError.pathError(pathError, "\(path).\(index).\(partialPath)")
}

throw error
} catch let pathError as UnboxPathError {
throw UnboxError.pathError(pathError, path.description)
}
}

Expand Down
188 changes: 186 additions & 2 deletions Tests/UnboxTests/UnboxTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,7 @@ class UnboxTests: XCTestCase {
_ = try unbox(dictionary: ["string" : []]) as Model
XCTFail("Unbox should have thrown for an invalid value")
} catch {
XCTAssertEqual("\(error)", "[UnboxError] An error occurred while unboxing path \"string\": Invalid value ([]) for key \"string\".")
XCTAssertEqual("\(error)", "[UnboxError] An error occurred while unboxing path \"string\": Invalid value ([]) for key \"string\", JSON type Array<Any> cannot be unboxed as String")
}
}

Expand Down Expand Up @@ -1635,7 +1635,7 @@ class UnboxTests: XCTestCase {
_ = try unbox(dictionary: dictionary) as Model
XCTFail("Should have thrown")
} catch {
XCTAssertEqual("\(error)", "[UnboxError] An error occurred while unboxing path \"array\": Invalid array element ([:]) at index 0.")
XCTAssertEqual("\(error)", "[UnboxError] An error occurred while unboxing path \"array\": Invalid array element ([:]) at index 0, JSON type Dictionary<AnyHashable, Any> cannot be unboxed as String")
}
}

Expand Down Expand Up @@ -1769,6 +1769,190 @@ class UnboxTests: XCTestCase {
XCTFail("\(error)")
}
}

func testErrorMessageForNestedUnboxFailureWithMisspelledProperty() {
struct Inner: Unboxable {
var property: String
init(unboxer: Unboxer) throws {
self.property = try unboxer.unbox(key: "property")
}
}

struct Outer: Unboxable {
var inner: Inner
init(unboxer: Unboxer) throws {
self.inner = try unboxer.unbox(key: "inner")
}
}

let dictionary: UnboxableDictionary = [
"inner": [
"proper": "foo"
]
]

do {
_ = try unbox(dictionary: dictionary) as Outer
XCTFail("Unexpected unboxing success")
} catch let UnboxError.pathError(_, path) {
XCTAssertEqual(path, "inner.property")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testErrorMessageForNestedUnboxFailureWithMistypedProperty() {
struct Inner: Unboxable {
var property: String
init(unboxer: Unboxer) throws {
self.property = try unboxer.unbox(key: "property")
}
}

struct Outer: Unboxable {
var inner: Inner
init(unboxer: Unboxer) throws {
self.inner = try unboxer.unbox(key: "inner")
}
}

let dictionary: UnboxableDictionary = [
"inner": ["property": [123]]
]

do {
_ = try unbox(dictionary: dictionary) as Outer
XCTFail("Unexpected unboxing success")
} catch let UnboxError.pathError(_, path) {
XCTAssertEqual(path, "inner.property")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testErrorMessageForNestedUnboxFailureWithArraysAndMisspelledProperty() {
struct Inner: Unboxable {
var property: String
init(unboxer: Unboxer) throws {
self.property = try unboxer.unbox(key: "property")
}
}

struct Outer: Unboxable {
var inner: [Inner]
init(unboxer: Unboxer) throws {
self.inner = try unboxer.unbox(key: "inner")
}
}

let dictionary: UnboxableDictionary = [
"inner": [
["property": "foo"],
["proper": "bar"]
]
]

do {
_ = try unbox(dictionary: dictionary) as Outer
XCTFail("Unexpected unboxing success")
} catch let UnboxError.pathError(_, path) {
XCTAssertEqual(path, "inner.1.property")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testErrorMessageForNestedUnboxFailureWithArraysAndMisTypedProperty() {
struct Inner: Unboxable {
var property: String
init(unboxer: Unboxer) throws {
self.property = try unboxer.unbox(key: "property")
}
}

struct Outer: Unboxable {
var inner: [Inner]
init(unboxer: Unboxer) throws {
self.inner = try unboxer.unbox(key: "inner")
}
}

let dictionary: UnboxableDictionary = [
"inner": [
["property": "foo"],
["property": [123]]
]
]

do {
_ = try unbox(dictionary: dictionary) as Outer
XCTFail("Unexpected unboxing success")
} catch let UnboxError.pathError(_, path) {
XCTAssertEqual(path, "inner.1.property")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testErrorMessageForInvalidValueHasTypeInformationPresent() {
struct Thing: Unboxable {
var id: Int
init(unboxer: Unboxer) throws {
self.id = try unboxer.unbox(key: "id")
}
}

let dictionary: UnboxableDictionary = ["id": "abc"]

do {
_ = try unbox(dictionary: dictionary) as Thing
XCTFail("Unexpected unboxing success")
} catch UnboxError.pathError(let error, _) {
XCTAssert(error.description.hasSuffix("JSON type String cannot be unboxed as Int"))
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testErrorMessageForInvalidValueHasTypeInformationPresentForDictionaryValues() {
struct Thing: Unboxable {
var id: [String: Int]
init(unboxer: Unboxer) throws {
self.id = try unboxer.unbox(key: "id")
}
}

let dictionary: UnboxableDictionary = ["id": ["abc": "def"]]

do {
_ = try unbox(dictionary: dictionary) as Thing
XCTFail("Unexpected unboxing success")
} catch UnboxError.pathError(let error, _) {
XCTAssert(error.description.hasSuffix("JSON type String cannot be unboxed as Int"), "Error message doesn't have the right suffix: \(error)")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testErrorMessageForInvalidValueHasTypeInformationPresentForArrayValues() {
struct Thing: Unboxable {
var id: [Int]
init(unboxer: Unboxer) throws {
self.id = try unboxer.unbox(key: "id")
}
}

let dictionary: UnboxableDictionary = ["id": ["def"]]

do {
_ = try unbox(dictionary: dictionary) as Thing
XCTFail("Unexpected unboxing success")
} catch UnboxError.pathError(let error, _) {
XCTAssert(error.description.hasSuffix("JSON type String cannot be unboxed as Int"), "Error message doesn't have the right suffix: \(error)")
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}

private func UnboxTestDictionaryWithAllRequiredKeysWithValidValues(nested: Bool) -> UnboxableDictionary {
Expand Down