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/.gitmodules b/.gitmodules index 2f857506..30480090 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,6 @@ -[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 -[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 -[submodule "Carthage/Checkouts/Himotoki"] - path = Carthage/Checkouts/Himotoki - url = https://github.com/ikesyo/Himotoki.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 c56f2cc7..b3df4043 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -17,46 +17,26 @@ 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 Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 7F45FD741A94E832006863BB /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD731A94E832006863BB /* Models.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 /* 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 */; }; 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 */; }; 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, ); }; }; - CD9B38231B2556FD00B1A224 /* Himotoki.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD9B38221B2556FD00B1A224 /* Himotoki.framework */; }; - CD9B38241B25578200B1A224 /* Himotoki.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD9B38221B2556FD00B1A224 /* Himotoki.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, 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 */ @@ -67,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 */; @@ -93,27 +66,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD701A94DA28006863BB /* Copy Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - CD9B38241B25578200B1A224 /* Himotoki.framework in Copy Frameworks */, - 7F45FD6D1A94DA28006863BB /* APIKit.framework in Copy Frameworks */, - ); - name = "Copy Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A221A97001500B1D3C0 /* Copy Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; 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 */, ); @@ -132,27 +91,17 @@ 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; }; - 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 = ""; }; + 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 /* 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; }; 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; }; + 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; }; - 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; }; - CD9B38221B2556FD00B1A224 /* Himotoki.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Himotoki.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -160,9 +109,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; 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 */, ); @@ -172,7 +119,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD5115201B1FFB4F00514240 /* Box.framework in Frameworks */, CD5115251B1FFBA900514240 /* Result.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -181,29 +127,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD51151F1B1FFAC000514240 /* Box.framework in Frameworks */, CD5115261B1FFBA900514240 /* Result.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD451A94D9A8006863BB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - CD5115231B1FFB8E00514240 /* Box.framework in Frameworks */, - CD5115291B1FFBA900514240 /* Result.framework in Frameworks */, - 7F45FD6C1A94DA28006863BB /* APIKit.framework in Frameworks */, - CD9B38231B2556FD00B1A224 /* Himotoki.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A111A96FE2600B1D3C0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; 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 */, ); @@ -215,9 +147,9 @@ 7F45FCD31A94D02C006863BB = { isa = PBXGroup; children = ( + 7FED2F111B34565D002AC9D7 /* Demo.playground */, 7F45FCDF1A94D02C006863BB /* APIKit */, 7FEC5A151A96FE2600B1D3C0 /* APIKitTests */, - 7F45FD491A94D9A8006863BB /* DemoApp */, 7F45FCDE1A94D02C006863BB /* Products */, ); sourceTree = ""; @@ -227,7 +159,6 @@ children = ( 7F45FCDD1A94D02C006863BB /* APIKit.framework */, 7F45FCFE1A94D04D006863BB /* APIKit.framework */, - 7F45FD481A94D9A8006863BB /* DemoApp.app */, 7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */, 7F0869941A978790001AD3E1 /* APIKitTests.xctest */, ); @@ -240,7 +171,8 @@ 7F45FCE21A94D02C006863BB /* APIKit.h */, 7F45FD171A94D085006863BB /* API.swift */, 7F68ABD91AC4412E00688D68 /* Request.swift */, - 7F68ABDC1AC4414500688D68 /* Method.swift */, + 7F68ABDC1AC4414500688D68 /* HTTPMethod.swift */, + 7F5FA6B41B3C58210090B0AF /* APIError.swift */, 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */, 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */, 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */, @@ -252,37 +184,12 @@ 7F45FCE01A94D02C006863BB /* Supporting Files */ = { isa = PBXGroup; children = ( - CD51151E1B1FFAC000514240 /* Box.framework */, CD5115241B1FFBA900514240 /* Result.framework */, 7F45FCE11A94D02C006863BB /* Info.plist */, ); 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 = ( - CD9B38221B2556FD00B1A224 /* Himotoki.framework */, - 7F45FD4B1A94D9A8006863BB /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; 7FEC5A151A96FE2600B1D3C0 /* APIKitTests */ = { isa = PBXGroup; children = ( @@ -297,7 +204,6 @@ 7FEC5A161A96FE2600B1D3C0 /* Supporting Files */ = { isa = PBXGroup; children = ( - CD51152A1B1FFCB200514240 /* Assertions.framework */, CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */, 7FEC5A171A96FE2600B1D3C0 /* Info.plist */, ); @@ -379,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 /* Copy 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" */; @@ -423,7 +310,8 @@ 7F45FCD41A94D02C006863BB /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0610; + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0700; ORGANIZATIONNAME = "Yosuke Ishikawa"; TargetAttributes = { 7F0869931A978790001AD3E1 = { @@ -435,9 +323,6 @@ 7F45FCFD1A94D04D006863BB = { CreatedOnToolsVersion = 6.1.1; }; - 7F45FD471A94D9A8006863BB = { - CreatedOnToolsVersion = 6.1.1; - }; 7FEC5A131A96FE2600B1D3C0 = { CreatedOnToolsVersion = 6.1.1; }; @@ -460,7 +345,6 @@ 7F45FCFD1A94D04D006863BB /* APIKit-Mac */, 7FEC5A131A96FE2600B1D3C0 /* APIKitTests-iOS */, 7F0869931A978790001AD3E1 /* APIKitTests-Mac */, - 7F45FD471A94D9A8006863BB /* DemoApp */, ); }; /* End PBXProject section */ @@ -487,16 +371,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD461A94D9A8006863BB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7F45FD521A94D9A9006863BB /* Main.storyboard in Resources */, - 7F45FD571A94D9A9006863BB /* LaunchScreen.xib in Resources */, - 7F45FD541A94D9A9006863BB /* Images.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7FEC5A121A96FE2600B1D3C0 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -523,9 +397,10 @@ 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 */, + 7F5FA6B51B3C58210090B0AF /* APIError.swift in Sources */, 7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -536,24 +411,13 @@ 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; - 7F45FD441A94D9A8006863BB /* Sources */ = { - 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; - }; 7FEC5A101A96FE2600B1D3C0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -572,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 */; @@ -584,25 +443,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; @@ -615,6 +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 = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = macosx; }; @@ -628,6 +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 = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = macosx; }; @@ -653,7 +495,9 @@ "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; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -699,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; @@ -729,7 +574,7 @@ 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"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -748,7 +593,7 @@ 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"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -773,7 +618,7 @@ 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; SKIP_INSTALL = YES; @@ -796,41 +641,13 @@ 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; SKIP_INSTALL = YES; }; 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_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_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - }; - name = Release; - }; 7FEC5A1D1A96FE2600B1D3C0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -840,6 +657,7 @@ ); INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = iphoneos; }; @@ -850,6 +668,7 @@ buildSettings = { INFOPLIST_FILE = APIKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ishkawa.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(PROJECT_NAME)Tests"; SDKROOT = iphoneos; }; @@ -894,15 +713,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/APIKit-Mac.xcscheme b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme index ed8993c5..56fe439b 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.xcworkspace/contents.xcworkspacedata b/APIKit.xcworkspace/contents.xcworkspacedata index 0d4562d4..a0806196 100644 --- a/APIKit.xcworkspace/contents.xcworkspacedata +++ b/APIKit.xcworkspace/contents.xcworkspacedata @@ -4,19 +4,10 @@ - - - - - - diff --git a/APIKit/API.swift b/APIKit/API.swift index 559e30f7..bcc993ab 100644 --- a/APIKit/API.swift +++ b/APIKit/API.swift @@ -1,142 +1,67 @@ import Foundation - -#if APIKIT_DYNAMIC_FRAMEWORK || COCOAPODS import Result -import Box -#endif - -public let APIKitErrorDomain = "APIKitErrorDomain" public class API { - // configurations - public class var baseURL: NSURL { - fatalError("API.baseURL must be overrided in subclasses.") - } - - public class var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: nil) - } - - public class var responseBodyParser: ResponseBodyParser { - return .JSON(readingOptions: nil) - } - 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, 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) - - default: - switch requestBodyBuilder.buildBodyFromObject(parameters) { - case .Success(let box): - request.HTTPBody = box.value - - case .Failure(let box): - 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 - } - } - - @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() - - if let URLRequest = request.URLRequest { - let task = URLSession.dataTaskWithRequest(URLRequest) - + 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> if let error = connectionError { - dispatch_async(mainQueue) { handler(.failure(error)) } - return + sessionResult = .Failure(.ConnectionError(error)) + } else { + sessionResult = .Success((data, URLResponse)) } - - let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0 - if !contains(self.acceptableStatusCodes, statusCode) { - let error = self.responseBodyParser.parseData(data).analysis( - ifSuccess: { self.responseErrorFromObject($0) }, - ifFailure: { $0 } - ) - - dispatch_async(mainQueue) { handler(.failure(error)) } - return + + let result: Result = sessionResult.flatMap { data, URLResponse in + request.parseData(data, URLResponse: URLResponse) } - - 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(dispatch_get_main_queue()) { + handler(result) } - - dispatch_async(mainQueue) { handler(mappedResponse) } } task.resume() - - return task - } else { - let userInfo = [NSLocalizedDescriptionKey: "failed to build request."] - let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) - dispatch_async(mainQueue) { handler(.failure(error)) } - - return nil } + + return dataTask } - + 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 tasks = (dataTasks + uploadTasks + downloadTasks).filter { task in + let allTasks = dataTasks as [NSURLSessionTask] + + uploadTasks as [NSURLSessionTask] + + downloadTasks as [NSURLSessionTask] + + let tasks = allTasks.filter { task in var request: T? switch task { case let x as NSURLSessionDataTask: request = x.request?.value as? T - + case let x as NSURLSessionDownloadTask: request = x.request?.value as? T @@ -156,12 +81,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 @@ -183,6 +102,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 +125,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_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } @@ -210,7 +137,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_ASSOCIATION_RETAIN_NONATOMIC) return responseBuffer } } @@ -222,15 +149,15 @@ 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_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } } -extension NSURLSessionDownloadTask { +private extension NSURLSessionDownloadTask { private var request: Box? { get { return objc_getAssociatedObject(self, &taskRequestKey) as? Box @@ -238,9 +165,9 @@ extension NSURLSessionDownloadTask { set { if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + objc_setAssociatedObject(self, &taskRequestKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } 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/Method.swift b/APIKit/HTTPMethod.swift similarity index 88% rename from APIKit/Method.swift rename to APIKit/HTTPMethod.swift index fea29ad3..6931e782 100644 --- a/APIKit/Method.swift +++ b/APIKit/HTTPMethod.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/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/Request.swift b/APIKit/Request.swift index 3043bf93..bec02bb6 100644 --- a/APIKit/Request.swift +++ b/APIKit/Request.swift @@ -1,9 +1,135 @@ import Foundation +import Result +/// Request protocol represents a request for Web API. +/// Following 5 items must be implemented. +/// - typealias Response +/// - var baseURL: NSURL +/// - var method: Method +/// - var path: String +/// - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? public protocol Request { - typealias Response: Any - - var URLRequest: NSURLRequest? { get } - - static func responseFromObject(object: AnyObject) -> Response? + /// Type represents a model object + typealias Response + + /// Configurations of request + var baseURL: NSURL { get } + var method: HTTPMethod { get } + var path: String { get } + var parameters: [String: AnyObject] { 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 } + + /// 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) -> 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? +} + +/// Default implementation of Request protocol +public extension Request { + public var parameters: [String: AnyObject] { + return [:] + } + + public var acceptableStatusCodes: Set { + return Set(200..<300) + } + + public var requestBodyBuilder: RequestBodyBuilder { + return .JSON(writingOptions: []) + } + + public var responseBodyParser: ResponseBodyParser { + return .JSON(readingOptions: []) + } + + public func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { + return URLRequest + } + + public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + return NSError(domain: "APIKitErrorDomain", code: 0, userInfo: nil) + } + + // 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 { + return .Failure(.InvalidBaseURL(baseURL)) + } + + let URLRequest = NSMutableURLRequest() + + switch method { + case .GET, .HEAD, .DELETE: + components.query = URLEncodedSerialization.stringFromDictionary(parameters) + + default: + do { + URLRequest.HTTPBody = try requestBodyBuilder.buildBodyFromObject(parameters) + } catch { + return .Failure(.RequestBodySerializationError(error)) + } + } + + components.path = (components.path ?? "").stringByAppendingPathComponent(path) + 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)) + } + + 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 .Success(response) + } } diff --git a/APIKit/RequestBodyBuilder.swift b/APIKit/RequestBodyBuilder.swift index 46156059..09bcb26a 100644 --- a/APIKit/RequestBodyBuilder.swift +++ b/APIKit/RequestBodyBuilder.swift @@ -1,15 +1,10 @@ import Foundation - -#if APIKIT_DYNAMIC_FRAMEWORK || COCOAPODS import Result -#endif - -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 { @@ -24,26 +19,20 @@ public enum RequestBodyBuilder { } } - 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) - } - - return try { error in - return NSJSONSerialization.dataWithJSONObject(object, options: writingOptions, error: error) + // If isValidJSONObject(_:) is false, dataWithJSONObject(_:options:) throws NSException. + guard NSJSONSerialization.isValidJSONObject(object) else { + throw NSError(domain: NSCocoaErrorDomain, code: 3840, userInfo: nil) } + return try NSJSONSerialization.dataWithJSONObject(object, options: writingOptions) case .URL(let encoding): - return try { error in - return URLEncodedSerialization.dataFromObject(object, encoding: encoding, error: error) - } + return try URLEncodedSerialization.dataFromObject(object, encoding: encoding) case .Custom(let (_, buildBodyFromObject)): - return buildBodyFromObject(object) + return try buildBodyFromObject(object) } } } diff --git a/APIKit/ResponseBodyParser.swift b/APIKit/ResponseBodyParser.swift index 62fb3e2a..09c84ae9 100644 --- a/APIKit/ResponseBodyParser.swift +++ b/APIKit/ResponseBodyParser.swift @@ -1,13 +1,10 @@ import Foundation - -#if APIKIT_DYNAMIC_FRAMEWORK || COCOAPODS import Result -#endif 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 { @@ -22,23 +19,19 @@ public enum ResponseBodyParser { } } - 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 try { error in - return NSJSONSerialization.JSONObjectWithData(data, options: readingOptions, error: error) + return [:] } + return try NSJSONSerialization.JSONObjectWithData(data, options: readingOptions) case .URL(let encoding): - return try { error in - return URLEncodedSerialization.objectFromData(data, encoding: encoding, error: error) - } + return try URLEncodedSerialization.objectFromData(data, encoding: encoding) - case .Custom(let (accept, parseData)): - return parseData(data) + case .Custom(let (_, parseData)): + return try parseData(data) } } } diff --git a/APIKit/URLEncodedSerialization.swift b/APIKit/URLEncodedSerialization.swift index 17204e20..0e2aeaf4 100644 --- a/APIKit/URLEncodedSerialization.swift +++ b/APIKit/URLEncodedSerialization.swift @@ -9,52 +9,51 @@ 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 Error: ErrorType { + 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(data, encoding) } - - if dictionary == nil { - let userInfo = [NSLocalizedDescriptionKey: "failed to decode urlencoded string."] - error.memory = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo) + + var dictionary = [String: String]() + for pair in string.componentsSeparatedByString("&") { + let contents = pair.componentsSeparatedByString("=") + + guard contents.count == 2 else { + throw Error.InvalidFormatString(string) + } + + dictionary[contents[0]] = unescape(contents[1]) } - + return dictionary } - public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding, error: NSErrorPointer) -> NSData? { - let string = stringFromObject(object) - 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 { + guard let dictionary = object as? [String: AnyObject] else { + throw Error.CannotCastObjectToDictionary(object) + } + + let string = stringFromDictionary(dictionary) + guard let data = string.dataUsingEncoding(encoding, allowLossyConversion: false) else { + throw Error.CannotGetDataFromString(string, encoding) } - + return data } - public class func stringFromObject(object: AnyObject) -> String { - 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) - } + 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))" } - - return join("&", pairs) + + return "&".join(pairs) } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift index 65143ec3..8a6c9f86 100644 --- a/APIKitTests/APITests.swift +++ b/APIKitTests/APITests.swift @@ -1,46 +1,49 @@ import Foundation import APIKit import XCTest -import Assertions import OHHTTPStubs -class APITests: XCTestCase { - class MockAPI: API { - override class var baseURL: NSURL { - return NSURL(string: "https://api.github.com")! +protocol MockAPIRequest: Request { +} + +extension MockAPIRequest { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } +} + +class MockAPI: API { + struct GetRoot: MockAPIRequest { + typealias Response = [String: AnyObject] + + var method: HTTPMethod { + return .GET } - - override class func responseErrorFromObject(object: AnyObject) -> NSError { - return NSError(domain: "MockAPIErrorDomain", code: 10000, userInfo: nil) + + var path: String { + return "/" } - - 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] - } - } + + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { + return object as? [String: AnyObject] } } - - 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"] - let data = NSJSONSerialization.dataWithJSONObject(dictionary, options: nil, error: nil)! + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) OHHTTPStubs.stubRequestsPassingTest({ request in return true @@ -49,13 +52,13 @@ 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(let box): - assert(box.value, ==, dictionary) - + case .Success(let dictionary): + XCTAssert(dictionary["key"] as? String == "value") + case .Failure: XCTFail() } @@ -76,45 +79,55 @@ 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: XCTFail() - case .Failure(let box): - let error = box.value - assertEqual(error.domain, error.domain) - assertEqual(error.code, error.code) + case .Failure(let error): + switch error { + case .ConnectionError(let error): + XCTAssert(error.domain == NSURLErrorDomain) + + default: + XCTFail() + } } - + expectation.fulfill() } waitForExpectationsWithTimeout(1.0, handler: nil) } - + func testFailureOfResponseStatusCode() { 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: []) return OHHTTPStubsResponse(data: data, statusCode: 400, headers: nil) }) let expectation = expectationWithDescription("wait for response") - let request = MockAPI.Endpoint.Get() + let request = MockAPI.GetRoot() MockAPI.sendRequest(request) { response in switch response { case .Success: XCTFail() - case .Failure(let box): - let error = box.value - assertEqual(error.domain, "MockAPIErrorDomain") - assertEqual(error.code, 10000) + case .Failure(let error): + switch error { + case .UnacceptableStatusCode(let statusCode, let error as NSError): + XCTAssert(statusCode == 400) + XCTAssert(error.domain == "APIKitErrorDomain") + + default: + XCTFail() + } } expectation.fulfill() @@ -133,17 +146,22 @@ 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: XCTFail() - case .Failure(let box): - let error = box.value - assert(error.domain, ==, NSCocoaErrorDomain) - assertEqual(error.code, 3840) + case .Failure(let error): + switch error { + case .ResponseBodyDeserializationError(let error as NSError): + XCTAssert(error.domain == NSCocoaErrorDomain) + XCTAssert(error.code == 3840) + + default: + XCTFail() + } } expectation.fulfill() @@ -164,32 +182,38 @@ 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: XCTFail() - case .Failure(let box): - let error = box.value - assert(error.domain, ==, NSURLErrorDomain) - assertEqual(error.code, NSURLErrorCancelled) + case .Failure(let error): + switch error { + case .ConnectionError(let error): + XCTAssert(error.domain == NSURLErrorDomain) + XCTAssert(error.code == NSURLErrorCancelled) + + default: + XCTFail() + } } expectation.fulfill() } - MockAPI.cancelRequest(MockAPI.Endpoint.Get.self) + MockAPI.cancelRequest(MockAPI.GetRoot.self) waitForExpectationsWithTimeout(1.0, handler: nil) } - + func testSuccessIfCancelingTestReturnsFalse() { 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: []) let response = OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) response.requestTime = 0.1 response.responseTime = 0.1 @@ -197,21 +221,21 @@ 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: break - case .Failure(let box): + case .Failure: XCTFail() } expectation.fulfill() } - MockAPI.cancelRequest(MockAPI.Endpoint.Get.self) { request in + MockAPI.cancelRequest(MockAPI.GetRoot.self) { request in return false } 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 cadfc654..bafb5f02 100644 --- a/APIKitTests/RequestBodyBuilderTests.swift +++ b/APIKitTests/RequestBodyBuilderTests.swift @@ -1,101 +1,97 @@ 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") + 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: nil) + let builder = RequestBodyBuilder.JSON(writingOptions: []) - 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) - - 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() } } func testJSONFailure() { let object = NSObject() - let builder = RequestBodyBuilder.JSON(writingOptions: nil) + let builder = RequestBodyBuilder.JSON(writingOptions: []) - switch builder.buildBodyFromObject(object) { - case .Success: + do { + try builder.buildBodyFromObject(object) XCTFail() - - case .Failure(let box): - let error = box.value - assertEqual(error.domain, APIKitRequestBodyBuidlerErrorDomain) - assertEqual(error.code, 0) + } catch { + let nserror = error as NSError + XCTAssert(nserror.domain == NSCocoaErrorDomain) + XCTAssert(nserror.code == 3840) } } 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() { let object = ["foo": 1, "bar": 2, "baz": 3] 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] - assertEqual(dictionary?["foo"], "1") - assertEqual(dictionary?["bar"], "2") - assertEqual(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) }) - assertEqual(builder.contentTypeHeader, "foo") + 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) - }) - - switch builder.buildBodyFromObject(string) { - case .Success(let box): - assertEqual(box.value, expectedData) + let builder = RequestBodyBuilder.Custom(contentTypeHeader: "") { object in + expectedData + } - case .Failure: + do { + let data = try builder.buildBodyFromObject(string) + XCTAssert(data == expectedData) + } catch { XCTFail() } } func testCustomFailure() { let string = "foo" - let expectedError = NSError() - let builder = RequestBodyBuilder.Custom(contentTypeHeader: "", buildBodyFromObject: { object in - return .failure(expectedError) - }) + let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) + let builder = RequestBodyBuilder.Custom(contentTypeHeader: "") { object in + throw expectedError + } - switch builder.buildBodyFromObject(string) { - case .Success: + do { + try builder.buildBodyFromObject(string) XCTFail() - - case .Failure(let box): - assertEqual(box.value, expectedError) + } catch { + XCTAssert((error as NSError) == expectedError) } } } diff --git a/APIKitTests/ResponseBodyParserTests.swift b/APIKitTests/ResponseBodyParserTests.swift index dc07740d..bf0d19e1 100644 --- a/APIKitTests/ResponseBodyParserTests.swift +++ b/APIKitTests/ResponseBodyParserTests.swift @@ -1,28 +1,26 @@ 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") + 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: nil) + let parser = ResponseBodyParser.JSON(readingOptions: []) - 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) - - case .Failure: + 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) + } catch { XCTFail() } } @@ -30,22 +28,21 @@ 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: []) - switch parser.parseData(data) { - case .Success: + do { + try parser.parseData(data) XCTFail() - - case .Failure(let box): - let error = box.value - assert(error.domain, ==, NSCocoaErrorDomain) - assertEqual(error.code, 3840) + } catch { + let nserror = error as NSError + XCTAssert(nserror.domain == NSCocoaErrorDomain) + XCTAssert(nserror.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() { @@ -53,53 +50,51 @@ class ResponseBodyParserTests: XCTestCase { let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! let parser = ResponseBodyParser.URL(encoding: NSUTF8StringEncoding) - 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") - - case .Failure: + 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") + } catch { XCTFail() } } func testCustomAcceptHeader() { - let parser = ResponseBodyParser.Custom(acceptHeader: "foo", parseData: { d in .success(d) }) - assertEqual(parser.acceptHeader, "foo") + let parser = ResponseBodyParser.Custom(acceptHeader: "foo") { data in + data + } + XCTAssert(parser.acceptHeader == "foo") } func testCustomSuccess() { - let expectedDictionary = ["foo": 1] let data = NSData() - let parser = ResponseBodyParser.Custom(acceptHeader: "", parseData: { data in - return .success(expectedDictionary) - }) - - switch parser.parseData(data) { - case .Success(let box): - let dictionary = box.value as? [String: Int] - assertEqual(dictionary, expectedDictionary) + let parser = ResponseBodyParser.Custom(acceptHeader: "") { data in + ["foo": 1] + } - case .Failure: + do { + let object = try parser.parseData(data) + let dictionary = object as? [String: Int] + XCTAssert(dictionary?["foo"] == 1) + } catch { XCTFail() } } 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) - }) + let parser = ResponseBodyParser.Custom(acceptHeader: "") { data in + throw expectedError + } - switch parser.parseData(data) { - case .Success: + do { + try parser.parseData(data) XCTFail() - - case .Failure(let box): - assertEqual(box.value, expectedError) + } catch { + XCTAssert((error as NSError) == expectedError) } } } diff --git a/Cartfile b/Cartfile index 168c6ce1..45118632 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "antitypical/Result" ~> 0.4 +github "antitypical/Result" "ba48b0c054230de5bd3d575b6ab8f8d292f9429a" diff --git a/Cartfile.private b/Cartfile.private index 1ffbb481..14347abb 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,3 +1 @@ -github "antitypical/Assertions" ~> 1.1 github "AliSoftware/OHHTTPStubs" ~> 4.0.0 -github "ikesyo/Himotoki" ~> 0.4.1 diff --git a/Cartfile.resolved b/Cartfile.resolved index e2e9cc58..08e79902 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,5 +1,2 @@ -github "antitypical/Assertions" "1.1.1" -github "robrix/Box" "1.2.2" -github "ikesyo/Himotoki" "0.4.1" github "AliSoftware/OHHTTPStubs" "4.0.2" -github "antitypical/Result" "0.4.3" +github "antitypical/Result" "ba48b0c054230de5bd3d575b6ab8f8d292f9429a" diff --git a/Carthage/Checkouts/Assertions b/Carthage/Checkouts/Assertions deleted file mode 160000 index 472e3ea4..00000000 --- a/Carthage/Checkouts/Assertions +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 472e3ea4c0410bb5c2e7745674ac071d037fb30d 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/Himotoki b/Carthage/Checkouts/Himotoki deleted file mode 160000 index da6ee253..00000000 --- a/Carthage/Checkouts/Himotoki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit da6ee253424441f6788e77fb0a5af66a2d59b91a diff --git a/Carthage/Checkouts/Result b/Carthage/Checkouts/Result index f9045c2a..ba48b0c0 160000 --- a/Carthage/Checkouts/Result +++ b/Carthage/Checkouts/Result @@ -1 +1 @@ -Subproject commit f9045c2a1fee1af321e29eea7c633be5a6a42532 +Subproject commit ba48b0c054230de5bd3d575b6ab8f8d292f9429a diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift new file mode 100644 index 00000000..2a380fb0 --- /dev/null +++ b/Demo.playground/Contents.swift @@ -0,0 +1,83 @@ +import XCPlayground +import UIKit +import APIKit + +XCPSetExecutionShouldContinueIndefinitely() + +//: Step 1: Define request protocol +protocol GitHubRequest: Request { + +} + +extension GitHubRequest { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } +} + +//: 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 { + // 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) -> Response? { + guard let dictionary = object as? [String: AnyObject] else { + return nil + } + + guard let rateLimit = RateLimit(dictionary: dictionary) else { + return nil + } + + return rateLimit + } + } +} + +//: Step 5: Send request +let request = GitHubAPI.GetRateLimit() + +GitHubAPI.sendRequest(request) { result in + switch result { + case .Success(let rateLimit): + "count: \(rateLimit.count)" + "reset: \(rateLimit.resetDate)" + + case .Failure(let error): + "error: \(error)" + } +} + diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground new file mode 100644 index 00000000..90e9a0fb --- /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 8e16451a..00000000 --- a/DemoApp/GitHub.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import APIKit -import Himotoki - -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? { - return object["items"].flatMap(decode) ?? [] - } - } - - // 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? { - return object["items"].flatMap(decode) ?? [] - } - } - } -} 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 d65f13a1..00000000 --- a/DemoApp/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - -.$(PRODUCT_NAME:rfc1034identifier) - 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 67be4e2c..00000000 --- a/DemoApp/Models.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import Himotoki - -struct Repository: Decodable { - let id: Int - let name: String - let owner: User - - static func decode(e: Extractor) -> Repository? { - let create = { Repository($0) } - return build( - e <| "id", - e <| "name", - e <| "owner" - ).map(create) - } -} - -struct User: Decodable { - let id: Int - let login: String - let avatarURL: NSURL - - static func decode(e: Extractor) -> User? { - let create = { User($0) } - return build( - e <| "id", - e <| "login", - (e <| "avatar_url").flatMap { NSURL(string: $0) } - ).map(create) - } -} 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 - } -} diff --git a/README.md b/README.md index 8586e60c..0a7bacca 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 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 -// parameters of request are validated by type system of Swift -let request = GitHub.Endpoint.SearchRepositories(query: "APIKit", sort: .Stars) +let request = GitHub.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() +GitHub.sendRequest(request) { result in + switch result { + case .Success(let response): + self.repositories = response // inferred as [Repository] + self.tableView.reloadData() - case .Failure(let box): - // if request fails, value in box is a NSError - println(box.value) + case .Failure(let error): + print(error) } } ``` @@ -42,125 +29,88 @@ 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` 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 a 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")! - } +protocol GitHubRequest: Request { - override class var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: nil) - } +} - 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" - } - - enum Order: String { - case Ascending = "asc" - case Descending = "desc" - } +class GitHubAPI: API { + struct GetRateLimit: GitHubRequest { + typealias Response = RateLimit - typealias Response = [Repository] + var method: HTTPMethod { + 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 responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { + guard let dictionary = object as? [String: AnyObject] else { + return nil } - init(query: String, sort: Sort = .Stars, order: Order = .Ascending) { - self.query = query - self.sort = sort - self.order = order + guard let rateLimit = RateLimit(dictionary: dictionary) else { + return nil } - class func responseFromObject(object: AnyObject) -> Response? { - var repositories = [Repository]() + return rateLimit + } + } +} - if let dictionaries = object["items"] as? [NSDictionary] { - for dictionary in dictionaries { - if let repository = Repository(dictionary: dictionary) { - repositories.append(repository) - } - } - } +struct RateLimit { + let count: Int + let resetDate: NSDate - return repositories - } + init?(dictionary: [String: AnyObject]) { + guard let count = dictionary["rate"]?["limit"] as? Int else { + return nil } - // define other requests here + guard let resetDateString = dictionary["rate"]?["reset"] as? NSTimeInterval else { + return nil + } + + self.count = count + self.resetDate = NSDate(timeIntervalSince1970: resetDateString) } } ``` @@ -168,15 +118,16 @@ class GitHub: API { ### Sending request ```swift -let request = GitHub.Endpoint.SearchRepositories(query: "APIKit", sort: .Stars) +let request = GitHubAPI.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,23 +135,86 @@ GitHub.sendRequest(request) { response in ### Canceling request ```swift -GitHub.cancelRequest(GitHub.Endpoint.SearchRepositories.self) +GitHub.cancelRequest(GitHubAPI.GetRateLimit.self) ``` -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 +GitHub.cancelRequest(GitHubAPI.SearchRepositories.self) { request in return request.query == "APIKit" } ``` -## Advanced usage +### Configuring request + +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 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 the 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 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 { + return Set(200..<300) +} +``` + +#### Building a model from a response + +```swift +func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { + guard let dictionary = object as? [String: AnyObject] else { + return nil + } + + guard let rateLimit = RateLimit(dictionary: dictionary) else { + return nil + } + + return rateLimit +} +``` -### Creating NSError from response object +#### 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 { @@ -208,20 +222,121 @@ 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 errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { + guard let dictionary = object as? [String: AnyObject] else { + return nil + } + + guard let message = dictionary["message"] as? String else { + return nil + } + + return GitHubError(message: message) +} +``` + +## 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 +let request = SomeAPI.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 -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) +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: HTTPMethod { + return .GET + } + + var path: String { + return "/paginated" + } + + let page: Int + + static func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { + guard let dictionaries = object as? [[String: AnyObject]] else { + return nil + } + + 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: diff --git a/circle.yml b/circle.yml index 981c1a01..d9563834 100644 --- a/circle.yml +++ b/circle.yml @@ -6,27 +6,29 @@ 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 + - 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."