From 8cf1116ff8100cfcefa4da55e8f2b1547f45f11a Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Wed, 10 Jun 2015 08:47:53 +0900 Subject: [PATCH 01/44] Remove Assertions from dependencies to migrate to swift 2 --- .gitmodules | 3 --- APIKit.xcodeproj/project.pbxproj | 9 +------ APIKit.xcworkspace/contents.xcworkspacedata | 3 --- APIKitTests/APITests.swift | 20 +++++++------- APIKitTests/RequestBodyBuilderTests.swift | 27 +++++++++---------- APIKitTests/ResponseBodyParserTests.swift | 30 ++++++++++----------- Cartfile.private | 1 - Cartfile.resolved | 1 - Carthage/Checkouts/Assertions | 1 - 9 files changed, 37 insertions(+), 58 deletions(-) delete mode 160000 Carthage/Checkouts/Assertions diff --git a/.gitmodules b/.gitmodules index cc81dcfa..0921f572 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "Carthage/Checkouts/Assertions"] - path = Carthage/Checkouts/Assertions - url = https://github.com/antitypical/Assertions.git [submodule "Carthage/Checkouts/OHHTTPStubs"] path = Carthage/Checkouts/OHHTTPStubs url = https://github.com/AliSoftware/OHHTTPStubs.git diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 64ca7a01..73d05424 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -46,13 +46,10 @@ CD5115271B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD5115281B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD5115291B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; - CD51152B1B1FFCB200514240 /* Assertions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152A1B1FFCB200514240 /* Assertions.framework */; }; - CD51152C1B1FFCB200514240 /* Assertions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152A1B1FFCB200514240 /* Assertions.framework */; }; CD51152E1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; CD51152F1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; CD5115301B1FFD7C00514240 /* Box.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CD5115311B1FFD8F00514240 /* Result.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - CD5115321B1FFD9200514240 /* Assertions.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152A1B1FFCB200514240 /* Assertions.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; CD5115331B1FFD9500514240 /* OHHTTPStubs.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; CD5115341B1FFDA600514240 /* APIKit.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -110,7 +107,6 @@ files = ( CD5115301B1FFD7C00514240 /* Box.framework in Copy Frameworks */, CD5115311B1FFD8F00514240 /* Result.framework in Copy Frameworks */, - CD5115321B1FFD9200514240 /* Assertions.framework in Copy Frameworks */, CD5115331B1FFD9500514240 /* OHHTTPStubs.framework in Copy Frameworks */, CD5115341B1FFDA600514240 /* APIKit.framework in Copy Frameworks */, ); @@ -147,7 +143,6 @@ 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseBodyParserTests.swift; sourceTree = ""; }; CD51151E1B1FFAC000514240 /* Box.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Box.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD5115241B1FFBA900514240 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - CD51152A1B1FFCB200514240 /* Assertions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Assertions.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -158,7 +153,6 @@ files = ( CD5115211B1FFB7000514240 /* Box.framework in Frameworks */, CD5115281B1FFBA900514240 /* Result.framework in Frameworks */, - CD51152C1B1FFCB200514240 /* Assertions.framework in Frameworks */, CD51152F1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */, 7F08699A1A978790001AD3E1 /* APIKit.framework in Frameworks */, ); @@ -198,7 +192,6 @@ files = ( CD5115221B1FFB7200514240 /* Box.framework in Frameworks */, CD5115271B1FFBA900514240 /* Result.framework in Frameworks */, - CD51152B1B1FFCB200514240 /* Assertions.framework in Frameworks */, CD51152E1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */, 7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */, ); @@ -291,7 +284,6 @@ 7FEC5A161A96FE2600B1D3C0 /* Supporting Files */ = { isa = PBXGroup; children = ( - CD51152A1B1FFCB200514240 /* Assertions.framework */, CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */, 7FEC5A171A96FE2600B1D3C0 /* Info.plist */, ); @@ -417,6 +409,7 @@ 7F45FCD41A94D02C006863BB /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 0610; ORGANIZATIONNAME = "Yosuke Ishikawa"; TargetAttributes = { diff --git a/APIKit.xcworkspace/contents.xcworkspacedata b/APIKit.xcworkspace/contents.xcworkspacedata index e5b1aafa..605ce9aa 100644 --- a/APIKit.xcworkspace/contents.xcworkspacedata +++ b/APIKit.xcworkspace/contents.xcworkspacedata @@ -10,9 +10,6 @@ - - diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 65143ec3..1f9c2aaa 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -1,7 +1,6 @@ import Foundation import APIKit import XCTest -import Assertions import OHHTTPStubs class APITests: XCTestCase { @@ -54,8 +53,8 @@ class APITests: XCTestCase { MockAPI.sendRequest(request) { response in switch response { case .Success(let box): - assert(box.value, ==, dictionary) - + XCTAssert(box.value["key"] as? String == "value") + case .Failure: XCTFail() } @@ -85,8 +84,7 @@ class APITests: XCTestCase { case .Failure(let box): let error = box.value - assertEqual(error.domain, error.domain) - assertEqual(error.code, error.code) + XCTAssert(error.domain == NSURLErrorDomain) } expectation.fulfill() @@ -113,8 +111,8 @@ class APITests: XCTestCase { case .Failure(let box): let error = box.value - assertEqual(error.domain, "MockAPIErrorDomain") - assertEqual(error.code, 10000) + XCTAssert(error.domain == "MockAPIErrorDomain") + XCTAssert(error.code == 10000) } expectation.fulfill() @@ -142,8 +140,8 @@ class APITests: XCTestCase { case .Failure(let box): let error = box.value - assert(error.domain, ==, NSCocoaErrorDomain) - assertEqual(error.code, 3840) + XCTAssert(error.domain == NSCocoaErrorDomain) + XCTAssert(error.code == 3840) } expectation.fulfill() @@ -173,8 +171,8 @@ class APITests: XCTestCase { case .Failure(let box): let error = box.value - assert(error.domain, ==, NSURLErrorDomain) - assertEqual(error.code, NSURLErrorCancelled) + XCTAssert(error.domain == NSURLErrorDomain) + XCTAssert(error.code == NSURLErrorCancelled) } expectation.fulfill() diff --git a/APIKitTests/RequestBodyBuilderTests.swift b/APIKitTests/RequestBodyBuilderTests.swift index cadfc654..d2e00bbb 100644 --- a/APIKitTests/RequestBodyBuilderTests.swift +++ b/APIKitTests/RequestBodyBuilderTests.swift @@ -1,13 +1,12 @@ import Foundation import APIKit -import Assertions import Result import XCTest class RequestBodyBuilderTests: XCTestCase { func testJSONHeader() { let builder = RequestBodyBuilder.JSON(writingOptions: nil) - assertEqual(builder.contentTypeHeader, "application/json") + XCTAssert(builder.contentTypeHeader == "application/json") } func testJSONSuccess() { @@ -17,9 +16,9 @@ class RequestBodyBuilderTests: XCTestCase { switch builder.buildBodyFromObject(object) { case .Success(let box): let dictionary = NSJSONSerialization.JSONObjectWithData(box.value, options: nil, error: nil) as? [String: Int] - assertEqual(dictionary?["foo"], 1) - assertEqual(dictionary?["bar"], 2) - assertEqual(dictionary?["baz"], 3) + XCTAssert(dictionary?["foo"] == 1) + XCTAssert(dictionary?["bar"] == 2) + XCTAssert(dictionary?["baz"] == 3) case .Failure: XCTFail() @@ -36,14 +35,14 @@ class RequestBodyBuilderTests: XCTestCase { case .Failure(let box): let error = box.value - assertEqual(error.domain, APIKitRequestBodyBuidlerErrorDomain) - assertEqual(error.code, 0) + XCTAssert(error.domain == APIKitRequestBodyBuidlerErrorDomain) + XCTAssert(error.code == 0) } } func testURLHeader() { let builder = RequestBodyBuilder.URL(encoding: NSUTF8StringEncoding) - assertEqual(builder.contentTypeHeader, "application/x-www-form-urlencoded") + XCTAssert(builder.contentTypeHeader == "application/x-www-form-urlencoded") } func testURLSuccess() { @@ -53,9 +52,9 @@ class RequestBodyBuilderTests: XCTestCase { switch builder.buildBodyFromObject(object) { case .Success(let box): let dictionary = URLEncodedSerialization.objectFromData(box.value, encoding: NSUTF8StringEncoding, error: nil) as? [String: String] - assertEqual(dictionary?["foo"], "1") - assertEqual(dictionary?["bar"], "2") - assertEqual(dictionary?["baz"], "3") + XCTAssert(dictionary?["foo"] == "1") + XCTAssert(dictionary?["bar"] == "2") + XCTAssert(dictionary?["baz"] == "3") case .Failure: XCTFail() @@ -64,7 +63,7 @@ class RequestBodyBuilderTests: XCTestCase { func testCustomHeader() { let builder = RequestBodyBuilder.Custom(contentTypeHeader: "foo", buildBodyFromObject: { o in .success(o as! NSData) }) - assertEqual(builder.contentTypeHeader, "foo") + XCTAssert(builder.contentTypeHeader == "foo") } func testCustomSuccess() { @@ -76,7 +75,7 @@ class RequestBodyBuilderTests: XCTestCase { switch builder.buildBodyFromObject(string) { case .Success(let box): - assertEqual(box.value, expectedData) + XCTAssert(box.value == expectedData) case .Failure: XCTFail() @@ -95,7 +94,7 @@ class RequestBodyBuilderTests: XCTestCase { XCTFail() case .Failure(let box): - assertEqual(box.value, expectedError) + XCTAssert(box.value == expectedError) } } } diff --git a/APIKitTests/ResponseBodyParserTests.swift b/APIKitTests/ResponseBodyParserTests.swift index dc07740d..002f5c62 100644 --- a/APIKitTests/ResponseBodyParserTests.swift +++ b/APIKitTests/ResponseBodyParserTests.swift @@ -1,13 +1,12 @@ import Foundation import APIKit -import Assertions import Result import XCTest class ResponseBodyParserTests: XCTestCase { func testJSONAcceptHeader() { let parser = ResponseBodyParser.JSON(readingOptions: nil) - assertEqual(parser.acceptHeader, "application/json") + XCTAssert(parser.acceptHeader == "application/json") } func testJSONSuccess() { @@ -18,9 +17,9 @@ class ResponseBodyParserTests: XCTestCase { switch parser.parseData(data) { case .Success(let box): let dictionary = box.value as? [String: Int] - assertEqual(dictionary?["foo"], 1) - assertEqual(dictionary?["bar"], 2) - assertEqual(dictionary?["baz"], 3) + XCTAssert(dictionary?["foo"] == 1) + XCTAssert(dictionary?["bar"] == 2) + XCTAssert(dictionary?["baz"] == 3) case .Failure: XCTFail() @@ -38,14 +37,14 @@ class ResponseBodyParserTests: XCTestCase { case .Failure(let box): let error = box.value - assert(error.domain, ==, NSCocoaErrorDomain) - assertEqual(error.code, 3840) + XCTAssert(error.domain == NSCocoaErrorDomain) + XCTAssert(error.code == 3840) } } func testURLAcceptHeader() { let parser = ResponseBodyParser.URL(encoding: NSUTF8StringEncoding) - assertEqual(parser.acceptHeader, "application/x-www-form-urlencoded") + XCTAssert(parser.acceptHeader == "application/x-www-form-urlencoded") } func testURLSuccess() { @@ -56,9 +55,9 @@ class ResponseBodyParserTests: XCTestCase { switch parser.parseData(data) { case .Success(let box): let dictionary = box.value as? [String: String] - assertEqual(dictionary?["foo"], "1") - assertEqual(dictionary?["bar"], "2") - assertEqual(dictionary?["baz"], "3") + XCTAssert(dictionary?["foo"] == "1") + XCTAssert(dictionary?["bar"] == "2") + XCTAssert(dictionary?["baz"] == "3") case .Failure: XCTFail() @@ -67,20 +66,19 @@ class ResponseBodyParserTests: XCTestCase { func testCustomAcceptHeader() { let parser = ResponseBodyParser.Custom(acceptHeader: "foo", parseData: { d in .success(d) }) - assertEqual(parser.acceptHeader, "foo") + XCTAssert(parser.acceptHeader == "foo") } func testCustomSuccess() { - let expectedDictionary = ["foo": 1] let data = NSData() let parser = ResponseBodyParser.Custom(acceptHeader: "", parseData: { data in - return .success(expectedDictionary) + return .success(["foo": 1]) }) switch parser.parseData(data) { case .Success(let box): let dictionary = box.value as? [String: Int] - assertEqual(dictionary, expectedDictionary) + XCTAssert(dictionary?["foo"] == 1) case .Failure: XCTFail() @@ -99,7 +97,7 @@ class ResponseBodyParserTests: XCTestCase { XCTFail() case .Failure(let box): - assertEqual(box.value, expectedError) + XCTAssert(box.value == expectedError) } } } diff --git a/Cartfile.private b/Cartfile.private index 68b7c812..14347abb 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,2 +1 @@ -github "antitypical/Assertions" ~> 1.1 github "AliSoftware/OHHTTPStubs" ~> 4.0.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 5131ddfe..1c55e41d 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,3 @@ -github "antitypical/Assertions" "1.1" github "robrix/Box" "1.2.2" github "AliSoftware/OHHTTPStubs" "4.0.2" github "antitypical/Result" "0.4.3" diff --git a/Carthage/Checkouts/Assertions b/Carthage/Checkouts/Assertions deleted file mode 160000 index 4b777f32..00000000 --- a/Carthage/Checkouts/Assertions +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4b777f32b8e682f8a1148861ee659e0bfbaea896 From 10297952177e05a1b37edfbc741ffb9fe32eadef Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Wed, 10 Jun 2015 09:56:18 +0900 Subject: [PATCH 02/44] Just convert to Swift 2 --- .gitmodules | 3 - APIKit.xcodeproj/project.pbxproj | 35 +++++----- .../xcschemes/APIKit-Mac.xcscheme | 5 +- .../xcschemes/APIKit-iOS.xcscheme | 5 +- .../xcshareddata/xcschemes/DemoApp.xcscheme | 11 +++- APIKit.xcworkspace/contents.xcworkspacedata | 3 - APIKit/API.swift | 66 +++++++++++-------- APIKit/Info.plist | 2 +- APIKit/RequestBodyBuilder.swift | 15 +++-- APIKit/ResponseBodyParser.swift | 18 +++-- APIKit/URLEncodedSerialization.swift | 3 +- APIKitTests/APITests.swift | 26 ++++---- APIKitTests/Info.plist | 2 +- APIKitTests/RequestBodyBuilderTests.swift | 27 ++++---- APIKitTests/ResponseBodyParserTests.swift | 27 ++++---- Cartfile | 2 +- Cartfile.resolved | 3 +- Carthage/Checkouts/Box | 1 - Carthage/Checkouts/Result | 2 +- DemoApp/Info.plist | 2 +- 20 files changed, 137 insertions(+), 121 deletions(-) delete mode 160000 Carthage/Checkouts/Box diff --git a/.gitmodules b/.gitmodules index 0921f572..c11e1697 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "Carthage/Checkouts/OHHTTPStubs"] path = Carthage/Checkouts/OHHTTPStubs url = https://github.com/AliSoftware/OHHTTPStubs.git -[submodule "Carthage/Checkouts/Box"] - path = Carthage/Checkouts/Box - url = https://github.com/robrix/Box.git [submodule "Carthage/Checkouts/Result"] path = Carthage/Checkouts/Result url = https://github.com/antitypical/Result.git diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 73d05424..fa8380e2 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -36,11 +36,6 @@ 7FCBE9E11A9734950075AFD9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */; }; 7FEC5A191A96FE2600B1D3C0 /* ResponseBodyParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */; }; 7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; }; - CD51151F1B1FFAC000514240 /* Box.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; }; - CD5115201B1FFB4F00514240 /* Box.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; }; - CD5115211B1FFB7000514240 /* Box.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; }; - CD5115221B1FFB7200514240 /* Box.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; }; - CD5115231B1FFB8E00514240 /* Box.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; }; CD5115251B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD5115261B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD5115271B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; @@ -48,10 +43,9 @@ CD5115291B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD51152E1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; CD51152F1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; - CD5115301B1FFD7C00514240 /* Box.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51151E1B1FFAC000514240 /* Box.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CD5115311B1FFD8F00514240 /* Result.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - CD5115331B1FFD9500514240 /* OHHTTPStubs.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - CD5115341B1FFDA600514240 /* APIKit.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CD5115331B1FFD9500514240 /* OHHTTPStubs.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CD5115341B1FFDA600514240 /* APIKit.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -105,7 +99,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - CD5115301B1FFD7C00514240 /* Box.framework in Copy Frameworks */, CD5115311B1FFD8F00514240 /* Result.framework in Copy Frameworks */, CD5115331B1FFD9500514240 /* OHHTTPStubs.framework in Copy Frameworks */, CD5115341B1FFDA600514240 /* APIKit.framework in Copy Frameworks */, @@ -141,7 +134,6 @@ 7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7FEC5A171A96FE2600B1D3C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseBodyParserTests.swift; sourceTree = ""; }; - CD51151E1B1FFAC000514240 /* Box.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Box.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD5115241B1FFBA900514240 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -151,7 +143,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD5115211B1FFB7000514240 /* Box.framework in Frameworks */, CD5115281B1FFBA900514240 /* Result.framework in Frameworks */, CD51152F1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */, 7F08699A1A978790001AD3E1 /* APIKit.framework in Frameworks */, @@ -162,7 +153,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD5115201B1FFB4F00514240 /* Box.framework in Frameworks */, CD5115251B1FFBA900514240 /* Result.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -171,7 +161,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD51151F1B1FFAC000514240 /* Box.framework in Frameworks */, CD5115261B1FFBA900514240 /* Result.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -180,7 +169,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD5115231B1FFB8E00514240 /* Box.framework in Frameworks */, CD5115291B1FFBA900514240 /* Result.framework in Frameworks */, 7F45FD6C1A94DA28006863BB /* APIKit.framework in Frameworks */, ); @@ -190,7 +178,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD5115221B1FFB7200514240 /* Box.framework in Frameworks */, CD5115271B1FFBA900514240 /* Result.framework in Frameworks */, CD51152E1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */, 7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */, @@ -240,7 +227,6 @@ 7F45FCE01A94D02C006863BB /* Supporting Files */ = { isa = PBXGroup; children = ( - CD51151E1B1FFAC000514240 /* Box.framework */, CD5115241B1FFBA900514240 /* Result.framework */, 7F45FCE11A94D02C006863BB /* Info.plist */, ); @@ -410,7 +396,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0610; + LastUpgradeCheck = 0700; ORGANIZATIONNAME = "Yosuke Ishikawa"; TargetAttributes = { 7F0869931A978790001AD3E1 = { @@ -602,6 +588,7 @@ INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = macosx; }; @@ -615,6 +602,7 @@ INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = macosx; }; @@ -641,6 +629,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -716,7 +705,8 @@ INFOPLIST_FILE = APIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -735,7 +725,8 @@ INFOPLIST_FILE = APIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -761,6 +752,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; @@ -784,6 +776,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; @@ -801,6 +794,7 @@ ); INFOPLIST_FILE = DemoApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; }; @@ -813,6 +807,7 @@ EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; INFOPLIST_FILE = DemoApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; }; @@ -827,6 +822,7 @@ ); INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = iphoneos; }; @@ -837,6 +833,7 @@ buildSettings = { INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = iphoneos; }; diff --git a/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme index ed8993c5..26e9b4e6 100644 --- a/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme +++ b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme @@ -1,6 +1,6 @@ + + + + + + - + - + - - diff --git a/APIKit/API.swift b/APIKit/API.swift index 9b2f471a..6343c47e 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -1,9 +1,5 @@ import Foundation - -#if APIKIT_DYNAMIC_FRAMEWORK || COCOAPODS import Result -import Box -#endif public let APIKitErrorDomain = "APIKitErrorDomain" @@ -13,12 +9,13 @@ public class API { fatalError("API.baseURL must be overrided in subclasses.") } + // How can I pass "no options" to public class var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: nil) + return .JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) } public class var responseBodyParser: ResponseBodyParser { - return .JSON(readingOptions: nil) + return .JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) } public class var defaultURLSession: NSURLSession { @@ -40,7 +37,7 @@ public class API { /// /// Returns a mutable URL request instance which is meant to be modified in /// subclasses or in `Request` protocol conforming types. - public class func URLRequest(#method: Method, path: String, parameters: [String: AnyObject] = [:], requestBodyBuilder: RequestBodyBuilder = requestBodyBuilder) -> NSMutableURLRequest? { + public class func URLRequest(method method: Method, path: String, parameters: [String: AnyObject] = [:], requestBodyBuilder: RequestBodyBuilder = requestBodyBuilder) -> NSMutableURLRequest? { if let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) { let request = NSMutableURLRequest() @@ -50,10 +47,10 @@ public class API { default: switch requestBodyBuilder.buildBodyFromObject(parameters) { - case .Success(let box): - request.HTTPBody = box.value + case .Success(let result): + request.HTTPBody = result - case .Failure(let box): + case .Failure: return nil } } @@ -70,11 +67,6 @@ public class API { } } - @availability(*, unavailable, renamed="URLRequest(method:path:parameters:requestBodyBuilder)") - public class func URLRequest(method: Method, _ path: String, _ parameters: [String: AnyObject] = [:], requestBodyBuilder: RequestBodyBuilder = requestBodyBuilder) -> NSURLRequest? { - return URLRequest(method: method, path: path, parameters: parameters, requestBodyBuilder: requestBodyBuilder) - } - // send request and build response object public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { let mainQueue = dispatch_get_main_queue() @@ -82,15 +74,15 @@ public class API { if let URLRequest = request.URLRequest { let task = URLSession.dataTaskWithRequest(URLRequest) - task.request = Box(request) - task.completionHandler = { data, URLResponse, connectionError in + task?.request = Box(request) + task?.completionHandler = { data, URLResponse, connectionError in if let error = connectionError { dispatch_async(mainQueue) { handler(.failure(error)) } return } let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 - if !contains(self.acceptableStatusCodes, statusCode) { + if !self.acceptableStatusCodes.contains(statusCode) { let error = self.responseBodyParser.parseData(data).analysis( ifSuccess: { self.responseErrorFromObject($0) }, ifFailure: { $0 } @@ -113,7 +105,7 @@ public class API { dispatch_async(mainQueue) { handler(mappedResponse) } } - task.resume() + task?.resume() return task } else { @@ -131,7 +123,19 @@ public class API { public class func cancelRequest(requestType: T.Type, URLSession: NSURLSession, passingTest test: T -> Bool = { r in true }) { URLSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in - let tasks = (dataTasks + uploadTasks + downloadTasks).filter { task in + // TODO: replace with cool code + var allTasks = [NSURLSessionTask]() + for task in dataTasks { + allTasks.append(task) + } + for task in uploadTasks { + allTasks.append(task) + } + for task in downloadTasks { + allTasks.append(task) + } + + let tasks = allTasks.filter { task in var request: T? switch task { case let x as NSURLSessionDataTask: @@ -183,6 +187,14 @@ public class URLSessionDelegate: NSObject, NSURLSessionDelegate, NSURLSessionDat } } +// Box is still necessary internally to store struct into associated object +private final class Box { + let value: T + init(_ value: T) { + self.value = value + } +} + // MARK: - NSURLSessionTask extensions private var taskRequestKey = 0 private var dataTaskResponseBufferKey = 0 @@ -198,9 +210,9 @@ private extension NSURLSessionDataTask { set { if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } @@ -210,7 +222,7 @@ private extension NSURLSessionDataTask { return responseBuffer } else { let responseBuffer = NSMutableData() - objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) return responseBuffer } } @@ -222,9 +234,9 @@ private extension NSURLSessionDataTask { set { if let value = newValue { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } @@ -238,9 +250,9 @@ extension NSURLSessionDownloadTask { set { if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } diff --git a/APIKit/Info.plist b/APIKit/Info.plist index a8a1b089..28a6057e 100644 --- a/APIKit/Info.plist +++ b/APIKit/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - -.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/APIKit/RequestBodyBuilder.swift b/APIKit/RequestBodyBuilder.swift index 46156059..0ef86ef3 100644 --- a/APIKit/RequestBodyBuilder.swift +++ b/APIKit/RequestBodyBuilder.swift @@ -1,8 +1,5 @@ import Foundation - -#if APIKIT_DYNAMIC_FRAMEWORK || COCOAPODS import Result -#endif public let APIKitRequestBodyBuidlerErrorDomain = "APIKitRequestBodyBuidlerErrorDomain" @@ -24,6 +21,7 @@ public enum RequestBodyBuilder { } } + // TODO: migrate to Swift 2 style error handling public func buildBodyFromObject(object: AnyObject) -> Result { switch self { case .JSON(let writingOptions): @@ -33,12 +31,17 @@ public enum RequestBodyBuilder { return .failure(error) } - return try { error in - return NSJSONSerialization.dataWithJSONObject(object, options: writingOptions, error: error) + let result: Result + do { + result = .success(try NSJSONSerialization.dataWithJSONObject(object, options: writingOptions)) + } catch { + result = .failure(error as NSError) } + return result + case .URL(let encoding): - return try { error in + return `try` { error in return URLEncodedSerialization.dataFromObject(object, encoding: encoding, error: error) } diff --git a/APIKit/ResponseBodyParser.swift b/APIKit/ResponseBodyParser.swift index 62fb3e2a..7c04266b 100644 --- a/APIKit/ResponseBodyParser.swift +++ b/APIKit/ResponseBodyParser.swift @@ -1,8 +1,5 @@ import Foundation - -#if APIKIT_DYNAMIC_FRAMEWORK || COCOAPODS import Result -#endif public enum ResponseBodyParser { case JSON(readingOptions: NSJSONReadingOptions) @@ -22,22 +19,29 @@ public enum ResponseBodyParser { } } + // TODO: migrate to Swift 2 style error handling public func parseData(data: NSData) -> Result { switch self { case .JSON(let readingOptions): if data.length == 0 { return .success([:]) } - return try { error in - return NSJSONSerialization.JSONObjectWithData(data, options: readingOptions, error: error) + + let result: Result + do { + result = .success(try NSJSONSerialization.JSONObjectWithData(data, options: readingOptions)) + } catch { + result = .failure(error as NSError) } + return result + case .URL(let encoding): - return try { error in + return `try` { error in return URLEncodedSerialization.objectFromData(data, encoding: encoding, error: error) } - case .Custom(let (accept, parseData)): + case .Custom(let (_, parseData)): return parseData(data) } } diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index d6c50cbc..8045b674 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -32,6 +32,7 @@ public class URLEncodedSerialization { return dictionary } + // TODO: migrate to Swift 2 error handling public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding, error: NSErrorPointer) -> NSData? { let string = stringFromObject(object, encoding: encoding) let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) @@ -55,6 +56,6 @@ public class URLEncodedSerialization { } } - return join("&", pairs) + return "&".join(pairs) } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 1f9c2aaa..c0ce06da 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -39,7 +39,7 @@ class APITests: XCTestCase { // MARK: - integration tests func testSuccess() { let dictionary = ["key": "value"] - let data = NSJSONSerialization.dataWithJSONObject(dictionary, options: nil, error: nil)! + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions(rawValue: 0)) OHHTTPStubs.stubRequestsPassingTest({ request in return true @@ -52,8 +52,8 @@ class APITests: XCTestCase { MockAPI.sendRequest(request) { response in switch response { - case .Success(let box): - XCTAssert(box.value["key"] as? String == "value") + case .Success(let dictionary): + XCTAssert(dictionary["key"] as? String == "value") case .Failure: XCTFail() @@ -82,8 +82,7 @@ class APITests: XCTestCase { case .Success: XCTFail() - case .Failure(let box): - let error = box.value + case .Failure(let error): XCTAssert(error.domain == NSURLErrorDomain) } @@ -97,7 +96,8 @@ class APITests: XCTestCase { OHHTTPStubs.stubRequestsPassingTest({ request in return true }, withStubResponse: { request in - let data = NSJSONSerialization.dataWithJSONObject([:], options: nil, error: nil)! + let dictionary: [String: String] = [:] + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions(rawValue: 0)) return OHHTTPStubsResponse(data: data, statusCode: 400, headers: nil) }) @@ -109,8 +109,7 @@ class APITests: XCTestCase { case .Success: XCTFail() - case .Failure(let box): - let error = box.value + case .Failure(let error): XCTAssert(error.domain == "MockAPIErrorDomain") XCTAssert(error.code == 10000) } @@ -138,8 +137,7 @@ class APITests: XCTestCase { case .Success: XCTFail() - case .Failure(let box): - let error = box.value + case .Failure(let error): XCTAssert(error.domain == NSCocoaErrorDomain) XCTAssert(error.code == 3840) } @@ -169,8 +167,7 @@ class APITests: XCTestCase { case .Success: XCTFail() - case .Failure(let box): - let error = box.value + case .Failure(let error): XCTAssert(error.domain == NSURLErrorDomain) XCTAssert(error.code == NSURLErrorCancelled) } @@ -187,7 +184,8 @@ class APITests: XCTestCase { OHHTTPStubs.stubRequestsPassingTest({ request in return true }, withStubResponse: { request in - let data = NSJSONSerialization.dataWithJSONObject([:], options: nil, error: nil)! + let dictionary: [String: String] = [:] + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions(rawValue: 0)) let response = OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) response.requestTime = 0.1 response.responseTime = 0.1 @@ -202,7 +200,7 @@ class APITests: XCTestCase { case .Success: break - case .Failure(let box): + case .Failure: XCTFail() } diff --git a/APIKitTests/Info.plist b/APIKitTests/Info.plist index 63b30a95..ba72822e 100644 --- a/APIKitTests/Info.plist +++ b/APIKitTests/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - -.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/APIKitTests/RequestBodyBuilderTests.swift b/APIKitTests/RequestBodyBuilderTests.swift index d2e00bbb..ba76ffe9 100644 --- a/APIKitTests/RequestBodyBuilderTests.swift +++ b/APIKitTests/RequestBodyBuilderTests.swift @@ -5,17 +5,17 @@ import XCTest class RequestBodyBuilderTests: XCTestCase { func testJSONHeader() { - let builder = RequestBodyBuilder.JSON(writingOptions: nil) + let builder = RequestBodyBuilder.JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) XCTAssert(builder.contentTypeHeader == "application/json") } func testJSONSuccess() { let object = ["foo": 1, "bar": 2, "baz": 3] - let builder = RequestBodyBuilder.JSON(writingOptions: nil) + let builder = RequestBodyBuilder.JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) switch builder.buildBodyFromObject(object) { - case .Success(let box): - let dictionary = NSJSONSerialization.JSONObjectWithData(box.value, options: nil, error: nil) as? [String: Int] + case .Success(let data): + let dictionary = try! NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(rawValue: 0)) as? [String: Int] XCTAssert(dictionary?["foo"] == 1) XCTAssert(dictionary?["bar"] == 2) XCTAssert(dictionary?["baz"] == 3) @@ -27,14 +27,13 @@ class RequestBodyBuilderTests: XCTestCase { func testJSONFailure() { let object = NSObject() - let builder = RequestBodyBuilder.JSON(writingOptions: nil) + let builder = RequestBodyBuilder.JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) switch builder.buildBodyFromObject(object) { case .Success: XCTFail() - case .Failure(let box): - let error = box.value + case .Failure(let error): XCTAssert(error.domain == APIKitRequestBodyBuidlerErrorDomain) XCTAssert(error.code == 0) } @@ -50,8 +49,8 @@ class RequestBodyBuilderTests: XCTestCase { let builder = RequestBodyBuilder.URL(encoding: NSUTF8StringEncoding) switch builder.buildBodyFromObject(object) { - case .Success(let box): - let dictionary = URLEncodedSerialization.objectFromData(box.value, encoding: NSUTF8StringEncoding, error: nil) as? [String: String] + case .Success(let data): + let dictionary = URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding, error: nil) as? [String: String] XCTAssert(dictionary?["foo"] == "1") XCTAssert(dictionary?["bar"] == "2") XCTAssert(dictionary?["baz"] == "3") @@ -74,8 +73,8 @@ class RequestBodyBuilderTests: XCTestCase { }) switch builder.buildBodyFromObject(string) { - case .Success(let box): - XCTAssert(box.value == expectedData) + case .Success(let data): + XCTAssert(data == expectedData) case .Failure: XCTFail() @@ -84,7 +83,7 @@ class RequestBodyBuilderTests: XCTestCase { func testCustomFailure() { let string = "foo" - let expectedError = NSError() + let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) let builder = RequestBodyBuilder.Custom(contentTypeHeader: "", buildBodyFromObject: { object in return .failure(expectedError) }) @@ -93,8 +92,8 @@ class RequestBodyBuilderTests: XCTestCase { case .Success: XCTFail() - case .Failure(let box): - XCTAssert(box.value == expectedError) + case .Failure(let error): + XCTAssert(error == expectedError) } } } diff --git a/APIKitTests/ResponseBodyParserTests.swift b/APIKitTests/ResponseBodyParserTests.swift index 002f5c62..83f097ed 100644 --- a/APIKitTests/ResponseBodyParserTests.swift +++ b/APIKitTests/ResponseBodyParserTests.swift @@ -5,18 +5,18 @@ import XCTest class ResponseBodyParserTests: XCTestCase { func testJSONAcceptHeader() { - let parser = ResponseBodyParser.JSON(readingOptions: nil) + let parser = ResponseBodyParser.JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) XCTAssert(parser.acceptHeader == "application/json") } func testJSONSuccess() { let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let parser = ResponseBodyParser.JSON(readingOptions: nil) + let parser = ResponseBodyParser.JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) switch parser.parseData(data) { - case .Success(let box): - let dictionary = box.value as? [String: Int] + case .Success(let object): + let dictionary = object as? [String: Int] XCTAssert(dictionary?["foo"] == 1) XCTAssert(dictionary?["bar"] == 2) XCTAssert(dictionary?["baz"] == 3) @@ -29,14 +29,13 @@ class ResponseBodyParserTests: XCTestCase { func testJSONFailure() { let string = "{\"foo\": 1, \"bar\": 2, \" 3}" let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let parser = ResponseBodyParser.JSON(readingOptions: nil) + let parser = ResponseBodyParser.JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) switch parser.parseData(data) { case .Success: XCTFail() - case .Failure(let box): - let error = box.value + case .Failure(let error): XCTAssert(error.domain == NSCocoaErrorDomain) XCTAssert(error.code == 3840) } @@ -53,8 +52,8 @@ class ResponseBodyParserTests: XCTestCase { let parser = ResponseBodyParser.URL(encoding: NSUTF8StringEncoding) switch parser.parseData(data) { - case .Success(let box): - let dictionary = box.value as? [String: String] + case .Success(let object): + let dictionary = object as? [String: String] XCTAssert(dictionary?["foo"] == "1") XCTAssert(dictionary?["bar"] == "2") XCTAssert(dictionary?["baz"] == "3") @@ -76,8 +75,8 @@ class ResponseBodyParserTests: XCTestCase { }) switch parser.parseData(data) { - case .Success(let box): - let dictionary = box.value as? [String: Int] + case .Success(let object): + let dictionary = object as? [String: Int] XCTAssert(dictionary?["foo"] == 1) case .Failure: @@ -86,7 +85,7 @@ class ResponseBodyParserTests: XCTestCase { } func testCustomFailure() { - let expectedError = NSError() + let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) let data = NSData() let parser = ResponseBodyParser.Custom(acceptHeader: "", parseData: { data in return .failure(expectedError) @@ -96,8 +95,8 @@ class ResponseBodyParserTests: XCTestCase { case .Success: XCTFail() - case .Failure(let box): - XCTAssert(box.value == expectedError) + case .Failure(let error): + XCTAssert(error == expectedError) } } } diff --git a/Cartfile b/Cartfile index 168c6ce1..a429fa7b 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "antitypical/Result" ~> 0.4 +github "antitypical/Result" ~> 0.5 diff --git a/Cartfile.resolved b/Cartfile.resolved index 1c55e41d..f998aaf1 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,2 @@ -github "robrix/Box" "1.2.2" github "AliSoftware/OHHTTPStubs" "4.0.2" -github "antitypical/Result" "0.4.3" +github "antitypical/Result" "0.5" diff --git a/Carthage/Checkouts/Box b/Carthage/Checkouts/Box deleted file mode 160000 index bbe4e612..00000000 --- a/Carthage/Checkouts/Box +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bbe4e612a03ffe0bbb0e2e476c2be4534b6777a5 diff --git a/Carthage/Checkouts/Result b/Carthage/Checkouts/Result index f9045c2a..665c1779 160000 --- a/Carthage/Checkouts/Result +++ b/Carthage/Checkouts/Result @@ -1 +1 @@ -Subproject commit f9045c2a1fee1af321e29eea7c633be5a6a42532 +Subproject commit 665c1779141d584d9e2f1d5570caaee9e905fd0e diff --git a/DemoApp/Info.plist b/DemoApp/Info.plist index d65f13a1..40c6215d 100644 --- a/DemoApp/Info.plist +++ b/DemoApp/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - -.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName From 9740c6b4a247a4570ea01cc1c0400cdadd1694cb Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Thu, 11 Jun 2015 00:15:35 +0900 Subject: [PATCH 03/44] Express no option by array literal --- APIKit/API.swift | 4 ++-- APIKitTests/APITests.swift | 6 +++--- APIKitTests/RequestBodyBuilderTests.swift | 8 ++++---- APIKitTests/ResponseBodyParserTests.swift | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 6343c47e..05a7d1dd 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -11,11 +11,11 @@ public class API { // How can I pass "no options" to public class var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) + return .JSON(writingOptions: []) } public class var responseBodyParser: ResponseBodyParser { - return .JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) + return .JSON(readingOptions: []) } public class var defaultURLSession: NSURLSession { diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index c0ce06da..b471ca98 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -39,7 +39,7 @@ class APITests: XCTestCase { // MARK: - integration tests func testSuccess() { let dictionary = ["key": "value"] - let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions(rawValue: 0)) + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) OHHTTPStubs.stubRequestsPassingTest({ request in return true @@ -97,7 +97,7 @@ class APITests: XCTestCase { return true }, withStubResponse: { request in let dictionary: [String: String] = [:] - let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions(rawValue: 0)) + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) return OHHTTPStubsResponse(data: data, statusCode: 400, headers: nil) }) @@ -185,7 +185,7 @@ class APITests: XCTestCase { return true }, withStubResponse: { request in let dictionary: [String: String] = [:] - let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions(rawValue: 0)) + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) let response = OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) response.requestTime = 0.1 response.responseTime = 0.1 diff --git a/APIKitTests/RequestBodyBuilderTests.swift b/APIKitTests/RequestBodyBuilderTests.swift index ba76ffe9..dd27b733 100644 --- a/APIKitTests/RequestBodyBuilderTests.swift +++ b/APIKitTests/RequestBodyBuilderTests.swift @@ -5,17 +5,17 @@ import XCTest class RequestBodyBuilderTests: XCTestCase { func testJSONHeader() { - let builder = RequestBodyBuilder.JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) + let builder = RequestBodyBuilder.JSON(writingOptions: []) XCTAssert(builder.contentTypeHeader == "application/json") } func testJSONSuccess() { let object = ["foo": 1, "bar": 2, "baz": 3] - let builder = RequestBodyBuilder.JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) + let builder = RequestBodyBuilder.JSON(writingOptions: []) switch builder.buildBodyFromObject(object) { case .Success(let data): - let dictionary = try! NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(rawValue: 0)) as? [String: Int] + let dictionary = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: Int] XCTAssert(dictionary?["foo"] == 1) XCTAssert(dictionary?["bar"] == 2) XCTAssert(dictionary?["baz"] == 3) @@ -27,7 +27,7 @@ class RequestBodyBuilderTests: XCTestCase { func testJSONFailure() { let object = NSObject() - let builder = RequestBodyBuilder.JSON(writingOptions: NSJSONWritingOptions(rawValue: 0)) + let builder = RequestBodyBuilder.JSON(writingOptions: []) switch builder.buildBodyFromObject(object) { case .Success: diff --git a/APIKitTests/ResponseBodyParserTests.swift b/APIKitTests/ResponseBodyParserTests.swift index 83f097ed..63bbdcde 100644 --- a/APIKitTests/ResponseBodyParserTests.swift +++ b/APIKitTests/ResponseBodyParserTests.swift @@ -5,14 +5,14 @@ import XCTest class ResponseBodyParserTests: XCTestCase { func testJSONAcceptHeader() { - let parser = ResponseBodyParser.JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) + let parser = ResponseBodyParser.JSON(readingOptions: []) XCTAssert(parser.acceptHeader == "application/json") } func testJSONSuccess() { let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let parser = ResponseBodyParser.JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) + let parser = ResponseBodyParser.JSON(readingOptions: []) switch parser.parseData(data) { case .Success(let object): @@ -29,7 +29,7 @@ class ResponseBodyParserTests: XCTestCase { func testJSONFailure() { let string = "{\"foo\": 1, \"bar\": 2, \" 3}" let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let parser = ResponseBodyParser.JSON(readingOptions: NSJSONReadingOptions(rawValue: 0)) + let parser = ResponseBodyParser.JSON(readingOptions: []) switch parser.parseData(data) { case .Success: From 0008a4f69a00eb68e19f7cb4ad6d6dc79761cf74 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Thu, 11 Jun 2015 00:19:39 +0900 Subject: [PATCH 04/44] Disable CI until CircleCI supports Swift 2 --- circle.yml | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/circle.yml b/circle.yml index 981c1a01..dd2d47d7 100644 --- a/circle.yml +++ b/circle.yml @@ -6,27 +6,30 @@ machine: dependencies: override: - - sudo chown :wheel /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ *.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim - - ./script/import-certificates - - sudo gem install cocoapods xcpretty --no-ri --no-rdoc - - brew update - - brew install carthage - - carthage bootstrap --use-submodules --no-use-binaries --no-build + # - sudo chown :wheel /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ *.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim + # - ./script/import-certificates + # - sudo gem install cocoapods xcpretty --no-ri --no-rdoc + # - brew update + # - brew install carthage + # - carthage bootstrap --use-submodules --no-use-binaries --no-build + - echo "disable CI until CircleCI supports Swift 2." cache_directories: - "~/Library/Caches/org.carthage.CarthageKit/binaries/" - "~/Library/Caches/org.carthage.CarthageKit/dependencies/" test: override: - - carthage build --no-skip-current - - pod lib lint - - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace test -scheme APIKit-iOS | xcpretty -c -r junit -o $CIRCLE_TEST_REPORTS/test-report-ios.xml - - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace test -scheme APIKit-Mac | xcpretty -c -r junit -o $CIRCLE_TEST_REPORTS/test-report-mac.xml - - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace build -scheme DemoApp -sdk iphonesimulator | xcpretty -c + # - carthage build --no-skip-current + # - pod lib lint + # - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace test -scheme APIKit-iOS | xcpretty -c -r junit -o $CIRCLE_TEST_REPORTS/test-report-ios.xml + # - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace test -scheme APIKit-Mac | xcpretty -c -r junit -o $CIRCLE_TEST_REPORTS/test-report-mac.xml + # - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace build -scheme DemoApp -sdk iphonesimulator | xcpretty -c + - echo "disable CI until CircleCI supports Swift 2." deployment: master: branch: master commands: - - carthage archive APIKit - - cp APIKit.framework.zip $CIRCLE_ARTIFACTS + # - carthage archive APIKit + # - cp APIKit.framework.zip $CIRCLE_ARTIFACTS + - echo "disable CI until CircleCI supports Swift 2." From 877863fd3b26d8a8075d362128adac41173068f2 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Thu, 11 Jun 2015 18:29:06 +0900 Subject: [PATCH 05/44] Start migrating to new design to optimized for Swift 2 --- .gitignore | 1 + APIKit.xcodeproj/project.pbxproj | 4 + APIKit/API.swift | 150 +++++++++++-------------------- APIKit/Errors.swift | 8 ++ APIKit/Request.swift | 78 ++++++++++++++-- APIKitTests/APITests.swift | 51 ++++++----- 6 files changed, 166 insertions(+), 126 deletions(-) create mode 100644 APIKit/Errors.swift diff --git a/.gitignore b/.gitignore index 6328f9b5..fd4ebee4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ DerivedData *.hmap *.ipa *.xcuserstate +*.xcscmblueprint # CocoaPods # diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index fa8380e2..8a57152e 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; }; 7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; }; 7F0869A81A979088001AD3E1 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* API.swift */; }; + 7F1426CE1B298AA300592D42 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1426CD1B298AA300592D42 /* Errors.swift */; }; 7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; }; @@ -111,6 +112,7 @@ /* Begin PBXFileReference section */ 7F0869941A978790001AD3E1 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerialization.swift; sourceTree = ""; }; + 7F1426CD1B298AA300592D42 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = ""; }; 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilderTests.swift; sourceTree = ""; }; 7F45FCDD1A94D02C006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -216,6 +218,7 @@ 7F45FD171A94D085006863BB /* API.swift */, 7F68ABD91AC4412E00688D68 /* Request.swift */, 7F68ABDC1AC4414500688D68 /* Method.swift */, + 7F1426CD1B298AA300592D42 /* Errors.swift */, 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */, 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */, 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */, @@ -499,6 +502,7 @@ 7F68ABDD1AC4414500688D68 /* Method.swift in Sources */, 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */, 7FCBE9E01A9734950075AFD9 /* ResponseBodyParser.swift in Sources */, + 7F1426CE1B298AA300592D42 /* Errors.swift in Sources */, 7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/APIKit/API.swift b/APIKit/API.swift index 05a7d1dd..34618641 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -4,119 +4,75 @@ import Result public let APIKitErrorDomain = "APIKitErrorDomain" public class API { - // configurations - public class var baseURL: NSURL { - fatalError("API.baseURL must be overrided in subclasses.") - } - - // How can I pass "no options" to - public class var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: []) - } - - public class var responseBodyParser: ResponseBodyParser { - return .JSON(readingOptions: []) - } - public class var defaultURLSession: NSURLSession { return internalDefaultURLSession } - public class var acceptableStatusCodes: Set { - return Set(200..<300) - } - private static let internalDefaultURLSession = NSURLSession( configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: URLSessionDelegate(), delegateQueue: nil ) - /// Creates a NSURLRequest instance from the specified HTTP method, path string - /// and parameters dictionary. - /// - /// Returns a mutable URL request instance which is meant to be modified in - /// subclasses or in `Request` protocol conforming types. - public class func URLRequest(method method: Method, path: String, parameters: [String: AnyObject] = [:], requestBodyBuilder: RequestBodyBuilder = requestBodyBuilder) -> NSMutableURLRequest? { - if let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) { - let request = NSMutableURLRequest() - - switch method { - case .GET, .HEAD, .DELETE: - components.query = URLEncodedSerialization.stringFromObject(parameters, encoding: NSUTF8StringEncoding) - - default: - switch requestBodyBuilder.buildBodyFromObject(parameters) { - case .Success(let result): - request.HTTPBody = result - - case .Failure: - return nil - } - } - - components.path = (components.path ?? "").stringByAppendingPathComponent(path) - request.URL = components.URL - request.HTTPMethod = method.rawValue - request.setValue(requestBodyBuilder.contentTypeHeader, forHTTPHeaderField: "Content-Type") - request.setValue(responseBodyParser.acceptHeader, forHTTPHeaderField: "Accept") - - return request - } else { - return nil - } - } - // send request and build response object public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { let mainQueue = dispatch_get_main_queue() - - if let URLRequest = request.URLRequest { - let task = URLSession.dataTaskWithRequest(URLRequest) - - task?.request = Box(request) - task?.completionHandler = { data, URLResponse, connectionError in - if let error = connectionError { - dispatch_async(mainQueue) { handler(.failure(error)) } - return - } - - let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 - if !self.acceptableStatusCodes.contains(statusCode) { - let error = self.responseBodyParser.parseData(data).analysis( - ifSuccess: { self.responseErrorFromObject($0) }, - ifFailure: { $0 } - ) - - dispatch_async(mainQueue) { handler(.failure(error)) } - return - } - - let mappedResponse: Result = self.responseBodyParser.parseData(data).flatMap { rawResponse in - if let response = T.responseFromObject(rawResponse) { - return .success(response) - } else { - let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - return .failure(error) - } - } - - dispatch_async(mainQueue) { handler(mappedResponse) } - } - - task?.resume() - - return task - } else { + let URLRequest: NSURLRequest + do { + URLRequest = try request.buildURLRequest() + } catch { let userInfo = [NSLocalizedDescriptionKey: "failed to build request."] let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) dispatch_async(mainQueue) { handler(.failure(error)) } - return nil } + + guard let task = URLSession.dataTaskWithRequest(URLRequest) else { + // TODO: throw error + abort() + } + + task.request = Box(request) + task.completionHandler = { data, URLResponse, connectionError in + if let error = connectionError { + dispatch_async(mainQueue) { handler(.failure(error)) } + return + } + + guard let HTTPURLResponse = URLResponse as? NSHTTPURLResponse else { + let userInfo = [NSLocalizedDescriptionKey: "failed to get NSHTTPURLResponse."] + let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + dispatch_async(mainQueue) { handler(.failure(error)) } + return + } + + if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { + let error = request.responseBodyParser.parseData(data).analysis( + ifSuccess: { request.buildErrorFromObject($0, URLResponse: HTTPURLResponse) }, + ifFailure: { $0 } + ) + dispatch_async(mainQueue) { handler(.failure(error)) } + return + } + + let mappedResponse: Result = request.responseBodyParser.parseData(data).flatMap { rawResponse in + do { + return .success(try request.buildResponseFromObject(rawResponse, URLResponse: HTTPURLResponse)) + } catch { + let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."] + let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + return .failure(error) + } + } + + dispatch_async(mainQueue) { handler(mappedResponse) } + } + + task.resume() + + return task } - + public class func cancelRequest(requestType: T.Type, passingTest test: T -> Bool = { r in true }) { cancelRequest(requestType, URLSession: defaultURLSession, passingTest: test) } @@ -160,12 +116,6 @@ public class API { } } } - - public class func responseErrorFromObject(object: AnyObject) -> NSError { - let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - return error - } } // MARK: - default implementation of URLSessionDelegate diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift new file mode 100644 index 00000000..4d5e47fe --- /dev/null +++ b/APIKit/Errors.swift @@ -0,0 +1,8 @@ +import Foundation + +// TODO: more detailed and comprehensive errors +public enum APIKitError: ErrorType { + case IllegalFormatURL + case IllegalParameter + case Unknown +} diff --git a/APIKit/Request.swift b/APIKit/Request.swift index 3043bf93..7b7304c4 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -1,9 +1,77 @@ import Foundation +import Result public protocol Request { - typealias Response: Any - - var URLRequest: NSURLRequest? { get } - - static func responseFromObject(object: AnyObject) -> Response? + // required + typealias Response + var method: Method { get } + var path: String { get } + func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response + + var parameters: [String: AnyObject] { get } + var baseURL: NSURL { get } + var acceptableStatusCodes: Set { get } + var requestBodyBuilder: RequestBodyBuilder { get } + var responseBodyParser: ResponseBodyParser { get } + + func buildURLRequest() throws -> NSURLRequest + func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> NSError +} + +public extension Request { + public var parameters: [String: AnyObject] { + return [:] + } + + public var baseURL: NSURL { + fatalError("Request.baseURL must be overrided.") + } + + public var acceptableStatusCodes: Set { + return Set(200..<300) + } + + public var requestBodyBuilder: RequestBodyBuilder { + return .JSON(writingOptions: []) + } + + public var responseBodyParser: ResponseBodyParser { + return .JSON(readingOptions: []) + } + + public func buildURLRequest() throws -> NSURLRequest { + guard let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { + throw APIKitError.IllegalFormatURL + } + + let request = NSMutableURLRequest() + + switch method { + case .GET, .HEAD, .DELETE: + components.query = URLEncodedSerialization.stringFromObject(parameters, encoding: NSUTF8StringEncoding) + + default: + switch requestBodyBuilder.buildBodyFromObject(parameters) { + case .Success(let result): + request.HTTPBody = result + + case .Failure: + throw APIKitError.IllegalParameter + } + } + + components.path = (components.path ?? "").stringByAppendingPathComponent(path) + request.URL = components.URL + request.HTTPMethod = method.rawValue + request.setValue(requestBodyBuilder.contentTypeHeader, forHTTPHeaderField: "Content-Type") + request.setValue(responseBodyParser.acceptHeader, forHTTPHeaderField: "Accept") + + return request + } + + public func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> NSError { + let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"] + let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + return error + } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index b471ca98..5ba17666 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -3,31 +3,40 @@ import APIKit import XCTest import OHHTTPStubs -class APITests: XCTestCase { - class MockAPI: API { - override class var baseURL: NSURL { - return NSURL(string: "https://api.github.com")! - } - - override class func responseErrorFromObject(object: AnyObject) -> NSError { - return NSError(domain: "MockAPIErrorDomain", code: 10000, userInfo: nil) - } - - class Endpoint { - class Get: Request { - typealias Response = [String: AnyObject] - - var URLRequest: NSURLRequest? { - return MockAPI.URLRequest(method: .GET, path: "/") - } - - class func responseFromObject(object: AnyObject) -> Response? { - return object as? [String: AnyObject] +protocol MockAPIRequest: Request { +} + +extension MockAPIRequest { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } + + func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> NSError { + return NSError(domain: "MockAPIErrorDomain", code: 10000, userInfo: nil) + } +} + +class MockAPI: API { + class Endpoint { + class Get: MockAPIRequest { + typealias Response = [String: AnyObject] + + var method = Method.GET + var path = "/" + + func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let response = object as? [String: AnyObject] else { + throw APIKitError.Unknown } + + return response } } } - +} + +class APITests: XCTestCase { + class AnotherMockAPI: API { } From f2f11d54f93a6f4ed08a782b8c31a98e86810150 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 00:46:45 +0900 Subject: [PATCH 06/44] More errors --- APIKit/API.swift | 56 +++++++++++++++++++--------------- APIKit/Errors.swift | 13 ++++++-- APIKit/Request.swift | 12 +++----- Cartfile | 2 +- Cartfile.resolved | 2 +- Carthage/Checkouts/OHHTTPStubs | 1 - Carthage/Checkouts/Result | 2 +- 7 files changed, 49 insertions(+), 39 deletions(-) delete mode 160000 Carthage/Checkouts/OHHTTPStubs diff --git a/APIKit/API.swift b/APIKit/API.swift index 34618641..ecfcfd9a 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -15,57 +15,63 @@ public class API { ) // send request and build response object - public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { - let mainQueue = dispatch_get_main_queue() + public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { + func notifyError(error: APIKitError) { + let queue = dispatch_get_main_queue() + dispatch_async(queue) { + handler(.failure(error)) + } + } + let URLRequest: NSURLRequest do { URLRequest = try request.buildURLRequest() } catch { - let userInfo = [NSLocalizedDescriptionKey: "failed to build request."] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - dispatch_async(mainQueue) { handler(.failure(error)) } + notifyError(.CannotBuildURLRequest) return nil } guard let task = URLSession.dataTaskWithRequest(URLRequest) else { - // TODO: throw error - abort() + notifyError(.CannotBuildURLSessionTask) + return nil } task.request = Box(request) task.completionHandler = { data, URLResponse, connectionError in if let error = connectionError { - dispatch_async(mainQueue) { handler(.failure(error)) } + notifyError(.ConnectionError(underlyingError: error)) return } guard let HTTPURLResponse = URLResponse as? NSHTTPURLResponse else { - let userInfo = [NSLocalizedDescriptionKey: "failed to get NSHTTPURLResponse."] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - dispatch_async(mainQueue) { handler(.failure(error)) } + notifyError(.NoURLResponse) + return + } + + let parseResult = request.responseBodyParser.parseData(data) + guard let object = parseResult.value else { + // TODO: rewrite without `!` + notifyError(.ResponseBodyParserError(underlyingError: parseResult.error!)) return } if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { - let error = request.responseBodyParser.parseData(data).analysis( - ifSuccess: { request.buildErrorFromObject($0, URLResponse: HTTPURLResponse) }, - ifFailure: { $0 } - ) - dispatch_async(mainQueue) { handler(.failure(error)) } + let error = request.buildErrorFromObject(object, URLResponse: HTTPURLResponse) + notifyError(.ResponseError(underlyingError: error)) return } - let mappedResponse: Result = request.responseBodyParser.parseData(data).flatMap { rawResponse in - do { - return .success(try request.buildResponseFromObject(rawResponse, URLResponse: HTTPURLResponse)) - } catch { - let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - return .failure(error) - } + let response: T.Response + do { + response = try request.buildResponseFromObject(object, URLResponse: HTTPURLResponse) + } catch { + notifyError(.CannotBuildResponseObject(underlyingError: error)) + return } - dispatch_async(mainQueue) { handler(mappedResponse) } + dispatch_async(dispatch_get_main_queue()) { + handler(.success(response)) + } } task.resume() diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift index 4d5e47fe..1ea20353 100644 --- a/APIKit/Errors.swift +++ b/APIKit/Errors.swift @@ -2,7 +2,14 @@ import Foundation // TODO: more detailed and comprehensive errors public enum APIKitError: ErrorType { - case IllegalFormatURL - case IllegalParameter - case Unknown + case InvalidBaseURL + case InvalidParameters + case CannotBuildURLRequest + case CannotBuildURLSessionTask + case CannotBuildResponseObject(underlyingError: ErrorType) + case NoURLResponse + case UnacceptableStatusCode + case ConnectionError(underlyingError: ErrorType) + case ResponseBodyParserError(underlyingError: ErrorType) + case ResponseError(underlyingError: ErrorType) } diff --git a/APIKit/Request.swift b/APIKit/Request.swift index 7b7304c4..7801063a 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -15,7 +15,7 @@ public protocol Request { var responseBodyParser: ResponseBodyParser { get } func buildURLRequest() throws -> NSURLRequest - func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> NSError + func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType } public extension Request { @@ -41,7 +41,7 @@ public extension Request { public func buildURLRequest() throws -> NSURLRequest { guard let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { - throw APIKitError.IllegalFormatURL + throw APIKitError.InvalidBaseURL } let request = NSMutableURLRequest() @@ -56,7 +56,7 @@ public extension Request { request.HTTPBody = result case .Failure: - throw APIKitError.IllegalParameter + throw APIKitError.InvalidParameters } } @@ -69,9 +69,7 @@ public extension Request { return request } - public func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> NSError { - let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - return error + public func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType { + return APIKitError.UnacceptableStatusCode } } diff --git a/Cartfile b/Cartfile index a429fa7b..45118632 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "antitypical/Result" ~> 0.5 +github "antitypical/Result" "ba48b0c054230de5bd3d575b6ab8f8d292f9429a" diff --git a/Cartfile.resolved b/Cartfile.resolved index f998aaf1..08e79902 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ github "AliSoftware/OHHTTPStubs" "4.0.2" -github "antitypical/Result" "0.5" +github "antitypical/Result" "ba48b0c054230de5bd3d575b6ab8f8d292f9429a" diff --git a/Carthage/Checkouts/OHHTTPStubs b/Carthage/Checkouts/OHHTTPStubs deleted file mode 160000 index 2e38ee3e..00000000 --- a/Carthage/Checkouts/OHHTTPStubs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2e38ee3e7a3e1dd01be31edb94368c2f4a840915 diff --git a/Carthage/Checkouts/Result b/Carthage/Checkouts/Result index 665c1779..ba48b0c0 160000 --- a/Carthage/Checkouts/Result +++ b/Carthage/Checkouts/Result @@ -1 +1 @@ -Subproject commit 665c1779141d584d9e2f1d5570caaee9e905fd0e +Subproject commit ba48b0c054230de5bd3d575b6ab8f8d292f9429a From debcda14212f09e98180f27c4ff61f8188ac498d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 00:49:48 +0900 Subject: [PATCH 07/44] Temp commit --- .gitmodules | 6 ------ Carthage/Checkouts/Result | 1 - 2 files changed, 7 deletions(-) delete mode 100644 .gitmodules delete mode 160000 Carthage/Checkouts/Result diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c11e1697..00000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "Carthage/Checkouts/OHHTTPStubs"] - path = Carthage/Checkouts/OHHTTPStubs - url = https://github.com/AliSoftware/OHHTTPStubs.git -[submodule "Carthage/Checkouts/Result"] - path = Carthage/Checkouts/Result - url = https://github.com/antitypical/Result.git diff --git a/Carthage/Checkouts/Result b/Carthage/Checkouts/Result deleted file mode 160000 index ba48b0c0..00000000 --- a/Carthage/Checkouts/Result +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ba48b0c054230de5bd3d575b6ab8f8d292f9429a From bb6b375ecdbd9d501f06ae1fe5ffce06aa3e8664 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 14:12:46 +0900 Subject: [PATCH 08/44] Fix tests --- .gitmodules | 6 +++++ APIKit/API.swift | 3 ++- APIKit/Errors.swift | 5 ++-- APIKit/Request.swift | 6 ++--- APIKitTests/APITests.swift | 47 ++++++++++++++++++++++++++-------- Carthage/Checkouts/OHHTTPStubs | 1 + Carthage/Checkouts/Result | 1 + 7 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 .gitmodules create mode 160000 Carthage/Checkouts/OHHTTPStubs create mode 160000 Carthage/Checkouts/Result diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..30480090 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "Carthage/Checkouts/Result"] + path = Carthage/Checkouts/Result + url = https://github.com/antitypical/Result.git +[submodule "Carthage/Checkouts/OHHTTPStubs"] + path = Carthage/Checkouts/OHHTTPStubs + url = https://github.com/AliSoftware/OHHTTPStubs.git diff --git a/APIKit/API.swift b/APIKit/API.swift index ecfcfd9a..740bd25a 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -19,6 +19,7 @@ public class API { func notifyError(error: APIKitError) { let queue = dispatch_get_main_queue() dispatch_async(queue) { + print(error) handler(.failure(error)) } } @@ -57,7 +58,7 @@ public class API { if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { let error = request.buildErrorFromObject(object, URLResponse: HTTPURLResponse) - notifyError(.ResponseError(underlyingError: error)) + notifyError(.UnacceptableStatusCode(error)) return } diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift index 1ea20353..94ea06f7 100644 --- a/APIKit/Errors.swift +++ b/APIKit/Errors.swift @@ -8,8 +8,7 @@ public enum APIKitError: ErrorType { case CannotBuildURLSessionTask case CannotBuildResponseObject(underlyingError: ErrorType) case NoURLResponse - case UnacceptableStatusCode - case ConnectionError(underlyingError: ErrorType) + case UnacceptableStatusCode(ErrorType?) + case ConnectionError(underlyingError: NSError) case ResponseBodyParserError(underlyingError: ErrorType) - case ResponseError(underlyingError: ErrorType) } diff --git a/APIKit/Request.swift b/APIKit/Request.swift index 7801063a..7bb8abb8 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -15,7 +15,7 @@ public protocol Request { var responseBodyParser: ResponseBodyParser { get } func buildURLRequest() throws -> NSURLRequest - func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType + func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? } public extension Request { @@ -69,7 +69,7 @@ public extension Request { return request } - public func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType { - return APIKitError.UnacceptableStatusCode + public func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + return nil } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 5ba17666..987618a4 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -6,13 +6,17 @@ import OHHTTPStubs protocol MockAPIRequest: Request { } +enum MockAPIErrors: ErrorType { + case Mock +} + extension MockAPIRequest { var baseURL: NSURL { return NSURL(string: "https://api.github.com")! } - func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> NSError { - return NSError(domain: "MockAPIErrorDomain", code: 10000, userInfo: nil) + func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + return MockAPIErrors.Mock } } @@ -26,7 +30,7 @@ class MockAPI: API { func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let response = object as? [String: AnyObject] else { - throw APIKitError.Unknown + throw MockAPIErrors.Mock } return response @@ -92,7 +96,13 @@ class APITests: XCTestCase { XCTFail() case .Failure(let error): - XCTAssert(error.domain == NSURLErrorDomain) + switch error { + case .ConnectionError(let error): + XCTAssert(error.domain == NSURLErrorDomain) + + default: + XCTFail() + } } expectation.fulfill() @@ -119,8 +129,13 @@ class APITests: XCTestCase { XCTFail() case .Failure(let error): - XCTAssert(error.domain == "MockAPIErrorDomain") - XCTAssert(error.code == 10000) + switch error { + case .UnacceptableStatusCode(let error): + XCTAssert(error is MockAPIErrors) + + default: + XCTFail() + } } expectation.fulfill() @@ -147,8 +162,14 @@ class APITests: XCTestCase { XCTFail() case .Failure(let error): - XCTAssert(error.domain == NSCocoaErrorDomain) - XCTAssert(error.code == 3840) + switch error { + case .ResponseBodyParserError(let error as NSError): + XCTAssert(error.domain == NSCocoaErrorDomain) + XCTAssert(error.code == 3840) + + default: + XCTFail() + } } expectation.fulfill() @@ -177,8 +198,14 @@ class APITests: XCTestCase { XCTFail() case .Failure(let error): - XCTAssert(error.domain == NSURLErrorDomain) - XCTAssert(error.code == NSURLErrorCancelled) + switch error { + case .ConnectionError(let error): + XCTAssert(error.domain == NSURLErrorDomain) + XCTAssert(error.code == NSURLErrorCancelled) + + default: + XCTFail() + } } expectation.fulfill() diff --git a/Carthage/Checkouts/OHHTTPStubs b/Carthage/Checkouts/OHHTTPStubs new file mode 160000 index 00000000..2e38ee3e --- /dev/null +++ b/Carthage/Checkouts/OHHTTPStubs @@ -0,0 +1 @@ +Subproject commit 2e38ee3e7a3e1dd01be31edb94368c2f4a840915 diff --git a/Carthage/Checkouts/Result b/Carthage/Checkouts/Result new file mode 160000 index 00000000..ba48b0c0 --- /dev/null +++ b/Carthage/Checkouts/Result @@ -0,0 +1 @@ +Subproject commit ba48b0c054230de5bd3d575b6ab8f8d292f9429a From e0ead8beae3555386a84c459cb1ece6119ddc391 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 18:57:18 +0900 Subject: [PATCH 09/44] Rewrite URLEncodedSerialization using throw in Swift 2 --- .../xcschemes/APIKit-Mac.xcscheme | 1 + .../xcschemes/APIKit-iOS.xcscheme | 1 + APIKit/Request.swift | 2 +- APIKit/RequestBodyBuilder.swift | 9 ++- APIKit/ResponseBodyParser.swift | 9 ++- APIKit/URLEncodedSerialization.swift | 72 +++++++++---------- APIKitTests/RequestBodyBuilderTests.swift | 2 +- 7 files changed, 54 insertions(+), 42 deletions(-) diff --git a/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme index 26e9b4e6..56fe439b 100644 --- a/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme +++ b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme @@ -26,6 +26,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES" buildConfiguration = "Release"> + do { + result = .success(try URLEncodedSerialization.dataFromObject(object, encoding: encoding)) + } catch { + result = .failure(error as NSError) } + return result + case .Custom(let (_, buildBodyFromObject)): return buildBodyFromObject(object) } diff --git a/APIKit/ResponseBodyParser.swift b/APIKit/ResponseBodyParser.swift index 7c04266b..40710ab1 100644 --- a/APIKit/ResponseBodyParser.swift +++ b/APIKit/ResponseBodyParser.swift @@ -37,10 +37,15 @@ public enum ResponseBodyParser { return result case .URL(let encoding): - return `try` { error in - return URLEncodedSerialization.objectFromData(data, encoding: encoding, error: error) + let result: Result + do { + result = .success(try URLEncodedSerialization.objectFromData(data, encoding: encoding)) + } catch { + result = .failure(error as NSError) } + return result + case .Custom(let (_, parseData)): return parseData(data) } diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index 8045b674..a6cd706d 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -9,53 +9,53 @@ private func unescape(string: String) -> String { } public class URLEncodedSerialization { - public class func objectFromData(data: NSData, encoding: NSStringEncoding, error: NSErrorPointer) -> AnyObject? { - var dictionary: [String: AnyObject]? - - if let string = NSString(data: data, encoding: encoding) as? String { - dictionary = [String: AnyObject]() - - for pair in string.componentsSeparatedByString("&") { - let contents = pair.componentsSeparatedByString("=") - - if contents.count == 2 { - dictionary?[contents[0]] = unescape(contents[1]) - } - } + public enum Errors: ErrorType { + case CannotGetStringFromData + case CannotGetDataFromString + case CannotCastObjectToDictionary + case InvalidFormatString + } + + public class func objectFromData(data: NSData, encoding: NSStringEncoding) throws -> AnyObject { + guard let string = NSString(data: data, encoding: encoding) as? String else { + throw Errors.CannotGetStringFromData } - - if dictionary == nil { - let userInfo = [NSLocalizedDescriptionKey: "failed to decode urlencoded string."] - error.memory = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + + var dictionary = [String: AnyObject]() + for pair in string.componentsSeparatedByString("&") { + let contents = pair.componentsSeparatedByString("=") + + guard contents.count == 2 else { + throw Errors.InvalidFormatString + } + + dictionary[contents[0]] = unescape(contents[1]) } - + return dictionary } - // TODO: migrate to Swift 2 error handling - public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding, error: NSErrorPointer) -> NSData? { - let string = stringFromObject(object, encoding: encoding) - let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) - - if data == nil { - let userInfo = [NSLocalizedDescriptionKey: "failed to decode urlencoded string."] - error.memory = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> NSData { + let string = try stringFromObject(object, encoding: encoding) + guard let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) else { + throw Errors.CannotGetDataFromString } - + return data } - public class func stringFromObject(object: AnyObject, encoding: NSStringEncoding) -> String { + public class func stringFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> String { + guard let dictionary = object as? [String: AnyObject] else { + throw Errors.CannotCastObjectToDictionary + } + var pairs = [String]() - - if let dictionary = object as? [String: AnyObject] { - for (key, value) in dictionary { - let string = (value as? String) ?? "\(value)" - let pair = "\(key)=\(escape(string))" - pairs.append(pair) - } + for (key, value) in dictionary { + let valueAsString = (value as? String) ?? "\(value)" + let pair = "\(key)=\(escape(valueAsString))" + pairs.append(pair) } - + return "&".join(pairs) } } diff --git a/APIKitTests/RequestBodyBuilderTests.swift b/APIKitTests/RequestBodyBuilderTests.swift index dd27b733..59a7087a 100644 --- a/APIKitTests/RequestBodyBuilderTests.swift +++ b/APIKitTests/RequestBodyBuilderTests.swift @@ -50,7 +50,7 @@ class RequestBodyBuilderTests: XCTestCase { switch builder.buildBodyFromObject(object) { case .Success(let data): - let dictionary = URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding, error: nil) as? [String: String] + let dictionary = try! URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding) as? [String: String] XCTAssert(dictionary?["foo"] == "1") XCTAssert(dictionary?["bar"] == "2") XCTAssert(dictionary?["baz"] == "3") From 90c8ae6a148e2cd30ebd6358963274d48e501414 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 19:18:44 +0900 Subject: [PATCH 10/44] Rewrite RequestBodyBuilder using throw --- APIKit/Request.swift | 8 +-- APIKit/RequestBodyBuilder.swift | 34 +++-------- APIKit/URLEncodedSerialization.swift | 4 +- APIKitTests/RequestBodyBuilderTests.swift | 74 +++++++++++------------ 4 files changed, 46 insertions(+), 74 deletions(-) diff --git a/APIKit/Request.swift b/APIKit/Request.swift index ffe624ad..ce97edf9 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -51,13 +51,7 @@ public extension Request { components.query = try URLEncodedSerialization.stringFromObject(parameters, encoding: NSUTF8StringEncoding) default: - switch requestBodyBuilder.buildBodyFromObject(parameters) { - case .Success(let result): - request.HTTPBody = result - - case .Failure: - throw APIKitError.InvalidParameters - } + request.HTTPBody = try requestBodyBuilder.buildBodyFromObject(parameters) } components.path = (components.path ?? "").stringByAppendingPathComponent(path) diff --git a/APIKit/RequestBodyBuilder.swift b/APIKit/RequestBodyBuilder.swift index 6ba6ccb9..7003915a 100644 --- a/APIKit/RequestBodyBuilder.swift +++ b/APIKit/RequestBodyBuilder.swift @@ -1,12 +1,10 @@ import Foundation import Result -public let APIKitRequestBodyBuidlerErrorDomain = "APIKitRequestBodyBuidlerErrorDomain" - public enum RequestBodyBuilder { case JSON(writingOptions: NSJSONWritingOptions) case URL(encoding: NSStringEncoding) - case Custom(contentTypeHeader: String, buildBodyFromObject: AnyObject -> Result) + case Custom(contentTypeHeader: String, buildBodyFromObject: AnyObject throws -> NSData) public var contentTypeHeader: String { switch self { @@ -21,37 +19,19 @@ public enum RequestBodyBuilder { } } - // TODO: migrate to Swift 2 style error handling - public func buildBodyFromObject(object: AnyObject) -> Result { + public func buildBodyFromObject(object: AnyObject) throws -> NSData { switch self { case .JSON(let writingOptions): - if !NSJSONSerialization.isValidJSONObject(object) { - let userInfo = [NSLocalizedDescriptionKey: "invalid object for JSON passed."] - let error = NSError(domain: APIKitRequestBodyBuidlerErrorDomain, code: 0, userInfo: userInfo) - return .failure(error) - } - - let result: Result - do { - result = .success(try NSJSONSerialization.dataWithJSONObject(object, options: writingOptions)) - } catch { - result = .failure(error as NSError) + guard NSJSONSerialization.isValidJSONObject(object) else { + throw NSError(domain: NSCocoaErrorDomain, code: 3840, userInfo: nil) } - - return result + return try NSJSONSerialization.dataWithJSONObject(object, options: writingOptions) case .URL(let encoding): - let result: Result - do { - result = .success(try URLEncodedSerialization.dataFromObject(object, encoding: encoding)) - } catch { - result = .failure(error as NSError) - } - - return result + return try URLEncodedSerialization.dataFromObject(object, encoding: encoding) case .Custom(let (_, buildBodyFromObject)): - return buildBodyFromObject(object) + return try buildBodyFromObject(object) } } } diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index a6cd706d..9267b6f1 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -16,12 +16,12 @@ public class URLEncodedSerialization { case InvalidFormatString } - public class func objectFromData(data: NSData, encoding: NSStringEncoding) throws -> AnyObject { + public class func objectFromData(data: NSData, encoding: NSStringEncoding) throws -> [String: String] { guard let string = NSString(data: data, encoding: encoding) as? String else { throw Errors.CannotGetStringFromData } - var dictionary = [String: AnyObject]() + var dictionary = [String: String]() for pair in string.componentsSeparatedByString("&") { let contents = pair.componentsSeparatedByString("=") diff --git a/APIKitTests/RequestBodyBuilderTests.swift b/APIKitTests/RequestBodyBuilderTests.swift index 59a7087a..bafb5f02 100644 --- a/APIKitTests/RequestBodyBuilderTests.swift +++ b/APIKitTests/RequestBodyBuilderTests.swift @@ -13,14 +13,13 @@ class RequestBodyBuilderTests: XCTestCase { let object = ["foo": 1, "bar": 2, "baz": 3] let builder = RequestBodyBuilder.JSON(writingOptions: []) - switch builder.buildBodyFromObject(object) { - case .Success(let data): - let dictionary = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: Int] - XCTAssert(dictionary?["foo"] == 1) - XCTAssert(dictionary?["bar"] == 2) - XCTAssert(dictionary?["baz"] == 3) - - case .Failure: + do { + let data = try builder.buildBodyFromObject(object) + let dictionary = try NSJSONSerialization.JSONObjectWithData(data, options: []) + XCTAssert(dictionary["foo"] == 1) + XCTAssert(dictionary["bar"] == 2) + XCTAssert(dictionary["baz"] == 3) + } catch { XCTFail() } } @@ -29,13 +28,13 @@ class RequestBodyBuilderTests: XCTestCase { let object = NSObject() let builder = RequestBodyBuilder.JSON(writingOptions: []) - switch builder.buildBodyFromObject(object) { - case .Success: + do { + try builder.buildBodyFromObject(object) XCTFail() - - case .Failure(let error): - XCTAssert(error.domain == APIKitRequestBodyBuidlerErrorDomain) - XCTAssert(error.code == 0) + } catch { + let nserror = error as NSError + XCTAssert(nserror.domain == NSCocoaErrorDomain) + XCTAssert(nserror.code == 3840) } } @@ -48,35 +47,35 @@ class RequestBodyBuilderTests: XCTestCase { let object = ["foo": 1, "bar": 2, "baz": 3] let builder = RequestBodyBuilder.URL(encoding: NSUTF8StringEncoding) - switch builder.buildBodyFromObject(object) { - case .Success(let data): - let dictionary = try! URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding) as? [String: String] - XCTAssert(dictionary?["foo"] == "1") - XCTAssert(dictionary?["bar"] == "2") - XCTAssert(dictionary?["baz"] == "3") - - case .Failure: + do { + let data = try builder.buildBodyFromObject(object) + let dictionary = try URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding) + XCTAssert(dictionary["foo"] == "1") + XCTAssert(dictionary["bar"] == "2") + XCTAssert(dictionary["baz"] == "3") + } catch { XCTFail() } } func testCustomHeader() { - let builder = RequestBodyBuilder.Custom(contentTypeHeader: "foo", buildBodyFromObject: { o in .success(o as! NSData) }) + let builder = RequestBodyBuilder.Custom(contentTypeHeader: "foo") { object in + NSData() + } XCTAssert(builder.contentTypeHeader == "foo") } func testCustomSuccess() { let string = "foo" let expectedData = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let builder = RequestBodyBuilder.Custom(contentTypeHeader: "", buildBodyFromObject: { object in - return .success(expectedData) - }) + let builder = RequestBodyBuilder.Custom(contentTypeHeader: "") { object in + expectedData + } - switch builder.buildBodyFromObject(string) { - case .Success(let data): + do { + let data = try builder.buildBodyFromObject(string) XCTAssert(data == expectedData) - - case .Failure: + } catch { XCTFail() } } @@ -84,16 +83,15 @@ class RequestBodyBuilderTests: XCTestCase { func testCustomFailure() { let string = "foo" let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) - let builder = RequestBodyBuilder.Custom(contentTypeHeader: "", buildBodyFromObject: { object in - return .failure(expectedError) - }) + let builder = RequestBodyBuilder.Custom(contentTypeHeader: "") { object in + throw expectedError + } - switch builder.buildBodyFromObject(string) { - case .Success: + do { + try builder.buildBodyFromObject(string) XCTFail() - - case .Failure(let error): - XCTAssert(error == expectedError) + } catch { + XCTAssert((error as NSError) == expectedError) } } } From 9a8c467cf7ea07d395ad005fa1258d750833b1fc Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 19:55:11 +0900 Subject: [PATCH 11/44] Rewrite ResponseBodyParser using throw --- APIKit/API.swift | 36 ++++++-------- APIKit/Errors.swift | 1 - APIKit/ResponseBodyParser.swift | 28 +++-------- APIKitTests/APITests.swift | 2 +- APIKitTests/ResponseBodyParserTests.swift | 58 +++++++++++------------ 5 files changed, 50 insertions(+), 75 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 740bd25a..ba7de7d3 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -49,29 +49,23 @@ public class API { return } - let parseResult = request.responseBodyParser.parseData(data) - guard let object = parseResult.value else { - // TODO: rewrite without `!` - notifyError(.ResponseBodyParserError(underlyingError: parseResult.error!)) - return - } - - if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { - let error = request.buildErrorFromObject(object, URLResponse: HTTPURLResponse) - notifyError(.UnacceptableStatusCode(error)) - return - } - - let response: T.Response do { - response = try request.buildResponseFromObject(object, URLResponse: HTTPURLResponse) - } catch { - notifyError(.CannotBuildResponseObject(underlyingError: error)) - return - } + let object = try request.responseBodyParser.parseData(data) + if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { + let error = request.buildErrorFromObject(object, URLResponse: HTTPURLResponse) + throw APIKitError.UnacceptableStatusCode(error) + } - dispatch_async(dispatch_get_main_queue()) { - handler(.success(response)) + let response = try request.buildResponseFromObject(object, URLResponse: HTTPURLResponse) + dispatch_async(dispatch_get_main_queue()) { + handler(.success(response)) + } + } catch { + if let e = error as? APIKitError { + notifyError(e) + } else { + notifyError(.CannotBuildResponseObject(underlyingError: error)) + } } } diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift index 94ea06f7..88f1ab79 100644 --- a/APIKit/Errors.swift +++ b/APIKit/Errors.swift @@ -10,5 +10,4 @@ public enum APIKitError: ErrorType { case NoURLResponse case UnacceptableStatusCode(ErrorType?) case ConnectionError(underlyingError: NSError) - case ResponseBodyParserError(underlyingError: ErrorType) } diff --git a/APIKit/ResponseBodyParser.swift b/APIKit/ResponseBodyParser.swift index 40710ab1..09c84ae9 100644 --- a/APIKit/ResponseBodyParser.swift +++ b/APIKit/ResponseBodyParser.swift @@ -4,7 +4,7 @@ import Result public enum ResponseBodyParser { case JSON(readingOptions: NSJSONReadingOptions) case URL(encoding: NSStringEncoding) - case Custom(acceptHeader: String, parseData: NSData -> Result) + case Custom(acceptHeader: String, parseData: NSData throws -> AnyObject) public var acceptHeader: String { switch self { @@ -19,35 +19,19 @@ public enum ResponseBodyParser { } } - // TODO: migrate to Swift 2 style error handling - public func parseData(data: NSData) -> Result { + public func parseData(data: NSData) throws -> AnyObject { switch self { case .JSON(let readingOptions): if data.length == 0 { - return .success([:]) + return [:] } - - let result: Result - do { - result = .success(try NSJSONSerialization.JSONObjectWithData(data, options: readingOptions)) - } catch { - result = .failure(error as NSError) - } - - return result + return try NSJSONSerialization.JSONObjectWithData(data, options: readingOptions) case .URL(let encoding): - let result: Result - do { - result = .success(try URLEncodedSerialization.objectFromData(data, encoding: encoding)) - } catch { - result = .failure(error as NSError) - } - - return result + return try URLEncodedSerialization.objectFromData(data, encoding: encoding) case .Custom(let (_, parseData)): - return parseData(data) + return try parseData(data) } } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 987618a4..028c4945 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -163,7 +163,7 @@ class APITests: XCTestCase { case .Failure(let error): switch error { - case .ResponseBodyParserError(let error as NSError): + case .CannotBuildResponseObject(let error as NSError): XCTAssert(error.domain == NSCocoaErrorDomain) XCTAssert(error.code == 3840) diff --git a/APIKitTests/ResponseBodyParserTests.swift b/APIKitTests/ResponseBodyParserTests.swift index 63bbdcde..bf0d19e1 100644 --- a/APIKitTests/ResponseBodyParserTests.swift +++ b/APIKitTests/ResponseBodyParserTests.swift @@ -14,14 +14,13 @@ class ResponseBodyParserTests: XCTestCase { let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! let parser = ResponseBodyParser.JSON(readingOptions: []) - switch parser.parseData(data) { - case .Success(let object): + do { + let object = try parser.parseData(data) let dictionary = object as? [String: Int] XCTAssert(dictionary?["foo"] == 1) XCTAssert(dictionary?["bar"] == 2) XCTAssert(dictionary?["baz"] == 3) - - case .Failure: + } catch { XCTFail() } } @@ -31,13 +30,13 @@ class ResponseBodyParserTests: XCTestCase { let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! let parser = ResponseBodyParser.JSON(readingOptions: []) - switch parser.parseData(data) { - case .Success: + do { + try parser.parseData(data) XCTFail() - - case .Failure(let error): - XCTAssert(error.domain == NSCocoaErrorDomain) - XCTAssert(error.code == 3840) + } catch { + let nserror = error as NSError + XCTAssert(nserror.domain == NSCocoaErrorDomain) + XCTAssert(nserror.code == 3840) } } @@ -51,35 +50,35 @@ class ResponseBodyParserTests: XCTestCase { let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! let parser = ResponseBodyParser.URL(encoding: NSUTF8StringEncoding) - switch parser.parseData(data) { - case .Success(let object): + do { + let object = try parser.parseData(data) let dictionary = object as? [String: String] XCTAssert(dictionary?["foo"] == "1") XCTAssert(dictionary?["bar"] == "2") XCTAssert(dictionary?["baz"] == "3") - - case .Failure: + } catch { XCTFail() } } func testCustomAcceptHeader() { - let parser = ResponseBodyParser.Custom(acceptHeader: "foo", parseData: { d in .success(d) }) + let parser = ResponseBodyParser.Custom(acceptHeader: "foo") { data in + data + } XCTAssert(parser.acceptHeader == "foo") } func testCustomSuccess() { let data = NSData() - let parser = ResponseBodyParser.Custom(acceptHeader: "", parseData: { data in - return .success(["foo": 1]) - }) + let parser = ResponseBodyParser.Custom(acceptHeader: "") { data in + ["foo": 1] + } - switch parser.parseData(data) { - case .Success(let object): + do { + let object = try parser.parseData(data) let dictionary = object as? [String: Int] XCTAssert(dictionary?["foo"] == 1) - - case .Failure: + } catch { XCTFail() } } @@ -87,16 +86,15 @@ class ResponseBodyParserTests: XCTestCase { func testCustomFailure() { let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) let data = NSData() - let parser = ResponseBodyParser.Custom(acceptHeader: "", parseData: { data in - return .failure(expectedError) - }) + let parser = ResponseBodyParser.Custom(acceptHeader: "") { data in + throw expectedError + } - switch parser.parseData(data) { - case .Success: + do { + try parser.parseData(data) XCTFail() - - case .Failure(let error): - XCTAssert(error == expectedError) + } catch { + XCTAssert((error as NSError) == expectedError) } } } From 48c97ba174bbd8cccb9d911dde8296751bfb758a Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 13 Jun 2015 22:30:50 +0900 Subject: [PATCH 12/44] Update README --- README.md | 289 +++++++++++++++++++++++++++++------------------------- 1 file changed, 158 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 8586e60c..99ea2c0f 100644 --- a/README.md +++ b/README.md @@ -4,37 +4,24 @@ APIKit [![Circle CI](https://img.shields.io/circleci/project/ishkawa/APIKit/master.svg?style=flat)](https://circleci.com/gh/ishkawa/APIKit) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -APIKit is a networking library for building type safe web API client in Swift. -By taking advantage of Swift, APIKit provides following features: +APIKit is a library for building type safe web API client in Swift. -- Enumerate all endpoints in nested class. -- Validate request parameters by type. -- Associate type of response with type of request using generics. -- Return model object or `NSError` as a non-optional value in handler (thanks to [Result](https://github.com/antitypical/Result)). - -so you can: - -- Call API without looking API documentation. -- Receive response as a non-optional model object. -- Write exhaustive completion handler easily. - -See the demo code below to understand good points of APIKit. +- Parameters of a request are validated by type system. +- Type of a response is inferred from type of its request. +- A result of request is represented by [Result](https://github.com/antitypical/Result), which is also known as Either. +- All the endpoints can be enumerated in nested class. ```swift -// parameters of request are validated by type system of Swift let request = GitHub.Endpoint.SearchRepositories(query: "APIKit", sort: .Stars) -GitHub.sendRequest(request) { response in - // no optional bindings are required to get response and error (thanks to Result) - switch response { - case .Success(let box): - // type of response is inferred from type of request - self.repositories = box.value - self.tableView?.reloadData() - - case .Failure(let box): - // if request fails, value in box is a NSError - println(box.value) +GitHub.sendRequest(request) { result in + switch result { + case .Success(let response): + self.repositories = response // inferred as [Repository] + self.tableView.reloadData() + + case .Failure(let error): + println(error) } } ``` @@ -42,125 +29,81 @@ GitHub.sendRequest(request) { response in ## Requirements -- Swift 1.2 +- Swift 2 - iOS 8.0 or later - Mac OS 10.9 or later -If you want to use APIKit with Swift 1.1, try [0.6.0](https://github.com/ishkawa/APIKit/releases/tag/0.6.0). +If you want to use APIKit with Swift 1.2, try [0.8.2](https://github.com/ishkawa/APIKit/releases/tag/0.8.2). ## Installation -You have 3 choices. - -#### 1. Using Carthage (Recommended) +#### [Carthage](https://github.com/Carthage/Carthage) - Insert `github "ishkawa/APIKit"` to your Cartfile. - Run `carthage update`. -- Drag `Carthage/Build` to your project. -- Select "General" tab in xcodeproj. -- Add frameworks below to "Embedded Binaries", and confirm they are also in "Linked Frameworks and Libraries". - - APIKit.framework - - Result.framework - - Box.framework +- Link your app with `APIKit.framework` and `Result.framework` in `Carthage/Checkouts`. -#### 2. Using CocoaPods +#### [CocoaPods](https://github.com/cocoapods/cocoapods) -- Insert `use_frameworks!` to your Podfile. - Insert `pod "APIKit"` to your Podfile. - Run `pod install`. -#### 3. Embedding project - -- Clone this repository: `git clone --recursive https://github.com/ishkawa/APIKit.git` -- Drag xcodeproj below to your project. The destination must be under your xcodeproj. - - `APIKit.xcodeproj` - - `Carthage/Checkouts/Result/Result.xcodeproj` - - `Carthage/Checkouts/Box/Box.xcodeproj` -- Select "Build Phase" in xcodeproj. -- Add build targets below to "Target Dependencies". If you develop Mac App, replace "iOS" in target name with "Mac". - - APIKit-iOS - - Result-iOS - - Box-iOS -- Select "General" tab in xcodeproj. -- Add frameworks below to "Embedded Binaries", and confirm they are also in "Linked Frameworks and Libraries". - - APIKit.framework - - Result.framework - - Box.framework - ## Usage -1. Create subclass of `API` that represents target web API. -2. Set base URL by overriding `baseURL`. -3. Set encoding of request body by overriding `requestBodyBuilder`. -4. Set encoding of response body by overriding `responseBodyParser`. -5. Define request classes that conforms to `Request` for each endpoints. - -### Example +1. Create a request protocol that inherits `Request` protocol. +2. Add `baseURL` variable in extension of request protocol. +3. Create a API class that inherits `API` class. +4. Define request types that conforms to request protocol in `Endpoint` class in API class. + 1. Create a type that represents a endpoint of the web API. + 2. Assign type that represents response object to `Response` typealiase. + 3. Add `method` and `path` variables. + 4. Implement `buildResponseFromObject(_:URLResponse:)` to build `Response` from raw object, which may be an array or a dictionary. ```swift -class GitHub: API { - override class var baseURL: NSURL { - return NSURL(string: "https://api.github.com")! - } - - override class var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: nil) - } +protocol GitHubRequest: Request { +} - override class var responseBodyParser: ResponseBodyParser { - return .JSON(readingOptions: nil) +extension GitHubRequest { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! } +} - class Endpoint { - // https://developer.github.com/v3/search/#search-repositories - class SearchRepositories: Request { - enum Sort: String { - case Stars = "stars" - case Forks = "forks" - case Updated = "updated" - } +class GitHubAPI: API { +} - enum Order: String { - case Ascending = "asc" - case Descending = "desc" - } +extension GitHubAPI.Endpoint { + struct GetRateLimit: GitHubRequest { + typealiase Response = RateLimit - typealias Response = [Repository] + var method: Method { + return .GET + } - let query: String - let sort: Sort - let order: Order + var path: String { + return "/rate_limit" + } - var URLRequest: NSURLRequest? { - return GitHub.URLRequest( - method: .GET, - path: "/search/repositories", - parameters: ["q": query, "sort": sort.rawValue, "order": order.rawValue] - ) + func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let dictionary = object as? [String: AnyObject] else { + throw SomeError } - init(query: String, sort: Sort = .Stars, order: Order = .Ascending) { - self.query = query - self.sort = sort - self.order = order + guard let rateLimit = RateLimit(dictionary) else { + throw SomeError } - class func responseFromObject(object: AnyObject) -> Response? { - var repositories = [Repository]() - - if let dictionaries = object["items"] as? [NSDictionary] { - for dictionary in dictionaries { - if let repository = Repository(dictionary: dictionary) { - repositories.append(repository) - } - } - } - - return repositories - } + return rateLimit } + } +} - // define other requests here +struct RateLimit { + let count: Int + let resetDate: NSDate + + init?(dictionary: [String: AnyObject]) { + ... } } ``` @@ -168,15 +111,16 @@ class GitHub: API { ### Sending request ```swift -let request = GitHub.Endpoint.SearchRepositories(query: "APIKit", sort: .Stars) +let request = GitHubAPI.Endpoint.GetRateLimit() -GitHub.sendRequest(request) { response in - switch response { - case .Success(let box): - // type of `box.value` is `[Repository]` (model object) +GitHubAPI.sendRequest(request) { result in + switch result { + case .Success(let rateLimit): + print("count: \(rateLimit.count)") + print("resetDate: \(rateLimit.resetDate)") - case .Failure(let box): - // type of `box.value` is `NSError` + case .Failure(let error): + print("error: \(error)") } } ``` @@ -184,7 +128,7 @@ GitHub.sendRequest(request) { response in ### Canceling request ```swift -GitHub.cancelRequest(GitHub.Endpoint.SearchRepositories.self) +GitHub.cancelRequest(GitHub.Endpoint.RateLimit) ``` If you want to filter requests to be cancelled, add closure that identifies the request shoule be cancelled or not. @@ -195,9 +139,25 @@ GitHub.cancelRequest(GitHub.Endpoint.SearchRepositories.self) { request in } ``` -## Advanced usage +### Configuring request + +#### Setting parameters +#### Setting serializer of a request +#### Setting serializer of a response +#### Adding fields to HTTP header of a request +#### Building NSURLRequest manually + +### Configuring response + +#### Setting acceptable status code + +```swift +var acceptableStatusCodes: Set { + return Set(200) +} +``` -### Creating NSError from response object +#### Building custom error from a response You can create detailed error using response object from Web API. For example, [GitHub API](https://developer.github.com/v3/#client-errors) returns error like this: @@ -211,17 +171,84 @@ For example, [GitHub API](https://developer.github.com/v3/#client-errors) return To create error that contains `message` in response, override `API.responseErrorFromObject(object:)` and return `NSError` using response object. ```swift -public override class func responseErrorFromObject(object: AnyObject) -> NSError { - if let message = (object as? NSDictionary)?["message"] as? String { - let userInfo = [NSLocalizedDescriptionKey: message] - return NSError(domain: "YourAppAPIErrorDomain", code: 40000, userInfo: userInfo) - } else { - let userInfo = [NSLocalizedDescriptionKey: "unresolved error occurred."] - return NSError(domain: "YourAppAPIErrorDomain", code: 40001, userInfo: userInfo) +func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { + guard let dictionary = object as? [String: AnyObject] else { + throw SomeError } + + guard let message = dictionary["message"] as? String else { + throw SomeError + } + + return GitHubError(message: message) } ``` +## Practical Example + +### Pagination + +```swift +let request = SomeAPI.Endpoint.SomePaginatedRequest(page: 1) + +SomeAPI.sendRequest(request) { result in + switch result { + case .Success(let response): + print("results: \(response.results)") + print("nextPage: \(response.nextPage)") + print("hasNext: \(response.hasNext)") + + case .Failure(let error): + print("error: \(error)") + } +} +``` + +```swift +struct PaginatedResponse { + var results: Array + var nextPage: Int { get } + var hasNext: Bool { get } + + init(results: Array, URLResponse: NSHTTPURLResponse) { + self.results = results + self.nextPage = /* get nextPage from `Link` field of URLResponse */ + self.hasNext = /* get hasNext from `Link` field of URLResponse */ + } +} + +struct SomePaginatedRequest: Request { + typealias Response = PaginatedResponse + + var method: Method { + return .GET + } + + var path: String { + return "/paginated" + } + + let page: Int + + static func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let dictionaries = object as? [[String: AnyObject]] else { + throw SomeError + } + + var somes = [Some]() + for dictionary in dictionaries { + if let some = Some(dictionary: dictionary) { + somes.append() + } + } + + return PaginatedResponse(results: somes, URLResponse: URLResponse) + } +} +``` + +## Advanced usage + ### NSURLSessionDelegate You can add custom behaviors of `NSURLSession` by following steps: From 378669780682ff461c2c0907bf50ee1a47d17780 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 22:00:26 +0900 Subject: [PATCH 13/44] Update MockAPI in tests --- APIKitTests/APITests.swift | 53 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 028c4945..56b2bf5b 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -6,35 +6,38 @@ import OHHTTPStubs protocol MockAPIRequest: Request { } -enum MockAPIErrors: ErrorType { - case Mock -} - extension MockAPIRequest { var baseURL: NSURL { return NSURL(string: "https://api.github.com")! } func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { - return MockAPIErrors.Mock + return MockAPI.Errrors.Mock } } class MockAPI: API { - class Endpoint { - class Get: MockAPIRequest { - typealias Response = [String: AnyObject] + enum Errrors: ErrorType { + case Mock + } - var method = Method.GET - var path = "/" + struct GetRoot: MockAPIRequest { + typealias Response = [String: AnyObject] - func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { - guard let response = object as? [String: AnyObject] else { - throw MockAPIErrors.Mock - } + var method: APIKit.Method { + return .GET + } - return response + var path: String { + return "/" + } + + func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let response = object as? [String: AnyObject] else { + throw Errrors.Mock } + + return response } } } @@ -61,7 +64,7 @@ class APITests: XCTestCase { }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() + let request = MockAPI.GetRoot() MockAPI.sendRequest(request) { response in switch response { @@ -88,8 +91,8 @@ class APITests: XCTestCase { }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() - + let request = MockAPI.GetRoot() + MockAPI.sendRequest(request) { response in switch response { case .Success: @@ -121,7 +124,7 @@ class APITests: XCTestCase { }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() + let request = MockAPI.GetRoot() MockAPI.sendRequest(request) { response in switch response { @@ -131,7 +134,7 @@ class APITests: XCTestCase { case .Failure(let error): switch error { case .UnacceptableStatusCode(let error): - XCTAssert(error is MockAPIErrors) + XCTAssert(error is MockAPI.Errrors) default: XCTFail() @@ -154,7 +157,7 @@ class APITests: XCTestCase { }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() + let request = MockAPI.GetRoot() MockAPI.sendRequest(request) { response in switch response { @@ -190,7 +193,7 @@ class APITests: XCTestCase { }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() + let request = MockAPI.GetRoot() MockAPI.sendRequest(request) { response in switch response { @@ -211,7 +214,7 @@ class APITests: XCTestCase { expectation.fulfill() } - MockAPI.cancelRequest(MockAPI.Endpoint.Get.self) + MockAPI.cancelRequest(MockAPI.GetRoot.self) waitForExpectationsWithTimeout(1.0, handler: nil) } @@ -229,7 +232,7 @@ class APITests: XCTestCase { }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() + let request = MockAPI.GetRoot() MockAPI.sendRequest(request) { response in switch response { @@ -243,7 +246,7 @@ class APITests: XCTestCase { expectation.fulfill() } - MockAPI.cancelRequest(MockAPI.Endpoint.Get.self) { request in + MockAPI.cancelRequest(MockAPI.GetRoot.self) { request in return false } From 9236f192a6d403252124b145f920ce4576a2c739 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 22:02:58 +0900 Subject: [PATCH 14/44] Fix typo --- APIKitTests/APITests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 56b2bf5b..117a64b2 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -12,12 +12,12 @@ extension MockAPIRequest { } func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { - return MockAPI.Errrors.Mock + return MockAPI.Errors.Mock } } class MockAPI: API { - enum Errrors: ErrorType { + enum Errors: ErrorType { case Mock } @@ -34,7 +34,7 @@ class MockAPI: API { func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let response = object as? [String: AnyObject] else { - throw Errrors.Mock + throw Errors.Mock } return response @@ -134,7 +134,7 @@ class APITests: XCTestCase { case .Failure(let error): switch error { case .UnacceptableStatusCode(let error): - XCTAssert(error is MockAPI.Errrors) + XCTAssert(error is MockAPI.Errors) default: XCTFail() From e2117a463e202df6814698c6e3646002f41c6e9c Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 22:30:44 +0900 Subject: [PATCH 15/44] Remove "build" from method name --- APIKit/API.swift | 4 ++-- APIKit/Request.swift | 34 ++++++++++++++++++++++++---------- APIKitTests/APITests.swift | 4 ++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index ba7de7d3..219eac51 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -52,11 +52,11 @@ public class API { do { let object = try request.responseBodyParser.parseData(data) if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { - let error = request.buildErrorFromObject(object, URLResponse: HTTPURLResponse) + let error = request.errorFromObject(object, URLResponse: HTTPURLResponse) throw APIKitError.UnacceptableStatusCode(error) } - let response = try request.buildResponseFromObject(object, URLResponse: HTTPURLResponse) + let response = try request.responseFromObject(object, URLResponse: HTTPURLResponse) dispatch_async(dispatch_get_main_queue()) { handler(.success(response)) } diff --git a/APIKit/Request.swift b/APIKit/Request.swift index ce97edf9..c0d4d308 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -1,21 +1,29 @@ import Foundation import Result +/// Request protocol represents a request for Web API. +/// Following 4 items must be implemented. +/// - typealias Response +/// - var method: Method +/// - var path: String +/// - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response public protocol Request { - // required + /// Type represents a model object typealias Response + + /// Configurations of request var method: Method { get } var path: String { get } - func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response - var parameters: [String: AnyObject] { get } + var baseURL: NSURL { get } var acceptableStatusCodes: Set { get } var requestBodyBuilder: RequestBodyBuilder { get } var responseBodyParser: ResponseBodyParser { get } - func buildURLRequest() throws -> NSURLRequest - func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? + func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response + func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? } public extension Request { @@ -39,7 +47,15 @@ public extension Request { return .JSON(readingOptions: []) } - public func buildURLRequest() throws -> NSURLRequest { + public func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { + return URLRequest + } + + public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + return nil + } + + internal func buildURLRequest() throws -> NSURLRequest { guard let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { throw APIKitError.InvalidBaseURL } @@ -60,10 +76,8 @@ public extension Request { request.setValue(requestBodyBuilder.contentTypeHeader, forHTTPHeaderField: "Content-Type") request.setValue(responseBodyParser.acceptHeader, forHTTPHeaderField: "Accept") - return request - } + try configureURLRequest(request) - public func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { - return nil + return request } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 117a64b2..bfba5a94 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -11,7 +11,7 @@ extension MockAPIRequest { return NSURL(string: "https://api.github.com")! } - func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { return MockAPI.Errors.Mock } } @@ -32,7 +32,7 @@ class MockAPI: API { return "/" } - func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let response = object as? [String: AnyObject] else { throw Errors.Mock } From 8af7ddd317b06fae3bb5f9e608d5d5caa0719b37 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 22:46:26 +0900 Subject: [PATCH 16/44] More documentation in Request protocol --- APIKit/Request.swift | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/APIKit/Request.swift b/APIKit/Request.swift index c0d4d308..e6b5a132 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -2,8 +2,9 @@ import Foundation import Result /// Request protocol represents a request for Web API. -/// Following 4 items must be implemented. +/// Following 5 items must be implemented. /// - typealias Response +/// - var baseURL: NSURL /// - var method: Method /// - var path: String /// - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response @@ -12,17 +13,31 @@ public protocol Request { typealias Response /// Configurations of request + var baseURL: NSURL { get } var method: Method { get } var path: String { get } var parameters: [String: AnyObject] { get } - var baseURL: NSURL { get } + /// You can add any configurations here + func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest + + /// Set of status code that indicates success. + /// `responseFromObject(_:URLResponse:)` will be called if this contains NSHTTPURLResponse.statusCode. + /// Otherwise, `errorFromObject(_:URLResponse:)` will be called. var acceptableStatusCodes: Set { get } + + /// An object that builds body of HTTP request. var requestBodyBuilder: RequestBodyBuilder { get } + + /// An object that parses body of HTTP response. var responseBodyParser: ResponseBodyParser { get } - func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest + /// Build `Response` instance from raw response object. + /// This method will be called if `acceptableStatusCode` contains status code of NSHTTPURLResponse. func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response + + /// Build `ErrorType` instance from raw response object. + /// This method will be called if `acceptableStatusCode` does not contain status code of NSHTTPURLResponse. func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? } From 58680ae41c7fbc3c77b5ef73057ef6b57106c218 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 22:48:06 +0900 Subject: [PATCH 17/44] Remove APIKitErrorDomain --- APIKit/API.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 219eac51..0dcb118c 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -1,8 +1,6 @@ import Foundation import Result -public let APIKitErrorDomain = "APIKitErrorDomain" - public class API { public class var defaultURLSession: NSURLSession { return internalDefaultURLSession From bbdefa9ac3026954b12eb7b0468b735fcfa0b856 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 22:49:55 +0900 Subject: [PATCH 18/44] Remove default implementation of baseURL --- APIKit/Request.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/APIKit/Request.swift b/APIKit/Request.swift index e6b5a132..c6f5134e 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -41,15 +41,12 @@ public protocol Request { func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? } +/// Default implementation of Request protocol public extension Request { public var parameters: [String: AnyObject] { return [:] } - public var baseURL: NSURL { - fatalError("Request.baseURL must be overrided.") - } - public var acceptableStatusCodes: Set { return Set(200..<300) } From 9864e60b0de538d0470b785613b1e7dddb865316 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 23:19:13 +0900 Subject: [PATCH 19/44] Replace DemoApp with playground --- APIKit.xcodeproj/project.pbxproj | 67 +---------- APIKit/Method.swift | 2 +- APIKit/Request.swift | 2 +- APIKitTests/APITests.swift | 2 +- Demo.playground/Contents.swift | 82 ++++++++++++++ Demo.playground/contents.xcplayground | 4 + Demo.playground/timeline.xctimeline | 6 + DemoApp/AppDelegate.swift | 11 -- DemoApp/Base.lproj/LaunchScreen.xib | 41 ------- DemoApp/Base.lproj/Main.storyboard | 59 ---------- DemoApp/GitHub.swift | 106 ------------------ .../AppIcon.appiconset/Contents.json | 68 ----------- DemoApp/Info.plist | 47 -------- DemoApp/Models.swift | 41 ------- DemoApp/ViewController.swift | 42 ------- 15 files changed, 97 insertions(+), 483 deletions(-) create mode 100644 Demo.playground/Contents.swift create mode 100644 Demo.playground/contents.xcplayground create mode 100644 Demo.playground/timeline.xctimeline delete mode 100644 DemoApp/AppDelegate.swift delete mode 100644 DemoApp/Base.lproj/LaunchScreen.xib delete mode 100644 DemoApp/Base.lproj/Main.storyboard delete mode 100644 DemoApp/GitHub.swift delete mode 100644 DemoApp/Images.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 DemoApp/Info.plist delete mode 100644 DemoApp/Models.swift delete mode 100644 DemoApp/ViewController.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 8a57152e..238a47cc 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -18,15 +18,8 @@ 7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; }; 7F45FD181A94D085006863BB /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* API.swift */; }; - 7F45FD4D1A94D9A8006863BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD4C1A94D9A8006863BB /* AppDelegate.swift */; }; - 7F45FD4F1A94D9A9006863BB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD4E1A94D9A9006863BB /* ViewController.swift */; }; - 7F45FD521A94D9A9006863BB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F45FD501A94D9A9006863BB /* Main.storyboard */; }; - 7F45FD541A94D9A9006863BB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7F45FD531A94D9A9006863BB /* Images.xcassets */; }; - 7F45FD571A94D9A9006863BB /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F45FD551A94D9A9006863BB /* LaunchScreen.xib */; }; - 7F45FD6B1A94D9F9006863BB /* GitHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD6A1A94D9F9006863BB /* GitHub.swift */; }; 7F45FD6C1A94DA28006863BB /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; }; 7F45FD6D1A94DA28006863BB /* APIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 7F45FD741A94E832006863BB /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD731A94E832006863BB /* Models.swift */; }; 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDD1AC4414500688D68 /* Method.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* Method.swift */; }; @@ -121,14 +114,6 @@ 7F45FCFE1A94D04D006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7F45FD171A94D085006863BB /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 7F45FD481A94D9A8006863BB /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7F45FD4B1A94D9A8006863BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7F45FD4C1A94D9A8006863BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7F45FD4E1A94D9A9006863BB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 7F45FD511A94D9A9006863BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 7F45FD531A94D9A9006863BB /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 7F45FD561A94D9A9006863BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 7F45FD6A1A94D9F9006863BB /* GitHub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHub.swift; sourceTree = ""; }; - 7F45FD731A94E832006863BB /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; 7F68ABD91AC4412E00688D68 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 7F68ABDC1AC4414500688D68 /* Method.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Method.swift; sourceTree = ""; }; 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = ""; }; @@ -136,6 +121,7 @@ 7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7FEC5A171A96FE2600B1D3C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseBodyParserTests.swift; sourceTree = ""; }; + 7FED2F111B34565D002AC9D7 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; }; CD5115241B1FFBA900514240 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -192,9 +178,9 @@ 7F45FCD31A94D02C006863BB = { isa = PBXGroup; children = ( + 7FED2F111B34565D002AC9D7 /* Demo.playground */, 7F45FCDF1A94D02C006863BB /* APIKit */, 7FEC5A151A96FE2600B1D3C0 /* APIKitTests */, - 7F45FD491A94D9A8006863BB /* DemoApp */, 7F45FCDE1A94D02C006863BB /* Products */, ); sourceTree = ""; @@ -236,29 +222,6 @@ name = "Supporting Files"; sourceTree = ""; }; - 7F45FD491A94D9A8006863BB /* DemoApp */ = { - isa = PBXGroup; - children = ( - 7F45FD4C1A94D9A8006863BB /* AppDelegate.swift */, - 7F45FD4E1A94D9A9006863BB /* ViewController.swift */, - 7F45FD6A1A94D9F9006863BB /* GitHub.swift */, - 7F45FD731A94E832006863BB /* Models.swift */, - 7F45FD501A94D9A9006863BB /* Main.storyboard */, - 7F45FD551A94D9A9006863BB /* LaunchScreen.xib */, - 7F45FD531A94D9A9006863BB /* Images.xcassets */, - 7F45FD4A1A94D9A8006863BB /* Supporting Files */, - ); - path = DemoApp; - sourceTree = ""; - }; - 7F45FD4A1A94D9A8006863BB /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 7F45FD4B1A94D9A8006863BB /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; 7FEC5A151A96FE2600B1D3C0 /* APIKitTests */ = { isa = PBXGroup; children = ( @@ -467,9 +430,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7F45FD521A94D9A9006863BB /* Main.storyboard in Resources */, - 7F45FD571A94D9A9006863BB /* LaunchScreen.xib in Resources */, - 7F45FD541A94D9A9006863BB /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -524,10 +484,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7F45FD741A94E832006863BB /* Models.swift in Sources */, - 7F45FD6B1A94D9F9006863BB /* GitHub.swift in Sources */, - 7F45FD4F1A94D9A9006863BB /* ViewController.swift in Sources */, - 7F45FD4D1A94D9A8006863BB /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -561,25 +517,6 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - 7F45FD501A94D9A9006863BB /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7F45FD511A94D9A9006863BB /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 7F45FD551A94D9A9006863BB /* LaunchScreen.xib */ = { - isa = PBXVariantGroup; - children = ( - 7F45FD561A94D9A9006863BB /* Base */, - ); - name = LaunchScreen.xib; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ 7F08699E1A978790001AD3E1 /* Debug */ = { isa = XCBuildConfiguration; diff --git a/APIKit/Method.swift b/APIKit/Method.swift index fea29ad3..6931e782 100644 --- a/APIKit/Method.swift +++ b/APIKit/Method.swift @@ -1,6 +1,6 @@ import Foundation -public enum Method: String { +public enum HTTPMethod: String { case GET = "GET" case POST = "POST" case PUT = "PUT" diff --git a/APIKit/Request.swift b/APIKit/Request.swift index c6f5134e..34ce5ae4 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -14,7 +14,7 @@ public protocol Request { /// Configurations of request var baseURL: NSURL { get } - var method: Method { get } + var method: HTTPMethod { get } var path: String { get } var parameters: [String: AnyObject] { get } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index bfba5a94..b2e391cb 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -24,7 +24,7 @@ class MockAPI: API { struct GetRoot: MockAPIRequest { typealias Response = [String: AnyObject] - var method: APIKit.Method { + var method: HTTPMethod { return .GET } diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift new file mode 100644 index 00000000..737e5759 --- /dev/null +++ b/Demo.playground/Contents.swift @@ -0,0 +1,82 @@ +import XCPlayground +import UIKit +import APIKit + +XCPSetExecutionShouldContinueIndefinitely() + +/// Sending request +let request = GitHubAPI.GetRateLimit() + +GitHubAPI.sendRequest(request) { result in + switch result { + case .Success(let rateLimit): + print("remaining count: \(rateLimit.count)") + print("reset date: \(rateLimit.resetDate)") + + case .Failure(let error): + print("error: \(error)") + } +} + +/// Defining request protocol +protocol GitHubRequest: Request { + +} + +extension GitHubRequest { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } +} + +/// Defining API class +class GitHubAPI: API { + enum Errors: ErrorType { + case Some + } + + // https://developer.github.com/v3/rate_limit/ + struct GetRateLimit: GitHubRequest { + typealias Response = RateLimit + + var method: HTTPMethod { + return .GET + } + + var path: String { + return "/rate_limit" + } + + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let dictionary = object as? [String: AnyObject] else { + throw Errors.Some + } + + guard let rateLimit = RateLimit(dictionary: dictionary) else { + throw Errors.Some + } + + return rateLimit + } + } +} + +/// Model object +struct RateLimit { + let count: Int + let resetDate: NSDate + + init?(dictionary: [String: AnyObject]) { + guard let count = dictionary["rate"]?["limit"] as? Int else { + return nil + } + + guard let resetDateString = dictionary["rate"]?["reset"] as? NSTimeInterval else { + return nil + } + + self.count = count + self.resetDate = NSDate(timeIntervalSince1970: resetDateString) + } +} + diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground new file mode 100644 index 00000000..af4f38cd --- /dev/null +++ b/Demo.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Demo.playground/timeline.xctimeline b/Demo.playground/timeline.xctimeline new file mode 100644 index 00000000..bf468afe --- /dev/null +++ b/Demo.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/DemoApp/AppDelegate.swift b/DemoApp/AppDelegate.swift deleted file mode 100644 index ca60435c..00000000 --- a/DemoApp/AppDelegate.swift +++ /dev/null @@ -1,11 +0,0 @@ -import UIKit - -@UIApplicationMain - -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - return true - } -} diff --git a/DemoApp/Base.lproj/LaunchScreen.xib b/DemoApp/Base.lproj/LaunchScreen.xib deleted file mode 100644 index 27849afa..00000000 --- a/DemoApp/Base.lproj/LaunchScreen.xib +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DemoApp/Base.lproj/Main.storyboard b/DemoApp/Base.lproj/Main.storyboard deleted file mode 100644 index 39b1c2e6..00000000 --- a/DemoApp/Base.lproj/Main.storyboard +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DemoApp/GitHub.swift b/DemoApp/GitHub.swift deleted file mode 100644 index 8e14e5ee..00000000 --- a/DemoApp/GitHub.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Foundation -import APIKit - -class GitHub: API { - override class var baseURL: NSURL { - return NSURL(string: "https://api.github.com")! - } - - class Endpoint { - // https://developer.github.com/v3/search/#search-repositories - class SearchRepositories: APIKit.Request { - enum Sort: String { - case Stars = "stars" - case Forks = "forks" - case Updated = "updated" - } - - enum Order: String { - case Ascending = "asc" - case Descending = "desc" - } - - typealias Response = [Repository] - - let query: String - let sort: Sort - let order: Order - - var URLRequest: NSURLRequest? { - return GitHub.URLRequest( - method: .GET, - path: "/search/repositories", - parameters: ["q": query, "sort": sort.rawValue, "order": order.rawValue] - ) - } - - init(query: String, sort: Sort = .Stars, order: Order = .Ascending) { - self.query = query - self.sort = sort - self.order = order - } - - class func responseFromObject(object: AnyObject) -> Response? { - var repositories = [Repository]() - - if let dictionaries = object["items"] as? [NSDictionary] { - for dictionary in dictionaries { - if let repository = Repository(dictionary: dictionary) { - repositories.append(repository) - } - } - } - - return repositories - } - } - - // https://developer.github.com/v3/search/#search-users - class SearchUsers: APIKit.Request { - enum Sort: String { - case Followers = "followers" - case Repositories = "repositories" - case Joined = "joined" - } - - enum Order: String { - case Ascending = "asc" - case Descending = "desc" - } - - typealias Response = [User] - - let query: String - let sort: Sort - let order: Order - - var URLRequest: NSURLRequest? { - return GitHub.URLRequest( - method: .GET, - path: "/search/users", - parameters: ["q": query, "sort": sort.rawValue, "order": order.rawValue] - ) - } - - init(query: String, sort: Sort = .Followers, order: Order = .Ascending) { - self.query = query - self.sort = sort - self.order = order - } - - class func responseFromObject(object: AnyObject) -> Response? { - var users = [User]() - - if let dictionaries = object["items"] as? [NSDictionary] { - for dictionary in dictionaries { - if let user = User(dictionary: dictionary) { - users.append(user) - } - } - } - - return users - } - } - } -} diff --git a/DemoApp/Images.xcassets/AppIcon.appiconset/Contents.json b/DemoApp/Images.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 36d2c80d..00000000 --- a/DemoApp/Images.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/DemoApp/Info.plist b/DemoApp/Info.plist deleted file mode 100644 index 40c6215d..00000000 --- a/DemoApp/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/DemoApp/Models.swift b/DemoApp/Models.swift deleted file mode 100644 index 6881fe5d..00000000 --- a/DemoApp/Models.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -struct Repository { - let id: Int - let name: String - let owner: User - - init?(dictionary: NSDictionary) { - if - let id = dictionary["id"] as? Int, - let userDictionary = dictionary["owner"] as? NSDictionary, - let name = dictionary["name"] as? String, - let user = User(dictionary: userDictionary) { - self.id = id - self.name = name - self.owner = user - } else { - return nil - } - } -} - -struct User { - let id: Int - let login: String - let avatarURL: NSURL - - init?(dictionary: NSDictionary) { - if - let id = dictionary["id"] as? Int, - let login = dictionary["login"] as? String, - let string = dictionary["avatar_url"] as? String, - let avatarURL = NSURL(string: string) { - self.id = id - self.login = login - self.avatarURL = avatarURL - } else { - return nil - } - } -} diff --git a/DemoApp/ViewController.swift b/DemoApp/ViewController.swift deleted file mode 100644 index 90943da3..00000000 --- a/DemoApp/ViewController.swift +++ /dev/null @@ -1,42 +0,0 @@ -import UIKit - -class ViewController: UITableViewController { - var repositories: [Repository] = [] { - didSet { - tableView?.reloadData() - } - } - - override func viewWillAppear(animated: Bool) { - super.viewWillAppear(animated) - - let request = GitHub.Endpoint.SearchRepositories(query: "APIKit") - - GitHub.sendRequest(request) { response in - switch response { - case .Success(let box): - self.repositories = box.value - - case .Failure(let box): - let alertController = UIAlertController(title: "Error", message: box.value.localizedDescription, preferredStyle: .Alert) - let action = UIAlertAction(title: "OK", style: .Default, handler: nil) - alertController.addAction(action) - self.presentViewController(alertController, animated: true, completion: nil) - } - } - } - - // MARK: UITableViewDataSource - override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return repositories.count - } - - override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell - let repository = repositories[indexPath.row] - - cell.textLabel?.text = "\(repository.owner.login)/\(repository.name)" - - return cell - } -} From c02e0e2f31d5697d88f0a42408a4ca11d18bdc8d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 23:32:10 +0900 Subject: [PATCH 20/44] Markup playground --- Demo.playground/Contents.swift | 70 ++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 737e5759..365bb4c7 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -4,21 +4,7 @@ import APIKit XCPSetExecutionShouldContinueIndefinitely() -/// Sending request -let request = GitHubAPI.GetRateLimit() - -GitHubAPI.sendRequest(request) { result in - switch result { - case .Success(let rateLimit): - print("remaining count: \(rateLimit.count)") - print("reset date: \(rateLimit.resetDate)") - - case .Failure(let error): - print("error: \(error)") - } -} - -/// Defining request protocol +//: Step 1: Define request protocol protocol GitHubRequest: Request { } @@ -29,10 +15,33 @@ extension GitHubRequest { } } -/// Defining API class +//: Step 2: Create API class class GitHubAPI: API { +} + +//: Step 3: Create model object +struct RateLimit { + let count: Int + let resetDate: NSDate + + init?(dictionary: [String: AnyObject]) { + guard let count = dictionary["rate"]?["limit"] as? Int else { + return nil + } + + guard let resetDateString = dictionary["rate"]?["reset"] as? NSTimeInterval else { + return nil + } + + self.count = count + self.resetDate = NSDate(timeIntervalSince1970: resetDateString) + } +} + +//: Step 4: Define requet type in API class +extension GitHubAPI { enum Errors: ErrorType { - case Some + case UnexpectedJSONStructure } // https://developer.github.com/v3/rate_limit/ @@ -49,11 +58,11 @@ class GitHubAPI: API { func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionary = object as? [String: AnyObject] else { - throw Errors.Some + throw Errors.UnexpectedJSONStructure } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw Errors.Some + throw Errors.UnexpectedJSONStructure } return rateLimit @@ -61,22 +70,17 @@ class GitHubAPI: API { } } -/// Model object -struct RateLimit { - let count: Int - let resetDate: NSDate - - init?(dictionary: [String: AnyObject]) { - guard let count = dictionary["rate"]?["limit"] as? Int else { - return nil - } +//: Step 5: Send request +let request = GitHubAPI.GetRateLimit() - guard let resetDateString = dictionary["rate"]?["reset"] as? NSTimeInterval else { - return nil - } +GitHubAPI.sendRequest(request) { result in + switch result { + case .Success(let rateLimit): + "count: \(rateLimit.count)" + "reset: \(rateLimit.resetDate)" - self.count = count - self.resetDate = NSDate(timeIntervalSince1970: resetDateString) + case .Failure(let error): + "error: \(error)" } } From 204f03426ae556d0d27a317ebbafa8e2d35f549a Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 23:47:38 +0900 Subject: [PATCH 21/44] Rename file Method.swift -> HTTPMethod.swift --- APIKit.xcodeproj/project.pbxproj | 12 ++++++------ APIKit/{Method.swift => HTTPMethod.swift} | 0 2 files changed, 6 insertions(+), 6 deletions(-) rename APIKit/{Method.swift => HTTPMethod.swift} (100%) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 238a47cc..02e1208a 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -22,8 +22,8 @@ 7F45FD6D1A94DA28006863BB /* APIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; - 7F68ABDD1AC4414500688D68 /* Method.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* Method.swift */; }; - 7F68ABDE1AC4414500688D68 /* Method.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* Method.swift */; }; + 7F68ABDD1AC4414500688D68 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */; }; + 7F68ABDE1AC4414500688D68 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */; }; 7FCBE9DD1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */; }; 7FCBE9DE1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */; }; 7FCBE9E01A9734950075AFD9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */; }; @@ -115,7 +115,7 @@ 7F45FD171A94D085006863BB /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 7F45FD481A94D9A8006863BB /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7F68ABD91AC4412E00688D68 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; - 7F68ABDC1AC4414500688D68 /* Method.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Method.swift; sourceTree = ""; }; + 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = ""; }; 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseBodyParser.swift; sourceTree = ""; }; 7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -203,7 +203,7 @@ 7F45FCE21A94D02C006863BB /* APIKit.h */, 7F45FD171A94D085006863BB /* API.swift */, 7F68ABD91AC4412E00688D68 /* Request.swift */, - 7F68ABDC1AC4414500688D68 /* Method.swift */, + 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */, 7F1426CD1B298AA300592D42 /* Errors.swift */, 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */, 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */, @@ -459,7 +459,7 @@ files = ( 7FCBE9DD1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */, 7F45FD181A94D085006863BB /* API.swift in Sources */, - 7F68ABDD1AC4414500688D68 /* Method.swift in Sources */, + 7F68ABDD1AC4414500688D68 /* HTTPMethod.swift in Sources */, 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */, 7FCBE9E01A9734950075AFD9 /* ResponseBodyParser.swift in Sources */, 7F1426CE1B298AA300592D42 /* Errors.swift in Sources */, @@ -473,7 +473,7 @@ files = ( 7FCBE9DE1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */, 7F0869A81A979088001AD3E1 /* API.swift in Sources */, - 7F68ABDE1AC4414500688D68 /* Method.swift in Sources */, + 7F68ABDE1AC4414500688D68 /* HTTPMethod.swift in Sources */, 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */, 7FCBE9E11A9734950075AFD9 /* ResponseBodyParser.swift in Sources */, 7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */, diff --git a/APIKit/Method.swift b/APIKit/HTTPMethod.swift similarity index 100% rename from APIKit/Method.swift rename to APIKit/HTTPMethod.swift From 0cf0f01858af052e2c3ec4a28d67e959426fcc60 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Fri, 19 Jun 2015 23:54:15 +0900 Subject: [PATCH 22/44] Reduce error types --- APIKit/API.swift | 6 +++--- APIKit/Errors.swift | 8 ++------ APIKit/Request.swift | 2 +- APIKitTests/APITests.swift | 5 ++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 0dcb118c..c1fb9524 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -31,7 +31,7 @@ public class API { } guard let task = URLSession.dataTaskWithRequest(URLRequest) else { - notifyError(.CannotBuildURLSessionTask) + notifyError(.CannotBuildURLRequest) return nil } @@ -43,7 +43,7 @@ public class API { } guard let HTTPURLResponse = URLResponse as? NSHTTPURLResponse else { - notifyError(.NoURLResponse) + notifyError(.UnexpectedResponse) return } @@ -62,7 +62,7 @@ public class API { if let e = error as? APIKitError { notifyError(e) } else { - notifyError(.CannotBuildResponseObject(underlyingError: error)) + notifyError(.UnexpectedResponse) } } } diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift index 88f1ab79..8d60dfb2 100644 --- a/APIKit/Errors.swift +++ b/APIKit/Errors.swift @@ -2,12 +2,8 @@ import Foundation // TODO: more detailed and comprehensive errors public enum APIKitError: ErrorType { - case InvalidBaseURL - case InvalidParameters case CannotBuildURLRequest - case CannotBuildURLSessionTask - case CannotBuildResponseObject(underlyingError: ErrorType) - case NoURLResponse - case UnacceptableStatusCode(ErrorType?) case ConnectionError(underlyingError: NSError) + case UnacceptableStatusCode(ErrorType?) + case UnexpectedResponse } diff --git a/APIKit/Request.swift b/APIKit/Request.swift index 34ce5ae4..90386ca0 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -69,7 +69,7 @@ public extension Request { internal func buildURLRequest() throws -> NSURLRequest { guard let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { - throw APIKitError.InvalidBaseURL + throw APIKitError.CannotBuildURLRequest } let request = NSMutableURLRequest() diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index b2e391cb..b841fe21 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -166,9 +166,8 @@ class APITests: XCTestCase { case .Failure(let error): switch error { - case .CannotBuildResponseObject(let error as NSError): - XCTAssert(error.domain == NSCocoaErrorDomain) - XCTAssert(error.code == 3840) + case .UnexpectedResponse: + break default: XCTFail() From ce7e9e14dedeeb3122e885b235a59827c85dce56 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 20 Jun 2015 08:45:40 +0900 Subject: [PATCH 23/44] Update README --- Demo.playground/Contents.swift | 1 + Demo.playground/contents.xcplayground | 2 +- README.md | 148 ++++++++++++++++++++------ 3 files changed, 120 insertions(+), 31 deletions(-) diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 365bb4c7..307ba432 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -17,6 +17,7 @@ extension GitHubRequest { //: Step 2: Create API class class GitHubAPI: API { + } //: Step 3: Create model object diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground index af4f38cd..90e9a0fb 100644 --- a/Demo.playground/contents.xcplayground +++ b/Demo.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 99ea2c0f..1881b898 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ GitHub.sendRequest(request) { result in self.tableView.reloadData() case .Failure(let error): - println(error) + print(error) } } ``` @@ -53,14 +53,15 @@ If you want to use APIKit with Swift 1.2, try [0.8.2](https://github.com/ishkawa 1. Create a request protocol that inherits `Request` protocol. 2. Add `baseURL` variable in extension of request protocol. 3. Create a API class that inherits `API` class. -4. Define request types that conforms to request protocol in `Endpoint` class in API class. - 1. Create a type that represents a endpoint of the web API. - 2. Assign type that represents response object to `Response` typealiase. +4. Define request types that conforms to request protocol in API class. + 1. Create a type that represents a request of the web API. + 2. Assign type that represents a response object to `Response` typealiase. 3. Add `method` and `path` variables. 4. Implement `buildResponseFromObject(_:URLResponse:)` to build `Response` from raw object, which may be an array or a dictionary. ```swift protocol GitHubRequest: Request { + } extension GitHubRequest { @@ -70,13 +71,10 @@ extension GitHubRequest { } class GitHubAPI: API { -} - -extension GitHubAPI.Endpoint { struct GetRateLimit: GitHubRequest { - typealiase Response = RateLimit + typealias Response = RateLimit - var method: Method { + var method: HTTPMethod { return .GET } @@ -84,13 +82,13 @@ extension GitHubAPI.Endpoint { return "/rate_limit" } - func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionary = object as? [String: AnyObject] else { - throw SomeError + throw Errors.UnexpectedJSONStructure } - guard let rateLimit = RateLimit(dictionary) else { - throw SomeError + guard let rateLimit = RateLimit(dictionary: dictionary) else { + throw Errors.UnexpectedJSONStructure } return rateLimit @@ -103,7 +101,16 @@ struct RateLimit { let resetDate: NSDate init?(dictionary: [String: AnyObject]) { - ... + guard let count = dictionary["rate"]?["limit"] as? Int else { + return nil + } + + guard let resetDateString = dictionary["rate"]?["reset"] as? NSTimeInterval else { + return nil + } + + self.count = count + self.resetDate = NSDate(timeIntervalSince1970: resetDateString) } } ``` @@ -141,26 +148,73 @@ GitHub.cancelRequest(GitHub.Endpoint.SearchRepositories.self) { request in ### Configuring request -#### Setting parameters -#### Setting serializer of a request -#### Setting serializer of a response -#### Adding fields to HTTP header of a request -#### Building NSURLRequest manually +APIKit uses following 4 properties in `Request` when build `NSURLRequest`. + +```swift +var baseURL: NSURL +var method: HTTPMethod +var path: String +var parameters: [String: AnyObject] +``` + +`parameters` will be converted into query parameter if `method` is one of `.GET`, `.HEAD` and `.DELETE`. Otherwise, it will be serialized by `requestBodyBuilder` and set to `HTTPBody` of `NSURLRequest`. + +#### Configuring format of HTTP body + +APIKit uses `requestBodyBuilder` when it serialize parameter into HTTP body of request, and it use `responseBodyParser` when it deserialize object from HTTP body of response. Default format of body of request and response is JSON. + +```swift +var requestBodyBuilder: RequestBodyBuilder +var responseBodyParser: ResponseBodyParser +``` + +You can specify format of HTTP body implement this property. + +```swift +var requestBodyBuilder: RequestBodyBuilder { + return .URL(encoding: NSUTF8StringEncoding) +} +``` + +#### Configuring manually + +``` +func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { + // You can add any configurations here +} +``` ### Configuring response #### Setting acceptable status code +APIKit decides if a request is succeeded or failed using `acceptableStatusCodes:`. If it contains the status code of a response, the request is judged as succeeded and `API` calls `responseFromObject(_:URLResponse:)` to get model from raw response. Otherwise, the request is judged as failed and `API` calls `errorFromObject(_:URLResponse:)` to get error from raw response. + ```swift var acceptableStatusCodes: Set { - return Set(200) + return Set(200..<300) +} +``` + +#### Building a model from a response + +```swift +func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let dictionary = object as? [String: AnyObject] else { + throw Errors.UnexpectedJSONStructure + } + + guard let rateLimit = RateLimit(dictionary: dictionary) else { + throw Errors.UnexpectedJSONStructure + } + + return rateLimit } ``` -#### Building custom error from a response +#### Building an error from a response -You can create detailed error using response object from Web API. -For example, [GitHub API](https://developer.github.com/v3/#client-errors) returns error like this: +For example, [GitHub API](https://developer.github.com/v3/#client-errors) returns an error like this: ```json { @@ -168,16 +222,16 @@ For example, [GitHub API](https://developer.github.com/v3/#client-errors) return } ``` -To create error that contains `message` in response, override `API.responseErrorFromObject(object:)` and return `NSError` using response object. +To create error that contains `message` in response, implement `errorFromObject(_:URLResponse:)` and return `ErrorType` using object. ```swift -func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { +func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { guard let dictionary = object as? [String: AnyObject] else { - throw SomeError + throw Errors.UnexpectedJSONStructure } guard let message = dictionary["message"] as? String else { - throw SomeError + throw Errors.UnexpectedJSONStructure } return GitHubError(message: message) @@ -186,6 +240,40 @@ func buildErrorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) thr ## Practical Example +### Authorization + +```swift +class GitHubAPI: API { + static var accessToken: String? +} + +protocol GitHubRequest: Request { + var authenticate: Bool { get } +} + +extension GitHubRequest { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } + + var authenticate: Bool { + return true + } + + func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { + if authenticate { + guard let accessToken = GitHubAPI.accessToken else { + throw APIKitError.CannotBuildURLRequest + } + + URLRequest.setValue("token \(accessToken)", forHTTPHeaderField: "Authorization") + } + + return URLRequest + } +} +``` + ### Pagination ```swift @@ -220,7 +308,7 @@ struct PaginatedResponse { struct SomePaginatedRequest: Request { typealias Response = PaginatedResponse - var method: Method { + var method: HTTPMethod { return .GET } @@ -230,9 +318,9 @@ struct SomePaginatedRequest: Request { let page: Int - static func buildResponseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + static func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionaries = object as? [[String: AnyObject]] else { - throw SomeError + throw Errors.UnexpectedJSONStructure } var somes = [Some]() From 2bae2d9d9b26c228e843e356788b5de8541128e7 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 20 Jun 2015 11:09:22 +0900 Subject: [PATCH 24/44] Refine errors --- APIKit/API.swift | 16 ++++++++++------ APIKit/Errors.swift | 7 +++---- APIKit/Request.swift | 8 ++++---- APIKitTests/APITests.swift | 4 ++-- Demo.playground/Contents.swift | 8 ++------ README.md | 14 +++++++------- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index c1fb9524..6a64871d 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -17,7 +17,6 @@ public class API { func notifyError(error: APIKitError) { let queue = dispatch_get_main_queue() dispatch_async(queue) { - print(error) handler(.failure(error)) } } @@ -26,19 +25,19 @@ public class API { do { URLRequest = try request.buildURLRequest() } catch { - notifyError(.CannotBuildURLRequest) + notifyError(.InvalidRequest) return nil } guard let task = URLSession.dataTaskWithRequest(URLRequest) else { - notifyError(.CannotBuildURLRequest) + notifyError(.InvalidRequest) return nil } task.request = Box(request) task.completionHandler = { data, URLResponse, connectionError in if let error = connectionError { - notifyError(.ConnectionError(underlyingError: error)) + notifyError(.ConnectionError(error: error)) return } @@ -50,8 +49,13 @@ public class API { do { let object = try request.responseBodyParser.parseData(data) if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { - let error = request.errorFromObject(object, URLResponse: HTTPURLResponse) - throw APIKitError.UnacceptableStatusCode(error) + do { + let error = try request.errorFromObject(object, URLResponse: HTTPURLResponse) + notifyError(.ResponseError(error: error)) + } catch { + notifyError(.UnexpectedResponse) + } + return } let response = try request.responseFromObject(object, URLResponse: HTTPURLResponse) diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift index 8d60dfb2..a1097f4d 100644 --- a/APIKit/Errors.swift +++ b/APIKit/Errors.swift @@ -1,9 +1,8 @@ import Foundation -// TODO: more detailed and comprehensive errors public enum APIKitError: ErrorType { - case CannotBuildURLRequest - case ConnectionError(underlyingError: NSError) - case UnacceptableStatusCode(ErrorType?) + case InvalidRequest case UnexpectedResponse + case ConnectionError(error: NSError) + case ResponseError(error: ErrorType) } diff --git a/APIKit/Request.swift b/APIKit/Request.swift index 90386ca0..e4965210 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -38,7 +38,7 @@ public protocol Request { /// Build `ErrorType` instance from raw response object. /// This method will be called if `acceptableStatusCode` does not contain status code of NSHTTPURLResponse. - func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? + func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType } /// Default implementation of Request protocol @@ -63,13 +63,13 @@ public extension Request { return URLRequest } - public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { - return nil + public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { + return APIKitError.UnexpectedResponse } internal func buildURLRequest() throws -> NSURLRequest { guard let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { - throw APIKitError.CannotBuildURLRequest + throw APIKitError.InvalidRequest } let request = NSMutableURLRequest() diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index b841fe21..4a317f6b 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -11,7 +11,7 @@ extension MockAPIRequest { return NSURL(string: "https://api.github.com")! } - func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { return MockAPI.Errors.Mock } } @@ -133,7 +133,7 @@ class APITests: XCTestCase { case .Failure(let error): switch error { - case .UnacceptableStatusCode(let error): + case .ResponseError(let error): XCTAssert(error is MockAPI.Errors) default: diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 307ba432..1e8ccde6 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -41,10 +41,6 @@ struct RateLimit { //: Step 4: Define requet type in API class extension GitHubAPI { - enum Errors: ErrorType { - case UnexpectedJSONStructure - } - // https://developer.github.com/v3/rate_limit/ struct GetRateLimit: GitHubRequest { typealias Response = RateLimit @@ -59,11 +55,11 @@ extension GitHubAPI { func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionary = object as? [String: AnyObject] else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } return rateLimit diff --git a/README.md b/README.md index 1881b898..c4a98761 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,11 @@ class GitHubAPI: API { func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionary = object as? [String: AnyObject] else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } return rateLimit @@ -201,11 +201,11 @@ var acceptableStatusCodes: Set { ```swift func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionary = object as? [String: AnyObject] else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } return rateLimit @@ -227,11 +227,11 @@ To create error that contains `message` in response, implement `errorFromObject( ```swift func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { guard let dictionary = object as? [String: AnyObject] else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } guard let message = dictionary["message"] as? String else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } return GitHubError(message: message) @@ -320,7 +320,7 @@ struct SomePaginatedRequest: Request { static func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let dictionaries = object as? [[String: AnyObject]] else { - throw Errors.UnexpectedJSONStructure + throw APIKitError.UnexpectedResponse } var somes = [Some]() From d7552276ec0bde96e0dcad9874d4329590bbe92b Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 20 Jun 2015 11:30:07 +0900 Subject: [PATCH 25/44] Replace ugly code --- APIKit/API.swift | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 6a64871d..b7688842 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -82,17 +82,9 @@ public class API { public class func cancelRequest(requestType: T.Type, URLSession: NSURLSession, passingTest test: T -> Bool = { r in true }) { URLSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in - // TODO: replace with cool code - var allTasks = [NSURLSessionTask]() - for task in dataTasks { - allTasks.append(task) - } - for task in uploadTasks { - allTasks.append(task) - } - for task in downloadTasks { - allTasks.append(task) - } + let allTasks = dataTasks as [NSURLSessionTask] + + uploadTasks as [NSURLSessionTask] + + downloadTasks as [NSURLSessionTask] let tasks = allTasks.filter { task in var request: T? From 0477e915afa224694572ecc772e607190600da3d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 20 Jun 2015 12:50:16 +0900 Subject: [PATCH 26/44] Grammar --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c4a98761..d713b52a 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ APIKit [![Circle CI](https://img.shields.io/circleci/project/ishkawa/APIKit/master.svg?style=flat)](https://circleci.com/gh/ishkawa/APIKit) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -APIKit is a library for building type safe web API client in Swift. +APIKit is a library for building type-safe web API client in Swift. -- Parameters of a request are validated by type system. -- Type of a response is inferred from type of its request. -- A result of request is represented by [Result](https://github.com/antitypical/Result), which is also known as Either. +- Parameters of a request are validated by type-system. +- Type of a response is inferred from the type of its request. +- A result of a request is represented by [Result](https://github.com/antitypical/Result), which is also known as Either. - All the endpoints can be enumerated in nested class. ```swift @@ -51,13 +51,13 @@ If you want to use APIKit with Swift 1.2, try [0.8.2](https://github.com/ishkawa ## Usage 1. Create a request protocol that inherits `Request` protocol. -2. Add `baseURL` variable in extension of request protocol. -3. Create a API class that inherits `API` class. -4. Define request types that conforms to request protocol in API class. +2. Add `baseURL` property in an extension of request protocol. +3. Create an API class that inherits `API` class. +4. Define request types that conform to request protocol in API class. 1. Create a type that represents a request of the web API. 2. Assign type that represents a response object to `Response` typealiase. 3. Add `method` and `path` variables. - 4. Implement `buildResponseFromObject(_:URLResponse:)` to build `Response` from raw object, which may be an array or a dictionary. + 4. Implement `buildResponseFromObject(_:URLResponse:)` to build `Response` from a raw object, which may be an array or a dictionary. ```swift protocol GitHubRequest: Request { @@ -138,7 +138,7 @@ GitHubAPI.sendRequest(request) { result in GitHub.cancelRequest(GitHub.Endpoint.RateLimit) ``` -If you want to filter requests to be cancelled, add closure that identifies the request shoule be cancelled or not. +If you want to filter requests to be cancelled, add closure that identifies the request should be cancelled or not. ```swift GitHub.cancelRequest(GitHub.Endpoint.SearchRepositories.self) { request in @@ -161,14 +161,14 @@ var parameters: [String: AnyObject] #### Configuring format of HTTP body -APIKit uses `requestBodyBuilder` when it serialize parameter into HTTP body of request, and it use `responseBodyParser` when it deserialize object from HTTP body of response. Default format of body of request and response is JSON. +APIKit uses `requestBodyBuilder` when it serialize parameter into HTTP body of a request, and it uses `responseBodyParser` when it deserialize an object from HTTP body of a response. Default format of the body of request and response is JSON. ```swift var requestBodyBuilder: RequestBodyBuilder var responseBodyParser: ResponseBodyParser ``` -You can specify format of HTTP body implement this property. +You can specify the format of HTTP body implement this property. ```swift var requestBodyBuilder: RequestBodyBuilder { @@ -188,7 +188,7 @@ func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURL #### Setting acceptable status code -APIKit decides if a request is succeeded or failed using `acceptableStatusCodes:`. If it contains the status code of a response, the request is judged as succeeded and `API` calls `responseFromObject(_:URLResponse:)` to get model from raw response. Otherwise, the request is judged as failed and `API` calls `errorFromObject(_:URLResponse:)` to get error from raw response. +APIKit decides if a request is succeeded or failed by using `acceptableStatusCodes:`. If it contains the status code of a response, the request is judged as succeeded and `API` calls `responseFromObject(_:URLResponse:)` to get a model from a raw response. Otherwise, the request is judged as failed and `API` calls `errorFromObject(_:URLResponse:)` to get an error from a raw response. ```swift var acceptableStatusCodes: Set { From 8b65d07a93cb932555c44abbb00b56f6d067c2be Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 20 Jun 2015 13:28:54 +0900 Subject: [PATCH 27/44] Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d713b52a..54df6caf 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ APIKit is a library for building type-safe web API client in Swift. - All the endpoints can be enumerated in nested class. ```swift -let request = GitHub.Endpoint.SearchRepositories(query: "APIKit", sort: .Stars) +let request = GitHub.SearchRepositories(query: "APIKit", sort: .Stars) GitHub.sendRequest(request) { result in switch result { @@ -118,7 +118,7 @@ struct RateLimit { ### Sending request ```swift -let request = GitHubAPI.Endpoint.GetRateLimit() +let request = GitHubAPI.GetRateLimit() GitHubAPI.sendRequest(request) { result in switch result { @@ -135,13 +135,13 @@ GitHubAPI.sendRequest(request) { result in ### Canceling request ```swift -GitHub.cancelRequest(GitHub.Endpoint.RateLimit) +GitHub.cancelRequest(GitHubAPI.GetRateLimit.self) ``` If you want to filter requests to be cancelled, add closure that identifies the request should be cancelled or not. ```swift -GitHub.cancelRequest(GitHub.Endpoint.SearchRepositories.self) { request in +GitHub.cancelRequest(GitHubAPI.SearchRepositories.self) { request in return request.query == "APIKit" } ``` @@ -277,7 +277,7 @@ extension GitHubRequest { ### Pagination ```swift -let request = SomeAPI.Endpoint.SomePaginatedRequest(page: 1) +let request = SomeAPI.SomePaginatedRequest(page: 1) SomeAPI.sendRequest(request) { result in switch result { From 961406e22bb22b2bc63ddc0b59ff1af781ecb47b Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 11:39:53 +0900 Subject: [PATCH 28/44] Remove DemoApp completely --- APIKit.xcodeproj/project.pbxproj | 113 ------------------ .../xcshareddata/xcschemes/DemoApp.xcscheme | 91 -------------- circle.yml | 1 - 3 files changed, 205 deletions(-) delete mode 100644 APIKit.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 02e1208a..472ed4d4 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -18,8 +18,6 @@ 7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; }; 7F45FD181A94D085006863BB /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* API.swift */; }; - 7F45FD6C1A94DA28006863BB /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; }; - 7F45FD6D1A94DA28006863BB /* APIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDD1AC4414500688D68 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */; }; @@ -34,7 +32,6 @@ CD5115261B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD5115271B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD5115281B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; - CD5115291B1FFBA900514240 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; CD51152E1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; CD51152F1B1FFCC700514240 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; CD5115311B1FFD8F00514240 /* Result.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -50,13 +47,6 @@ remoteGlobalIDString = 7F45FCFD1A94D04D006863BB; remoteInfo = "APIKit-Mac"; }; - 7F45FD6E1A94DA28006863BB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7F45FCD41A94D02C006863BB /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7F45FCDC1A94D02C006863BB; - remoteInfo = "APIKit-iOS"; - }; 7FEC5A1B1A96FE2600B1D3C0 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7F45FCD41A94D02C006863BB /* Project object */; @@ -76,17 +66,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD701A94DA28006863BB /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 7F45FD6D1A94DA28006863BB /* APIKit.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A221A97001500B1D3C0 /* Copy Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -113,7 +92,6 @@ 7F45FCE21A94D02C006863BB /* APIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = APIKit.h; sourceTree = ""; }; 7F45FCFE1A94D04D006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7F45FD171A94D085006863BB /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; - 7F45FD481A94D9A8006863BB /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7F68ABD91AC4412E00688D68 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = ""; }; @@ -153,15 +131,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD451A94D9A8006863BB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - CD5115291B1FFBA900514240 /* Result.framework in Frameworks */, - 7F45FD6C1A94DA28006863BB /* APIKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A111A96FE2600B1D3C0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -190,7 +159,6 @@ children = ( 7F45FCDD1A94D02C006863BB /* APIKit.framework */, 7F45FCFE1A94D04D006863BB /* APIKit.framework */, - 7F45FD481A94D9A8006863BB /* DemoApp.app */, 7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */, 7F0869941A978790001AD3E1 /* APIKitTests.xctest */, ); @@ -317,25 +285,6 @@ productReference = 7F45FCFE1A94D04D006863BB /* APIKit.framework */; productType = "com.apple.product-type.framework"; }; - 7F45FD471A94D9A8006863BB /* DemoApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7F45FD681A94D9A9006863BB /* Build configuration list for PBXNativeTarget "DemoApp" */; - buildPhases = ( - 7F45FD441A94D9A8006863BB /* Sources */, - 7F45FD451A94D9A8006863BB /* Frameworks */, - 7F45FD461A94D9A8006863BB /* Resources */, - 7F45FD701A94DA28006863BB /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 7F45FD6F1A94DA28006863BB /* PBXTargetDependency */, - ); - name = DemoApp; - productName = DemoApp; - productReference = 7F45FD481A94D9A8006863BB /* DemoApp.app */; - productType = "com.apple.product-type.application"; - }; 7FEC5A131A96FE2600B1D3C0 /* APIKitTests-iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 7FEC5A1F1A96FE2600B1D3C0 /* Build configuration list for PBXNativeTarget "APIKitTests-iOS" */; @@ -374,9 +323,6 @@ 7F45FCFD1A94D04D006863BB = { CreatedOnToolsVersion = 6.1.1; }; - 7F45FD471A94D9A8006863BB = { - CreatedOnToolsVersion = 6.1.1; - }; 7FEC5A131A96FE2600B1D3C0 = { CreatedOnToolsVersion = 6.1.1; }; @@ -399,7 +345,6 @@ 7F45FCFD1A94D04D006863BB /* APIKit-Mac */, 7FEC5A131A96FE2600B1D3C0 /* APIKitTests-iOS */, 7F0869931A978790001AD3E1 /* APIKitTests-Mac */, - 7F45FD471A94D9A8006863BB /* DemoApp */, ); }; /* End PBXProject section */ @@ -426,13 +371,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD461A94D9A8006863BB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A121A96FE2600B1D3C0 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -480,13 +418,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD441A94D9A8006863BB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A101A96FE2600B1D3C0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -505,11 +436,6 @@ target = 7F45FCFD1A94D04D006863BB /* APIKit-Mac */; targetProxy = 7F08699B1A978790001AD3E1 /* PBXContainerItemProxy */; }; - 7F45FD6F1A94DA28006863BB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7F45FCDC1A94D02C006863BB /* APIKit-iOS */; - targetProxy = 7F45FD6E1A94DA28006863BB /* PBXContainerItemProxy */; - }; 7FEC5A1C1A96FE2600B1D3C0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7F45FCDC1A94D02C006863BB /* APIKit-iOS */; @@ -724,36 +650,6 @@ }; name = Release; }; - 7F45FD641A94D9A9006863BB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = DemoApp/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - }; - name = Debug; - }; - 7F45FD651A94D9A9006863BB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; - INFOPLIST_FILE = DemoApp/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - }; - name = Release; - }; 7FEC5A1D1A96FE2600B1D3C0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -819,15 +715,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7F45FD681A94D9A9006863BB /* Build configuration list for PBXNativeTarget "DemoApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7F45FD641A94D9A9006863BB /* Debug */, - 7F45FD651A94D9A9006863BB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 7FEC5A1F1A96FE2600B1D3C0 /* Build configuration list for PBXNativeTarget "APIKitTests-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/APIKit.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme b/APIKit.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme deleted file mode 100644 index 7fbaa3cc..00000000 --- a/APIKit.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/circle.yml b/circle.yml index dd2d47d7..d9563834 100644 --- a/circle.yml +++ b/circle.yml @@ -23,7 +23,6 @@ test: # - pod lib lint # - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace test -scheme APIKit-iOS | xcpretty -c -r junit -o $CIRCLE_TEST_REPORTS/test-report-ios.xml # - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace test -scheme APIKit-Mac | xcpretty -c -r junit -o $CIRCLE_TEST_REPORTS/test-report-mac.xml - # - set -o pipefail && xcodebuild -workspace APIKit.xcworkspace build -scheme DemoApp -sdk iphonesimulator | xcpretty -c - echo "disable CI until CircleCI supports Swift 2." deployment: From 17f42e25538d0cd8e476e039807e0b8fd692b76d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 11:43:26 +0900 Subject: [PATCH 29/44] Set product bundle identifier --- APIKit.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 472ed4d4..e6d01d71 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -455,7 +455,7 @@ INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = macosx; }; @@ -469,7 +469,7 @@ INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = macosx; }; @@ -573,7 +573,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_SWIFT_FLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -593,7 +593,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_SWIFT_FLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -619,7 +619,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; @@ -643,7 +643,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; @@ -659,7 +659,7 @@ ); INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = iphoneos; }; @@ -670,7 +670,7 @@ buildSettings = { INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "-.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = iphoneos; }; From 53bff0047253016a9af7753498f8c4d53a86ef34 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 11:44:45 +0900 Subject: [PATCH 30/44] Remove OTHER_SWIFT_FLAGS --- APIKit.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index e6d01d71..a9de36a2 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -572,7 +572,6 @@ INFOPLIST_FILE = APIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; @@ -592,7 +591,6 @@ INFOPLIST_FILE = APIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; @@ -618,7 +616,6 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; @@ -642,7 +639,6 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - OTHER_SWIFT_FLAGS = "-DAPIKIT_DYNAMIC_FRAMEWORK"; PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; From e963f672e292cdd47f331583c1f9c7fd82af3ce7 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 11:53:46 +0900 Subject: [PATCH 31/44] Refactor error handling in closure of NSURLSessionTask --- APIKit/API.swift | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index b7688842..3ae462ca 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -49,25 +49,18 @@ public class API { do { let object = try request.responseBodyParser.parseData(data) if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { - do { - let error = try request.errorFromObject(object, URLResponse: HTTPURLResponse) - notifyError(.ResponseError(error: error)) - } catch { - notifyError(.UnexpectedResponse) - } - return + let responseError = try request.errorFromObject(object, URLResponse: HTTPURLResponse) + throw APIKitError.ResponseError(error: responseError) } let response = try request.responseFromObject(object, URLResponse: HTTPURLResponse) dispatch_async(dispatch_get_main_queue()) { handler(.success(response)) } + } catch let error as APIKitError { + notifyError(error) } catch { - if let e = error as? APIKitError { - notifyError(e) - } else { - notifyError(.UnexpectedResponse) - } + notifyError(.UnexpectedResponse) } } From ce975d39842e079a8090fe6c940dc423ff1dbb02 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 11:59:01 +0900 Subject: [PATCH 32/44] Omit label of associated value in APIKitError --- APIKit/API.swift | 4 ++-- APIKit/Errors.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 3ae462ca..6b58d41a 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -37,7 +37,7 @@ public class API { task.request = Box(request) task.completionHandler = { data, URLResponse, connectionError in if let error = connectionError { - notifyError(.ConnectionError(error: error)) + notifyError(.ConnectionError(error)) return } @@ -50,7 +50,7 @@ public class API { let object = try request.responseBodyParser.parseData(data) if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { let responseError = try request.errorFromObject(object, URLResponse: HTTPURLResponse) - throw APIKitError.ResponseError(error: responseError) + throw APIKitError.ResponseError(responseError) } let response = try request.responseFromObject(object, URLResponse: HTTPURLResponse) diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift index a1097f4d..a9c4ef45 100644 --- a/APIKit/Errors.swift +++ b/APIKit/Errors.swift @@ -3,6 +3,6 @@ import Foundation public enum APIKitError: ErrorType { case InvalidRequest case UnexpectedResponse - case ConnectionError(error: NSError) - case ResponseError(error: ErrorType) + case ConnectionError(NSError) + case ResponseError(ErrorType) } From 14a0994c478f856437514f150ddfa7df61199c9b Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 12:00:43 +0900 Subject: [PATCH 33/44] Use "Error" for name of error type instead of "Errors" --- APIKit/URLEncodedSerialization.swift | 10 +++++----- APIKitTests/APITests.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index 9267b6f1..596272a7 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -9,7 +9,7 @@ private func unescape(string: String) -> String { } public class URLEncodedSerialization { - public enum Errors: ErrorType { + public enum Error: ErrorType { case CannotGetStringFromData case CannotGetDataFromString case CannotCastObjectToDictionary @@ -18,7 +18,7 @@ public class URLEncodedSerialization { public class func objectFromData(data: NSData, encoding: NSStringEncoding) throws -> [String: String] { guard let string = NSString(data: data, encoding: encoding) as? String else { - throw Errors.CannotGetStringFromData + throw Error.CannotGetStringFromData } var dictionary = [String: String]() @@ -26,7 +26,7 @@ public class URLEncodedSerialization { let contents = pair.componentsSeparatedByString("=") guard contents.count == 2 else { - throw Errors.InvalidFormatString + throw Error.InvalidFormatString } dictionary[contents[0]] = unescape(contents[1]) @@ -38,7 +38,7 @@ public class URLEncodedSerialization { public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> NSData { let string = try stringFromObject(object, encoding: encoding) guard let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) else { - throw Errors.CannotGetDataFromString + throw Error.CannotGetDataFromString } return data @@ -46,7 +46,7 @@ public class URLEncodedSerialization { public class func stringFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> String { guard let dictionary = object as? [String: AnyObject] else { - throw Errors.CannotCastObjectToDictionary + throw Error.CannotCastObjectToDictionary } var pairs = [String]() diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 4a317f6b..16d34a6f 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -12,12 +12,12 @@ extension MockAPIRequest { } func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { - return MockAPI.Errors.Mock + return MockAPI.Error.Mock } } class MockAPI: API { - enum Errors: ErrorType { + enum Error: ErrorType { case Mock } @@ -34,7 +34,7 @@ class MockAPI: API { func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { guard let response = object as? [String: AnyObject] else { - throw Errors.Mock + throw Error.Mock } return response @@ -134,7 +134,7 @@ class APITests: XCTestCase { case .Failure(let error): switch error { case .ResponseError(let error): - XCTAssert(error is MockAPI.Errors) + XCTAssert(error is MockAPI.Error) default: XCTFail() From 45b62fc263a1dbf3b497cdd4ea3955a07818a2d9 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 12:03:26 +0900 Subject: [PATCH 34/44] Minor refactoring in URLEncodedSerialization --- APIKit/URLEncodedSerialization.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index 596272a7..4bced96d 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -49,11 +49,9 @@ public class URLEncodedSerialization { throw Error.CannotCastObjectToDictionary } - var pairs = [String]() - for (key, value) in dictionary { + let pairs = dictionary.map { key, value -> String in let valueAsString = (value as? String) ?? "\(value)" - let pair = "\(key)=\(escape(valueAsString))" - pairs.append(pair) + return "\(key)=\(escape(valueAsString))" } return "&".join(pairs) From a70960d0761e255c0c32417c7cd52f55e7e5b71b Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 12:04:45 +0900 Subject: [PATCH 35/44] Omit objc_AssociationPolicy --- APIKit/API.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 6b58d41a..61530f7a 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -148,9 +148,9 @@ private extension NSURLSessionDataTask { set { if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &taskRequestKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &taskRequestKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } @@ -160,7 +160,7 @@ private extension NSURLSessionDataTask { return responseBuffer } else { let responseBuffer = NSMutableData() - objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return responseBuffer } } @@ -172,9 +172,9 @@ private extension NSURLSessionDataTask { set { if let value = newValue { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } @@ -188,9 +188,9 @@ extension NSURLSessionDownloadTask { set { if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &taskRequestKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, &taskRequestKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } From 688eaaf0090ba31b430c5e7fdc313cfb0b2b91e3 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 12:05:58 +0900 Subject: [PATCH 36/44] Set ENABLE_BITCODE YES --- APIKit.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index a9de36a2..c4ad110c 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -495,6 +495,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -542,6 +543,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; From ad5273f875dc7a61c55a38a27c1bc65d5fc573f8 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 12:15:49 +0900 Subject: [PATCH 37/44] Add comment about isValidJSONObject() --- APIKit/RequestBodyBuilder.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/APIKit/RequestBodyBuilder.swift b/APIKit/RequestBodyBuilder.swift index 7003915a..09bcb26a 100644 --- a/APIKit/RequestBodyBuilder.swift +++ b/APIKit/RequestBodyBuilder.swift @@ -22,6 +22,7 @@ public enum RequestBodyBuilder { public func buildBodyFromObject(object: AnyObject) throws -> NSData { switch self { case .JSON(let writingOptions): + // If isValidJSONObject(_:) is false, dataWithJSONObject(_:options:) throws NSException. guard NSJSONSerialization.isValidJSONObject(object) else { throw NSError(domain: NSCocoaErrorDomain, code: 3840, userInfo: nil) } From b501b3ba13ca16599ab3e48117f7947b2802b15b Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 12:54:11 +0900 Subject: [PATCH 38/44] Add Errors.swift to APIKit-Mac --- APIKit.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index c4ad110c..1c4886ca 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; }; 7F45FD181A94D085006863BB /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* API.swift */; }; + 7F5188A21B390FC40048D451 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1426CD1B298AA300592D42 /* Errors.swift */; }; 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDD1AC4414500688D68 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */; }; @@ -415,6 +416,7 @@ 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */, 7FCBE9E11A9734950075AFD9 /* ResponseBodyParser.swift in Sources */, 7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */, + 7F5188A21B390FC40048D451 /* Errors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From ef3d410840cfe27fc26756162a9d4d91d282ba1d Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 23 Jun 2015 14:46:28 +0900 Subject: [PATCH 39/44] Change stringFromObject in URLEncodedSerialization to stringFromDictionary --- APIKit/Request.swift | 2 +- APIKit/URLEncodedSerialization.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/APIKit/Request.swift b/APIKit/Request.swift index e4965210..c4d7ebdd 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -76,7 +76,7 @@ public extension Request { switch method { case .GET, .HEAD, .DELETE: - components.query = try URLEncodedSerialization.stringFromObject(parameters, encoding: NSUTF8StringEncoding) + components.query = URLEncodedSerialization.stringFromDictionary(parameters) default: request.HTTPBody = try requestBodyBuilder.buildBodyFromObject(parameters) diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index 4bced96d..aa1a8fe9 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -36,7 +36,11 @@ public class URLEncodedSerialization { } public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> NSData { - let string = try stringFromObject(object, encoding: encoding) + guard let dictionary = object as? [String: AnyObject] else { + throw Error.CannotCastObjectToDictionary + } + + let string = stringFromDictionary(dictionary) guard let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) else { throw Error.CannotGetDataFromString } @@ -44,11 +48,7 @@ public class URLEncodedSerialization { return data } - public class func stringFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> String { - guard let dictionary = object as? [String: AnyObject] else { - throw Error.CannotCastObjectToDictionary - } - + public class func stringFromDictionary(dictionary: [String: AnyObject]) -> String { let pairs = dictionary.map { key, value -> String in let valueAsString = (value as? String) ?? "\(value)" return "\(key)=\(escape(valueAsString))" From c34d70c1289f402248a5f41bd1059f7e8145053a Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Wed, 24 Jun 2015 18:46:46 +0900 Subject: [PATCH 40/44] More detailed error in URLEncodedSerialization --- APIKit/URLEncodedSerialization.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index aa1a8fe9..0e2aeaf4 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -10,15 +10,15 @@ private func unescape(string: String) -> String { public class URLEncodedSerialization { public enum Error: ErrorType { - case CannotGetStringFromData - case CannotGetDataFromString - case CannotCastObjectToDictionary - case InvalidFormatString + case CannotGetStringFromData(NSData, NSStringEncoding) + case CannotGetDataFromString(String, NSStringEncoding) + case CannotCastObjectToDictionary(AnyObject) + case InvalidFormatString(String) } public class func objectFromData(data: NSData, encoding: NSStringEncoding) throws -> [String: String] { guard let string = NSString(data: data, encoding: encoding) as? String else { - throw Error.CannotGetStringFromData + throw Error.CannotGetStringFromData(data, encoding) } var dictionary = [String: String]() @@ -26,7 +26,7 @@ public class URLEncodedSerialization { let contents = pair.componentsSeparatedByString("=") guard contents.count == 2 else { - throw Error.InvalidFormatString + throw Error.InvalidFormatString(string) } dictionary[contents[0]] = unescape(contents[1]) @@ -37,12 +37,12 @@ public class URLEncodedSerialization { public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> NSData { guard let dictionary = object as? [String: AnyObject] else { - throw Error.CannotCastObjectToDictionary + throw Error.CannotCastObjectToDictionary(object) } let string = stringFromDictionary(dictionary) guard let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) else { - throw Error.CannotGetDataFromString + throw Error.CannotGetDataFromString(string, encoding) } return data From 39d9ab275b0febbcacd38e19b18ddee3e8b5fd47 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Thu, 25 Jun 2015 20:36:23 +0900 Subject: [PATCH 41/44] Refine errors --- APIKit.xcodeproj/project.pbxproj | 10 ++-- APIKit/API.swift | 82 ++++++++++++-------------------- APIKit/APIError.swift | 31 ++++++++++++ APIKit/Errors.swift | 8 ---- APIKit/Request.swift | 68 ++++++++++++++++++++------ APIKitTests/APITests.swift | 42 +++++++--------- 6 files changed, 135 insertions(+), 106 deletions(-) create mode 100644 APIKit/APIError.swift delete mode 100644 APIKit/Errors.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 1c4886ca..b3df4043 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -13,12 +13,11 @@ 7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; }; 7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; }; 7F0869A81A979088001AD3E1 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* API.swift */; }; - 7F1426CE1B298AA300592D42 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1426CD1B298AA300592D42 /* Errors.swift */; }; 7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; }; 7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; }; 7F45FD181A94D085006863BB /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* API.swift */; }; - 7F5188A21B390FC40048D451 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1426CD1B298AA300592D42 /* Errors.swift */; }; + 7F5FA6B51B3C58210090B0AF /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5FA6B41B3C58210090B0AF /* APIError.swift */; }; 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABD91AC4412E00688D68 /* Request.swift */; }; 7F68ABDD1AC4414500688D68 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */; }; @@ -85,7 +84,6 @@ /* Begin PBXFileReference section */ 7F0869941A978790001AD3E1 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerialization.swift; sourceTree = ""; }; - 7F1426CD1B298AA300592D42 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = ""; }; 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilderTests.swift; sourceTree = ""; }; 7F45FCDD1A94D02C006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -93,6 +91,7 @@ 7F45FCE21A94D02C006863BB /* APIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = APIKit.h; sourceTree = ""; }; 7F45FCFE1A94D04D006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7F45FD171A94D085006863BB /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + 7F5FA6B41B3C58210090B0AF /* APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; 7F68ABD91AC4412E00688D68 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = ""; }; @@ -173,7 +172,7 @@ 7F45FD171A94D085006863BB /* API.swift */, 7F68ABD91AC4412E00688D68 /* Request.swift */, 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */, - 7F1426CD1B298AA300592D42 /* Errors.swift */, + 7F5FA6B41B3C58210090B0AF /* APIError.swift */, 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */, 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */, 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */, @@ -401,7 +400,7 @@ 7F68ABDD1AC4414500688D68 /* HTTPMethod.swift in Sources */, 7F68ABDA1AC4412E00688D68 /* Request.swift in Sources */, 7FCBE9E01A9734950075AFD9 /* ResponseBodyParser.swift in Sources */, - 7F1426CE1B298AA300592D42 /* Errors.swift in Sources */, + 7F5FA6B51B3C58210090B0AF /* APIError.swift in Sources */, 7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -416,7 +415,6 @@ 7F68ABDB1AC4412E00688D68 /* Request.swift in Sources */, 7FCBE9E11A9734950075AFD9 /* ResponseBodyParser.swift in Sources */, 7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */, - 7F5188A21B390FC40048D451 /* Errors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/APIKit/API.swift b/APIKit/API.swift index 61530f7a..e346a9a1 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -13,66 +13,44 @@ public class API { ) // send request and build response object - public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { - func notifyError(error: APIKitError) { - let queue = dispatch_get_main_queue() - dispatch_async(queue) { - handler(.failure(error)) - } - } - - let URLRequest: NSURLRequest - do { - URLRequest = try request.buildURLRequest() - } catch { - notifyError(.InvalidRequest) - return nil - } - - guard let task = URLSession.dataTaskWithRequest(URLRequest) else { - notifyError(.InvalidRequest) - return nil - } - - task.request = Box(request) - task.completionHandler = { data, URLResponse, connectionError in - if let error = connectionError { - notifyError(.ConnectionError(error)) - return - } - - guard let HTTPURLResponse = URLResponse as? NSHTTPURLResponse else { - notifyError(.UnexpectedResponse) - return - } - - do { - let object = try request.responseBodyParser.parseData(data) - if !request.acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { - let responseError = try request.errorFromObject(object, URLResponse: HTTPURLResponse) - throw APIKitError.ResponseError(responseError) + public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) { + switch request.createTaskInURLSession(URLSession) { + case .Failure(let error): + handler(.Failure(error)) + + case .Success(let task): + task.request = Box(request) + task.completionHandler = { data, URLResponse, connectionError in + let mainQueue = dispatch_get_main_queue() + + if let error = connectionError { + dispatch_async(mainQueue) { + handler(.Failure(.ConnectionError(error))) + } + return } - let response = try request.responseFromObject(object, URLResponse: HTTPURLResponse) - dispatch_async(dispatch_get_main_queue()) { - handler(.success(response)) + switch request.parseData(data, URLResponse: URLResponse) { + case .Failure(let error): + dispatch_async(mainQueue) { + handler(.Failure(error)) + } + + case .Success(let response): + dispatch_async(mainQueue) { + handler(.Success(response)) + } } - } catch let error as APIKitError { - notifyError(error) - } catch { - notifyError(.UnexpectedResponse) } + + task.resume() } - - task.resume() - - return task } public class func cancelRequest(requestType: T.Type, passingTest test: T -> Bool = { r in true }) { cancelRequest(requestType, URLSession: defaultURLSession, passingTest: test) } - + public class func cancelRequest(requestType: T.Type, URLSession: NSURLSession, passingTest test: T -> Bool = { r in true }) { URLSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in let allTasks = dataTasks as [NSURLSessionTask] @@ -84,7 +62,7 @@ public class API { switch task { case let x as NSURLSessionDataTask: request = x.request?.value as? T - + case let x as NSURLSessionDownloadTask: request = x.request?.value as? T @@ -180,7 +158,7 @@ private extension NSURLSessionDataTask { } } -extension NSURLSessionDownloadTask { +private extension NSURLSessionDownloadTask { private var request: Box? { get { return objc_getAssociatedObject(self, &taskRequestKey) as? Box diff --git a/APIKit/APIError.swift b/APIKit/APIError.swift new file mode 100644 index 00000000..84b9ca44 --- /dev/null +++ b/APIKit/APIError.swift @@ -0,0 +1,31 @@ +import Foundation + +public enum APIError: ErrorType { + /// Error of `NSURLSession`. + case ConnectionError(NSError) + + /// Invalid `Request.baseURL`. + case InvalidBaseURL(NSURL) + + /// Error in `Request.configureURLRequest()`. + case ConfigurationError(ErrorType) + + /// Error in `RequestBodyBuilder.buildBodyFromObject()`. + case RequestBodySerializationError(ErrorType) + + /// Failed to create `NSURLSessionDataTask` from `NSURLSession.dataTaskWithRequest()`. + case FailedToCreateURLSessionTask + + /// Indicates `NSHTTPURLResponse.statusCode` is not contained in `Request.statusCode`. + /// Second associated value is return value of `errorFromObject()`. + case UnacceptableStatusCode(Int, ErrorType) + + /// Error in `ResponseBodyParser.parseData()`. + case ResponseBodyDeserializationError(ErrorType) + + /// Indicates `responseFromObject()` or `errorFromObject()` returned nil. + case InvalidResponseStructure(AnyObject) + + /// Failed to cast `URLResponse` to `NSHTTPURLResponse`. + case NotHTTPURLResponse(NSURLResponse?) +} diff --git a/APIKit/Errors.swift b/APIKit/Errors.swift deleted file mode 100644 index a9c4ef45..00000000 --- a/APIKit/Errors.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -public enum APIKitError: ErrorType { - case InvalidRequest - case UnexpectedResponse - case ConnectionError(NSError) - case ResponseError(ErrorType) -} diff --git a/APIKit/Request.swift b/APIKit/Request.swift index c4d7ebdd..bdfc24bc 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -34,11 +34,11 @@ public protocol Request { /// Build `Response` instance from raw response object. /// This method will be called if `acceptableStatusCode` contains status code of NSHTTPURLResponse. - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? /// Build `ErrorType` instance from raw response object. /// This method will be called if `acceptableStatusCode` does not contain status code of NSHTTPURLResponse. - func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType + func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? } /// Default implementation of Request protocol @@ -63,33 +63,73 @@ public extension Request { return URLRequest } - public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { - return APIKitError.UnexpectedResponse + public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + return NSError(domain: "APIKitErrorDomain", code: 0, userInfo: nil) } - internal func buildURLRequest() throws -> NSURLRequest { + // Use Result here because `throws` loses type info of an error (in Swift 2 beta 2) + internal func createTaskInURLSession(URLSession: NSURLSession) -> Result { guard let components = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { - throw APIKitError.InvalidRequest + return .Failure(.InvalidBaseURL(baseURL)) } - let request = NSMutableURLRequest() + let URLRequest = NSMutableURLRequest() switch method { case .GET, .HEAD, .DELETE: components.query = URLEncodedSerialization.stringFromDictionary(parameters) default: - request.HTTPBody = try requestBodyBuilder.buildBodyFromObject(parameters) + do { + URLRequest.HTTPBody = try requestBodyBuilder.buildBodyFromObject(parameters) + } catch { + return .Failure(.RequestBodySerializationError(error)) + } } components.path = (components.path ?? "").stringByAppendingPathComponent(path) - request.URL = components.URL - request.HTTPMethod = method.rawValue - request.setValue(requestBodyBuilder.contentTypeHeader, forHTTPHeaderField: "Content-Type") - request.setValue(responseBodyParser.acceptHeader, forHTTPHeaderField: "Accept") + URLRequest.URL = components.URL + URLRequest.HTTPMethod = method.rawValue + URLRequest.setValue(requestBodyBuilder.contentTypeHeader, forHTTPHeaderField: "Content-Type") + URLRequest.setValue(responseBodyParser.acceptHeader, forHTTPHeaderField: "Accept") + + do { + try configureURLRequest(URLRequest) + } catch { + return .Failure(.ConfigurationError(error)) + } + + guard let task = URLSession.dataTaskWithRequest(URLRequest) else { + return .Failure(.FailedToCreateURLSessionTask) + } + + return .Success(task) + } + + // Use Result here because `throws` loses type info of an error (in Swift 2 beta 2) + internal func parseData(data: NSData, URLResponse: NSURLResponse?) -> Result { + guard let HTTPURLResponse = URLResponse as? NSHTTPURLResponse else { + return .Failure(.NotHTTPURLResponse(URLResponse)) + } + + let object: AnyObject + do { + object = try responseBodyParser.parseData(data) + } catch { + return .Failure(.ResponseBodyDeserializationError(error)) + } - try configureURLRequest(request) + if !acceptableStatusCodes.contains(HTTPURLResponse.statusCode) { + guard let error = errorFromObject(object, URLResponse: HTTPURLResponse) else { + return .Failure(.InvalidResponseStructure(object)) + } + return .Failure(.UnacceptableStatusCode(HTTPURLResponse.statusCode, error)) + } + + guard let response = responseFromObject(object, URLResponse: HTTPURLResponse) else { + return .Failure(.InvalidResponseStructure(object)) + } - return request + return .Success(response) } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 16d34a6f..8a6c9f86 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -10,17 +10,9 @@ extension MockAPIRequest { var baseURL: NSURL { return NSURL(string: "https://api.github.com")! } - - func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { - return MockAPI.Error.Mock - } } class MockAPI: API { - enum Error: ErrorType { - case Mock - } - struct GetRoot: MockAPIRequest { typealias Response = [String: AnyObject] @@ -32,26 +24,22 @@ class MockAPI: API { return "/" } - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { - guard let response = object as? [String: AnyObject] else { - throw Error.Mock - } - - return response + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { + return object as? [String: AnyObject] } } } -class APITests: XCTestCase { +class AnotherMockAPI: API { - class AnotherMockAPI: API { - } - +} + +class APITests: XCTestCase { override func tearDown() { OHHTTPStubs.removeAllStubs() super.tearDown() } - + // MARK: - integration tests func testSuccess() { let dictionary = ["key": "value"] @@ -107,13 +95,13 @@ class APITests: XCTestCase { XCTFail() } } - + expectation.fulfill() } waitForExpectationsWithTimeout(1.0, handler: nil) } - + func testFailureOfResponseStatusCode() { OHHTTPStubs.stubRequestsPassingTest({ request in return true @@ -133,8 +121,9 @@ class APITests: XCTestCase { case .Failure(let error): switch error { - case .ResponseError(let error): - XCTAssert(error is MockAPI.Error) + case .UnacceptableStatusCode(let statusCode, let error as NSError): + XCTAssert(statusCode == 400) + XCTAssert(error.domain == "APIKitErrorDomain") default: XCTFail() @@ -166,8 +155,9 @@ class APITests: XCTestCase { case .Failure(let error): switch error { - case .UnexpectedResponse: - break + case .ResponseBodyDeserializationError(let error as NSError): + XCTAssert(error.domain == NSCocoaErrorDomain) + XCTAssert(error.code == 3840) default: XCTFail() @@ -217,7 +207,7 @@ class APITests: XCTestCase { waitForExpectationsWithTimeout(1.0, handler: nil) } - + func testSuccessIfCancelingTestReturnsFalse() { OHHTTPStubs.stubRequestsPassingTest({ request in return true From 43c4708f255d6b8dc8fbeefe9ed28bba3c818029 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sat, 27 Jun 2015 02:44:13 +0900 Subject: [PATCH 42/44] Refactor completion handler --- APIKit/API.swift | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index e346a9a1..48f8dc46 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -21,25 +21,19 @@ public class API { case .Success(let task): task.request = Box(request) task.completionHandler = { data, URLResponse, connectionError in - let mainQueue = dispatch_get_main_queue() - + let sessionResult: Result<(NSData, NSURLResponse?), APIError> if let error = connectionError { - dispatch_async(mainQueue) { - handler(.Failure(.ConnectionError(error))) - } - return + sessionResult = .Failure(.ConnectionError(error)) + } else { + sessionResult = .Success((data, URLResponse)) } - switch request.parseData(data, URLResponse: URLResponse) { - case .Failure(let error): - dispatch_async(mainQueue) { - handler(.Failure(error)) - } + let result: Result = sessionResult.flatMap { data, URLResponse in + request.parseData(data, URLResponse: URLResponse) + } - case .Success(let response): - dispatch_async(mainQueue) { - handler(.Success(response)) - } + dispatch_async(dispatch_get_main_queue()) { + handler(result) } } From 199f9526e91c583c48261abbe5e0aeeb91906f98 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sun, 28 Jun 2015 12:43:03 +0900 Subject: [PATCH 43/44] Restore return value of sendRequest() --- APIKit/API.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/APIKit/API.swift b/APIKit/API.swift index 48f8dc46..bcc993ab 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -13,12 +13,15 @@ public class API { ) // send request and build response object - public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) { + public class func sendRequest(request: T, URLSession: NSURLSession = defaultURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { + var dataTask: NSURLSessionDataTask? + switch request.createTaskInURLSession(URLSession) { case .Failure(let error): handler(.Failure(error)) case .Success(let task): + dataTask = task task.request = Box(request) task.completionHandler = { data, URLResponse, connectionError in let sessionResult: Result<(NSData, NSURLResponse?), APIError> @@ -39,6 +42,8 @@ public class API { task.resume() } + + return dataTask } public class func cancelRequest(requestType: T.Type, passingTest test: T -> Bool = { r in true }) { From 2e03412a6ed3ba25d7a3abc3cf281267cbfce606 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Sun, 28 Jun 2015 12:49:00 +0900 Subject: [PATCH 44/44] Update README and doc --- APIKit/Request.swift | 2 +- Demo.playground/Contents.swift | 6 +++--- README.md | 22 +++++++++++----------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/APIKit/Request.swift b/APIKit/Request.swift index bdfc24bc..bec02bb6 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -7,7 +7,7 @@ import Result /// - var baseURL: NSURL /// - var method: Method /// - var path: String -/// - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response +/// - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? public protocol Request { /// Type represents a model object typealias Response diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 1e8ccde6..2a380fb0 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -53,13 +53,13 @@ extension GitHubAPI { return "/rate_limit" } - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { guard let dictionary = object as? [String: AnyObject] else { - throw APIKitError.UnexpectedResponse + return nil } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw APIKitError.UnexpectedResponse + return nil } return rateLimit diff --git a/README.md b/README.md index 54df6caf..0a7bacca 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,13 @@ class GitHubAPI: API { return "/rate_limit" } - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { guard let dictionary = object as? [String: AnyObject] else { - throw APIKitError.UnexpectedResponse + return nil } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw APIKitError.UnexpectedResponse + return nil } return rateLimit @@ -199,13 +199,13 @@ var acceptableStatusCodes: Set { #### Building a model from a response ```swift -func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { +func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { guard let dictionary = object as? [String: AnyObject] else { - throw APIKitError.UnexpectedResponse + return nil } guard let rateLimit = RateLimit(dictionary: dictionary) else { - throw APIKitError.UnexpectedResponse + return nil } return rateLimit @@ -225,13 +225,13 @@ For example, [GitHub API](https://developer.github.com/v3/#client-errors) return To create error that contains `message` in response, implement `errorFromObject(_:URLResponse:)` and return `ErrorType` using object. ```swift -func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> ErrorType { +func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { guard let dictionary = object as? [String: AnyObject] else { - throw APIKitError.UnexpectedResponse + return nil } guard let message = dictionary["message"] as? String else { - throw APIKitError.UnexpectedResponse + return nil } return GitHubError(message: message) @@ -318,9 +318,9 @@ struct SomePaginatedRequest: Request { let page: Int - static func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + static func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { guard let dictionaries = object as? [[String: AnyObject]] else { - throw APIKitError.UnexpectedResponse + return nil } var somes = [Some]()