From 27543c2b7f5ae77263ad0a204af33de3fbe12180 Mon Sep 17 00:00:00 2001 From: Caleb Davenport Date: Wed, 18 Nov 2015 17:17:25 -0800 Subject: [PATCH] Add new `DecoderType` protocol. #8 `DecoderType` is a new protocol that better encapsulates the idea of decoding an object from a given `JSON`. It allows for two abilities that `JSONDecodable` did not: 1) Multiple decoders for the same type can be defined. This is illustrated by the two `NSDate` decoders provided by Alexander. 2) The decoder type is separate from the type being decoded. `JSONDecodable` is now deprecated. --- Alexander.xcodeproj/project.pbxproj | 18 ++++++++ Alexander/DecoderType.swift | 50 +++++++++++++++++++++ Alexander/JSON.swift | 12 ++--- Alexander/JSONCodable.swift | 13 +----- Tests/AlexanderTests.swift | 70 ++--------------------------- Tests/UserDecoderTests.swift | 66 +++++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 85 deletions(-) create mode 100644 Alexander/DecoderType.swift create mode 100644 Tests/UserDecoderTests.swift diff --git a/Alexander.xcodeproj/project.pbxproj b/Alexander.xcodeproj/project.pbxproj index 47569ba..601b952 100644 --- a/Alexander.xcodeproj/project.pbxproj +++ b/Alexander.xcodeproj/project.pbxproj @@ -22,6 +22,13 @@ 3AE5C3941BFD3EBC00F1080F /* AlexanderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C3931BFD3EBC00F1080F /* AlexanderTests.swift */; }; 3AE5C3951BFD3EBC00F1080F /* AlexanderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C3931BFD3EBC00F1080F /* AlexanderTests.swift */; }; 3AE5C3961BFD3EBC00F1080F /* AlexanderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C3931BFD3EBC00F1080F /* AlexanderTests.swift */; }; + 3AE5C39C1BFD522300F1080F /* DecoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C39B1BFD522300F1080F /* DecoderType.swift */; }; + 3AE5C39D1BFD522300F1080F /* DecoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C39B1BFD522300F1080F /* DecoderType.swift */; }; + 3AE5C39E1BFD522300F1080F /* DecoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C39B1BFD522300F1080F /* DecoderType.swift */; }; + 3AE5C39F1BFD522300F1080F /* DecoderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C39B1BFD522300F1080F /* DecoderType.swift */; }; + 3AE5C3A11BFD551800F1080F /* UserDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C3A01BFD551800F1080F /* UserDecoderTests.swift */; }; + 3AE5C3A21BFD551800F1080F /* UserDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C3A01BFD551800F1080F /* UserDecoderTests.swift */; }; + 3AE5C3A31BFD551800F1080F /* UserDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE5C3A01BFD551800F1080F /* UserDecoderTests.swift */; }; 3B62AAD91B82452800491A67 /* Defines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B62AAD81B82452800491A67 /* Defines.swift */; }; 3B62AADA1B82452800491A67 /* Defines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B62AAD81B82452800491A67 /* Defines.swift */; }; 3B62AADC1B824C1200491A67 /* JSONCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B62AADB1B824C1200491A67 /* JSONCodable.swift */; }; @@ -71,6 +78,8 @@ 3A512F101BE16BB60091A124 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3A512F121BE16BB60091A124 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3AE5C3931BFD3EBC00F1080F /* AlexanderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlexanderTests.swift; sourceTree = ""; }; + 3AE5C39B1BFD522300F1080F /* DecoderType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecoderType.swift; sourceTree = ""; }; + 3AE5C3A01BFD551800F1080F /* UserDecoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDecoderTests.swift; sourceTree = ""; }; 3B62AAD81B82452800491A67 /* Defines.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Defines.swift; sourceTree = ""; }; 3B62AADB1B824C1200491A67 /* JSONCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONCodable.swift; sourceTree = ""; }; 3BEDAE1F1B5DC30500AD52FE /* Alexander.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alexander.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -187,6 +196,7 @@ isa = PBXGroup; children = ( 3AE5C3931BFD3EBC00F1080F /* AlexanderTests.swift */, + 3AE5C3A01BFD551800F1080F /* UserDecoderTests.swift */, 3A512F0C1BE16BB60091A124 /* Configuration */, ); path = Tests; @@ -253,6 +263,7 @@ isa = PBXGroup; children = ( 3BEDAE5C1B5DC4B200AD52FE /* JSON.swift */, + 3AE5C39B1BFD522300F1080F /* DecoderType.swift */, 3B62AADB1B824C1200491A67 /* JSONCodable.swift */, 3B62AAD81B82452800491A67 /* Defines.swift */, 3A512EC91BE168C30091A124 /* Configuration */, @@ -537,6 +548,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3AE5C39E1BFD522300F1080F /* DecoderType.swift in Sources */, 3A512F051BE16B250091A124 /* JSONCodable.swift in Sources */, 3A512F041BE16B250091A124 /* JSON.swift in Sources */, 3A512F061BE16B250091A124 /* Defines.swift in Sources */, @@ -548,6 +560,7 @@ buildActionMask = 2147483647; files = ( 3AE5C3961BFD3EBC00F1080F /* AlexanderTests.swift in Sources */, + 3AE5C3A31BFD551800F1080F /* UserDecoderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -555,6 +568,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3AE5C39F1BFD522300F1080F /* DecoderType.swift in Sources */, 3A512F081BE16B250091A124 /* JSONCodable.swift in Sources */, 3A512F071BE16B250091A124 /* JSON.swift in Sources */, 3A512F091BE16B250091A124 /* Defines.swift in Sources */, @@ -565,6 +579,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3AE5C39C1BFD522300F1080F /* DecoderType.swift in Sources */, 3B62AAD91B82452800491A67 /* Defines.swift in Sources */, 3B62AADC1B824C1200491A67 /* JSONCodable.swift in Sources */, 3BEDAE5D1B5DC4B200AD52FE /* JSON.swift in Sources */, @@ -576,6 +591,7 @@ buildActionMask = 2147483647; files = ( 3AE5C3941BFD3EBC00F1080F /* AlexanderTests.swift in Sources */, + 3AE5C3A11BFD551800F1080F /* UserDecoderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -583,6 +599,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3AE5C39D1BFD522300F1080F /* DecoderType.swift in Sources */, 3B62AADA1B82452800491A67 /* Defines.swift in Sources */, 3BEDAE7D1B5DC58800AD52FE /* JSON.swift in Sources */, 3A512EC81BE15A380091A124 /* JSONCodable.swift in Sources */, @@ -594,6 +611,7 @@ buildActionMask = 2147483647; files = ( 3AE5C3951BFD3EBC00F1080F /* AlexanderTests.swift in Sources */, + 3AE5C3A21BFD551800F1080F /* UserDecoderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Alexander/DecoderType.swift b/Alexander/DecoderType.swift new file mode 100644 index 0000000..c15ad94 --- /dev/null +++ b/Alexander/DecoderType.swift @@ -0,0 +1,50 @@ +// +// DecoderType.swift +// Alexander +// +// Created by Caleb Davenport on 11/18/15. +// Copyright © 2015 Hodinkee. All rights reserved. +// + +public protocol DecoderType { + typealias Value + static func decode(JSON: Alexander.JSON) -> Value? +} + +extension JSON { + public func decode(decoder: T.Type) -> T.Value? { + return decode(T.decode) + } + + public func decodeArray(decoder: T.Type) -> [T.Value]? { + return decodeArray(T.decode) + } +} + +public struct NSTimeIntervalDecoder: DecoderType { + public typealias Value = NSTimeInterval + public static func decode(JSON: Alexander.JSON) -> Value? { + return JSON.object as? NSTimeInterval + } +} + +public struct NSDateTimeIntervalSince1970Decoder: DecoderType { + public typealias Value = NSDate + public static func decode(JSON: Alexander.JSON) -> Value? { + return NSTimeIntervalDecoder.decode(JSON).flatMap({ NSDate(timeIntervalSince1970: $0) }) + } +} + +public struct NSDateTimeIntervalSinceReferenceDateDecoder: DecoderType { + public typealias Value = NSDate + public static func decode(JSON: Alexander.JSON) -> Value? { + return NSTimeIntervalDecoder.decode(JSON).flatMap({ NSDate(timeIntervalSinceReferenceDate: $0) }) + } +} + +public struct NSURLDecoder: DecoderType { + public typealias Value = NSURL + public static func decode(JSON: Alexander.JSON) -> Value? { + return JSON.stringValue.flatMap({ NSURL(string: $0) }) + } +} diff --git a/Alexander/JSON.swift b/Alexander/JSON.swift index dd69720..2c1e44e 100644 --- a/Alexander/JSON.swift +++ b/Alexander/JSON.swift @@ -106,7 +106,6 @@ extension JSON: CustomDebugStringConvertible { } return "Invalid JSON." } - } extension JSON { @@ -129,17 +128,18 @@ extension JSON { return boolValue } - @available(*, deprecated, message = "Use decode(NSURL) instead.") + @available(*, deprecated, message = "Use decode(NSURLDecoder) instead.") public var url: NSURL? { - return decode(NSURL) + return decode(NSURLDecoder) } + @available(*, deprecated, message = "Use decode(NSTimeIntervalDecoder) instead.") public var timeInterval: NSTimeInterval? { - return object as? NSTimeInterval + return decode(NSTimeIntervalDecoder) } - @available(*, deprecated, message = "Use decode(NSDate) instead.") + @available(*, deprecated, message = "Use decode(NSDateTimeIntervalSince1970Decoder) instead.") public var date: NSDate? { - return decode(NSDate) + return decode(NSDateTimeIntervalSince1970Decoder) } } diff --git a/Alexander/JSONCodable.swift b/Alexander/JSONCodable.swift index 5590208..e8ab333 100644 --- a/Alexander/JSONCodable.swift +++ b/Alexander/JSONCodable.swift @@ -10,6 +10,7 @@ public protocol JSONEncodable { var JSON: Alexander.JSON { get } } +@available(*, deprecated, message = "Use DecoderType instead.") public protocol JSONDecodable { static func decode(JSON: Alexander.JSON) -> Self? } @@ -23,15 +24,3 @@ public extension Alexander.JSON { return decodeArray(T.decode) } } - -extension NSURL: JSONDecodable { - public static func decode(JSON: Alexander.JSON) -> Self? { - return JSON.stringValue.flatMap(self.init) - } -} - -extension NSDate: JSONDecodable { - public static func decode(JSON: Alexander.JSON) -> Self? { - return (JSON.object as? NSTimeInterval).flatMap({ self.init(timeIntervalSince1970: $0) }) - } -} diff --git a/Tests/AlexanderTests.swift b/Tests/AlexanderTests.swift index d6a3f98..8807dca 100644 --- a/Tests/AlexanderTests.swift +++ b/Tests/AlexanderTests.swift @@ -10,7 +10,6 @@ import XCTest import Alexander final class AlexanderTests: XCTestCase { - func testArray() { let JSON = Alexander.JSON(object: ["1","2", "a", "B", "D"]) XCTAssertEqual(JSON.array?.count, 5) @@ -83,73 +82,10 @@ final class AlexanderTests: XCTestCase { XCTAssertEqual(decodedSeasons, [ Season.Winter, Season.Summer, Season.Spring ]) } - func testValidDecodableObjectSingle() { - struct User: JSONDecodable { - var ID: String - var name: String - - static func decode(JSON: Alexander.JSON) -> User? { - guard let ID = JSON["id"]?.stringValue, let name = JSON["name"]?.stringValue else { - return nil - } - return User(ID: ID, name: name) - } - } - - let user = User(ID: "1", name: "Caleb") - let object = [ "id": user.ID, "name": user.name ] - let JSON = Alexander.JSON(object: object) - guard let decodedUser = JSON.decode(User) else { - XCTFail() - return - } - - XCTAssertEqual(decodedUser.ID, user.ID) - XCTAssertEqual(decodedUser.name, user.name) - } - - func testValidDecodableObjectArray() { - struct User: JSONDecodable { - var ID: String - var name: String - - static func decode(JSON: Alexander.JSON) -> User? { - guard let ID = JSON["id"]?.stringValue, let name = JSON["name"]?.stringValue else { - return nil - } - return User(ID: ID, name: name) - } - } - - let users = [ - User(ID: "1", name: "Caleb"), - User(ID: "2", name: "Jon") - ] - let object = [ - "users": [ - [ "id": users[0].ID, "name": users[0].name ], - [ "id": users[1].ID, "name": users[1].name ] - ] - ] - let JSON = Alexander.JSON(object: object) - guard let decodedUsers = JSON["users"]?.decodeArray(User) else { - XCTFail() - return - } - - XCTAssertEqual(decodedUsers.count, 2) - - XCTAssertEqual(decodedUsers[0].ID, users[0].ID) - XCTAssertEqual(decodedUsers[0].name, users[0].name) - - XCTAssertEqual(decodedUsers[1].ID, users[1].ID) - XCTAssertEqual(decodedUsers[1].name, users[1].name) - } - func testURLHelpers() { let JSON = Alexander.JSON(object: "https://www.hodinkee.com") - guard let URL = JSON.decode(NSURL) else { + guard let URL = JSON.decode(NSURLDecoder) else { XCTFail() return } @@ -159,7 +95,7 @@ final class AlexanderTests: XCTestCase { func testDateHelpers() { let JSON = Alexander.JSON(object: 978307200) - XCTAssertEqual(JSON.timeInterval, NSDate(timeIntervalSinceReferenceDate: 0).timeIntervalSince1970) - XCTAssertEqual(JSON.decode(NSDate), NSDate(timeIntervalSinceReferenceDate: 0)) + XCTAssertEqual(JSON.decode(NSTimeIntervalDecoder), NSDate(timeIntervalSinceReferenceDate: 0).timeIntervalSince1970) + XCTAssertEqual(JSON.decode(NSDateTimeIntervalSince1970Decoder), NSDate(timeIntervalSinceReferenceDate: 0)) } } diff --git a/Tests/UserDecoderTests.swift b/Tests/UserDecoderTests.swift new file mode 100644 index 0000000..9553e14 --- /dev/null +++ b/Tests/UserDecoderTests.swift @@ -0,0 +1,66 @@ +// +// UserDecoderTests.swift +// Alexander +// +// Created by Caleb Davenport on 11/18/15. +// Copyright © 2015 Hodinkee. All rights reserved. +// + +import XCTest +import Alexander + +final class UserDecoderTests: XCTestCase { + func testDecodeValidUser() { + let user = User(ID: "1", name: "Caleb") + let object = [ "id": user.ID, "name": user.name ] + let JSON = Alexander.JSON(object: object) + guard let decodedUser = JSON.decode(UserDecoder) else { + XCTFail() + return + } + + XCTAssertEqual(decodedUser.ID, user.ID) + XCTAssertEqual(decodedUser.name, user.name) + } + + func testDecodeValidUsersArray() { + let users = [ + User(ID: "1", name: "Caleb"), + User(ID: "2", name: "Jon") + ] + let object = [ + "users": [ + [ "id": users[0].ID, "name": users[0].name ], + [ "id": users[1].ID, "name": users[1].name ] + ] + ] + let JSON = Alexander.JSON(object: object) + guard let decodedUsers = JSON["users"]?.decodeArray(UserDecoder) where decodedUsers.count == 2 else { + XCTFail() + return + } + + XCTAssertEqual(decodedUsers.count, 2) + + XCTAssertEqual(decodedUsers[0].ID, users[0].ID) + XCTAssertEqual(decodedUsers[0].name, users[0].name) + + XCTAssertEqual(decodedUsers[1].ID, users[1].ID) + XCTAssertEqual(decodedUsers[1].name, users[1].name) + } +} + +struct User { + var ID: String + var name: String +} + +struct UserDecoder: DecoderType { + typealias Value = User + static func decode(JSON: Alexander.JSON) -> Value? { + guard let ID = JSON["id"]?.stringValue, let name = JSON["name"]?.stringValue else { + return nil + } + return User(ID: ID, name: name) + } +}