From 34253db95b85b277ff430a8e7d81ce8333414635 Mon Sep 17 00:00:00 2001 From: Syo Ikeda Date: Wed, 16 Mar 2016 14:00:04 +0900 Subject: [PATCH] Add some protocol extensions for Decodable --- Sources/Decodable.swift | 15 ++++++++ Sources/Extractor.swift | 6 +-- Sources/StandardLib.swift | 40 ++++++++++++++++++++ Sources/decode.swift | 25 +++--------- Tests/Himotoki/DecodableTest.swift | 18 ++++----- Tests/Himotoki/DecodeErrorTest.swift | 10 ++--- Tests/Himotoki/NestedObjectParsingTest.swift | 4 +- Tests/Himotoki/RawRepresentableTest.swift | 2 +- Tests/Himotoki/TransformerTest.swift | 4 +- 9 files changed, 83 insertions(+), 41 deletions(-) diff --git a/Sources/Decodable.swift b/Sources/Decodable.swift index b2a00ed..0212628 100644 --- a/Sources/Decodable.swift +++ b/Sources/Decodable.swift @@ -10,3 +10,18 @@ public protocol Decodable { /// - Throws: DecodeError static func decode(e: Extractor) throws -> Self } + +// MARK: - Extensions + +extension Decodable { + /// - Throws: DecodeError + public static func decodeValue(JSON: AnyJSON) throws -> Self { + let extractor = Extractor(JSON) + return try self.decode(extractor) + } + + /// - Throws: DecodeError + public static func decodeValue(JSON: AnyJSON, rootKeyPath: KeyPath) throws -> Self { + return try Extractor(JSON).value(rootKeyPath) + } +} diff --git a/Sources/Extractor.swift b/Sources/Extractor.swift index bcc43a5..75a25aa 100644 --- a/Sources/Extractor.swift +++ b/Sources/Extractor.swift @@ -45,7 +45,7 @@ public struct Extractor { } do { - return try decodeValue(rawValue) + return try T.decodeValue(rawValue) } catch let DecodeError.MissingKeyPath(missing) { throw DecodeError.MissingKeyPath(keyPath + missing) } catch let DecodeError.TypeMismatch(expected, actual, _) { @@ -73,7 +73,7 @@ public struct Extractor { /// - Throws: DecodeError public func arrayOptional(keyPath: KeyPath) throws -> [T]? { - return try _rawValue(keyPath).map(decodeArray) + return try _rawValue(keyPath).map([T].decode) } /// - Throws: DecodeError @@ -87,7 +87,7 @@ public struct Extractor { /// - Throws: DecodeError public func dictionaryOptional(keyPath: KeyPath) throws -> [String: T]? { - return try _rawValue(keyPath).map(decodeDictionary) + return try _rawValue(keyPath).map([String: T].decode) } } diff --git a/Sources/StandardLib.swift b/Sources/StandardLib.swift index 0091eeb..b8529e6 100644 --- a/Sources/StandardLib.swift +++ b/Sources/StandardLib.swift @@ -42,6 +42,46 @@ extension Bool: Decodable { } } +// MARK: - Extensions + +extension CollectionType where Generator.Element: Decodable { + /// - Throws: DecodeError + public static func decode(JSON: AnyJSON) throws -> [Generator.Element] { + guard let array = JSON as? [AnyJSON] else { + throw typeMismatch("Array", actual: JSON, keyPath: nil) + } + + return try array.map(Generator.Element.decodeValue) + } + + /// - Throws: DecodeError + public static func decode(JSON: AnyJSON, rootKeyPath: KeyPath) throws -> [Generator.Element] { + return try Extractor(JSON).array(rootKeyPath) + } +} + +extension DictionaryLiteralConvertible where Value: Decodable { + /// - Throws: DecodeError + public static func decode(JSON: AnyJSON) throws -> [String: Value] { + guard let dictionary = JSON as? [String: AnyJSON] else { + throw typeMismatch("Dictionary", actual: JSON, keyPath: nil) + } + + var result = [String: Value](minimumCapacity: dictionary.count) + try dictionary.forEach { key, value in + result[key] = try Value.decodeValue(value) + } + return result + } + + /// - Throws: DecodeError + public static func decode(JSON: AnyJSON, rootKeyPath: KeyPath) throws -> [String: Value] { + return try Extractor(JSON).dictionary(rootKeyPath) + } +} + +// MARK: Helpers + internal func castOrFail(e: Extractor) throws -> T { return try castOrFail(e.rawValue) } diff --git a/Sources/decode.swift b/Sources/decode.swift index e8f412f..969d289 100644 --- a/Sources/decode.swift +++ b/Sources/decode.swift @@ -8,45 +8,32 @@ /// - Throws: DecodeError public func decodeValue(JSON: AnyJSON) throws -> T { - let extractor = Extractor(JSON) - return try T.decode(extractor) + return try T.decodeValue(JSON) } /// - Throws: DecodeError public func decodeValue(JSON: AnyJSON, rootKeyPath: KeyPath) throws -> T { - return try decodeValue(JSON) <| rootKeyPath + return try T.decodeValue(JSON, rootKeyPath: rootKeyPath) } /// - Throws: DecodeError public func decodeArray(JSON: AnyJSON) throws -> [T] { - guard let array = JSON as? [AnyJSON] else { - throw typeMismatch("Array", actual: JSON, keyPath: nil) - } - - return try array.map(decodeValue) + return try [T].decode(JSON) } /// - Throws: DecodeError public func decodeArray(JSON: AnyJSON, rootKeyPath: KeyPath) throws -> [T] { - return try decodeValue(JSON) <|| rootKeyPath + return try [T].decode(JSON, rootKeyPath: rootKeyPath) } /// - Throws: DecodeError public func decodeDictionary(JSON: AnyJSON) throws -> [String: T] { - guard let dictionary = JSON as? [String: AnyJSON] else { - throw typeMismatch("Dictionary", actual: JSON, keyPath: nil) - } - - var result = [String: T](minimumCapacity: dictionary.count) - try dictionary.forEach { key, value in - result[key] = try decodeValue(value) as T - } - return result + return try [String: T].decode(JSON) } /// - Throws: DecodeError public func decodeDictionary(JSON: AnyJSON, rootKeyPath: KeyPath) throws -> [String: T] { - return try decodeValue(JSON) <|-| rootKeyPath + return try [String: T].decode(JSON, rootKeyPath: rootKeyPath) } // MARK: - Deprecated diff --git a/Tests/Himotoki/DecodableTest.swift b/Tests/Himotoki/DecodableTest.swift index 536dd0f..ff6b289 100644 --- a/Tests/Himotoki/DecodableTest.swift +++ b/Tests/Himotoki/DecodableTest.swift @@ -47,7 +47,7 @@ class DecodableTest: XCTestCase { var JSON = personJSON // Succeeding case - let person: Person? = try? decodeValue(JSON) + let person = try? Person.decodeValue(JSON) XCTAssert(person != nil) XCTAssert(person?.firstName == "ABC") XCTAssert(person?.lastName == "DEF") @@ -78,7 +78,7 @@ class DecodableTest: XCTestCase { do { JSON["bool"] = nil JSON["group"] = nil - try decodeValue(JSON) as Person + try Person.decodeValue(JSON) } catch let DecodeError.MissingKeyPath(keyPath) { XCTAssert(keyPath == "bool") } catch { @@ -87,7 +87,7 @@ class DecodableTest: XCTestCase { do { JSON["age"] = "foo_bar" - try decodeValue(JSON) as Person + try Person.decodeValue(JSON) } catch let DecodeError.TypeMismatch(expected, actual, keyPath) { XCTAssertEqual(keyPath, "age") XCTAssertTrue(actual.containsString("foo_bar")) @@ -102,7 +102,7 @@ class DecodableTest: XCTestCase { let peopleJSON = Array(count: 500, repeatedValue: personJSON) measureBlock { - let _: [Person]? = try? decodeArray(peopleJSON) + _ = try? [Person].decode(peopleJSON) } } #endif @@ -110,7 +110,7 @@ class DecodableTest: XCTestCase { func testGroup() { var JSON: JSONDictionary = [ "name": "Himotoki", "floor": 12 ] - let g: Group? = try? decodeValue(JSON) + let g = try? Group.decodeValue(JSON) XCTAssert(g != nil) XCTAssert(g?.name == "Himotoki") XCTAssert(g?.floor == 12) @@ -118,7 +118,7 @@ class DecodableTest: XCTestCase { JSON["name"] = nil do { - try decodeValue(JSON) as Group + try Group.decodeValue(JSON) } catch let DecodeError.MissingKeyPath(keyPath) { XCTAssert(keyPath == "name") } catch { @@ -130,7 +130,7 @@ class DecodableTest: XCTestCase { let JSON: JSONDictionary = [ "name": "Himotoki", "floor": 12 ] let array: JSONArray = [ JSON, JSON ] - let values: [Group]? = try? decodeArray(array) + let values = try? [Group].decode(array) XCTAssert(values != nil) XCTAssert(values?.count == 2) } @@ -139,7 +139,7 @@ class DecodableTest: XCTestCase { let JSON: JSONDictionary = [ "name": "Himotoki", "floor": 12 ] let dictionary: JSONDictionary = [ "1": JSON, "2": JSON ] - let values: [String: Group]? = try? decodeDictionary(dictionary) + let values = try? [String: Group].decode(dictionary) XCTAssert(values != nil) XCTAssert(values?.count == 2) } @@ -158,7 +158,7 @@ class DecodableTest: XCTestCase { "uint64": NSNumber(unsignedLongLong: UInt64.max), ] - let numbers: Numbers? = try? decodeValue(JSON) + let numbers = try? Numbers.decodeValue(JSON) XCTAssert(numbers != nil) XCTAssert(numbers?.int == Int.min) XCTAssert(numbers?.uint == UInt.max) diff --git a/Tests/Himotoki/DecodeErrorTest.swift b/Tests/Himotoki/DecodeErrorTest.swift index 6e6844c..51da098 100644 --- a/Tests/Himotoki/DecodeErrorTest.swift +++ b/Tests/Himotoki/DecodeErrorTest.swift @@ -55,7 +55,7 @@ class DecodeErrorTest: XCTestCase { func testSuccessOfCastOrFail() { do { let d: [String: AnyJSON] = [ "url": "https://swift.org/" ] - _ = try decodeValue(d) as URLHolder + _ = try URLHolder.decodeValue(d) } catch { XCTFail("error: \(error)") } @@ -64,7 +64,7 @@ class DecodeErrorTest: XCTestCase { func testMissingKeyPathInDecodeError() { do { let d: [String: AnyJSON] = [ "url": "" ] - let _: URLHolder = try decodeValue(d) + _ = try URLHolder.decodeValue(d) } catch let DecodeError.MissingKeyPath(keyPath) { XCTAssertEqual(keyPath, "url") } catch { @@ -74,12 +74,12 @@ class DecodeErrorTest: XCTestCase { func testMissingKeyPathAndDecodeFailure() { let d: [String: AnyJSON] = [:] - let a: A = try! decodeValue(d) + let a = try! A.decodeValue(d) XCTAssertNil(a.b) do { let d: [String: AnyJSON] = [ "b": [:] as JSONDictionary ] - let _: A = try decodeValue(d) + _ = try A.decodeValue(d) XCTFail("DecodeError.MissingKeyPath should be thrown if decoding optional value failed") } catch let DecodeError.MissingKeyPath(keyPath) { XCTAssertEqual(keyPath, [ "b", "string" ]) @@ -91,7 +91,7 @@ class DecodeErrorTest: XCTestCase { func testCustomError() { do { let d: [String: AnyJSON] = [ "url": "file:///Users/foo/bar" ] - let _: URLHolder = try decodeValue(d) + _ = try URLHolder.decodeValue(d) } catch let DecodeError.Custom(message) { XCTAssertEqual(message, "File URL is not supported") } catch { diff --git a/Tests/Himotoki/NestedObjectParsingTest.swift b/Tests/Himotoki/NestedObjectParsingTest.swift index 586cc1b..7034c0c 100644 --- a/Tests/Himotoki/NestedObjectParsingTest.swift +++ b/Tests/Himotoki/NestedObjectParsingTest.swift @@ -13,14 +13,14 @@ class NestedObjectParsingTest: XCTestCase { func testParseNestedObjectSuccess() { let JSON: JSONDictionary = [ "nested": [ "name": "Foo Bar" ] as JSONDictionary ] - let success: WithNestedObject? = try? decodeValue(JSON) + let success = try? WithNestedObject.decodeValue(JSON) XCTAssertNotNil(success) XCTAssertEqual(success?.nestedName, "Foo Bar") } func testParseNestedObjectFailure() { let JSON: JSONDictionary = [ "nested": "Foo Bar" ] - let failure: WithNestedObject? = try? decodeValue(JSON) + let failure = try? WithNestedObject.decodeValue(JSON) XCTAssertNil(failure) } } diff --git a/Tests/Himotoki/RawRepresentableTest.swift b/Tests/Himotoki/RawRepresentableTest.swift index d3fa699..57c2b51 100644 --- a/Tests/Himotoki/RawRepresentableTest.swift +++ b/Tests/Himotoki/RawRepresentableTest.swift @@ -38,7 +38,7 @@ class RawRepresentableTest: XCTestCase { "double_2": 4.0, ] - let e: Extractor = try! decodeValue(JSON) + let e = try! Extractor.decodeValue(JSON) let A: StringEnum? = try? e <| "string_1" let D: StringEnum? = try? e <| "string_2" diff --git a/Tests/Himotoki/TransformerTest.swift b/Tests/Himotoki/TransformerTest.swift index d0a76ec..e2846ab 100644 --- a/Tests/Himotoki/TransformerTest.swift +++ b/Tests/Himotoki/TransformerTest.swift @@ -54,7 +54,7 @@ class TransformerTest: XCTestCase { "dictionaryOptional": [ "a": URLString, "b": URLString ] as JSONDictionary, ] - guard let decoded: URLsByTransformer = try? decodeValue(JSON) else { + guard let decoded = try? URLsByTransformer.decodeValue(JSON) else { XCTFail() return } @@ -74,7 +74,7 @@ class TransformerTest: XCTestCase { ] do { - _ = try decodeValue(JSON) as URLsByTransformer + _ = try URLsByTransformer.decodeValue(JSON) } catch let DecodeError.Custom(message) { XCTAssertEqual(message, "Invalid URL string: \(URLString)") } catch {