diff --git a/.gitmodules b/.gitmodules index 30480090..550c8e31 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/antitypical/Result.git [submodule "Carthage/Checkouts/OHHTTPStubs"] path = Carthage/Checkouts/OHHTTPStubs - url = https://github.com/AliSoftware/OHHTTPStubs.git + url = https://github.com/ishkawa/OHHTTPStubs.git diff --git a/.travis.yml b/.travis.yml index 82dbd116..a7c8a2b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,8 @@ script: - pod lib lint - set -o pipefail - xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit | xcpretty -c - - xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator -destination 'name=iPhone 6,OS=9.1' | xcpretty -c - - xcodebuild build -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator -destination 'name=Apple TV 1080p,OS=9.1' | xcpretty -c + - xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator | xcpretty -c + - xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator | xcpretty -c before_deploy: - ./script/import-certificates diff --git a/APIKit.podspec b/APIKit.podspec index b4ad7622..6e2b2752 100644 --- a/APIKit.podspec +++ b/APIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "APIKit" - s.version = "1.4.1" + s.version = "2.0.0" s.summary = "A networking library for building type safe web API client in Swift." s.homepage = "https://github.com/ishkawa/APIKit" @@ -9,7 +9,7 @@ Pod::Spec.new do |s| } s.ios.deployment_target = "8.0" - s.osx.deployment_target = "10.9" + s.osx.deployment_target = "10.10" if s.respond_to?(:watchos) s.watchos.deployment_target = "2.0" end @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" end - s.source_files = "Sources/*.swift" + s.source_files = "Sources/**/*.{swift,h,m}" s.source = { :git => "https://github.com/ishkawa/APIKit.git", :tag => "#{s.version}", @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.license = { :type => "MIT", :text => <<-LICENSE - Copyright (c) 2015 Yosuke Ishikawa + Copyright (c) 2015 - 2016 Yosuke Ishikawa Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index b0a0307e..b30dd845 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -12,19 +12,41 @@ 141F12321C1C9AC70026D415 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; }; 141F12361C1C9AC70026D415 /* Result.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 141F12371C1C9AC70026D415 /* OHHTTPStubs.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 7F09BF901C8AE8DB00F4A59A /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F09BF8A1C8AE8DB00F4A59A /* APITests.swift */; }; - 7F09BF921C8AE8DB00F4A59A /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F09BF8C1C8AE8DB00F4A59A /* RequestBodyBuilderTests.swift */; }; 7F09BF931C8AE8DB00F4A59A /* RequestTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F09BF8D1C8AE8DB00F4A59A /* RequestTypeTests.swift */; }; - 7F09BF941C8AE8DB00F4A59A /* ResponseBodyParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F09BF8E1C8AE8DB00F4A59A /* ResponseBodyParserTests.swift */; }; - 7F7E8F141C8AD4B1008A13A9 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F0A1C8AD4B1008A13A9 /* APIError.swift */; }; + 7F10D8EE1CD5CF8D00722F66 /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F10D8ED1CD5CF8D00722F66 /* CallbackQueue.swift */; }; + 7F10D8F01CD5D10100722F66 /* SessionCallbackQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F10D8EF1CD5D10100722F66 /* SessionCallbackQueueTests.swift */; }; + 7F18BD0F1C972C38003A31DF /* BodyParametersType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18BD0E1C972C38003A31DF /* BodyParametersType.swift */; }; + 7F18BD111C972C69003A31DF /* JSONBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18BD101C972C69003A31DF /* JSONBodyParameters.swift */; }; + 7F18BD131C972E5A003A31DF /* FormURLEncodedBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18BD121C972E5A003A31DF /* FormURLEncodedBodyParameters.swift */; }; + 7F18BD1A1C9730ED003A31DF /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18BD181C9730ED003A31DF /* URLEncodedSerialization.swift */; }; + 7F2A15BD1CEB5F67009A12A2 /* NSData+NSInputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2A15BC1CEB5F67009A12A2 /* NSData+NSInputStream.swift */; }; 7F7E8F151C8AD4B1008A13A9 /* APIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F7E8F0B1C8AD4B1008A13A9 /* APIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7F7E8F161C8AD4B1008A13A9 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F0C1C8AD4B1008A13A9 /* HTTPMethod.swift */; }; - 7F7E8F181C8AD4B1008A13A9 /* MultipartFormDataSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F0E1C8AD4B1008A13A9 /* MultipartFormDataSerialization.swift */; }; - 7F7E8F191C8AD4B1008A13A9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F0F1C8AD4B1008A13A9 /* RequestBodyBuilder.swift */; }; 7F7E8F1A1C8AD4B1008A13A9 /* RequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F101C8AD4B1008A13A9 /* RequestType.swift */; }; - 7F7E8F1B1C8AD4B1008A13A9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F111C8AD4B1008A13A9 /* ResponseBodyParser.swift */; }; 7F7E8F1C1C8AD4B1008A13A9 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F121C8AD4B1008A13A9 /* Session.swift */; }; - 7F7E8F1D1C8AD4B1008A13A9 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E8F131C8AD4B1008A13A9 /* URLEncodedSerialization.swift */; }; + 7F85FB801C9CF12600CEE132 /* JSONDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB7F1C9CF12600CEE132 /* JSONDataParser.swift */; }; + 7F85FB831C9CF25D00CEE132 /* JSONDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB821C9CF25D00CEE132 /* JSONDataParserTests.swift */; }; + 7F85FB871C9CF47300CEE132 /* FormURLEncodedDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB861C9CF47300CEE132 /* FormURLEncodedDataParser.swift */; }; + 7F85FB891C9CF7B000CEE132 /* FormURLEncodedDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB881C9CF7B000CEE132 /* FormURLEncodedDataParserTests.swift */; }; + 7F85FB8E1C9D317300CEE132 /* NSURLSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB8C1C9D317300CEE132 /* NSURLSessionAdapter.swift */; }; + 7F85FB8F1C9D317300CEE132 /* SessionAdapterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB8D1C9D317300CEE132 /* SessionAdapterType.swift */; }; + 7F85FB921C9D336D00CEE132 /* NSURLSessionAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB911C9D336D00CEE132 /* NSURLSessionAdapterTests.swift */; }; + 7F85FB9A1C9D3DA700CEE132 /* TestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB981C9D3DA700CEE132 /* TestRequest.swift */; }; + 7F85FB9B1C9D3DA700CEE132 /* TestSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB991C9D3DA700CEE132 /* TestSessionAdapter.swift */; }; + 7F85FB9F1C9D3F0B00CEE132 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FB9E1C9D3F0B00CEE132 /* SessionTests.swift */; }; + 7F85FBA11C9D637B00CEE132 /* TestSessionTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F85FBA01C9D637B00CEE132 /* TestSessionTask.swift */; }; + 7FA19A331C985CEC005D25AE /* MultipartFormDataBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A311C98542E005D25AE /* MultipartFormDataBodyParameters.swift */; }; + 7FA19A3A1C98642F005D25AE /* FormURLEncodedBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A371C98642F005D25AE /* FormURLEncodedBodyParametersTests.swift */; }; + 7FA19A3B1C98642F005D25AE /* JSONBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A381C98642F005D25AE /* JSONBodyParametersTests.swift */; }; + 7FA19A3C1C98642F005D25AE /* MultipartFormDataParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A391C98642F005D25AE /* MultipartFormDataParametersTests.swift */; }; + 7FA19A411C9CBF2A005D25AE /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A3E1C9CBF2A005D25AE /* RequestError.swift */; }; + 7FA19A421C9CBF2A005D25AE /* ResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A3F1C9CBF2A005D25AE /* ResponseError.swift */; }; + 7FA19A431C9CBF2A005D25AE /* SessionTaskError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A401C9CBF2A005D25AE /* SessionTaskError.swift */; }; + 7FA19A461C9CC9D0005D25AE /* DataParserType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA19A451C9CC9D0005D25AE /* DataParserType.swift */; }; + 7FAC40341C8F2C900098C4B2 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FAC40331C8F2C900098C4B2 /* Box.swift */; }; + 7FAC64AE1CDC7ADE00F1BB45 /* AbstractInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FAC64AC1CDC7ADE00F1BB45 /* AbstractInputStream.m */; }; + 7FB650DF1CEA0E6B00366992 /* StringDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB650DE1CEA0E6B00366992 /* StringDataParser.swift */; }; + 7FB650E11CEA0F3D00366992 /* StringDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB650E01CEA0F3D00366992 /* StringDataParserTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,22 +82,44 @@ 141F123C1C1C9AC70026D415 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = APIKit.xcconfig; path = Configurations/APIKit.xcconfig; sourceTree = ""; }; 141F12401C1C9EA30026D415 /* Tests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Tests.xcconfig; path = Configurations/Tests.xcconfig; sourceTree = ""; }; - 7F09BF8A1C8AE8DB00F4A59A /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = ""; }; 7F09BF8B1C8AE8DB00F4A59A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7F09BF8C1C8AE8DB00F4A59A /* RequestBodyBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilderTests.swift; sourceTree = ""; }; 7F09BF8D1C8AE8DB00F4A59A /* RequestTypeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTypeTests.swift; sourceTree = ""; }; - 7F09BF8E1C8AE8DB00F4A59A /* ResponseBodyParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseBodyParserTests.swift; sourceTree = ""; }; - 7F7E8F0A1C8AD4B1008A13A9 /* APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; + 7F10D8ED1CD5CF8D00722F66 /* CallbackQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallbackQueue.swift; sourceTree = ""; }; + 7F10D8EF1CD5D10100722F66 /* SessionCallbackQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionCallbackQueueTests.swift; sourceTree = ""; }; + 7F18BD0E1C972C38003A31DF /* BodyParametersType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BodyParametersType.swift; sourceTree = ""; }; + 7F18BD101C972C69003A31DF /* JSONBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONBodyParameters.swift; sourceTree = ""; }; + 7F18BD121C972E5A003A31DF /* FormURLEncodedBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormURLEncodedBodyParameters.swift; sourceTree = ""; }; + 7F18BD181C9730ED003A31DF /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerialization.swift; sourceTree = ""; }; + 7F2A15BC1CEB5F67009A12A2 /* NSData+NSInputStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSData+NSInputStream.swift"; sourceTree = ""; }; 7F7E8F0B1C8AD4B1008A13A9 /* APIKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APIKit.h; sourceTree = ""; }; 7F7E8F0C1C8AD4B1008A13A9 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 7F7E8F0D1C8AD4B1008A13A9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7F7E8F0E1C8AD4B1008A13A9 /* MultipartFormDataSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataSerialization.swift; sourceTree = ""; }; - 7F7E8F0F1C8AD4B1008A13A9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = ""; }; 7F7E8F101C8AD4B1008A13A9 /* RequestType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestType.swift; sourceTree = ""; }; - 7F7E8F111C8AD4B1008A13A9 /* ResponseBodyParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseBodyParser.swift; sourceTree = ""; }; 7F7E8F121C8AD4B1008A13A9 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; - 7F7E8F131C8AD4B1008A13A9 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerialization.swift; sourceTree = ""; }; + 7F85FB7F1C9CF12600CEE132 /* JSONDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONDataParser.swift; sourceTree = ""; }; + 7F85FB821C9CF25D00CEE132 /* JSONDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONDataParserTests.swift; sourceTree = ""; }; + 7F85FB861C9CF47300CEE132 /* FormURLEncodedDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormURLEncodedDataParser.swift; sourceTree = ""; }; + 7F85FB881C9CF7B000CEE132 /* FormURLEncodedDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormURLEncodedDataParserTests.swift; sourceTree = ""; }; + 7F85FB8C1C9D317300CEE132 /* NSURLSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLSessionAdapter.swift; sourceTree = ""; }; + 7F85FB8D1C9D317300CEE132 /* SessionAdapterType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionAdapterType.swift; sourceTree = ""; }; + 7F85FB911C9D336D00CEE132 /* NSURLSessionAdapterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLSessionAdapterTests.swift; sourceTree = ""; }; + 7F85FB981C9D3DA700CEE132 /* TestRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestRequest.swift; sourceTree = ""; }; + 7F85FB991C9D3DA700CEE132 /* TestSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionAdapter.swift; sourceTree = ""; }; + 7F85FB9E1C9D3F0B00CEE132 /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; + 7F85FBA01C9D637B00CEE132 /* TestSessionTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionTask.swift; sourceTree = ""; }; 7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; }; + 7FA19A311C98542E005D25AE /* MultipartFormDataBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataBodyParameters.swift; sourceTree = ""; }; + 7FA19A371C98642F005D25AE /* FormURLEncodedBodyParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormURLEncodedBodyParametersTests.swift; sourceTree = ""; }; + 7FA19A381C98642F005D25AE /* JSONBodyParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONBodyParametersTests.swift; sourceTree = ""; }; + 7FA19A391C98642F005D25AE /* MultipartFormDataParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataParametersTests.swift; sourceTree = ""; }; + 7FA19A3E1C9CBF2A005D25AE /* RequestError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestError.swift; sourceTree = ""; }; + 7FA19A3F1C9CBF2A005D25AE /* ResponseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseError.swift; sourceTree = ""; }; + 7FA19A401C9CBF2A005D25AE /* SessionTaskError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTaskError.swift; sourceTree = ""; }; + 7FA19A451C9CC9D0005D25AE /* DataParserType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataParserType.swift; sourceTree = ""; }; + 7FAC40331C8F2C900098C4B2 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; + 7FAC64AC1CDC7ADE00F1BB45 /* AbstractInputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AbstractInputStream.m; sourceTree = ""; }; + 7FB650DE1CEA0E6B00366992 /* StringDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringDataParser.swift; sourceTree = ""; }; + 7FB650E01CEA0F3D00366992 /* StringDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringDataParserTests.swift; sourceTree = ""; }; CD5115241B1FFBA900514240 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD51152D1B1FFCC700514240 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -124,10 +168,13 @@ 7F09BF881C8AE8DB00F4A59A /* APIKit */ = { isa = PBXGroup; children = ( - 7F09BF8A1C8AE8DB00F4A59A /* APITests.swift */, - 7F09BF8C1C8AE8DB00F4A59A /* RequestBodyBuilderTests.swift */, + 7F85FB9E1C9D3F0B00CEE132 /* SessionTests.swift */, + 7F10D8EF1CD5D10100722F66 /* SessionCallbackQueueTests.swift */, 7F09BF8D1C8AE8DB00F4A59A /* RequestTypeTests.swift */, - 7F09BF8E1C8AE8DB00F4A59A /* ResponseBodyParserTests.swift */, + 7F85FB901C9D335F00CEE132 /* SessionAdapterType */, + 7F85FB811C9CF24900CEE132 /* DataParserType */, + 7FA19A361C98642F005D25AE /* BodyParametersType */, + 7F85FB971C9D3DA700CEE132 /* TestComponents */, 7F09BF951C8AE8EB00F4A59A /* Supporting Files */, ); path = APIKit; @@ -142,6 +189,27 @@ name = "Supporting Files"; sourceTree = ""; }; + 7F18BD0D1C972C38003A31DF /* BodyParametersType */ = { + isa = PBXGroup; + children = ( + 7F18BD0E1C972C38003A31DF /* BodyParametersType.swift */, + 7F18BD101C972C69003A31DF /* JSONBodyParameters.swift */, + 7F18BD121C972E5A003A31DF /* FormURLEncodedBodyParameters.swift */, + 7FA19A311C98542E005D25AE /* MultipartFormDataBodyParameters.swift */, + 7F2A15BC1CEB5F67009A12A2 /* NSData+NSInputStream.swift */, + 7FAC64AC1CDC7ADE00F1BB45 /* AbstractInputStream.m */, + ); + path = BodyParametersType; + sourceTree = ""; + }; + 7F18BD161C9730ED003A31DF /* Serializations */ = { + isa = PBXGroup; + children = ( + 7F18BD181C9730ED003A31DF /* URLEncodedSerialization.swift */, + ); + path = Serializations; + sourceTree = ""; + }; 7F45FCD31A94D02C006863BB = { isa = PBXGroup; children = ( @@ -168,11 +236,13 @@ 7F7E8F121C8AD4B1008A13A9 /* Session.swift */, 7F7E8F101C8AD4B1008A13A9 /* RequestType.swift */, 7F7E8F0C1C8AD4B1008A13A9 /* HTTPMethod.swift */, - 7F7E8F0A1C8AD4B1008A13A9 /* APIError.swift */, - 7F7E8F0F1C8AD4B1008A13A9 /* RequestBodyBuilder.swift */, - 7F7E8F111C8AD4B1008A13A9 /* ResponseBodyParser.swift */, - 7F7E8F131C8AD4B1008A13A9 /* URLEncodedSerialization.swift */, - 7F7E8F0E1C8AD4B1008A13A9 /* MultipartFormDataSerialization.swift */, + 7F10D8ED1CD5CF8D00722F66 /* CallbackQueue.swift */, + 7FAC40331C8F2C900098C4B2 /* Box.swift */, + 7F85FB8B1C9D317300CEE132 /* SessionAdapterType */, + 7F18BD0D1C972C38003A31DF /* BodyParametersType */, + 7FA19A441C9CC9A2005D25AE /* DataParserType */, + 7F18BD161C9730ED003A31DF /* Serializations */, + 7FA19A3D1C9CBF2A005D25AE /* Error */, 7F7E8F1E1C8AD4E6008A13A9 /* Supporting Files */, ); path = Sources; @@ -188,6 +258,74 @@ name = "Supporting Files"; sourceTree = ""; }; + 7F85FB811C9CF24900CEE132 /* DataParserType */ = { + isa = PBXGroup; + children = ( + 7F85FB821C9CF25D00CEE132 /* JSONDataParserTests.swift */, + 7F85FB881C9CF7B000CEE132 /* FormURLEncodedDataParserTests.swift */, + 7FB650E01CEA0F3D00366992 /* StringDataParserTests.swift */, + ); + path = DataParserType; + sourceTree = ""; + }; + 7F85FB8B1C9D317300CEE132 /* SessionAdapterType */ = { + isa = PBXGroup; + children = ( + 7F85FB8D1C9D317300CEE132 /* SessionAdapterType.swift */, + 7F85FB8C1C9D317300CEE132 /* NSURLSessionAdapter.swift */, + ); + path = SessionAdapterType; + sourceTree = ""; + }; + 7F85FB901C9D335F00CEE132 /* SessionAdapterType */ = { + isa = PBXGroup; + children = ( + 7F85FB911C9D336D00CEE132 /* NSURLSessionAdapterTests.swift */, + ); + path = SessionAdapterType; + sourceTree = ""; + }; + 7F85FB971C9D3DA700CEE132 /* TestComponents */ = { + isa = PBXGroup; + children = ( + 7F85FB981C9D3DA700CEE132 /* TestRequest.swift */, + 7F85FB991C9D3DA700CEE132 /* TestSessionAdapter.swift */, + 7F85FBA01C9D637B00CEE132 /* TestSessionTask.swift */, + ); + path = TestComponents; + sourceTree = ""; + }; + 7FA19A361C98642F005D25AE /* BodyParametersType */ = { + isa = PBXGroup; + children = ( + 7FA19A371C98642F005D25AE /* FormURLEncodedBodyParametersTests.swift */, + 7FA19A381C98642F005D25AE /* JSONBodyParametersTests.swift */, + 7FA19A391C98642F005D25AE /* MultipartFormDataParametersTests.swift */, + ); + path = BodyParametersType; + sourceTree = ""; + }; + 7FA19A3D1C9CBF2A005D25AE /* Error */ = { + isa = PBXGroup; + children = ( + 7FA19A401C9CBF2A005D25AE /* SessionTaskError.swift */, + 7FA19A3E1C9CBF2A005D25AE /* RequestError.swift */, + 7FA19A3F1C9CBF2A005D25AE /* ResponseError.swift */, + ); + path = Error; + sourceTree = ""; + }; + 7FA19A441C9CC9A2005D25AE /* DataParserType */ = { + isa = PBXGroup; + children = ( + 7FA19A451C9CC9D0005D25AE /* DataParserType.swift */, + 7F85FB7F1C9CF12600CEE132 /* JSONDataParser.swift */, + 7F85FB861C9CF47300CEE132 /* FormURLEncodedDataParser.swift */, + 7FB650DE1CEA0E6B00366992 /* StringDataParser.swift */, + ); + path = DataParserType; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -245,7 +383,7 @@ 7F45FCD41A94D02C006863BB /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0700; + LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0730; ORGANIZATIONNAME = "Yosuke Ishikawa"; }; @@ -291,13 +429,26 @@ buildActionMask = 2147483647; files = ( 7F7E8F1A1C8AD4B1008A13A9 /* RequestType.swift in Sources */, - 7F7E8F1B1C8AD4B1008A13A9 /* ResponseBodyParser.swift in Sources */, + 7FA19A331C985CEC005D25AE /* MultipartFormDataBodyParameters.swift in Sources */, + 7F85FB871C9CF47300CEE132 /* FormURLEncodedDataParser.swift in Sources */, + 7FA19A431C9CBF2A005D25AE /* SessionTaskError.swift in Sources */, 7F7E8F1C1C8AD4B1008A13A9 /* Session.swift in Sources */, - 7F7E8F181C8AD4B1008A13A9 /* MultipartFormDataSerialization.swift in Sources */, - 7F7E8F1D1C8AD4B1008A13A9 /* URLEncodedSerialization.swift in Sources */, - 7F7E8F191C8AD4B1008A13A9 /* RequestBodyBuilder.swift in Sources */, + 7F18BD0F1C972C38003A31DF /* BodyParametersType.swift in Sources */, + 7F2A15BD1CEB5F67009A12A2 /* NSData+NSInputStream.swift in Sources */, + 7FAC40341C8F2C900098C4B2 /* Box.swift in Sources */, + 7FB650DF1CEA0E6B00366992 /* StringDataParser.swift in Sources */, + 7F18BD131C972E5A003A31DF /* FormURLEncodedBodyParameters.swift in Sources */, + 7FA19A461C9CC9D0005D25AE /* DataParserType.swift in Sources */, + 7F18BD111C972C69003A31DF /* JSONBodyParameters.swift in Sources */, + 7F10D8EE1CD5CF8D00722F66 /* CallbackQueue.swift in Sources */, + 7FA19A411C9CBF2A005D25AE /* RequestError.swift in Sources */, + 7F85FB8F1C9D317300CEE132 /* SessionAdapterType.swift in Sources */, + 7F85FB801C9CF12600CEE132 /* JSONDataParser.swift in Sources */, + 7F18BD1A1C9730ED003A31DF /* URLEncodedSerialization.swift in Sources */, + 7FAC64AE1CDC7ADE00F1BB45 /* AbstractInputStream.m in Sources */, + 7F85FB8E1C9D317300CEE132 /* NSURLSessionAdapter.swift in Sources */, 7F7E8F161C8AD4B1008A13A9 /* HTTPMethod.swift in Sources */, - 7F7E8F141C8AD4B1008A13A9 /* APIError.swift in Sources */, + 7FA19A421C9CBF2A005D25AE /* ResponseError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -305,10 +456,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7F09BF901C8AE8DB00F4A59A /* APITests.swift in Sources */, + 7FA19A3C1C98642F005D25AE /* MultipartFormDataParametersTests.swift in Sources */, + 7FB650E11CEA0F3D00366992 /* StringDataParserTests.swift in Sources */, + 7F10D8F01CD5D10100722F66 /* SessionCallbackQueueTests.swift in Sources */, + 7F85FB891C9CF7B000CEE132 /* FormURLEncodedDataParserTests.swift in Sources */, + 7F85FBA11C9D637B00CEE132 /* TestSessionTask.swift in Sources */, + 7FA19A3A1C98642F005D25AE /* FormURLEncodedBodyParametersTests.swift in Sources */, + 7F85FB831C9CF25D00CEE132 /* JSONDataParserTests.swift in Sources */, + 7F85FB9F1C9D3F0B00CEE132 /* SessionTests.swift in Sources */, + 7F85FB921C9D336D00CEE132 /* NSURLSessionAdapterTests.swift in Sources */, 7F09BF931C8AE8DB00F4A59A /* RequestTypeTests.swift in Sources */, - 7F09BF921C8AE8DB00F4A59A /* RequestBodyBuilderTests.swift in Sources */, - 7F09BF941C8AE8DB00F4A59A /* ResponseBodyParserTests.swift in Sources */, + 7FA19A3B1C98642F005D25AE /* JSONBodyParametersTests.swift in Sources */, + 7F85FB9A1C9D3DA700CEE132 /* TestRequest.swift in Sources */, + 7F85FB9B1C9D3DA700CEE132 /* TestSessionAdapter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -327,6 +487,11 @@ isa = XCBuildConfiguration; baseConfigurationReference = 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; + DEFINES_MODULE = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; @@ -334,6 +499,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; + DEFINES_MODULE = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + SWIFT_OBJC_BRIDGING_HEADER = ""; }; name = Release; }; diff --git a/Cartfile.private b/Cartfile.private index 95efad6b..00053c40 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1 +1 @@ -github "AliSoftware/OHHTTPStubs" ~> 4.6.0 +github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e" diff --git a/Cartfile.resolved b/Cartfile.resolved index 8d6262f8..537244e6 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ -github "AliSoftware/OHHTTPStubs" "4.6.0" +github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e" github "antitypical/Result" "2.0.0" diff --git a/Carthage/Checkouts/OHHTTPStubs b/Carthage/Checkouts/OHHTTPStubs index c2898353..75f74c9c 160000 --- a/Carthage/Checkouts/OHHTTPStubs +++ b/Carthage/Checkouts/OHHTTPStubs @@ -1 +1 @@ -Subproject commit c2898353ae0f57d02dca30cfb46d2f6c6d19bf04 +Subproject commit 75f74c9c19620a37f436b38e2bc20a07310b999e diff --git a/Configurations/Base.xcconfig b/Configurations/Base.xcconfig index 649a371d..c83fcc49 100644 --- a/Configurations/Base.xcconfig +++ b/Configurations/Base.xcconfig @@ -25,7 +25,7 @@ VERSION_INFO_PREFIX = VERSIONING_SYSTEM = apple-generic CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer -MACOSX_DEPLOYMENT_TARGET = 10.9 +MACOSX_DEPLOYMENT_TARGET = 10.10 IPHONEOS_DEPLOYMENT_TARGET = 8.0 WATCHOS_DEPLOYMENT_TARGET = 2.0 TVOS_DEPLOYMENT_TARGET = 9.0 diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index b41266db..1f856dd9 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -47,13 +47,10 @@ struct GetRateLimitRequest: GitHubRequestType { 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 + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let dictionary = object as? [String: AnyObject], + let rateLimit = RateLimit(dictionary: dictionary) else { + throw ResponseError.UnexpectedObject(object) } return rateLimit @@ -66,10 +63,10 @@ let request = GetRateLimitRequest() Session.sendRequest(request) { result in switch result { case .Success(let rateLimit): - debugPrint("count: \(rateLimit.count)") - debugPrint("reset: \(rateLimit.resetDate)") + print("count: \(rateLimit.count)") + print("reset: \(rateLimit.resetDate)") case .Failure(let error): - debugPrint("error: \(error)") + print("error: \(error)") } } diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground index 90e9a0fb..35968656 100644 --- a/Demo.playground/contents.xcplayground +++ b/Demo.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/Documentation/APIKit2MigrationGuide.md b/Documentation/APIKit2MigrationGuide.md new file mode 100644 index 00000000..7db5e943 --- /dev/null +++ b/Documentation/APIKit2MigrationGuide.md @@ -0,0 +1,106 @@ +# APIKit 2 Migration Guide + +APIKit 2.0 introduces several breaking changes to add functionality and to improve modeling of web API. + +- Abstraction of backend +- Improved error handling modeling +- Separation of convenience parameters and type-safe parameters + +## Errors + +- [**Deleted**] `APIError` +- [**Added**] `SessionTaskError` + +Errors cases of `Session.sendRequest(_:handler:)` is reduced to 3 cases listed below: + +```swift +public enum SessionTaskError: ErrorType { + /// Error of networking backend such as `NSURLSession`. + case ConnectionError(NSError) + + /// Error while creating `NSURLRequest` from `Request`. + case RequestError(ErrorType) + + /// Error while creating `RequestType.Response` from `(NSData, NSURLResponse)`. + case ResponseError(ErrorType) +} +``` + +These error cases describes *where* the error occurred, not *what* is the error. You can throw any kind of error while building `NSURLRequest` and converting `NSData` to `Response`. `Session` catches the error you threw and wrap it into one of the cases defined in `SessionTaskError`. For example, if you throw `SomeError` in `responseFromObject(_:URLResponse:)`, the closure of `Session.sendRequest(_:handler:)` receives `.Failure(.ResponseError(SomeError))`. + +## RequestType + +### Converting AnyObject to Response + +- [**Deleted**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response?` +- [**Added**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response` + +### Handling response errors + +In 1.x, `Session` checks if the actual status code is contained in `RequestType.acceptableStatusCodes`. If it is not, `Session` calls `errorFromObject()` to obtain custom error from response object. In 2.x, `Session` always call `interceptObject()` before calling `responseFromObject()`, so you can validate `AnyObject` and `NSHTTPURLResponse` in `interceptObject()` and throw error initialized with them. + +- [**Deleted**] `var acceptableStatusCodes: Set { get }` +- [**Deleted**] `func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType?` +- [**Added**] `func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject` + +For example, the code below checks HTTP status code, and if the status code is not 2xx, it throws an error initialized with error JSON GitHub API returns. + +```swift +func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject { + guard (200..<300).contains(URLResponse.statusCode) else { + // https://developer.github.com/v3/#client-errors + throw GitHubError(object: object) + } + + return object +} +``` + +### Parameters + +To satisfy both ease and accuracy, `parameters` property is separated into 1 convenience property and 2 actual properties. If you implement convenience parameters only, 2 actual parameters are computed by default implementation of `RequestType`. + +- [**Deleted**] `var parameters: [String: AnyObject]` +- [**Deleted**] `var objectParameters: AnyObject` +- [**Deleted**] `var requestBodyBuilder: RequestBodyBuilder` +- [**Added**] `var parameters: AnyObject?` (convenience property) +- [**Added**] `var bodyParameters: BodyParametersType?` (actual property) +- [**Added**] `var queryParameters: [String: AnyObject]?` (actual property) + +Related types: + +- [**Deleted**] `enum RequestBodyBuilder` +- [**Added**] `protocol BodyParametersType` + +APIKit provides 3 parameters types that conform to `BodyParametersType`: + +- [**Added**] `class JSONBodyParameters` +- [**Added**] `class FormURLEncodedBodyParameters` +- [**Added**] `class MultipartFormDataBodyParameters` + +### Data parsers + +- [**Deleted**] `var responseBodyParser: ResponseBodyParser` +- [**Added**] `var dataParser: DataParserType` + +Related types: + +- [**Deleted**] `enum ResponseBodyParser` +- [**Added**] `protocol DataParserType` +- [**Added**] `class JSONDataParser` +- [**Added**] `class FormURLEncodedDataParser` +- [**Added**] `class StringDataParser` + +### Configuring NSURLRequest + +`configureURLRequest()` in 1.x is renamed to `interceptURLRequest()` for the consistency with `interceptObject()`. + +- [**Deleted**] `func configureURLRequest(URLRequest: NSMutableURLRequest) -> NSMutableURLRequest` +- [**Added**] `func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest` + +## NSURLSession + +- [**Deleted**] `class URLSessionDelegate` +- [**Added**] `protocol SessionTaskType` +- [**Added**] `protocol SessionAdapterType` +- [**Added**] `class NSURLSessionAdapter` diff --git a/Documentation/ConvenienceParametersAndActualParameters.md b/Documentation/ConvenienceParametersAndActualParameters.md new file mode 100644 index 00000000..e5a37de0 --- /dev/null +++ b/Documentation/ConvenienceParametersAndActualParameters.md @@ -0,0 +1,96 @@ +# Convenience Parameters and Actual Parameters + +To satisfy both ease and accuracy, `RequestType` has 2 kind of parameters properties, convenience property and actual properties. If you implement convenience parameters only, actual parameters are computed by default implementation of `RequestType`. + +1. [Convenience parameters](#convenience-parameters) +2. [Actual parameters](#actual-parameters) + +## Convenience parameters + +Most documentations of web APIs express parameters in dictionary-like notation: + +|Name |Type |Description | +|-------|--------|-------------------------------------------------------------------------------------------------| +|`q` |`string`|The search keywords, as well as any qualifiers. | +|`sort` |`string`|The sort field. One of `stars`, `forks`, or `updated`. Default: results are sorted by best match.| +|`order`|`string`|The sort order if `sort` parameter is provided. One of `asc` or `desc`. Default: `desc` | + + `RequestType` has a property `var parameter: AnyObject?` to express parameters in this kind of notation. That is the convenience parameters. + +```swift +struct SomeRequest: RequestType { + ... + + var parameters: AnyObject? { + return [ + "q": "Swift", + "sort": "stars", + "order": "desc", + ] + } +} +``` + +`RequestType` provides default implementation of `parameters` `nil`. + +```swift +public extension RequestType { + public var parameters: AnyObject? { + return nil + } +} +``` + +## Actual parameters + +Actually, we have to translate dictionary-like notation in API docs into HTTP/HTTPS request. There are 2 places to express parameters, URL query and body. `RequestType` has interface to express them, `var queryParameters: [String: AnyObject]?` and `var bodyParameters: BodyParametersType?`. Those are the actual parameters. + +If you implement convenience parameters only, the actual parameters are computed from the convenience parameters depending on HTTP method. Here is the default implementation of actual parameters: + +```swift +public extension RequestType { + public var queryParameters: [String: AnyObject]? { + guard let parameters = parameters as? [String: AnyObject] where method.prefersQueryParameters else { + return nil + } + + return parameters + } + + public var bodyParameters: BodyParametersType? { + guard let parameters = parameters where !method.prefersQueryParameters else { + return nil + } + + return JSONBodyParameters(JSONObject: parameters) + } +} +``` + +If you implement actual parameters for the HTTP method, the convenience parameters will be ignored. + +### BodyParametersType + +There are several MIME types to express parameters such as `application/json`, `application/x-www-form-urlencoded` and `multipart/form-data; boundary=foobarbaz`. Because parameters types to express these MIME types are different, type of `bodyParameters` is a protocol `BodyParametersType`. + +`BodyParametersType` defines 2 components, `contentType` and `buildEntity()`. You can create custom body parameters type that conforms to `BodyParametersType`. + +```swift +public enum RequestBodyEntity { + case Data(NSData) + case InputStream(NSInputStream) +} + +public protocol BodyParametersType { + var contentType: String { get } + func buildEntity() throws -> RequestBodyEntity +} +``` + +APIKit provides 3 body parameters type listed below: + +|Name |Parameters Type | +|---------------------------------|----------------------------------------| +|`JSONBodyParameters` |`AnyObject` | +|`FormURLEncodedBodyParameters` |`[String: AnyObject]` | +|`MultipartFormDataBodyParameters`|`[MultipartFormDataBodyParameters.Part]`| diff --git a/Documentation/CustomizingNetworkingBackend.md b/Documentation/CustomizingNetworkingBackend.md new file mode 100644 index 00000000..2216bdaf --- /dev/null +++ b/Documentation/CustomizingNetworkingBackend.md @@ -0,0 +1,57 @@ +# Customizing Networking Backend + +APIKit uses `NSURLSession` as networking backend by default. Since `Session` has abstraction layer of backend called `SessionAdapterType`, you can change the backend of `Session` like below: + +- Third party HTTP client like [Alamofire](https://github.com/Alamofire/Alamofire) +- Mock backend like [`TestSessionAdapter`](../Tests/APIKit/TestComponents/TestSessionAdapter.swift) +- `NSURLSession` with custom configuration and delegate + +Demo implementation of Alamofire adapter is available [here](https://github.com/ishkawa/APIKit-AlamofireAdapter). + +## SessionAdapterType + +`SessionAdapterType` provides an interface to get `(NSData?, NSURLResponse?, NSError?)` from `NSURLRequest` and returns `SessionTaskType` for cancellation. + +```swift +public protocol SessionAdapterType { + public func createTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) -> SessionTaskType + public func getTasksWithHandler(handler: [SessionTaskType] -> Void) +} + +public protocol SessionTaskType : class { + public func resume() + public func cancel() +} +``` + + +## How Session works with SessionAdapterType + +`Session` takes an instance of type that conforms `SessionAdapterType` as a parameter of initializer. + +```swift +public class Session { + public let adapter: SessionAdapterType + + public init(adapter: SessionAdapterType) { + self.adapter = adapter + } + + ... +} +``` + +Once it is initialized with a session adapter, it sends `NSURLRequest` and receives `(NSData?, NSURLResponse?, NSError?)` via the interfaces which are defined in `SessionAdapterType`. + +```swift +func sendRequest(request: T, handler: (Result) -> Void = {r in}) -> SessionTaskType? { + let URLRequest: NSURLRequest = ... + let task = adapter.createTaskWithURLRequest(URLRequest) { data, URLResponse, error in + ... + } + + task.resume() + + return task +} +``` diff --git a/Documentation/DefiningRequestProtocolForWebService.md b/Documentation/DefiningRequestProtocolForWebService.md new file mode 100644 index 00000000..6dadbbfa --- /dev/null +++ b/Documentation/DefiningRequestProtocolForWebService.md @@ -0,0 +1,161 @@ +# Defining Request Protocol for Web Service + +Most web APIs have common configurations such as base URL, authorization header fields and MIME type to accept. For example, GitHub API has common base URL `https://api.github.com`, authorization header field `Authorization` and MIME type `application/json`. Protocol to express such common interfaces and default implementations is useful in defining many request types. + +We define `GitHubRequestType` to give common configuration for example. + +1. [Giving default implementation to RequestType components](#giving-default-implementation-to-requesttype-components) +2. [Throwing custom errors web API returns](#throwing-custom-errors-web-api-returns) + +## Giving default implementation to RequestType components + +### Base URL + +First of all, we give default implementation for `baseURL`. + +```swift +protocol GitHubRequestType: RequestType { + +} + +extension GitHubRequestType { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } +} +``` + +### JSON Mapping + +There are several JSON mapping library such as [Himotoki](https://github.com/ikesyo/Himotoki), [Argo](https://github.com/thoughtbot/Argo) and [Unbox](https://github.com/JohnSundell/Unbox). These libraries provide protocol that define interface to decode `AnyObject` into JSON model type. If you adopt one of them, you can give default implementation to `responseFromObject(_:URLResponse:)`. Here is an example of default implementation with Himotoki: + +```swift +import Himotoki + +extension GitHubRequestType where Response: Decodable { + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + return try decodeValue(object) + } +} +``` + +### Defining request types + +Since `GitHubRequestType` has default implementations of `baseURL` and `responseFromObject(_:URLResponse:)`, all you have to implement to conform to `GitHubRequestType` are 3 components, `Response`, `method` and `path`. + +```swift +import Himotoki + +final class GitHubAPI { + struct RateLimitRequest: GitHubRequestType { + typealias Response = RateLimit + + var method: HTTPMethod { + return .GET + } + + var path: String { + return "/rate_limit" + } + } + + struct SearchRepositoriesRequest: GitHubRequestType { + let query: String + + // MARK: RequestType + typealias Response = SearchResponse + + var method: HTTPMethod { + return .GET + } + + var path: String { + return "/search/repositories" + } + + var parameters: AnyObject? { + return ["q": query] + } + } +} + +struct RateLimit: Decodable { + let limit: Int + let remaining: Int + + static func decode(e: Extractor) throws -> RateLimit { + return try RateLimit( + limit: e.value(["rate", "limit"]), + remaining: e.value(["rate", "remaining"])) + } +} + +struct SearchResponse: Decodable { + let items: [Item] + let totalCount: Int + + static func decode(e: Extractor) throws -> SearchResponse { + return try SearchResponse( + items: e.array("items"), + totalCount: e.value("total_count")) + } +} +``` + +It is useful for code completion to nest request types in a utility class like `GitHubAPI` above. + +## Throwing custom errors web API returns + +Most web APIs define error response to notify what happened on the server. For example, GitHub API defines errors [like this](https://developer.github.com/v3/#client-errors). `interceptObject(_:URLResponse:)` in `RequestType` gives us a chance to determine if the response is an error. If the response is an error, you can create custom error object from the response object and throw the error in `interceptObject(_:URLResponse:)`. + +Here is an example of handling [GitHub API errors](https://developer.github.com/v3/#client-errors): + +```swift +// https://developer.github.com/v3/#client-errors +struct GitHubError { + let message: String + + init(object: AnyObject) { + message = object["message"] as? String ?? "Unknown error occurred" + } +} + +extension GitHubRequestType { + func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard (200..<300).contains(URLResponse.statusCode) else { + throw GitHubError(object: AnyObject) + } + + return object + } +} +``` + +The custom error you throw in `interceptObject(_:URLResponse:)` can be retrieved from call-site as `.Failure(.ResponseError(GitHubError))`. + +```swift +let request = SomeGitHubRequest() + +Session.sendRequest(request) { result in + switch result { + case .Success(let response): + print(response) + + case .Failure(let error): + printSessionTaskError(error) + } +} + +func printSessionTaskError(error: SessionTaskError) { + switch sessionTaskError { + case .ResponseError(let error as GitHubError): + print(error.message) // Prints message from GitHub API + + case .ConnectionError(let error): + print("Connection error: \(error)") + + default: + print("System error :bow:") + } +} +``` diff --git a/Documentation/GettingStarted.md b/Documentation/GettingStarted.md new file mode 100644 index 00000000..cce84722 --- /dev/null +++ b/Documentation/GettingStarted.md @@ -0,0 +1,127 @@ +# Getting started + +1. [Library overview](#library-overview) +2. [Defining request type](#defining-request-type) +3. [Sending request](#sending-request) +4. [Canceling request](#canceling-request) + +## Library overview + +The main units of APIKit are `RequestType` protocol and `Session` class. `RequestType` has properties that represent components of HTTP/HTTPS request. `Session` receives an instance of a type that conforms to `RequestType`, then it returns the result of the request. The response type is inferred from the request type, so response type changes depending on the request type. + +```swift +// SearchRepositoriesRequest conforms to RequestType +let request = SearchRepositoriesRequest(query: "APIKit", sort: .Stars) + +// Session receives an instance of a type that conforms to RequestType. +Session.sendRequest(request) { result in + switch result { + case .Success(let repositories): + // Type of `repositories` is `[Repository]`, + // which is inferred from `SearchRepositoriesRequest`. + print(repositories) + + case .Failure(let error): + print(error) + } +} +``` + +## Defining request type + +`RequestType` defines several properties and methods. Since many of them have default implementation, components which is necessary for conforming to `RequestType` are following 5 components: + +- `typealias Response` +- `var baseURL: NSURL` +- `var method: HTTPMethod` +- `var path: String` +- `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response` + +```swift +struct RateLimitRequest: GitHubRequestType { + typealias Response = RateLimit + + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } + + var method: HTTPMethod { + return .GET + } + + var path: String { + return "/rate_limit" + } + + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + guard let dictionary = object as? [String: AnyObject], + let rateLimit = RateLimit(dictionary: dictionary) else { + throw ResponseError.UnexpectedObject(object) + } + + return rateLimit + } +} + +struct RateLimit { + let limit: Int + let remaining: Int + + init?(dictionary: [String: AnyObject]) { + guard let limit = dictionary["rate"]?["limit"] as? Int, + let remaining = dictionary["rate"]?["limit"] as? Int else { + return nil + } + + self.limit = limit + self.remaining = remaining + } +} +``` + +## Sending request + +`Session.sendRequest(_:handler:)` is a method to send a request that conforms to `RequestType`. The result of the request is expressed as `Result`. `Result` is from [antitypical/Result](https://github.com/antitypical/Result), which is generic enumeration with 2 cases `.Success` and `.Failure`. `Request` is a type parameter of `Session.sendRequest(_:handler:)` which conforms to `RequestType`. + +For example, when `Session.sendRequest(_:handler:)` receives `RateLimitRequest` as a type parameter `Request`, the result type will be `Result`. + +```swift +let request = RateLimitRequest() + +Session.sendRequest(request) { result in + switch result { + case .Success(let rateLimit): + // Type of `rateLimit` is inferred as `RateLimit`, + // which is also known as `RateLimitRequest.Response`. + print("count: \(rateLimit.count)") + print("resetDate: \(rateLimit.resetDate)") + + case .Failure(let error): + print("error: \(error)") + } +} +``` + +`SessionTaskError` is an error enumeration that has 3 cases: + +- `ConnectionError`: Error of networking backend stack. +- `RequestError`: Error while creating `NSURLRequest` from `Request`. +- `ResponseError`: Error while creating `RequestType.Response` from `(NSData, NSURLResponse)`. + +## Canceling request + +`Session.cancelRequest()` also has a type parameter `Request` that conforms to `RequestType`. `Session.cancelRequest()` takes 2 parameters `requestType: Request.Type` and `test: Request-> Bool`. `requestType` is a type of request to cancel, and `test` is a closure that determines if request should be cancelled. + +For example, when `Session.cancel()` receives `RateLimitRequest.Type` and `{ request in true }` as parameters, `Session` finds all session tasks associated with `RateLimitRequest` in the backend queue. Next, execute `{ request in true }` for each session tasks and cancel the task if it returns `true`. Since `{ request in true }` always returns `true`, all request associated with `RateLimitRequest` will be cancelled. + +```swift +Session.cancelRequest(RateLimitRequest.Type) { request in + return true +} +``` + +`Session.cancelRequest` has default parameter for predicate closure, so you can omit the predicate closure like below: + +```swift +Session.cancelRequest(RateLimitRequest.Type) +``` diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..0319e67b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +**Copyright (c) 2015 - 2016 Yosuke Ishikawa** + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md index 2d968f9c..cf63e5f8 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,17 @@ APIKit [![Build Status](https://travis-ci.org/ishkawa/APIKit.svg?branch=master)](https://travis-ci.org/ishkawa/APIKit) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -APIKit is a library for building type-safe web API client in Swift. - -- 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. +APIKit is a type-safe networking abstraction layer that associates request type with response type. ```swift -let request = GetSearchRepositoriesRequest(query: "APIKit", sort: .Stars) +let request = SearchRepositoriesRequest(query: "APIKit", sort: .Stars) Session.sendRequest(request) { result in switch result { - case .Success(let response): - self.repositories = response // inferred as [Repository] - self.tableView.reloadData() + case .Success(let repositories): + // Type of `repositories` is `[Repository]`, + // which is inferred from `SearchRepositoriesRequest`. + print(repositories) case .Failure(let error): print(error) @@ -27,410 +24,32 @@ Session.sendRequest(request) { result in ## Requirements -- Swift 2.2 -- Mac OS 10.9 or later -- iOS 8.0 or later -- watchOS 2.0 or later -- tvOS 9.0 or later +- Swift 2.2+ +- iOS 8.0+ / Mac OS 10.10+ / watchOS 2.0+ / tvOS 9.0+ ## Installation #### [Carthage](https://github.com/Carthage/Carthage) -- Insert `github "ishkawa/APIKit" ~> 1.3.0` to your Cartfile. +- Insert `github "ishkawa/APIKit" ~> 2.0.0` to your Cartfile. - Run `carthage update`. - Link your app with `APIKit.framework` and `Result.framework` in `Carthage/Checkouts`. #### [CocoaPods](https://github.com/cocoapods/cocoapods) -- Insert `pod 'APIKit', '~> 1.3.0'` to your Podfile. +- Insert `pod 'APIKit', '~> 2.0.0'` to your Podfile. - Run `pod install`. -## Usage - -1. Create a request protocol that inherits `RequestType` protocol. -2. Add `baseURL` property in the extension of the request protocol. -3. Define request types that conform to request protocol. - 1. Create a type that represents request of the web API. - 2. Assign type that represents a response object to `Response` typealiase. - 3. Add `method` and `path` variables. - 4. Implement `responseFromObject(_:URLResponse:)` to build `Response` from a raw object, which may be an array or a dictionary. - -```swift -protocol GitHubRequestType: RequestType { - -} - -extension GitHubRequestType { - var baseURL: NSURL { - return NSURL(string: "https://api.github.com")! - } -} - -struct GetRateLimitRequest: GitHubRequestType { - 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 - } -} - -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) - } -} -``` - -### Sending request - -```swift -let request = GetRateLimitRequest() - -Session.sendRequest(request) { result in - switch result { - case .Success(let rateLimit): - print("count: \(rateLimit.count)") - print("resetDate: \(rateLimit.resetDate)") - - case .Failure(let error): - print("error: \(error)") - } -} -``` - -### Canceling request - -```swift -Session.cancelRequest(GetRateLimitRequest.self) -``` - -If you want to filter requests to be cancelled, add closure that identifies the request should be cancelled or not. - -```swift -Session.cancelRequest(GetSearchRepositoriesRequest.self) { request in - return request.query == "APIKit" -} -``` - -### Configuring request - -APIKit uses following 4 properties in `RequestType` 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`. -You can pass `NSNull()` as a value for nullable keys if you'd like to preserve the keys when its value is absent. - -#### 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 `Session` calls `responseFromObject(_:URLResponse:)` to get a model from a raw response. Otherwise, the request is judged as failed and `Session` 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 -} -``` - -#### Building an error from a response - -For example, [GitHub API](https://developer.github.com/v3/#client-errors) returns an error like this: - -```json -{ - "message": "Validation Failed" -} -``` - -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 -var GithubAccessToken: String? - -protocol GitHubRequest: RequestType { - 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 = GitHubAccessToken else { - throw APIKitError.CannotBuildURLRequest - } - - URLRequest.setValue("token \(accessToken)", forHTTPHeaderField: "Authorization") - } - - return URLRequest - } -} -``` - -### Pagination - -```swift -let request = GetSomePaginatedRequest(page: 1) - -Session.sendRequest(request) { result in - switch result { - case .Success(let response): - print("results: \(response.results)") - print("nextPage: \(response.nextPage)") - print("hasNext: \(response.hasNext)") - - case .Failure(let error): - print("error: \(error)") - } -} -``` - -```swift -struct PaginatedResponse { - var results: [T] - var nextPage: Int { get } - var hasNext: Bool { get } - - init(results: [T], 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: RequestType { - 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) - } -} -``` - -### Combination with Himotoki - -[Himotoki](https://github.com/ikesyo/Himotoki) is a type-safe JSON decoding library that can be combined with APIKit. It makes implementing `responseFromObject(_:URLResponse:)` very easy. If your model type conforms to `Decodable` protocol in Himotoki, a request can be defined like below: - -```swift -// model type -struct RateLimit: Decodable { - let count: Int - let resetUNIXTime: NSTimeInterval - - static func decode(e: Extractor) throws -> RateLimit { - return try build(self.init)( - e.value(["rate", "limit"]), - e.value(["rate", "reset"]) - ) - } -} - -// request type -struct GetRateLimitRequest: GitHubRequest { - typealias Response = RateLimit - - var method: HTTPMethod { - return .GET - } - - var path: String { - return "/rate_limit" - } - - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { - return try? decodeValue(object) // get Response from AnyObject using Himotoki - } -} -``` - -Additionally, you can provide default implementation of `responseFromObject(_:URLResponse:)` if `Response` typealias of a request type conforms to `Decodable`. - -```swift -extension GitHubRequest where Self.Response: Decodable { - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Self.Response? { - return try? decodeValue(object) - } -} -``` - -As a result, you can omit implementing `responseFromObject(_:URLResponse:)` in the definition of a request type. - -```swift -struct GetRateLimitRequest: GitHubRequest { - typealias Response = RateLimit - - var method: HTTPMethod { - return .GET - } - - var path: String { - return "/rate_limit" - } -} -``` - -See [this gist post](https://gist.github.com/ishkawa/59dd67042289ee4b5cab) for more practical example. - -## Advanced usage - -### NSURLSessionDelegate - -You can add custom behaviors of `NSURLSession` by following steps: - -1. Create a subclass of `URLSessionDelegate` (e.g. `MyURLSessionDelegate`). -2. Implement additional delegate methods in it. -3. Create a new `Session` instance that has `MyURLSessionDelegate` as a delegate of `NSURLSession`. - -```swift -let session = Session(URLSession: NSURLSession( - configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), - delegate: MyURLSessionDelegate(), - delegateQueue: nil) -) -``` - -This can add following features: - -- Hook events of NSURLSession -- Handle authentication challenges -- Convert a data task to NSURLSessionDownloadTask - -NOTE: `URLSessionDelegate` also implements delegate methods of `NSURLSession` to implement wrapper of `NSURLSession`, so you should call super if you override following methods. - -- `func URLSession(_:task:didCompleteWithError:)` -- `func URLSession(_:dataTask:didReceiveData:)` -- `func URLSession(_:dataTask:didBecomeDownloadTask:)` - +## Documentation -## License +- [Getting started](Documentation/GettingStarted.md) +- [Defining Request Protocol for Web Service](Documentation/DefiningRequestProtocolForWebService.md) +- [Convenience Parameters and Actual Parameters](Documentation/ConvenienceParametersAndActualParameters.md) -Copyright (c) 2015 Yosuke Ishikawa +### Advanced Guides -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +- [Customizing Networking Backend](Documentation/CustomizingNetworkingBackend.md) -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +### Migration Guides -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- [APIKit 2 Migration Guide](Documentation/APIKit2MigrationGuide.md) diff --git a/Sources/APIError.swift b/Sources/APIError.swift deleted file mode 100644 index a0818050..00000000 --- a/Sources/APIError.swift +++ /dev/null @@ -1,28 +0,0 @@ -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) - - /// 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/Sources/APIKit.h b/Sources/APIKit.h index 474afd7f..2435ed6d 100644 --- a/Sources/APIKit.h +++ b/Sources/APIKit.h @@ -2,3 +2,11 @@ FOUNDATION_EXPORT double APIKitVersionNumber; FOUNDATION_EXPORT const unsigned char APIKitVersionString[]; + +@interface AbstractInputStream : NSInputStream + +// Workaround for http://www.openradar.me/19809067 +// This issue only occurs on iOS 8 +- (instancetype)init; + +@end diff --git a/Sources/BodyParametersType/AbstractInputStream.m b/Sources/BodyParametersType/AbstractInputStream.m new file mode 100644 index 00000000..3ec0b6c0 --- /dev/null +++ b/Sources/BodyParametersType/AbstractInputStream.m @@ -0,0 +1,10 @@ +#import "APIKit.h" + +@implementation AbstractInputStream + +- (instancetype)init +{ + return [super init]; +} + +@end diff --git a/Sources/BodyParametersType/BodyParametersType.swift b/Sources/BodyParametersType/BodyParametersType.swift new file mode 100644 index 00000000..6ee549bf --- /dev/null +++ b/Sources/BodyParametersType/BodyParametersType.swift @@ -0,0 +1,20 @@ +import Foundation + +/// `RequestBodyEntity` represents entity of HTTP body. +public enum RequestBodyEntity { + /// Expresses entity as `NSData`. The associated value will be set to `NSURLRequest.HTTPBody`. + case Data(NSData) + + /// Expresses entity as `NSInputStream`. The associated value will be set to `NSURLRequest.HTTPBodyStream`. + case InputStream(NSInputStream) +} + +/// `BodyParametersType` provides interface to parse HTTP response body and to state `Content-Type` to accept. +public protocol BodyParametersType { + /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. + var contentType: String { get } + + /// Builds `RequestBodyEntity`. + /// Throws: `ErrorType` + func buildEntity() throws -> RequestBodyEntity +} diff --git a/Sources/BodyParametersType/FormURLEncodedBodyParameters.swift b/Sources/BodyParametersType/FormURLEncodedBodyParameters.swift new file mode 100644 index 00000000..03334b42 --- /dev/null +++ b/Sources/BodyParametersType/FormURLEncodedBodyParameters.swift @@ -0,0 +1,29 @@ +import Foundation + +/// `FormURLEncodedBodyParameters` serializes form object for HTTP body and states its content type is form. +public struct FormURLEncodedBodyParameters: BodyParametersType { + /// The form object to be serialized. + public let form: [String: AnyObject] + + /// The string encoding of the serialized form. + public let encoding: NSStringEncoding + + /// Returns `FormURLEncodedBodyParameters` that is initialized with form object and encoding. + public init(formObject: [String: AnyObject], encoding: NSStringEncoding = NSUTF8StringEncoding) { + self.form = formObject + self.encoding = encoding + } + + // MARK: - BodyParametersType + + /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. + public var contentType: String { + return "application/x-www-form-urlencoded" + } + + /// Builds `RequestBodyEntity.Data` that represents `form`. + /// - Throws: `URLEncodedSerialization.Error` if `URLEncodedSerialization` fails to serialize form object. + public func buildEntity() throws -> RequestBodyEntity { + return .Data(try URLEncodedSerialization.dataFromObject(form, encoding: encoding)) + } +} diff --git a/Sources/BodyParametersType/JSONBodyParameters.swift b/Sources/BodyParametersType/JSONBodyParameters.swift new file mode 100644 index 00000000..55c33788 --- /dev/null +++ b/Sources/BodyParametersType/JSONBodyParameters.swift @@ -0,0 +1,34 @@ +import Foundation + +/// `JSONBodyParameters` serializes JSON object for HTTP body and states its content type is JSON. +public struct JSONBodyParameters: BodyParametersType { + /// The JSON object to be serialized. + public let JSONObject: AnyObject + + /// The writing options for serialization. + public let writingOptions: NSJSONWritingOptions + + /// Returns `JSONBodyParameters` that is initialized with JSON object and writing options. + public init(JSONObject: AnyObject, writingOptions: NSJSONWritingOptions = []) { + self.JSONObject = JSONObject + self.writingOptions = writingOptions + } + + // MARK: - BodyParametersType + + /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. + public var contentType: String { + return "application/json" + } + + /// Builds `RequestBodyEntity.Data` that represents `JSONObject`. + /// - Throws: `NSError` if `NSJSONSerialization` fails to serialize `JSONObject`. + public func buildEntity() throws -> RequestBodyEntity { + // If isValidJSONObject(_:) is false, dataWithJSONObject(_:options:) throws NSException. + guard NSJSONSerialization.isValidJSONObject(JSONObject) else { + throw NSError(domain: NSCocoaErrorDomain, code: 3840, userInfo: nil) + } + + return .Data(try NSJSONSerialization.dataWithJSONObject(JSONObject, options: writingOptions)) + } +} diff --git a/Sources/BodyParametersType/MultipartFormDataBodyParameters.swift b/Sources/BodyParametersType/MultipartFormDataBodyParameters.swift new file mode 100644 index 00000000..7e46f9cb --- /dev/null +++ b/Sources/BodyParametersType/MultipartFormDataBodyParameters.swift @@ -0,0 +1,321 @@ +import Foundation + +#if os(iOS) || os(watchOS) || os(tvOS) + import MobileCoreServices +#elseif os(OSX) + import CoreServices +#endif + +/// `FormURLEncodedBodyParameters` serializes array of `Part` for HTTP body and states its content type is multipart/form-data. +public struct MultipartFormDataBodyParameters: BodyParametersType { + /// `EntityType` represents wheather the entity is expressed as `NSData` or `NSInputStream`. + public enum EntityType { + /// Expresses the entity as `NSData`, which has faster upload speed and lager memory usage. + case Data + + /// Expresses the entity as `NSInputStream`, which has smaller memory usage and slower upload speed. + case InputStream + } + + public let parts: [Part] + public let boundary: String + public let entityType: EntityType + + public init(parts: [Part], boundary: String = String(format: "%08x%08x", arc4random(), arc4random()), entityType: EntityType = .Data) { + self.parts = parts + self.boundary = boundary + self.entityType = entityType + } + + // MARK: BodyParametersType + + /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. + public var contentType: String { + return "multipart/form-data; boundary=\(boundary)" + } + + /// Builds `RequestBodyEntity.Data` that represents `form`. + public func buildEntity() throws -> RequestBodyEntity { + let inputStream = MultipartInputStream(parts: parts, boundary: boundary) + + switch entityType { + case .InputStream: + return .InputStream(inputStream) + + case .Data: + return .Data(try NSData(inputStream: inputStream)) + } + } +} + +public extension MultipartFormDataBodyParameters { + /// Part represents single part of multipart/form-data. + public struct Part { + public enum Error: ErrorType { + case IllegalValue(Any) + case IllegalFileURL(NSURL) + case CannotGetFileSize(NSURL) + } + + public let inputStream: NSInputStream + public let name: String + public let mimeType: String? + public let fileName: String? + public let length: Int + + /// Returns Part instance that has data presentation of passed value. + /// `value` will be converted via `String(_:)` and serialized via `String.dataUsingEncoding(_:)`. + /// If `mimeType` or `fileName` are `nil`, the fields will be omitted. + public init(value: Any, name: String, mimeType: String? = nil, fileName: String? = nil, encoding: NSStringEncoding = NSUTF8StringEncoding) throws { + guard let data = String(value).dataUsingEncoding(encoding) else { + throw Error.IllegalValue(value) + } + + self.inputStream = NSInputStream(data: data) + self.name = name + self.mimeType = mimeType + self.fileName = fileName + self.length = data.length + } + + /// Returns Part instance that has input stream of specifed data. + /// If `mimeType` or `fileName` are `nil`, the fields will be omitted. + public init(data: NSData, name: String, mimeType: String? = nil, fileName: String? = nil) { + self.inputStream = NSInputStream(data: data) + self.name = name + self.mimeType = mimeType + self.fileName = fileName + self.length = data.length + } + + /// Returns Part instance that has input stream of specifed file URL. + /// If `mimeType` or `fileName` are `nil`, values for the fields will be detected from URL. + public init(fileURL: NSURL, name: String, mimeType: String? = nil, fileName: String? = nil) throws { + guard let inputStream = NSInputStream(URL: fileURL) else { + throw Error.IllegalFileURL(fileURL) + } + + let fileSize = fileURL.path + .flatMap { try? NSFileManager.defaultManager().attributesOfItemAtPath($0) } + .flatMap { $0[NSFileSize] as? NSNumber } + .map { $0.integerValue } + + guard let bodyLength = fileSize else { + throw Error.CannotGetFileSize(fileURL) + } + + let detectedMimeType = fileURL.pathExtension + .flatMap { UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, $0, nil)?.takeRetainedValue() } + .flatMap { UTTypeCopyPreferredTagWithClass($0, kUTTagClassMIMEType)?.takeRetainedValue() } + .map { $0 as String } + + self.inputStream = inputStream + self.name = name + self.mimeType = mimeType ?? detectedMimeType ?? "application/octet-stream" + self.fileName = fileName ?? fileURL.lastPathComponent + self.length = bodyLength + } + } + + internal class PartInputStream: AbstractInputStream { + let headerData: NSData + let footerData: NSData + let bodyPart: Part + + let totalLength: Int + var totalSentLength: Int + + init(part: Part, boundary: String) { + let header: String + switch (part.mimeType, part.fileName) { + case (let mimeType?, let fileName?): + header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(fileName)\"\r\nContent-Type: \(mimeType)\r\n\r\n" + + case (let mimeType?, _): + header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(part.name)\"; \r\nContent-Type: \(mimeType)\r\n\r\n" + + default: + header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(part.name)\"\r\n\r\n" + } + + headerData = header.dataUsingEncoding(NSUTF8StringEncoding)! + footerData = "\r\n".dataUsingEncoding(NSUTF8StringEncoding)! + bodyPart = part + totalLength = headerData.length + bodyPart.length + footerData.length + totalSentLength = 0 + + super.init() + } + + var headerRange: Range { + return 0.. { + return headerRange.endIndex..<(headerRange.endIndex + bodyPart.length) + } + + var footerRange: Range { + return bodyRange.endIndex..<(bodyRange.endIndex + footerData.length) + } + + // MARK: NSInputStream + override var hasBytesAvailable: Bool { + return totalSentLength < totalLength + } + + override func read(buffer: UnsafeMutablePointer, maxLength: Int) -> Int { + var sentLength = 0 + + while sentLength < maxLength && totalSentLength < totalLength { + let offsetBuffer = buffer + sentLength + let availableLength = maxLength - sentLength + + switch totalSentLength { + case headerRange: + let readLength = min(headerRange.endIndex - totalSentLength, availableLength) + let readRange = NSRange(location: totalSentLength - headerRange.startIndex, length: readLength) + headerData.getBytes(offsetBuffer, range: readRange) + sentLength += readLength + totalSentLength += sentLength + + case bodyRange: + if bodyPart.inputStream.streamStatus == .NotOpen { + bodyPart.inputStream.open() + } + + let readLength = bodyPart.inputStream.read(offsetBuffer, maxLength: availableLength) + sentLength += readLength + totalSentLength += readLength + + case footerRange: + let readLength = min(footerRange.endIndex - totalSentLength, availableLength) + let range = NSRange(location: totalSentLength - footerRange.startIndex, length: readLength) + footerData.getBytes(offsetBuffer, range: range) + sentLength += readLength + totalSentLength += readLength + + default: + print("Illegal range access: \(totalSentLength) is out of \(headerRange.startIndex)..<\(footerRange.endIndex)") + return -1 + } + } + + return sentLength; + } + } + + internal class MultipartInputStream: AbstractInputStream { + let boundary: String + let partStreams: [PartInputStream] + let footerData: NSData + + let totalLength: Int + var totalSentLength: Int + + private var privateStreamStatus = NSStreamStatus.NotOpen + + init(parts: [Part], boundary: String) { + self.boundary = boundary + self.partStreams = parts.map { PartInputStream(part: $0, boundary: boundary) } + self.footerData = "--\(boundary)--\r\n".dataUsingEncoding(NSUTF8StringEncoding)! + self.totalLength = partStreams.reduce(footerData.length) { $0 + $1.totalLength } + self.totalSentLength = 0 + super.init() + } + + var partsRange: Range { + return 0.. { + return partsRange.endIndex..<(partsRange.endIndex + footerData.length) + } + + var currentPartInputStream: PartInputStream? { + var currentOffset = 0 + + for partStream in partStreams { + let partStreamRange = currentOffset..<(currentOffset + partStream.totalLength) + if partStreamRange.contains(totalSentLength) { + return partStream + } + + currentOffset += partStream.totalLength + } + + return nil + } + + // MARK: NSInputStream + // NOTE: NSInputStream does not have its own implementation because it is a class cluster. + override var streamStatus: NSStreamStatus { + return privateStreamStatus + } + + override var hasBytesAvailable: Bool { + return totalSentLength < totalLength + } + + override func open() { + privateStreamStatus = .Open + } + + override func close() { + privateStreamStatus = .Closed + } + + override func read(buffer: UnsafeMutablePointer, maxLength: Int) -> Int { + privateStreamStatus = .Reading + + var sentLength = 0 + + while sentLength < maxLength && totalSentLength < totalLength { + let offsetBuffer = buffer + sentLength + let availableLength = maxLength - sentLength + + switch totalSentLength { + case partsRange: + guard let partStream = currentPartInputStream else { + print("Illegal offset \(totalLength) for part streams \(partsRange)") + return -1 + } + + let readLength = partStream.read(offsetBuffer, maxLength: availableLength) + sentLength += readLength + totalSentLength += readLength + + case footerRange: + let readLength = min(footerRange.endIndex - totalSentLength, availableLength) + let range = NSRange(location: totalSentLength - footerRange.startIndex, length: readLength) + footerData.getBytes(offsetBuffer, range: range) + sentLength += readLength + totalSentLength += readLength + + default: + print("Illegal range access: \(totalSentLength) is out of \(partsRange.startIndex)..<\(footerRange.endIndex)") + return -1 + } + + if privateStreamStatus != .Closed && !hasBytesAvailable { + privateStreamStatus = .AtEnd + } + } + + return sentLength + } + + override var delegate: NSStreamDelegate? { + get { return nil } + set { } + } + + override func scheduleInRunLoop(runLoop: NSRunLoop, forMode mode: String) { + + } + + override func removeFromRunLoop(runLoop: NSRunLoop, forMode mode: String) { + + } + } +} diff --git a/Sources/BodyParametersType/NSData+NSInputStream.swift b/Sources/BodyParametersType/NSData+NSInputStream.swift new file mode 100644 index 00000000..c5f01fb3 --- /dev/null +++ b/Sources/BodyParametersType/NSData+NSInputStream.swift @@ -0,0 +1,38 @@ +import Foundation + +extension NSData { + enum InputStreamError: ErrorType { + case InvalidDataCapacity(Int) + case UnreadableStream(NSInputStream) + } + + convenience init(inputStream: NSInputStream, capacity: Int = Int(UInt16.max)) throws { + guard let data = NSMutableData(capacity: capacity) else { + throw InputStreamError.InvalidDataCapacity(capacity) + } + + let bufferSize = min(Int(UInt16.max), capacity) + let buffer = UnsafeMutablePointer.alloc(bufferSize) + + var readSize: Int + + repeat { + readSize = inputStream.read(buffer, maxLength: bufferSize) + + switch readSize { + case let x where x > 0: + data.appendBytes(buffer, length: readSize) + + case let x where x < 0: + throw InputStreamError.UnreadableStream(inputStream) + + default: + break + } + } while readSize > 0 + + buffer.dealloc(bufferSize) + + self.init(data: data) + } +} diff --git a/Sources/Box.swift b/Sources/Box.swift new file mode 100644 index 00000000..baacebee --- /dev/null +++ b/Sources/Box.swift @@ -0,0 +1,9 @@ +import Foundation + +internal final class Box { + let value: T + + init(_ value: T) { + self.value = value + } +} diff --git a/Sources/CallbackQueue.swift b/Sources/CallbackQueue.swift new file mode 100644 index 00000000..59352d05 --- /dev/null +++ b/Sources/CallbackQueue.swift @@ -0,0 +1,38 @@ +import Foundation + +/// `CallbackQueue` represents queue where `handler` of `Session.sendRequest(_:handler:)` runs. +public enum CallbackQueue { + /// Dispatches callback closure on main queue asynchronously. + case Main + + /// Dispatches callback closure on the queue where backend adapter callback runs. + case SessionQueue + + /// Dispatches callback closure on associated operation queue. + case OperationQueue(NSOperationQueue) + + /// Dispatches callback closure on associated dispatch queue. + case DispatchQueue(dispatch_queue_t) + + internal func execute(closure: () -> Void) { + switch self { + case .Main: + dispatch_async(dispatch_get_main_queue()) { + closure() + } + + case .SessionQueue: + closure() + + case .OperationQueue(let operationQueue): + operationQueue.addOperationWithBlock { + closure() + } + + case .DispatchQueue(let dispatchQueue): + dispatch_async(dispatchQueue) { + closure() + } + } + } +} diff --git a/Sources/DataParserType/DataParserType.swift b/Sources/DataParserType/DataParserType.swift new file mode 100644 index 00000000..0777043b --- /dev/null +++ b/Sources/DataParserType/DataParserType.swift @@ -0,0 +1,11 @@ +import Foundation + +/// `DataParserType` protocol provides inteface to parse HTTP response body and to state Content-Type to accept. +public protocol DataParserType { + /// Value for `Accept` header field of HTTP request. + var contentType: String? { get } + + /// Return `AnyObject` that expresses structure of response such as JSON and XML. + /// - Throws: `ErrorType` when parser encountered invalid format data. + func parseData(data: NSData) throws -> AnyObject +} diff --git a/Sources/DataParserType/FormURLEncodedDataParser.swift b/Sources/DataParserType/FormURLEncodedDataParser.swift new file mode 100644 index 00000000..536033b0 --- /dev/null +++ b/Sources/DataParserType/FormURLEncodedDataParser.swift @@ -0,0 +1,43 @@ +import Foundation + +/// `FormURLEncodedDataParser` parses form URL encoded response data. +public class FormURLEncodedDataParser: DataParserType { + public enum Error: ErrorType { + case CannotGetStringFromData(NSData) + } + + /// The string encoding of the data. + public let encoding: NSStringEncoding + + /// Returns `FormURLEncodedDataParser` with the string encoding. + public init(encoding: NSStringEncoding) { + self.encoding = encoding + } + + // MARK: - DataParserType + + /// Value for `Accept` header field of HTTP request. + public var contentType: String? { + return "application/x-www-form-urlencoded" + } + + /// Return `AnyObject` that expresses structure of response. + /// - Throws: `FormURLEncodedDataParser.Error` when the parser fails to initialize `NSString` from `NSData`. + public func parseData(data: NSData) throws -> AnyObject { + guard let string = NSString(data: data, encoding: encoding) as? String else { + throw Error.CannotGetStringFromData(data) + } + + let components = NSURLComponents() + components.percentEncodedQuery = string + + let queryItems = components.queryItems ?? [] + var dictionary = [String: AnyObject]() + + for queryItem in queryItems { + dictionary[queryItem.name] = queryItem.value + } + + return dictionary + } +} diff --git a/Sources/DataParserType/JSONDataParser.swift b/Sources/DataParserType/JSONDataParser.swift new file mode 100644 index 00000000..227ea1fd --- /dev/null +++ b/Sources/DataParserType/JSONDataParser.swift @@ -0,0 +1,29 @@ +import Foundation + +/// `JSONDataParser` response JSON data. +public class JSONDataParser: DataParserType { + /// Options for reading the JSON data and creating the objects. + public let readingOptions: NSJSONReadingOptions + + /// Returns `JSONDataParser` with the reading options. + public init(readingOptions: NSJSONReadingOptions) { + self.readingOptions = readingOptions + } + + // MARK: - DataParserType + + /// Value for `Accept` header field of HTTP request. + public var contentType: String? { + return "application/json" + } + + /// Return `AnyObject` that expresses structure of JSON response. + /// - Throws: `NSError` when `NSJSONSerialization` fails to deserialize `NSData` into `AnyObject`. + public func parseData(data: NSData) throws -> AnyObject { + guard data.length > 0 else { + return [:] + } + + return try NSJSONSerialization.JSONObjectWithData(data, options: readingOptions) + } +} diff --git a/Sources/DataParserType/StringDataParser.swift b/Sources/DataParserType/StringDataParser.swift new file mode 100644 index 00000000..aa54a014 --- /dev/null +++ b/Sources/DataParserType/StringDataParser.swift @@ -0,0 +1,33 @@ +import Foundation + +/// `StringDataParser` parses data and convert it to string. +public class StringDataParser: DataParserType { + public enum Error: ErrorType { + case InvalidData(NSData) + } + + /// The string encoding of the data. + public let encoding: NSStringEncoding + + /// Returns `FormURLEncodedDataParser` with the string encoding. + public init(encoding: NSStringEncoding = NSUTF8StringEncoding) { + self.encoding = encoding + } + + // MARK: - DataParserType + + /// Value for `Accept` header field of HTTP request. + public var contentType: String? { + return nil + } + + /// Return `String` that converted from `NSData`. + /// - Throws: `StringDataParser.Error` when the parser fails to initialize `NSString` from `NSData`. + public func parseData(data: NSData) throws -> AnyObject { + guard let string = NSString(data: data, encoding: encoding) else { + throw Error.InvalidData(data) + } + + return string + } +} diff --git a/Sources/Error/RequestError.swift b/Sources/Error/RequestError.swift new file mode 100644 index 00000000..222ff38d --- /dev/null +++ b/Sources/Error/RequestError.swift @@ -0,0 +1,10 @@ +import Foundation + +/// `RequestError` represents a common error that occurs while building `NSURLRequest` from `RequestType`. +public enum RequestError: ErrorType { + /// Indicates `baseURL` of a type that conforms `RequestType` is invalid. + case InvalidBaseURL(NSURL) + + /// Indicates `NSURLRequest` built by `RequestType.buildURLRequest` is unexpected. + case UnexpectedURLRequest(NSURLRequest) +} diff --git a/Sources/Error/ResponseError.swift b/Sources/Error/ResponseError.swift new file mode 100644 index 00000000..ef0a5c72 --- /dev/null +++ b/Sources/Error/ResponseError.swift @@ -0,0 +1,15 @@ +import Foundation + +/// `ResponseError` represents a common error that occurs while getting `RequestType.Response` +/// from raw result tuple `(NSData?, NSURLResponse?, NSError?)`. +public enum ResponseError: ErrorType { + /// Indicates the session adapter returned `NSURLResponse` that fails to down-cast to `NSHTTPURLResponse`. + case NonHTTPURLResponse(NSURLResponse?) + + /// Indicates `NSHTTPURLResponse.statusCode` is not acceptable. + /// In most cases, *acceptable* means the value is in `200..<300`. + case UnacceptableStatusCode(Int) + + /// Indicates `AnyObject` that represents the response is unexpected. + case UnexpectedObject(AnyObject) +} diff --git a/Sources/Error/SessionTaskError.swift b/Sources/Error/SessionTaskError.swift new file mode 100644 index 00000000..7a31fa4c --- /dev/null +++ b/Sources/Error/SessionTaskError.swift @@ -0,0 +1,13 @@ +import Foundation + +/// `SessionTaskError` represents an error that occurs while task for a request. +public enum SessionTaskError: ErrorType { + /// Error of `NSURLSession`. + case ConnectionError(ErrorType) + + /// Error while creating `NSURLReqeust` from `Request`. + case RequestError(ErrorType) + + /// Error while creating `RequestType.Response` from `(NSData, NSURLResponse)`. + case ResponseError(ErrorType) +} diff --git a/Sources/HTTPMethod.swift b/Sources/HTTPMethod.swift index 7bee9406..ecb9f7e0 100644 --- a/Sources/HTTPMethod.swift +++ b/Sources/HTTPMethod.swift @@ -1,5 +1,6 @@ import Foundation +/// `HTTPMethod` represents HTTP methods. public enum HTTPMethod: String { case GET case POST @@ -10,4 +11,15 @@ public enum HTTPMethod: String { case TRACE case OPTIONS case CONNECT + + /// Indicates if the query parameters are suitable for parameters. + var prefersQueryParameters: Bool { + switch self { + case .GET, .HEAD, .DELETE: + return true + + default: + return false + } + } } diff --git a/Sources/Info.plist b/Sources/Info.plist index dc9750ea..9866188c 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.3.0 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/MultipartFormDataSerialization.swift b/Sources/MultipartFormDataSerialization.swift deleted file mode 100644 index 28bef7a5..00000000 --- a/Sources/MultipartFormDataSerialization.swift +++ /dev/null @@ -1,718 +0,0 @@ -import Foundation - -#if os(iOS) || os(watchOS) || os(tvOS) - import MobileCoreServices -#elseif os(OSX) - import CoreServices -#endif - -public final class MultipartFormDataSerialization { - public enum Error: ErrorType { - case CannotCastObjectToSupportedType(AnyObject) - case UnsupportedType(String, AnyObject) - - case NSURLErrorBadURL(String) - case NSURLErrorCannotOpenFile(String) - case InputStreamReadFailed(String) - case OutputStreamWriteFailed(String) - } - - public final class Parameter { - private enum `Type` { - case DataWithMimeType(data: NSData, mimeType: String) - case DataWithFileNameMimeType(data: NSData, fileName: String, mimeType: String) - case FileURLWithFileNameMimeType(fileURL: NSURL, fileName: String, mimeType: String) - } - - private let type: Type - - public init(data: NSData, mimeType: String) { - type = .DataWithMimeType(data: data, mimeType: mimeType) - } - - public init(data: NSData, fileName: String, mimeType: String) { - type = .DataWithFileNameMimeType(data: data, fileName: fileName, mimeType: mimeType) - } - - public init(fileURL: NSURL, fileName: String, mimeType: String) { - type = .FileURLWithFileNameMimeType(fileURL: fileURL, fileName: fileName, mimeType: mimeType) - } - } - - public static func dataFromObject(object: AnyObject) throws -> (boundary: String, body: NSData) { - switch object { - case let array as [(String, AnyObject)]: - return try dataFromArray(array) - - case let dictionary as [String : AnyObject] : - let array = dictionary.map { key, val in (key, val) } - return try dataFromArray(array) - - default: - throw Error.CannotCastObjectToSupportedType(object) - } - } - - private static func dataFromArray(array: [(String, AnyObject)]) throws -> (boundary: String, body: NSData) { - let encoder = MultipartFormData() - - for (key, val) in array { - switch val { - case let parameter as Parameter: - switch parameter.type { - case .DataWithMimeType(let data, let mimeType): - encoder.appendBodyPart(data: data, name: key, mimeType: mimeType) - - case .DataWithFileNameMimeType(let data, let fileName, let mimeType): - encoder.appendBodyPart(data: data, name: key, fileName: fileName, mimeType: mimeType) - - case .FileURLWithFileNameMimeType(let fileURL, let fileName, let mimeType): - encoder.appendBodyPart(fileURL: fileURL, name: key, fileName: fileName, mimeType: mimeType) - } - - case let data as NSData: - encoder.appendBodyPart(data: data, name: key) - - case let fileURL as NSURL where fileURL.fileURL: - encoder.appendBodyPart(fileURL: fileURL, name: key) - - // avoid cast problem e.g. "Bool -> AnyObject -> Int" - case let bool as NSNumber where bool.isKindOfClass(objc_getClass("__NSCFBoolean") as! AnyClass): - guard let data = "\(bool.boolValue)".dataUsingEncoding(NSUTF8StringEncoding) else { - throw Error.UnsupportedType(key, val) - } - encoder.appendBodyPart(data: data, name: key) - - case let number as NSNumber: - guard let data = "\(number)".dataUsingEncoding(NSUTF8StringEncoding) else { - throw Error.UnsupportedType(key, val) - } - encoder.appendBodyPart(data: data, name: key) - - case let string as String: - guard let data = string.dataUsingEncoding(NSUTF8StringEncoding) else { - throw Error.UnsupportedType(key, val) - } - encoder.appendBodyPart(data: data, name: key) - - default: - throw Error.UnsupportedType(key, val) - } - } - - return (encoder.boundary, try encoder.encode()) - } -} - - -/** - Copied from [Alamofire](https://github.com/Alamofire/Alamofire). -**/ - -/** - Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode - multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead - to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the - data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for - larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset. - For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well - and the w3 form documentation. - - https://www.ietf.org/rfc/rfc2388.txt - - https://www.ietf.org/rfc/rfc2045.txt - - https://www.w3.org/TR/html401/interact/forms.html#h-17.13 - */ -private final class MultipartFormData { - // MARK: - Helper Types - - struct EncodingCharacters { - static let CRLF = "\r\n" - } - - struct BoundaryGenerator { - enum BoundaryType { - case Initial, Encapsulated, Final - } - - static func randomBoundary() -> String { - return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random()) - } - - static func boundaryData(boundaryType boundaryType: BoundaryType, boundary: String) -> NSData { - let boundaryText: String - - switch boundaryType { - case .Initial: - boundaryText = "--\(boundary)\(EncodingCharacters.CRLF)" - case .Encapsulated: - boundaryText = "\(EncodingCharacters.CRLF)--\(boundary)\(EncodingCharacters.CRLF)" - case .Final: - boundaryText = "\(EncodingCharacters.CRLF)--\(boundary)--\(EncodingCharacters.CRLF)" - } - - return boundaryText.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - } - } - - class BodyPart { - let headers: [String: String] - let bodyStream: NSInputStream - let bodyContentLength: UInt64 - var hasInitialBoundary = false - var hasFinalBoundary = false - - init(headers: [String: String], bodyStream: NSInputStream, bodyContentLength: UInt64) { - self.headers = headers - self.bodyStream = bodyStream - self.bodyContentLength = bodyContentLength - } - } - - // MARK: - Properties - - /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`. - var contentType: String { return "multipart/form-data; boundary=\(boundary)" } - - /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries. - var contentLength: UInt64 { return bodyParts.reduce(0) { $0 + $1.bodyContentLength } } - - /// The boundary used to separate the body parts in the encoded form data. - let boundary: String - - private var bodyParts: [BodyPart] - private var bodyPartError: ErrorType? - private let streamBufferSize: Int - - // MARK: - Lifecycle - - /** - Creates a multipart form data object. - - returns: The multipart form data object. - */ - init() { - self.boundary = BoundaryGenerator.randomBoundary() - self.bodyParts = [] - - /** - * The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more - * information, please refer to the following article: - * - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html - */ - - self.streamBufferSize = 1024 - } - - // MARK: - Body Parts - - /** - Creates a body part from the data and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - `Content-Disposition: form-data; name=#{name}` (HTTP Header) - - Encoded data - - Multipart form boundary - - parameter data: The data to encode into the multipart form data. - - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header. - */ - private func appendBodyPart(data data: NSData, name: String) { - let headers = contentHeaders(name: name) - let stream = NSInputStream(data: data) - let length = UInt64(data.length) - - appendBodyPart(stream: stream, length: length, headers: headers) - } - - /** - Creates a body part from the data and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - `Content-Disposition: form-data; name=#{name}` (HTTP Header) - - `Content-Type: #{generated mimeType}` (HTTP Header) - - Encoded data - - Multipart form boundary - - parameter data: The data to encode into the multipart form data. - - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header. - - parameter mimeType: The MIME type to associate with the data content type in the `Content-Type` HTTP header. - */ - func appendBodyPart(data data: NSData, name: String, mimeType: String) { - let headers = contentHeaders(name: name, mimeType: mimeType) - let stream = NSInputStream(data: data) - let length = UInt64(data.length) - - appendBodyPart(stream: stream, length: length, headers: headers) - } - - /** - Creates a body part from the data and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) - - `Content-Type: #{mimeType}` (HTTP Header) - - Encoded file data - - Multipart form boundary - - parameter data: The data to encode into the multipart form data. - - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header. - - parameter fileName: The filename to associate with the data in the `Content-Disposition` HTTP header. - - parameter mimeType: The MIME type to associate with the data in the `Content-Type` HTTP header. - */ - func appendBodyPart(data data: NSData, name: String, fileName: String, mimeType: String) { - let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) - let stream = NSInputStream(data: data) - let length = UInt64(data.length) - - appendBodyPart(stream: stream, length: length, headers: headers) - } - - /** - Creates a body part from the file and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header) - - `Content-Type: #{generated mimeType}` (HTTP Header) - - Encoded file data - - Multipart form boundary - The filename in the `Content-Disposition` HTTP header is generated from the last path component of the - `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the - system associated MIME type. - - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data. - - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header. - */ - func appendBodyPart(fileURL fileURL: NSURL, name: String) { - if let - fileName = fileURL.lastPathComponent, - pathExtension = fileURL.pathExtension - { - let mimeType = mimeTypeForPathExtension(pathExtension) - appendBodyPart(fileURL: fileURL, name: name, fileName: fileName, mimeType: mimeType) - } else { - let failureReason = "Failed to extract the fileName of the provided URL: \(fileURL)" - setBodyPartError(MultipartFormDataSerialization.Error.NSURLErrorBadURL(failureReason)) - } - } - - /** - Creates a body part from the file and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header) - - Content-Type: #{mimeType} (HTTP Header) - - Encoded file data - - Multipart form boundary - - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data. - - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header. - - parameter fileName: The filename to associate with the file content in the `Content-Disposition` HTTP header. - - parameter mimeType: The MIME type to associate with the file content in the `Content-Type` HTTP header. - */ - func appendBodyPart(fileURL fileURL: NSURL, name: String, fileName: String, mimeType: String) { - let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) - - //============================================================ - // Check 1 - is file URL? - //============================================================ - - guard fileURL.fileURL else { - let failureReason = "The file URL does not point to a file URL: \(fileURL)" - let error = MultipartFormDataSerialization.Error.NSURLErrorBadURL(failureReason) - setBodyPartError(error) - return - } - - //============================================================ - // Check 2 - is file URL reachable? - //============================================================ - - var isReachable = true - - if #available(OSX 10.10, *) { - isReachable = fileURL.checkPromisedItemIsReachableAndReturnError(nil) - } - - guard isReachable else { - let error = MultipartFormDataSerialization.Error.NSURLErrorBadURL("The file URL is not reachable: \(fileURL)") - setBodyPartError(error) - return - } - - //============================================================ - // Check 3 - is file URL a directory? - //============================================================ - - var isDirectory: ObjCBool = false - - guard let - path = fileURL.path - where NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDirectory) && !isDirectory else - { - let failureReason = "The file URL is a directory, not a file: \(fileURL)" - let error = MultipartFormDataSerialization.Error.NSURLErrorBadURL(failureReason) - setBodyPartError(error) - return - } - - //============================================================ - // Check 4 - can the file size be extracted? - //============================================================ - - var bodyContentLength: UInt64? - - do { - if let - path = fileURL.path, - fileSize = try NSFileManager.defaultManager().attributesOfItemAtPath(path)[NSFileSize] as? NSNumber - { - bodyContentLength = fileSize.unsignedLongLongValue - } - } catch { - // No-op - } - - guard let length = bodyContentLength else { - let failureReason = "Could not fetch attributes from the file URL: \(fileURL)" - let error = MultipartFormDataSerialization.Error.NSURLErrorBadURL(failureReason) - setBodyPartError(error) - return - } - - //============================================================ - // Check 5 - can a stream be created from file URL? - //============================================================ - - guard let stream = NSInputStream(URL: fileURL) else { - let failureReason = "Failed to create an input stream from the file URL: \(fileURL)" - let error = MultipartFormDataSerialization.Error.NSURLErrorCannotOpenFile(failureReason) - setBodyPartError(error) - return - } - - appendBodyPart(stream: stream, length: length, headers: headers) - } - - /** - Creates a body part from the stream and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) - - `Content-Type: #{mimeType}` (HTTP Header) - - Encoded stream data - - Multipart form boundary - - parameter stream: The input stream to encode in the multipart form data. - - parameter length: The content length of the stream. - - parameter name: The name to associate with the stream content in the `Content-Disposition` HTTP header. - - parameter fileName: The filename to associate with the stream content in the `Content-Disposition` HTTP header. - - parameter mimeType: The MIME type to associate with the stream content in the `Content-Type` HTTP header. - */ - func appendBodyPart( - stream stream: NSInputStream, - length: UInt64, - name: String, - fileName: String, - mimeType: String) - { - let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) - appendBodyPart(stream: stream, length: length, headers: headers) - } - - /** - Creates a body part with the headers, stream and length and appends it to the multipart form data object. - The body part data will be encoded using the following format: - - HTTP headers - - Encoded stream data - - Multipart form boundary - - parameter stream: The input stream to encode in the multipart form data. - - parameter length: The content length of the stream. - - parameter headers: The HTTP headers for the body part. - */ - func appendBodyPart(stream stream: NSInputStream, length: UInt64, headers: [String: String]) { - let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) - bodyParts.append(bodyPart) - } - - // MARK: - Data Encoding - - /** - Encodes all the appended body parts into a single `NSData` object. - It is important to note that this method will load all the appended body parts into memory all at the same - time. This method should only be used when the encoded data will have a small memory footprint. For large data - cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method. - - throws: An `NSError` if encoding encounters an error. - - returns: The encoded `NSData` if encoding is successful. - */ - func encode() throws -> NSData { - if let bodyPartError = bodyPartError { - throw bodyPartError - } - - let encoded = NSMutableData() - - bodyParts.first?.hasInitialBoundary = true - bodyParts.last?.hasFinalBoundary = true - - for bodyPart in bodyParts { - let encodedData = try encodeBodyPart(bodyPart) - encoded.appendData(encodedData) - } - - return encoded - } - - /** - Writes the appended body parts into the given file URL. - This process is facilitated by reading and writing with input and output streams, respectively. Thus, - this approach is very memory efficient and should be used for large body part data. - - parameter fileURL: The file URL to write the multipart form data into. - - throws: An `NSError` if encoding encounters an error. - */ - func writeEncodedDataToDisk(fileURL: NSURL) throws { - if let bodyPartError = bodyPartError { - throw bodyPartError - } - - if let path = fileURL.path where NSFileManager.defaultManager().fileExistsAtPath(path) { - let failureReason = "A file already exists at the given file URL: \(fileURL)" - throw MultipartFormDataSerialization.Error.NSURLErrorBadURL(failureReason) - } else if !fileURL.fileURL { - let failureReason = "The URL does not point to a valid file: \(fileURL)" - throw MultipartFormDataSerialization.Error.NSURLErrorBadURL(failureReason) - } - - let outputStream: NSOutputStream - - if let possibleOutputStream = NSOutputStream(URL: fileURL, append: false) { - outputStream = possibleOutputStream - } else { - let failureReason = "Failed to create an output stream with the given URL: \(fileURL)" - throw MultipartFormDataSerialization.Error.NSURLErrorCannotOpenFile(failureReason) - } - - outputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode) - outputStream.open() - - self.bodyParts.first?.hasInitialBoundary = true - self.bodyParts.last?.hasFinalBoundary = true - - for bodyPart in self.bodyParts { - try writeBodyPart(bodyPart, toOutputStream: outputStream) - } - - outputStream.close() - outputStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode) - } - - // MARK: - Private - Body Part Encoding - - private func encodeBodyPart(bodyPart: BodyPart) throws -> NSData { - let encoded = NSMutableData() - - let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() - encoded.appendData(initialData) - - let headerData = encodeHeaderDataForBodyPart(bodyPart) - encoded.appendData(headerData) - - let bodyStreamData = try encodeBodyStreamDataForBodyPart(bodyPart) - encoded.appendData(bodyStreamData) - - if bodyPart.hasFinalBoundary { - encoded.appendData(finalBoundaryData()) - } - - return encoded - } - - private func encodeHeaderDataForBodyPart(bodyPart: BodyPart) -> NSData { - var headerText = "" - - for (key, value) in bodyPart.headers { - headerText += "\(key): \(value)\(EncodingCharacters.CRLF)" - } - headerText += EncodingCharacters.CRLF - - return headerText.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - } - - private func encodeBodyStreamDataForBodyPart(bodyPart: BodyPart) throws -> NSData { - let inputStream = bodyPart.bodyStream - inputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode) - inputStream.open() - - var error: ErrorType? - let encoded = NSMutableData() - - while inputStream.hasBytesAvailable { - var buffer = [UInt8](count: streamBufferSize, repeatedValue: 0) - let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) - - if inputStream.streamError != nil { - error = inputStream.streamError - break - } - - if bytesRead > 0 { - encoded.appendBytes(buffer, length: bytesRead) - } else if bytesRead < 0 { - let failureReason = "Failed to read from input stream: \(inputStream)" - error = MultipartFormDataSerialization.Error.InputStreamReadFailed(failureReason) - break - } else { - break - } - } - - inputStream.close() - inputStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode) - - if let error = error { - throw error - } - - return encoded - } - - // MARK: - Private - Writing Body Part to Output Stream - - private func writeBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) throws { - try writeInitialBoundaryDataForBodyPart(bodyPart, toOutputStream: outputStream) - try writeHeaderDataForBodyPart(bodyPart, toOutputStream: outputStream) - try writeBodyStreamForBodyPart(bodyPart, toOutputStream: outputStream) - try writeFinalBoundaryDataForBodyPart(bodyPart, toOutputStream: outputStream) - } - - private func writeInitialBoundaryDataForBodyPart( - bodyPart: BodyPart, - toOutputStream outputStream: NSOutputStream) - throws - { - let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() - return try writeData(initialData, toOutputStream: outputStream) - } - - private func writeHeaderDataForBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) throws { - let headerData = encodeHeaderDataForBodyPart(bodyPart) - return try writeData(headerData, toOutputStream: outputStream) - } - - private func writeBodyStreamForBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) throws { - let inputStream = bodyPart.bodyStream - inputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode) - inputStream.open() - - while inputStream.hasBytesAvailable { - var buffer = [UInt8](count: streamBufferSize, repeatedValue: 0) - let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) - - if let streamError = inputStream.streamError { - throw streamError - } - - if bytesRead > 0 { - if buffer.count != bytesRead { - buffer = Array(buffer[0.. 0 { - if outputStream.hasSpaceAvailable { - let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite) - - if let streamError = outputStream.streamError { - throw streamError - } - - if bytesWritten < 0 { - let failureReason = "Failed to write to output stream: \(outputStream)" - throw MultipartFormDataSerialization.Error.OutputStreamWriteFailed(failureReason) - } - - bytesToWrite -= bytesWritten - - if bytesToWrite > 0 { - buffer = Array(buffer[bytesWritten.. String { - if let - id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, nil)?.takeRetainedValue(), - contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() - { - return contentType as String - } - - return "application/octet-stream" - } - - // MARK: - Private - Content Headers - - private func contentHeaders(name name: String) -> [String: String] { - return ["Content-Disposition": "form-data; name=\"\(name)\""] - } - - private func contentHeaders(name name: String, mimeType: String) -> [String: String] { - return [ - "Content-Disposition": "form-data; name=\"\(name)\"", - "Content-Type": "\(mimeType)" - ] - } - - private func contentHeaders(name name: String, fileName: String, mimeType: String) -> [String: String] { - return [ - "Content-Disposition": "form-data; name=\"\(name)\"; filename=\"\(fileName)\"", - "Content-Type": "\(mimeType)" - ] - } - - // MARK: - Private - Boundary Encoding - - private func initialBoundaryData() -> NSData { - return BoundaryGenerator.boundaryData(boundaryType: .Initial, boundary: boundary) - } - - private func encapsulatedBoundaryData() -> NSData { - return BoundaryGenerator.boundaryData(boundaryType: .Encapsulated, boundary: boundary) - } - - private func finalBoundaryData() -> NSData { - return BoundaryGenerator.boundaryData(boundaryType: .Final, boundary: boundary) - } - - // MARK: - Private - Errors - - private func setBodyPartError(error: ErrorType) { - if bodyPartError == nil { - bodyPartError = error - } - } -} diff --git a/Sources/RequestBodyBuilder.swift b/Sources/RequestBodyBuilder.swift deleted file mode 100644 index cbb8f74f..00000000 --- a/Sources/RequestBodyBuilder.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import Result - -public enum RequestBodyBuilder { - case JSON(writingOptions: NSJSONWritingOptions) - case URL(encoding: NSStringEncoding) - case MultipartFormData - case Custom(contentTypeHeader: String, buildBodyFromObject: AnyObject throws -> NSData) - - /// - Throws: NSError, URLEncodedSerialization.Error, ErrorType - public func buildBodyFromObject(object: AnyObject) throws -> (contentTypeHeader: String, body: NSData) { - switch self { - case .JSON(let writingOptions): - // If isValidJSONObject(_:) is false, dataWithJSONObject(_:options:) throws NSException. - guard NSJSONSerialization.isValidJSONObject(object) else { - throw NSError(domain: NSCocoaErrorDomain, code: 3840, userInfo: nil) - } - return ("application/json", try NSJSONSerialization.dataWithJSONObject(object, options: writingOptions)) - - case .URL(let encoding): - return ("application/x-www-form-urlencoded", try URLEncodedSerialization.dataFromObject(object, encoding: encoding)) - - case .MultipartFormData: - let (boundary, body) = try MultipartFormDataSerialization.dataFromObject(object) - return ("multipart/form-data; boundary=\(boundary)", body) - case .Custom(let (contentTypeHeader, buildBodyFromObject)): - return (contentTypeHeader, try buildBodyFromObject(object)) - } - } -} diff --git a/Sources/RequestType.swift b/Sources/RequestType.swift index 8d87328f..e5adf67e 100644 --- a/Sources/RequestType.swift +++ b/Sources/RequestType.swift @@ -1,163 +1,148 @@ import Foundation import Result -/// RequestType protocol represents a request for Web API. +/// `RequestType` 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? +/// - `typealias Response` +/// - `var baseURL: NSURL` +/// - `var method: HTTPMethod` +/// - `var path: String` +/// - `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response` public protocol RequestType { - /// Type represents a model object + /// The response type associated with the request type. associatedtype Response - /// Configurations of request + /// The base URL. var baseURL: NSURL { get } + + /// The HTTP request method. var method: HTTPMethod { get } + + /// The path URL component. var path: String { get } - /// A parameter dictionary for the request. You can pass `NSNull()` as a - /// value for nullable keys, those should be existed in the encoded query or - /// the request body. - var parameters: [String: AnyObject] { get } - var objectParameters: AnyObject { get } - - /// Additional HTTP header fields. RequestType will add `Accept` and `Content-Type` automatically. - /// You can override values for those fields here. - var HTTPHeaderFields: [String: String] { get } - - /// You can add any configurations here - /// - /// - Throws: ErrorType - 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? + /// The convenience property for `queryParameters` and `bodyParameters`. If the implementation of + /// `queryParameters` and `bodyParameters` are not provided, the values for them will be computed + /// from this property depending on `method`. + var parameters: AnyObject? { get } + + /// The actual parameters for the URL query. The values of this property will be escaped using `URLEncodedSerializetion`. + /// If this property is not implemented and `method.prefersQueryParameter` is `true`, the value of this property + /// will be computed from `parameters`. + var queryParameters: [String: AnyObject]? { get } + + /// The actual parameters for the HTTP body. If this property is not implemented and `method.prefersQueryParameter` is `false`, + /// the value of this property will be computed from `parameters` using `JSONBodyParameters`. + var bodyParameters: BodyParametersType? { get } + + /// The HTTP header fields. In addition to fields defined in this property, `Accept` and `Content-Type` + /// fields will be added by `dataParser` and `bodyParameters`. If you define `Accept` and `Content-Type` + /// in this property, the values in this property are preferred. + var headerFields: [String: String] { get } + + /// The parser object that states `Content-Type` to accept and parses response body. + var dataParser: DataParserType { get } + + /// Intercepts `NSURLRequest` which is created by `RequestType.buildURLRequest()`. If an error is + /// thrown in this method, the result of `Session.sendRequest()` truns `.Failure(.RequestError(error))`. + /// - Throws: `ErrorType` + func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest + + /// Intercepts response `AnyObject` and `NSHTTPURLResponse`. If an error is thrown in this method, + /// the result of `Session.sendRequest()` turns `.Failure(.ResponseError(error))`. + /// The default implementation of this method is provided to throw `RequestError.UnacceptableStatusCode` + /// if the HTTP status code is not in `200..<300`. + /// - Throws: `ErrorType` + func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject + + /// Build `Response` instance from raw response object. This method is called after + /// `interceptObject(:URLResponse:)` if it does not throw any error. + /// - Throws: `ErrorType` + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response } -/// Default implementation of RequestType protocol public extension RequestType { - public var objectParameters: AnyObject { - return [] + public var parameters: AnyObject? { + return nil } - public var parameters: [String: AnyObject] { - return [:] - } + public var queryParameters: [String: AnyObject]? { + guard let parameters = parameters as? [String: AnyObject] where method.prefersQueryParameters else { + return nil + } - public var HTTPHeaderFields: [String: String] { - return [:] + return parameters } - - public var acceptableStatusCodes: Set { - return Set(200..<300) + + public var bodyParameters: BodyParametersType? { + guard let parameters = parameters where !method.prefersQueryParameters else { + return nil + } + + return JSONBodyParameters(JSONObject: parameters) } - public var requestBodyBuilder: RequestBodyBuilder { - return .JSON(writingOptions: []) + public var headerFields: [String: String] { + return [:] } - public var responseBodyParser: ResponseBodyParser { - return .JSON(readingOptions: []) + public var dataParser: DataParserType { + return JSONDataParser(readingOptions: []) } - public func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { + public func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { return URLRequest } - public func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? { - return NSError(domain: "APIKitErrorDomain", code: 0, userInfo: ["object":object, "URLResponse": URLResponse]) + public func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject { + guard (200..<300).contains(URLResponse.statusCode) else { + throw ResponseError.UnacceptableStatusCode(URLResponse.statusCode) + } + return object } - // Use Result here because `throws` loses type info of an error. - // This method is not overridable. If you need to add customization, override configureURLRequest. - public func buildURLRequest() -> Result { + /// Builds `NSURLRequest` from properties of `self`. + /// - Throws: `RequestError`, `ErrorType` + public func buildURLRequest() throws -> NSURLRequest { let URL = path.isEmpty ? baseURL : baseURL.URLByAppendingPathComponent(path) guard let components = NSURLComponents(URL: URL, resolvingAgainstBaseURL: true) else { - return .Failure(.InvalidBaseURL(baseURL)) + throw RequestError.InvalidBaseURL(baseURL) } let URLRequest = NSMutableURLRequest() - let parameters = self.parameters - switch method { - case .GET, .HEAD, .DELETE: - if parameters.count > 0 { - components.percentEncodedQuery = URLEncodedSerialization.stringFromDictionary(parameters) - } + if let queryParameters = queryParameters where !queryParameters.isEmpty { + components.percentEncodedQuery = URLEncodedSerialization.stringFromDictionary(queryParameters) + } - default: - do { - if parameters.count > 0 { - let (contentTypeHeader, body) = try requestBodyBuilder.buildBodyFromObject(parameters) - URLRequest.HTTPBody = body - URLRequest.setValue(contentTypeHeader, forHTTPHeaderField: "Content-Type") - } else if let count = objectParameters.count where count > 0 { - let (contentTypeHeader, body) = try requestBodyBuilder.buildBodyFromObject(objectParameters) - URLRequest.HTTPBody = body - URLRequest.setValue(contentTypeHeader, forHTTPHeaderField: "Content-Type") - } - } catch { - return .Failure(.RequestBodySerializationError(error)) + if let bodyParameters = bodyParameters { + URLRequest.setValue(bodyParameters.contentType, forHTTPHeaderField: "Content-Type") + + switch try bodyParameters.buildEntity() { + case .Data(let data): + URLRequest.HTTPBody = data + + case .InputStream(let inputStream): + URLRequest.HTTPBodyStream = inputStream } } URLRequest.URL = components.URL URLRequest.HTTPMethod = method.rawValue - URLRequest.setValue(responseBodyParser.acceptHeader, forHTTPHeaderField: "Accept") - - HTTPHeaderFields.forEach { key, value in + URLRequest.setValue(dataParser.contentType, forHTTPHeaderField: "Accept") + + headerFields.forEach { key, value in URLRequest.setValue(value, forHTTPHeaderField: key) } - do { - return .Success(try configureURLRequest(URLRequest)) - } catch { - return .Failure(.ConfigurationError(error)) - } + return (try interceptURLRequest(URLRequest)) } - // Use Result here because `throws` loses type info of an error (in Swift 2 beta 2) - public 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) + /// Builds `Response` from response `NSData`. + /// - Throws: `ResponseError`, `ErrorType` + public func parseData(data: NSData, URLResponse: NSHTTPURLResponse) throws -> Response { + let parsedObject = try dataParser.parseData(data) + let passedObject = try interceptObject(parsedObject, URLResponse: URLResponse) + return try responseFromObject(passedObject, URLResponse: URLResponse) } } diff --git a/Sources/ResponseBodyParser.swift b/Sources/ResponseBodyParser.swift deleted file mode 100644 index acd1bfc9..00000000 --- a/Sources/ResponseBodyParser.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import Result - -public enum ResponseBodyParser { - case JSON(readingOptions: NSJSONReadingOptions) - case URL(encoding: NSStringEncoding) - case Custom(acceptHeader: String, parseData: NSData throws -> AnyObject) - - public var acceptHeader: String { - switch self { - case .JSON: - return "application/json" - - case .URL: - return "application/x-www-form-urlencoded" - - case .Custom(let (type, _)): - return type - } - } - - /// - Throws: NSError, URLEncodedSerialization.Error, ErrorType - public func parseData(data: NSData) throws -> AnyObject { - switch self { - case .JSON(let readingOptions): - if data.length == 0 { - return [:] - } - return try NSJSONSerialization.JSONObjectWithData(data, options: readingOptions) - - case .URL(let encoding): - return try URLEncodedSerialization.objectFromData(data, encoding: encoding) - - case .Custom(let (_, parseData)): - return try parseData(data) - } - } -} diff --git a/Sources/URLEncodedSerialization.swift b/Sources/Serializations/URLEncodedSerialization.swift similarity index 91% rename from Sources/URLEncodedSerialization.swift rename to Sources/Serializations/URLEncodedSerialization.swift index 44b8f36c..7d8351c2 100644 --- a/Sources/URLEncodedSerialization.swift +++ b/Sources/Serializations/URLEncodedSerialization.swift @@ -6,34 +6,34 @@ private func escape(string: String) -> String { let generalDelimiters = ":#[]@" let subDelimiters = "!$&'()*+,;=" let reservedCharacters = generalDelimiters + subDelimiters - + let allowedCharacterSet = NSMutableCharacterSet() allowedCharacterSet.formUnionWithCharacterSet(NSCharacterSet.URLQueryAllowedCharacterSet()) allowedCharacterSet.removeCharactersInString(reservedCharacters) - + // Crashes due to internal bug in iOS 7 ~ iOS 8.2. // References: // - https://github.com/Alamofire/Alamofire/issues/206 // - https://github.com/AFNetworking/AFNetworking/issues/3028 // return string.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? string - + let batchSize = 50 var index = string.startIndex - + var escaped = "" - + while index != string.endIndex { let startIndex = index let endIndex = index.advancedBy(batchSize, limit: string.endIndex) let range = startIndex.. String { return CFURLCreateStringByReplacingPercentEscapes(nil, string, nil) as String } +/// `URLEncodedSerialization` parses `NSData` and `String` as urlencoded, +/// and returns dictionary that represents the data or the string. public final class URLEncodedSerialization { public enum Error: ErrorType { case CannotGetStringFromData(NSData, NSStringEncoding) @@ -49,6 +51,7 @@ public final class URLEncodedSerialization { case InvalidFormatString(String) } + /// Returns `[String: String]` that represents urlencoded `NSData`. /// - Throws: URLEncodedSerialization.Error public static func objectFromData(data: NSData, encoding: NSStringEncoding) throws -> [String: String] { guard let string = String(data: data, encoding: encoding) else { @@ -69,6 +72,7 @@ public final class URLEncodedSerialization { return dictionary } + /// Returns urlencoded `NSData` from the object. /// - Throws: URLEncodedSerialization.Error public static func dataFromObject(object: AnyObject, encoding: NSStringEncoding) throws -> NSData { guard let dictionary = object as? [String: AnyObject] else { @@ -82,13 +86,14 @@ public final class URLEncodedSerialization { return data } - + + /// Returns urlencoded `NSData` from the string. public static func stringFromDictionary(dictionary: [String: AnyObject]) -> String { let pairs = dictionary.map { key, value -> String in if value is NSNull { return "\(escape(key))" } - + let valueAsString = (value as? String) ?? "\(value)" return "\(escape(key))=\(escape(valueAsString))" } diff --git a/Sources/Session.swift b/Sources/Session.swift index f04092b0..358b36e9 100644 --- a/Sources/Session.swift +++ b/Sources/Session.swift @@ -1,197 +1,122 @@ import Foundation import Result -public class Session { - public let URLSession: NSURLSession - - public init(URLSession: NSURLSession) { - self.URLSession = URLSession - } - - // send request and build response object - public func sendRequest(request: T, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { - switch request.buildURLRequest() { - case .Failure(let error): - dispatch_async(dispatch_get_main_queue()) { - handler(.Failure(error)) - } - return nil +private var taskRequestKey = 0 - case .Success(let URLRequest): - let dataTask = URLSession.dataTaskWithRequest(URLRequest) - dataTask.request = Box(request) - dataTask.completionHandler = { data, URLResponse, connectionError in - let sessionResult: Result<(NSData, NSURLResponse?), APIError> - if let error = connectionError { - sessionResult = .Failure(.ConnectionError(error)) - } else { - sessionResult = .Success((data, URLResponse)) - } +/// `Session` manages tasks for HTTP/HTTPS requests. +public class Session { + /// The adapter that connects `Session` instance and lower level backend. + public let adapter: SessionAdapterType - let result: Result = sessionResult.flatMap { data, URLResponse in - request.parseData(data, URLResponse: URLResponse) - } + /// The default callback queue for `sendRequest(_:handler:)`. + public let callbackQueue: CallbackQueue - dispatch_async(dispatch_get_main_queue()) { - handler(result) - } - } - - dataTask.resume() - - return dataTask - } + /// Returns `Session` instance that is initialized with `adapter`. + /// - parameter adapter: The adapter that connects lower level backend with Session interface. + /// - parameter callbackQueue: The default callback queue for `sendRequest(_:handler:)`. + public init(adapter: SessionAdapterType, callbackQueue: CallbackQueue = .Main) { + self.adapter = adapter + self.callbackQueue = callbackQueue } - - public func cancelRequest(requestType: T.Type, passingTest test: T -> Bool = { r in true }) { - URLSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in - let allTasks = dataTasks as [NSURLSessionTask] - + uploadTasks as [NSURLSessionTask] - + downloadTasks as [NSURLSessionTask] - - allTasks.filter { task in - let 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 - - default: - request = nil - } - - if let request = request { - return test(request) - } else { - return false - } - }.forEach { $0.cancel() } - } - } - + // Shared session for class methods - private static let privateSharedSession = Session(URLSession: NSURLSession( - configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), - delegate: URLSessionDelegate(), - delegateQueue: nil - )) + private static let privateSharedSession: Session = { + let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() + let adapter = NSURLSessionAdapter(configuration: configuration) + return Session(adapter: adapter) + }() + /// The shared `Session` instance for class methods, `Session.sendRequest(_:handler:)` and `Session.cancelRequest(_:passingTest:)`. public class var sharedSession: Session { return privateSharedSession } - public class func sendRequest(request: T, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { - return sharedSession.sendRequest(request, handler: handler) + /// Calls `sendRequest(_:handler:)` of `sharedSession`. + /// - parameter request: The request to be sent. + /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. + /// - parameter handler: The closure that receives result of the request. + /// - returns: The new session task. + public class func sendRequest(request: Request, callbackQueue: CallbackQueue? = nil, handler: (Result) -> Void = {r in}) -> SessionTaskType? { + return sharedSession.sendRequest(request, callbackQueue: callbackQueue, handler: handler) } - - public class func cancelRequest(requestType: T.Type, passingTest test: T -> Bool = { r in true }) { + + /// Calls `cancelRequest(_:passingTest:)` of `sharedSession`. + public class func cancelRequest(requestType: Request.Type, passingTest test: Request -> Bool = { r in true }) { sharedSession.cancelRequest(requestType, passingTest: test) } -} -@available(*, unavailable, renamed="Session") -public typealias API = Session + /// Sends a request and receives the result as the argument of `handler` closure. This method takes + /// a type parameter `Request` that conforms to `RequestType` protocol. The result of passed request is + /// expressed as `Result`. Since the response type + /// `Request.Response` is inferred from `Request` type parameter, the it changes depending on the request type. + /// - parameter request: The request to be sent. + /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. + /// - parameter handler: The closure that receives result of the request. + /// - returns: The new session task. + public func sendRequest(request: Request, callbackQueue: CallbackQueue? = nil, handler: (Result) -> Void = {r in}) -> SessionTaskType? { + let callbackQueue = callbackQueue ?? self.callbackQueue + + let URLRequest: NSURLRequest + do { + URLRequest = try request.buildURLRequest() + } catch { + callbackQueue.execute { + handler(.Failure(.RequestError(error))) + } + return nil + } -extension Session { - @available(*, unavailable, message="Use separated Session instance instead.") - public static func sendRequest(request: T, URLSession: NSURLSession, handler: (Result) -> Void = {r in}) -> NSURLSessionDataTask? { - abort() - } - - @available(*, unavailable, message="Use separated Session instance instead.") - public static func cancelRequest(requestType: T.Type, URLSession: NSURLSession, passingTest test: T -> Bool = { r in true }) { - abort() - } -} + let task = adapter.createTaskWithURLRequest(URLRequest) { data, URLResponse, error in + let result: Result -// MARK: - default implementation of URLSessionDelegate -public class URLSessionDelegate: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate { - // MARK: NSURLSessionTaskDelegate - public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError connectionError: NSError?) { - if let dataTask = task as? NSURLSessionDataTask { - dataTask.completionHandler?(dataTask.responseBuffer, dataTask.response, connectionError) - } - } + switch (data, URLResponse, error) { + case (_, _, let error?): + result = .Failure(.ConnectionError(error)) - // MARK: NSURLSessionDataDelegate - public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) { - dataTask.responseBuffer.appendData(data) - } - - public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didBecomeDownloadTask downloadTask: NSURLSessionDownloadTask) { - downloadTask.request = dataTask.request - } -} + case (let data?, let URLResponse as NSHTTPURLResponse, _): + do { + result = .Success(try request.parseData(data, URLResponse: URLResponse)) + } catch { + result = .Failure(.ResponseError(error)) + } -// Box is still necessary internally to store struct into associated object -private final class Box { - let value: T - init(_ value: T) { - self.value = value - } -} + default: + result = .Failure(.ResponseError(ResponseError.NonHTTPURLResponse(URLResponse))) + } -// MARK: - NSURLSessionTask extensions -private var taskRequestKey = 0 -private var dataTaskResponseBufferKey = 0 -private var dataTaskCompletionHandlerKey = 0 - -private extension NSURLSessionDataTask { - // `var request: RequestType?` is not available in Swift 2.0 - // ("protocol can only be used as a generic constraint") - private var request: Box? { - get { - return objc_getAssociatedObject(self, &taskRequestKey) as? Box - } - - set { - if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + callbackQueue.execute { + handler(result) } } + + setRequest(request, forTask: task) + task.resume() + + return task } - - private var responseBuffer: NSMutableData { - if let responseBuffer = objc_getAssociatedObject(self, &dataTaskResponseBufferKey) as? NSMutableData { - return responseBuffer - } else { - let responseBuffer = NSMutableData() - objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return responseBuffer + + /// Cancels requests that passes the test. + /// - parameter requestType: The request type to cancel. + /// - parameter test: The test closure that determines if a request should be cancelled or not. + public func cancelRequest(requestType: Request.Type, passingTest test: Request -> Bool = { r in true }) { + adapter.getTasksWithHandler { [weak self] tasks in + return tasks + .filter { task in + if let request = self?.requestForTask(task) as Request? { + return test(request) + } else { + return false + } + } + .forEach { $0.cancel() } } } - - private var completionHandler: ((NSData, NSURLResponse?, NSError?) -> Void)? { - get { - return (objc_getAssociatedObject(self, &dataTaskCompletionHandlerKey) as? Box<(NSData, NSURLResponse?, NSError?) -> Void>)?.value - } - - set { - if let value = newValue { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } else { - objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } + + private func setRequest(request: Request, forTask task: SessionTaskType) { + objc_setAssociatedObject(task, &taskRequestKey, Box(request), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } -} -private extension NSURLSessionDownloadTask { - private var request: Box? { - get { - return objc_getAssociatedObject(self, &taskRequestKey) as? Box - } - - set { - if let value = newValue { - objc_setAssociatedObject(self, &taskRequestKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } else { - objc_setAssociatedObject(self, &taskRequestKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } + private func requestForTask(task: SessionTaskType) -> Request? { + return (objc_getAssociatedObject(task, &taskRequestKey) as? Box)?.value } } diff --git a/Sources/SessionAdapterType/NSURLSessionAdapter.swift b/Sources/SessionAdapterType/NSURLSessionAdapter.swift new file mode 100644 index 00000000..90c18d6e --- /dev/null +++ b/Sources/SessionAdapterType/NSURLSessionAdapter.swift @@ -0,0 +1,75 @@ +import Foundation + +extension NSURLSessionTask: SessionTaskType { + +} + +private var dataTaskResponseBufferKey = 0 +private var taskAssociatedObjectCompletionHandlerKey = 0 + +/// `NSURLSessionAdapter` connects `NSURLSession` with `Session`. +/// +/// If you want to add custom behavior of `NSURLSession` by implementing delegate methods defined in +/// `NSURLSessionDelegate` and related protocols, define a subclass of `NSURLSessionAdapter` and implment +/// delegate methods that you want to implement. Since `NSURLSessionAdapter` also implements delegate methods +/// `URLSession(_:task: didCompleteWithError:)` and `URLSession(_:dataTask:didReceiveData:)`, you have to call +/// `super` in these methods if you implement them. +public class NSURLSessionAdapter: NSObject, SessionAdapterType, NSURLSessionDelegate { + /// The undelying `NSURLSession` instance. + public var URLSession: NSURLSession! + + /// Returns `NSURLSessionAdapter` initialized with `NSURLSessionConfiguration`. + public init(configuration: NSURLSessionConfiguration) { + super.init() + self.URLSession = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil) + } + + /// Creates `NSURLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. + public func createTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) -> SessionTaskType { + let task = URLSession.dataTaskWithRequest(URLRequest, completionHandler: handler) + + setBuffer(NSMutableData(), forTask: task) + setHandler(handler, forTask: task) + + task.resume() + + return task + } + + /// Aggregates `NSURLSessionTask` instances in `URLSession` using `getTasksWithCompletionHandler(_:)`. + public func getTasksWithHandler(handler: [SessionTaskType] -> Void) { + URLSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in + let allTasks = dataTasks as [NSURLSessionTask] + + uploadTasks as [NSURLSessionTask] + + downloadTasks as [NSURLSessionTask] + + handler(allTasks.map { $0 }) + } + } + + private func setBuffer(buffer: NSMutableData, forTask task: NSURLSessionTask) { + objc_setAssociatedObject(task, &dataTaskResponseBufferKey, buffer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func bufferForTask(task: NSURLSessionTask) -> NSMutableData? { + return objc_getAssociatedObject(task, &dataTaskResponseBufferKey) as? NSMutableData + } + + private func setHandler(handler: (NSData?, NSURLResponse?, NSError?) -> Void, forTask task: NSURLSessionTask) { + objc_setAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey, Box(handler), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func handlerForTask(task: NSURLSessionTask) -> ((NSData?, NSURLResponse?, NSError?) -> Void)? { + return (objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? Box<(NSData?, NSURLResponse?, NSError?) -> Void>)?.value + } + + // MARK: NSURLSessionTaskDelegate + public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError connectionError: NSError?) { + handlerForTask(task)?(bufferForTask(task), task.response, connectionError) + } + + // MARK: NSURLSessionDataDelegate + public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) { + bufferForTask(dataTask)?.appendData(data) + } +} diff --git a/Sources/SessionAdapterType/SessionAdapterType.swift b/Sources/SessionAdapterType/SessionAdapterType.swift new file mode 100644 index 00000000..46f4cde2 --- /dev/null +++ b/Sources/SessionAdapterType/SessionAdapterType.swift @@ -0,0 +1,18 @@ +import Foundation + +/// `SessionTaskType` protocol represents a task for a request. +public protocol SessionTaskType: class { + func resume() + func cancel() +} + +/// `SessionAdapterType` protocol provides interface to connect lower level networking backend with `Session`. +/// APIKit provides `NSURLSessionAdapter`, which conforms to `SessionAdapterType`, to connect `NSURLSession` +/// with `Session`. +public protocol SessionAdapterType { + /// Returns instance that conforms to `SessionTaskType`. `handler` must be called after success or failure. + func createTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) -> SessionTaskType + + /// Collects tasks from backend networking stack. `handler` must be called after collecting. + func getTasksWithHandler(handler: [SessionTaskType] -> Void) +} diff --git a/Tests/APIKit/APITests.swift b/Tests/APIKit/APITests.swift deleted file mode 100644 index ab1a9aba..00000000 --- a/Tests/APIKit/APITests.swift +++ /dev/null @@ -1,244 +0,0 @@ -import Foundation -import APIKit -import XCTest -import OHHTTPStubs - -protocol MockSessionRequestType: RequestType { -} - -extension MockSessionRequestType { - var baseURL: NSURL { - return NSURL(string: "https://api.github.com")! - } -} - -class MockSession: Session { - struct GetRoot: MockSessionRequestType { - typealias Response = [String: AnyObject] - - var method: HTTPMethod { - return .GET - } - - var path: String { - return "/" - } - - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { - return object as? [String: AnyObject] - } - } -} - -class AnotherMockSession: Session { - -} - -class APITests: XCTestCase { - override func tearDown() { - OHHTTPStubs.removeAllStubs() - super.tearDown() - } - - // MARK: - integration tests - func testSuccess() { - let dictionary = ["key": "value"] - let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) - - OHHTTPStubs.stubRequestsPassingTest({ request in - return true - }, withStubResponse: { request in - return OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) - }) - - let expectation = expectationWithDescription("wait for response") - let request = MockSession.GetRoot() - - MockSession.sendRequest(request) { response in - switch response { - case .Success(let dictionary): - XCTAssertEqual(dictionary["key"] as? String, "value") - - case .Failure: - XCTFail() - } - - expectation.fulfill() - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testFailureOfConnection() { - let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil) - - OHHTTPStubs.stubRequestsPassingTest({ request in - return true - }, withStubResponse: { request in - return OHHTTPStubsResponse(error: error) - }) - - let expectation = expectationWithDescription("wait for response") - let request = MockSession.GetRoot() - - MockSession.sendRequest(request) { response in - switch response { - case .Success: - XCTFail() - - case .Failure(let error): - switch error { - case .ConnectionError(let error): - XCTAssertEqual(error.domain, NSURLErrorDomain) - - default: - XCTFail() - } - } - - expectation.fulfill() - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testFailureOfResponseStatusCode() { - OHHTTPStubs.stubRequestsPassingTest({ request in - return true - }, withStubResponse: { request in - 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 = MockSession.GetRoot() - - MockSession.sendRequest(request) { response in - switch response { - case .Success: - XCTFail() - - case .Failure(let error): - switch error { - case .UnacceptableStatusCode(let statusCode, let error as NSError): - XCTAssertEqual(statusCode, 400) - XCTAssertEqual(error.domain, "APIKitErrorDomain") - XCTAssertNotNil(error.userInfo) - default: - XCTFail() - } - } - - expectation.fulfill() - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testFailureOfDecodingResponseBody() { - let data = "{\"broken\": \"json}".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - - OHHTTPStubs.stubRequestsPassingTest({ request in - return true - }, withStubResponse: { request in - return OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) - }) - - let expectation = expectationWithDescription("wait for response") - let request = MockSession.GetRoot() - - MockSession.sendRequest(request) { response in - switch response { - case .Success: - XCTFail() - - case .Failure(let error): - switch error { - case .ResponseBodyDeserializationError(let error as NSError): - XCTAssertEqual(error.domain, NSCocoaErrorDomain) - XCTAssertEqual(error.code, 3840) - - default: - XCTFail() - } - } - - expectation.fulfill() - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - // MARK: cancelling - func testFailureByCanceling() { - OHHTTPStubs.stubRequestsPassingTest({ request in - return true - }, withStubResponse: { request in - let response = OHHTTPStubsResponse(data: NSData(), statusCode: 200, headers: nil) - response.requestTime = 0.1 - response.responseTime = 0.1 - return response - }) - - let expectation = expectationWithDescription("wait for response") - let request = MockSession.GetRoot() - - MockSession.sendRequest(request) { response in - switch response { - case .Success: - XCTFail() - - case .Failure(let error): - switch error { - case .ConnectionError(let error): - XCTAssertEqual(error.domain, NSURLErrorDomain) - XCTAssertEqual(error.code, NSURLErrorCancelled) - - default: - XCTFail() - } - } - - expectation.fulfill() - } - - MockSession.cancelRequest(MockSession.GetRoot.self) - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testSuccessIfCancelingTestReturnsFalse() { - OHHTTPStubs.stubRequestsPassingTest({ request in - return true - }, withStubResponse: { request in - 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 - return response - }) - - let expectation = expectationWithDescription("wait for response") - let request = MockSession.GetRoot() - - MockSession.sendRequest(request) { response in - switch response { - case .Success: - break - - case .Failure: - XCTFail() - } - - expectation.fulfill() - } - - MockSession.cancelRequest(MockSession.GetRoot.self) { request in - return false - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } -} diff --git a/Tests/APIKit/BodyParametersType/FormURLEncodedBodyParametersTests.swift b/Tests/APIKit/BodyParametersType/FormURLEncodedBodyParametersTests.swift new file mode 100644 index 00000000..c823da25 --- /dev/null +++ b/Tests/APIKit/BodyParametersType/FormURLEncodedBodyParametersTests.swift @@ -0,0 +1,34 @@ +import Foundation +import XCTest +import APIKit + +class FormURLEncodedBodyParametersTests: XCTestCase { + func testURLSuccess() { + let object = ["foo": 1, "bar": 2, "baz": 3] + let parameters = FormURLEncodedBodyParameters(formObject: object) + XCTAssertEqual(parameters.contentType, "application/x-www-form-urlencoded") + + do { + guard case .Data(let data) = try parameters.buildEntity() else { + XCTFail() + return + } + + let createdObject = try URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding) + XCTAssertEqual(createdObject["foo"], "1") + XCTAssertEqual(createdObject["bar"], "2") + XCTAssertEqual(createdObject["baz"], "3") + } catch { + XCTFail() + } + } + + // NSURLComponents crashes on iOS 8.2 or earlier while escaping long CJK string. + // This test ensures that FormURLEncodedBodyParameters avoids this issue correctly. + func testLongCJKString() { + let key = "key" + let value = "一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十" + let parameters = FormURLEncodedBodyParameters(formObject: [key: value]) + _ = try? parameters.buildEntity() + } +} diff --git a/Tests/APIKit/BodyParametersType/JSONBodyParametersTests.swift b/Tests/APIKit/BodyParametersType/JSONBodyParametersTests.swift new file mode 100644 index 00000000..c7d20d95 --- /dev/null +++ b/Tests/APIKit/BodyParametersType/JSONBodyParametersTests.swift @@ -0,0 +1,39 @@ +import Foundation +import XCTest +import APIKit + +class JSONBodyParametersTests: XCTestCase { + func testJSONSuccess() { + let object = ["foo": 1, "bar": 2, "baz": 3] + let parameters = JSONBodyParameters(JSONObject: object) + XCTAssertEqual(parameters.contentType, "application/json") + + do { + guard case .Data(let data) = try parameters.buildEntity() else { + XCTFail() + return + } + + let dictionary = try NSJSONSerialization.JSONObjectWithData(data, options: []) + XCTAssertEqual(dictionary["foo"], 1) + XCTAssertEqual(dictionary["bar"], 2) + XCTAssertEqual(dictionary["baz"], 3) + } catch { + XCTFail() + } + } + + func testJSONFailure() { + let object = NSObject() + let parameters = JSONBodyParameters(JSONObject: object) + + do { + try parameters.buildEntity() + XCTFail() + } catch { + let nserror = error as NSError + XCTAssertEqual(nserror.domain, NSCocoaErrorDomain) + XCTAssertEqual(nserror.code, 3840) + } + } +} diff --git a/Tests/APIKit/BodyParametersType/MultipartFormDataParametersTests.swift b/Tests/APIKit/BodyParametersType/MultipartFormDataParametersTests.swift new file mode 100644 index 00000000..72ca3852 --- /dev/null +++ b/Tests/APIKit/BodyParametersType/MultipartFormDataParametersTests.swift @@ -0,0 +1,88 @@ +import Foundation +import XCTest +import APIKit + +class MultipartFormDataParametersTests: XCTestCase { + func testMultipartFormDataSuccess() { + let value1 = "1".dataUsingEncoding(NSUTF8StringEncoding)! + let value2 = "2".dataUsingEncoding(NSUTF8StringEncoding)! + + let parameters = MultipartFormDataBodyParameters(parts: [ + MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), + MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), + ]) + + do { + guard case .Data(let data) = try parameters.buildEntity() else { + XCTFail() + return + } + + let encodedData = String(data: data, encoding:NSUTF8StringEncoding)! + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.characters.count) + let match = regexp.matchesInString(parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let boundary = (parameters.contentType as NSString).substringWithRange(match.first!.rangeAtIndex(1)) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") + } catch { + XCTFail() + } + } + + func testStringValue() { + let part = try! MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") + let parameters = MultipartFormDataBodyParameters(parts: [part]) + + do { + guard case .Data(let data) = try parameters.buildEntity() else { + XCTFail() + return + } + + let string = String(data: data, encoding:NSUTF8StringEncoding)! + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nabcdef\r\n--\(parameters.boundary)--\r\n") + } catch { + XCTFail() + } + } + + func testIntValue() { + let part = try! MultipartFormDataBodyParameters.Part(value: 123, name: "foo") + let parameters = MultipartFormDataBodyParameters(parts: [part]) + + do { + guard case .Data(let data) = try parameters.buildEntity() else { + XCTFail() + return + } + + let string = String(data: data, encoding:NSUTF8StringEncoding)! + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n123\r\n--\(parameters.boundary)--\r\n") + } catch { + XCTFail() + } + } + + func testDoubleValue() { + let part = try! MultipartFormDataBodyParameters.Part(value: 3.14, name: "foo") + let parameters = MultipartFormDataBodyParameters(parts: [part]) + + do { + guard case .Data(let data) = try parameters.buildEntity() else { + XCTFail() + return + } + + let string = String(data: data, encoding:NSUTF8StringEncoding)! + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--\(parameters.boundary)--\r\n") + } catch { + XCTFail() + } + } +} diff --git a/Tests/APIKit/DataParserType/FormURLEncodedDataParserTests.swift b/Tests/APIKit/DataParserType/FormURLEncodedDataParserTests.swift new file mode 100644 index 00000000..28ada081 --- /dev/null +++ b/Tests/APIKit/DataParserType/FormURLEncodedDataParserTests.swift @@ -0,0 +1,26 @@ +import XCTest +import APIKit +import XCTest + +class FormURLEncodedDataParserTests: XCTestCase { + func testURLAcceptHeader() { + let parser = FormURLEncodedDataParser(encoding: NSUTF8StringEncoding) + XCTAssertEqual(parser.contentType, "application/x-www-form-urlencoded") + } + + func testURLSuccess() { + let string = "foo=1&bar=2&baz=3" + let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! + let parser = FormURLEncodedDataParser(encoding: NSUTF8StringEncoding) + + do { + let object = try parser.parseData(data) + let dictionary = object as? [String: String] + XCTAssertEqual(dictionary?["foo"], "1") + XCTAssertEqual(dictionary?["bar"], "2") + XCTAssertEqual(dictionary?["baz"], "3") + } catch { + XCTFail() + } + } +} diff --git a/Tests/APIKit/DataParserType/JSONDataParserTests.swift b/Tests/APIKit/DataParserType/JSONDataParserTests.swift new file mode 100644 index 00000000..10083ca5 --- /dev/null +++ b/Tests/APIKit/DataParserType/JSONDataParserTests.swift @@ -0,0 +1,26 @@ +import XCTest +import APIKit +import XCTest + +class JSONDataParserTests: XCTestCase { + func testContentType() { + let parser = JSONDataParser(readingOptions: []) + XCTAssertEqual(parser.contentType, "application/json") + } + + func testJSONSuccess() { + let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" + let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! + let parser = JSONDataParser(readingOptions: []) + + do { + let object = try parser.parseData(data) + let dictionary = object as? [String: Int] + XCTAssertEqual(dictionary?["foo"], 1) + XCTAssertEqual(dictionary?["bar"], 2) + XCTAssertEqual(dictionary?["baz"], 3) + } catch { + XCTFail() + } + } +} diff --git a/Tests/APIKit/DataParserType/StringDataParserTests.swift b/Tests/APIKit/DataParserType/StringDataParserTests.swift new file mode 100644 index 00000000..e7a15945 --- /dev/null +++ b/Tests/APIKit/DataParserType/StringDataParserTests.swift @@ -0,0 +1,23 @@ +import XCTest +import Foundation +import APIKit + +class StringDataParserTests: XCTestCase { + func testAcceptHeader() { + let parser = StringDataParser(encoding: NSUTF8StringEncoding) + XCTAssertNil(parser.contentType) + } + + func testParseData() { + let string = "abcdef" + let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! + let parser = StringDataParser(encoding: NSUTF8StringEncoding) + + do { + let object = try parser.parseData(data) + XCTAssertEqual(object as? String, string) + } catch { + XCTFail() + } + } +} diff --git a/Tests/APIKit/RequestBodyBuilderTests.swift b/Tests/APIKit/RequestBodyBuilderTests.swift deleted file mode 100644 index 393360b4..00000000 --- a/Tests/APIKit/RequestBodyBuilderTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation -import APIKit -import Result -import XCTest - -class RequestBodyBuilderTests: XCTestCase { - func testJSONSuccess() { - let object = ["foo": 1, "bar": 2, "baz": 3] - let builder = RequestBodyBuilder.JSON(writingOptions: []) - - do { - let (contentTypeHeader, data) = try builder.buildBodyFromObject(object) - let dictionary = try NSJSONSerialization.JSONObjectWithData(data, options: []) - XCTAssertEqual(contentTypeHeader, "application/json") - XCTAssertEqual(dictionary["foo"], 1) - XCTAssertEqual(dictionary["bar"], 2) - XCTAssertEqual(dictionary["baz"], 3) - } catch { - XCTFail() - } - } - - func testJSONFailure() { - let object = NSObject() - let builder = RequestBodyBuilder.JSON(writingOptions: []) - - do { - try builder.buildBodyFromObject(object) - XCTFail() - } catch { - let nserror = error as NSError - XCTAssertEqual(nserror.domain, NSCocoaErrorDomain) - XCTAssertEqual(nserror.code, 3840) - } - } - - func testURLSuccess() { - let object = ["foo": 1, "bar": 2, "baz": 3] - let builder = RequestBodyBuilder.URL(encoding: NSUTF8StringEncoding) - - do { - let (contentTypeHeader, data) = try builder.buildBodyFromObject(object) - let dictionary = try URLEncodedSerialization.objectFromData(data, encoding: NSUTF8StringEncoding) - XCTAssertEqual(contentTypeHeader, "application/x-www-form-urlencoded") - XCTAssertEqual(dictionary["foo"], "1") - XCTAssertEqual(dictionary["bar"], "2") - XCTAssertEqual(dictionary["baz"], "3") - } catch { - XCTFail() - } - } - - func testMultipartFormDataSuccess() { - let value1 = "1".dataUsingEncoding(NSUTF8StringEncoding)! - let value2 = "2".dataUsingEncoding(NSUTF8StringEncoding)! - let object: [String : AnyObject] = ["foo": value1, "bar": value2] - let builder = RequestBodyBuilder.MultipartFormData - - do { - let (contentTypeHeader, data) = try builder.buildBodyFromObject(object) - let encodedData = String(data: data, encoding:NSUTF8StringEncoding)! - let returnCode = "\r\n" - // Boundary changed each time - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let match = regexp.matchesInString(contentTypeHeader, options: [], range: NSMakeRange(0, (contentTypeHeader as NSString).length)) - XCTAssertTrue(match.count > 0) - let boundary = (contentTypeHeader as NSString).substringWithRange(match.first!.rangeAtIndex(1)) - - XCTAssertEqual(contentTypeHeader, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") - } catch { - XCTFail() - } - } - - func testCustomSuccess() { - let string = "foo" - let expectedData = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let builder = RequestBodyBuilder.Custom(contentTypeHeader: "foo") { object in - expectedData - } - - do { - let (contentTypeHeader, data) = try builder.buildBodyFromObject(string) - XCTAssertEqual(contentTypeHeader, "foo") - XCTAssertEqual(data, expectedData) - } catch { - XCTFail() - } - } - - func testCustomFailure() { - let string = "foo" - let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) - let builder = RequestBodyBuilder.Custom(contentTypeHeader: "") { object in - throw expectedError - } - - do { - try builder.buildBodyFromObject(string) - XCTFail() - } catch { - XCTAssertEqual((error as NSError), expectedError) - } - } -} diff --git a/Tests/APIKit/RequestTypeTests.swift b/Tests/APIKit/RequestTypeTests.swift index f8dde1a3..2b387ad7 100644 --- a/Tests/APIKit/RequestTypeTests.swift +++ b/Tests/APIKit/RequestTypeTests.swift @@ -1,563 +1,420 @@ import XCTest -import OHHTTPStubs import APIKit class RequestTypeTests: XCTestCase { - struct SearchRequest: MockSessionRequestType { - let query: String - - // MARK: RequestType - typealias Response = [String: AnyObject] - - var method: HTTPMethod { - return .GET - } - - var path: String { - return "/" - } - - var parameters: [String: AnyObject] { - return [ - "q": query, - "dummy": NSNull() - ] - } - - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { - return object as? [String: AnyObject] - } + func testJapanesesQueryParameters() { + let request = TestRequest(parameters: ["q": "こんにちは"]) + let URLRequest = try? request.buildURLRequest() + XCTAssertEqual(URLRequest?.URL?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") } - - struct JsonRpcRequest: MockSessionRequestType { - // MARK: RequestType - typealias Response = [String: AnyObject] - - var method: HTTPMethod { - return .POST - } - - var path: String { - return "/" - } - - var objectParameters: AnyObject { - return [ - ["id": "1"], - ["id": "2"], - [ - "hello", "yellow" - ] - ] - } - - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { - return object as? [String: AnyObject] - } + + func testSymbolQueryParameters() { + let request = TestRequest(parameters: ["q": "!\"#$%&'()0=~|`{}*+<>?/_"]) + let URLRequest = try? request.buildURLRequest() + XCTAssertEqual(URLRequest?.URL?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?/_") } - struct InvalidJsonRequest: MockSessionRequestType { - // MARK: RequestType - typealias Response = [String: AnyObject] - - var method: HTTPMethod { - return .POST - } - - var path: String { - return "/" - } + func testNullQueryParameters() { + let request = TestRequest(parameters: ["null": NSNull()]) + let URLRequest = try? request.buildURLRequest() + XCTAssertEqual(URLRequest?.URL?.query, "null") + } + + func testheaderFields() { + let request = TestRequest(headerFields: ["Foo": "f", "Accept": "a", "Content-Type": "c"]) + let URLReqeust = try? request.buildURLRequest() + XCTAssertEqual(URLReqeust?.valueForHTTPHeaderField("Foo"), "f") + XCTAssertEqual(URLReqeust?.valueForHTTPHeaderField("Accept"), "a") + XCTAssertEqual(URLReqeust?.valueForHTTPHeaderField("Content-Type"), "c") + } - /// - Note: JSON object should contain an array or an object - /// - SeeAlso: http://json.org - var objectParameters: AnyObject { - return "hello" - } + func testPOSTJSONRequest() { + let parameters: [AnyObject] = [ + ["id": "1"], + ["id": "2"], + ["hello", "yellow"] + ] - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { - return object as? [String: AnyObject] - } - } + let request = TestRequest(method: .POST, parameters: parameters) + XCTAssert(request.parameters?.count == 3) - // request type for URL building tests - struct ParameterizedRequest: RequestType { - typealias Response = Void - - init?(baseURL: String = "https://example.com", path: String = "/", method: HTTPMethod = .GET, parameters: [String: AnyObject] = [:], HTTPHeaderFields: [String: String] = [:], customizeURLRequest: NSMutableURLRequest throws -> NSMutableURLRequest = { $0 }) { - guard let baseURL = NSURL(string: baseURL) else { - return nil - } - - self.baseURL = baseURL - self.path = path - self.method = method - self.parameters = parameters - self.HTTPHeaderFields = HTTPHeaderFields - self.customizeURLRequest = customizeURLRequest - } - - let baseURL: NSURL - let method: HTTPMethod - let path: String - let parameters: [String: AnyObject] - let HTTPHeaderFields: [String: String] + let URLRequest = try? request.buildURLRequest() + XCTAssertNotNil(URLRequest?.HTTPBody) - let customizeURLRequest: NSMutableURLRequest throws -> NSMutableURLRequest + let json = URLRequest?.HTTPBody.flatMap { try? NSJSONSerialization.JSONObjectWithData($0, options: []) } as? [AnyObject] + XCTAssertEqual(json?.count, 3) + XCTAssertEqual(json?[0]["id"], "1") + XCTAssertEqual(json?[1]["id"], "2") - func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { - return try customizeURLRequest(URLRequest) - } - - func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? { - abort() - } + let array = json?[2] as? [String] + XCTAssertEqual(array?[0], "hello") + XCTAssertEqual(array?[1], "yellow") } - override func tearDown() { - OHHTTPStubs.removeAllStubs() - super.tearDown() + func testPOSTInvalidJSONRequest() { + let request = TestRequest(method: .POST, parameters: "foo") + let URLRequest = try? request.buildURLRequest() + XCTAssertNil(URLRequest?.HTTPBody) } - func testJapanesesURLQueryParameterEncoding() { - OHHTTPStubs.stubRequestsPassingTest({ request in - XCTAssertEqual(request.URL?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF&dummy") - return true - }, withStubResponse: { request in - return OHHTTPStubsResponse(data: NSData(), statusCode: 200, headers: nil) - }) - - let request = SearchRequest(query: "こんにちは") - let expectation = expectationWithDescription("waiting for the response.") - - Session.sendRequest(request) { result in - expectation.fulfill() - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testSymbolURLQueryParameterEncoding() { - OHHTTPStubs.stubRequestsPassingTest({ request in - XCTAssertEqual(request.URL?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?_&dummy") - return true - }, withStubResponse: { request in - return OHHTTPStubsResponse(data: NSData(), statusCode: 200, headers: nil) - }) - - let request = SearchRequest(query: "!\"#$%&'()0=~|`{}*+<>?_") - let expectation = expectationWithDescription("waiting for the response.") - - Session.sendRequest(request) { result in - expectation.fulfill() - } - - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testHTTPHeaderFields() { - let request = ParameterizedRequest(HTTPHeaderFields: ["Foo": "f", "Accept": "a", "Content-Type": "c"]) - let URLReqeust = request?.buildURLRequest().value - XCTAssertEqual(URLReqeust?.valueForHTTPHeaderField("Foo"), "f") - XCTAssertEqual(URLReqeust?.valueForHTTPHeaderField("Accept"), "a") - XCTAssertEqual(URLReqeust?.valueForHTTPHeaderField("Content-Type"), "c") - } - func testBuildURL() { // MARK: - baseURL = https://example.com XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "").absoluteURL, NSURL(string: "https://example.com") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/").absoluteURL, NSURL(string: "https://example.com/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "foo").absoluteURL, NSURL(string: "https://example.com/foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/foo?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/").absoluteURL, NSURL(string: "https://example.com/foo/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/foo/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "foo/bar").absoluteURL, NSURL(string: "https://example.com/foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/bar").absoluteURL, NSURL(string: "https://example.com/foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/bar", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/foo/bar?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/bar/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/bar/").absoluteURL, NSURL(string: "https://example.com/foo/bar/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/bar/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/foo/bar/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com", path: "/foo/bar//")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com", path: "/foo/bar//").absoluteURL, NSURL(string: "https://example.com/foo/bar//") ) // MARK: - baseURL = https://example.com/ XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "").absoluteURL, NSURL(string: "https://example.com/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/").absoluteURL, NSURL(string: "https://example.com//") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com//?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "foo").absoluteURL, NSURL(string: "https://example.com/foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo").absoluteURL, NSURL(string: "https://example.com//foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com//foo?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo/").absoluteURL, NSURL(string: "https://example.com//foo/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com//foo/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "foo/bar").absoluteURL, NSURL(string: "https://example.com/foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo/bar").absoluteURL, NSURL(string: "https://example.com//foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo/bar", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com//foo/bar?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo/bar/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo/bar/").absoluteURL, NSURL(string: "https://example.com//foo/bar/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "/foo/bar/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com//foo/bar/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/", path: "foo//bar//")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/", path: "foo//bar//").absoluteURL, NSURL(string: "https://example.com/foo//bar//") ) // MARK: - baseURL = https://example.com/api XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "").absoluteURL, NSURL(string: "https://example.com/api") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/").absoluteURL, NSURL(string: "https://example.com/api/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "foo").absoluteURL, NSURL(string: "https://example.com/api/foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo").absoluteURL, NSURL(string: "https://example.com/api/foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api/foo?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo/").absoluteURL, NSURL(string: "https://example.com/api/foo/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api/foo/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "foo/bar").absoluteURL, NSURL(string: "https://example.com/api/foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo/bar").absoluteURL, NSURL(string: "https://example.com/api/foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo/bar", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api/foo/bar?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo/bar/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo/bar/").absoluteURL, NSURL(string: "https://example.com/api/foo/bar/") ) - + XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "/foo/bar/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api/foo/bar/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api", path: "foo//bar//")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api", path: "foo//bar//").absoluteURL, NSURL(string: "https://example.com/api/foo//bar//") ) // MARK: - baseURL = https://example.com/api/ XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "").absoluteURL, NSURL(string: "https://example.com/api/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/").absoluteURL, NSURL(string: "https://example.com/api//") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api//?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "foo").absoluteURL, NSURL(string: "https://example.com/api/foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo").absoluteURL, NSURL(string: "https://example.com/api//foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api//foo?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo/").absoluteURL, NSURL(string: "https://example.com/api//foo/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api//foo/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "foo/bar").absoluteURL, NSURL(string: "https://example.com/api/foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar").absoluteURL, NSURL(string: "https://example.com/api//foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo/bar", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api//foo/bar?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo/bar/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar/").absoluteURL, NSURL(string: "https://example.com/api//foo/bar/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "/foo/bar/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com/api//foo/bar/?p=1") ) - + XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com/api/", path: "foo//bar//")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com/api/", path: "foo//bar//").absoluteURL, NSURL(string: "https://example.com/api/foo//bar//") ) // MARK: - baseURL = https://example.com/// XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "").absoluteURL, NSURL(string: "https://example.com///") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/").absoluteURL, NSURL(string: "https://example.com////") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com////?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "foo").absoluteURL, NSURL(string: "https://example.com///foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo").absoluteURL, NSURL(string: "https://example.com////foo") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com////foo?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo/").absoluteURL, NSURL(string: "https://example.com////foo/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com////foo/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "foo/bar").absoluteURL, NSURL(string: "https://example.com///foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo/bar")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo/bar").absoluteURL, NSURL(string: "https://example.com////foo/bar") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo/bar", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com////foo/bar?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo/bar/")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo/bar/").absoluteURL, NSURL(string: "https://example.com////foo/bar/") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "/foo/bar/", parameters: ["p": 1])?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, NSURL(string: "https://example.com////foo/bar/?p=1") ) XCTAssertEqual( - ParameterizedRequest(baseURL: "https://example.com///", path: "foo//bar//")?.buildURLRequest().value?.URL, + TestRequest(baseURL: "https://example.com///", path: "foo//bar//").absoluteURL, NSURL(string: "https://example.com///foo//bar//") ) } - func testJsonRpcRequest() { - let request = JsonRpcRequest() - XCTAssert(request.objectParameters.count == 3) - switch request.buildURLRequest() { - case .Success(let urlReq): - XCTAssert(urlReq.HTTPBody != nil) - let json = try! NSJSONSerialization.JSONObjectWithData(urlReq.HTTPBody!, options: NSJSONReadingOptions.AllowFragments) as! [AnyObject] - XCTAssert(json.count == 3) - XCTAssert(json[0]["id"]! == "1") - XCTAssert(json[1]["id"]! == "2") - let arr = json[2] as! [String] - XCTAssert(arr[0] == "hello") - XCTAssert(arr[1] == "yellow") - case .Failure: - XCTFail() - } - let expectation = expectationWithDescription("waiting for the response.") - Session.sendRequest(request) { result in - expectation.fulfill() - } - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testInvalidJsonRequest() { - let request = InvalidJsonRequest() - switch request.buildURLRequest() { - case .Success(let urlReq): - XCTAssert(urlReq.HTTPBody == nil) - case .Failure: - XCTFail() - } - let expectation = expectationWithDescription("waiting for the response.") - Session.sendRequest(request) { result in - expectation.fulfill() - } - waitForExpectationsWithTimeout(1.0, handler: nil) - } - - func testConfigureURLRequest() { + func testInterceptURLRequest() { let URL = NSURL(string: "https://example.com/customize")! - let request = ParameterizedRequest() { _ in + let request = TestRequest() { _ in return NSMutableURLRequest(URL: URL) } - XCTAssertEqual(request?.buildURLRequest().value?.URL, URL) + XCTAssertEqual((try? request.buildURLRequest())?.URL, URL) } } diff --git a/Tests/APIKit/ResponseBodyParserTests.swift b/Tests/APIKit/ResponseBodyParserTests.swift deleted file mode 100644 index a502dab3..00000000 --- a/Tests/APIKit/ResponseBodyParserTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation -import APIKit -import Result -import XCTest - -class ResponseBodyParserTests: XCTestCase { - func testJSONAcceptHeader() { - let parser = ResponseBodyParser.JSON(readingOptions: []) - XCTAssertEqual(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: []) - - do { - let object = try parser.parseData(data) - let dictionary = object as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { - XCTFail() - } - } - - func testJSONFailure() { - let string = "{\"foo\": 1, \"bar\": 2, \" 3}" - let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let parser = ResponseBodyParser.JSON(readingOptions: []) - - do { - try parser.parseData(data) - XCTFail() - } catch { - let nserror = error as NSError - XCTAssertEqual(nserror.domain, NSCocoaErrorDomain) - XCTAssertEqual(nserror.code, 3840) - } - } - - func testURLAcceptHeader() { - let parser = ResponseBodyParser.URL(encoding: NSUTF8StringEncoding) - XCTAssertEqual(parser.acceptHeader, "application/x-www-form-urlencoded") - } - - func testURLSuccess() { - let string = "foo=1&bar=2&baz=3" - let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let parser = ResponseBodyParser.URL(encoding: NSUTF8StringEncoding) - - do { - let object = try parser.parseData(data) - let dictionary = object as? [String: String] - XCTAssertEqual(dictionary?["foo"], "1") - XCTAssertEqual(dictionary?["bar"], "2") - XCTAssertEqual(dictionary?["baz"], "3") - } catch { - XCTFail() - } - } - - func testCustomAcceptHeader() { - let parser = ResponseBodyParser.Custom(acceptHeader: "foo") { data in - data - } - XCTAssertEqual(parser.acceptHeader, "foo") - } - - func testCustomSuccess() { - let data = NSData() - let parser = ResponseBodyParser.Custom(acceptHeader: "") { data in - ["foo": 1] - } - - do { - let object = try parser.parseData(data) - let dictionary = object as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - } catch { - XCTFail() - } - } - - func testCustomFailure() { - let expectedError = NSError(domain: "Foo", code: 1234, userInfo: nil) - let data = NSData() - let parser = ResponseBodyParser.Custom(acceptHeader: "") { data in - throw expectedError - } - - do { - try parser.parseData(data) - XCTFail() - } catch { - XCTAssertEqual((error as NSError), expectedError) - } - } -} diff --git a/Tests/APIKit/SessionAdapterType/NSURLSessionAdapterTests.swift b/Tests/APIKit/SessionAdapterType/NSURLSessionAdapterTests.swift new file mode 100644 index 00000000..791f7de9 --- /dev/null +++ b/Tests/APIKit/SessionAdapterType/NSURLSessionAdapterTests.swift @@ -0,0 +1,92 @@ +import Foundation +import APIKit +import XCTest +import OHHTTPStubs + +protocol MockSessionRequestType: RequestType { +} + +extension MockSessionRequestType { + var baseURL: NSURL { + return NSURL(string: "https://api.github.com")! + } +} + +class NSURLSessionAdapterTests: XCTestCase { + var session: Session! + + override func setUp() { + super.setUp() + + let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() + let adapter = NSURLSessionAdapter(configuration: configuration) + session = Session(adapter: adapter) + } + + override func tearDown() { + OHHTTPStubs.removeAllStubs() + super.tearDown() + } + + // MARK: - integration tests + func testSuccess() { + let dictionary = ["key": "value"] + let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) + + OHHTTPStubs.stubRequestsPassingTest({ request in + return true + }, withStubResponse: { request in + return OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) + }) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { response in + switch response { + case .Success(let dictionary): + XCTAssertEqual(dictionary["key"], "value") + + case .Failure: + XCTFail() + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(10.0, handler: nil) + } + + func testConnectionError() { + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil) + + OHHTTPStubs.stubRequestsPassingTest({ request in + return true + }, withStubResponse: { request in + return OHHTTPStubsResponse(error: error) + }) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { response in + switch response { + case .Success: + XCTFail() + + case .Failure(let error): + switch error { + case .ConnectionError(let error as NSError): + XCTAssertEqual(error.domain, NSURLErrorDomain) + + default: + XCTFail() + } + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(10.0, handler: nil) + } +} \ No newline at end of file diff --git a/Tests/APIKit/SessionCallbackQueueTests.swift b/Tests/APIKit/SessionCallbackQueueTests.swift new file mode 100644 index 00000000..7eb6ce31 --- /dev/null +++ b/Tests/APIKit/SessionCallbackQueueTests.swift @@ -0,0 +1,101 @@ +import Foundation +import APIKit +import XCTest +import OHHTTPStubs + +class SessionCallbackQueueTests: XCTestCase { + var adapter: TestSessionAdapter! + var session: Session! + + override func setUp() { + super.setUp() + + adapter = TestSessionAdapter() + adapter.data = try! NSJSONSerialization.dataWithJSONObject(["key": "value"], options: []) + + session = Session(adapter: adapter, callbackQueue: .Main) + } + + func testMain() { + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request, callbackQueue: .Main) { result in + XCTAssert(NSThread.isMainThread()) + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testSessionQueue() { + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request, callbackQueue: .SessionQueue) { result in + // This depends on implementation of TestSessionAdapter + XCTAssert(NSThread.isMainThread()) + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testOperationQueue() { + let operationQueue = NSOperationQueue() + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request, callbackQueue: .OperationQueue(operationQueue)) { result in + XCTAssertEqual(NSOperationQueue.currentQueue(), operationQueue) + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testDispatchQueue() { + let dispatchQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request, callbackQueue: .DispatchQueue(dispatchQueue)) { result in + // There is no way to test current dispatch queue. + XCTAssert(!NSThread.isMainThread()) + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + // MARK: Test Session.callbackQueue + func testImplicitSessionCallbackQueue() { + let operationQueue = NSOperationQueue() + let session = Session(adapter: adapter, callbackQueue: .OperationQueue(operationQueue)) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { result in + XCTAssertEqual(NSOperationQueue.currentQueue(), operationQueue) + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testExplicitSessionCallbackQueue() { + let operationQueue = NSOperationQueue() + let session = Session(adapter: adapter, callbackQueue: .OperationQueue(operationQueue)) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request, callbackQueue: nil) { result in + XCTAssertEqual(NSOperationQueue.currentQueue(), operationQueue) + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } +} diff --git a/Tests/APIKit/SessionTests.swift b/Tests/APIKit/SessionTests.swift new file mode 100644 index 00000000..3dc88190 --- /dev/null +++ b/Tests/APIKit/SessionTests.swift @@ -0,0 +1,132 @@ +import Foundation +import APIKit +import XCTest +import OHHTTPStubs + +class SessionTests: XCTestCase { + var adapter: TestSessionAdapter! + var session: Session! + + override func setUp() { + super.setUp() + + adapter = TestSessionAdapter() + session = Session(adapter: adapter) + } + + func testSuccess() { + let dictionary = ["key": "value"] + adapter.data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: []) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { response in + switch response { + case .Success(let dictionary): + XCTAssertEqual(dictionary["key"], "value") + + case .Failure: + XCTFail() + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + // MARK: Response error + func testParseDataError() { + adapter.data = "{\"broken\": \"json}".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { result in + if case .Failure(let error) = result, + case .ResponseError(let responseError as NSError) = error { + XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) + XCTAssertEqual(responseError.code, 3840) + } else { + XCTFail() + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testUnacceptableStatusCodeError() { + adapter.URLResponse = NSHTTPURLResponse(URL: NSURL(), statusCode: 400, HTTPVersion: nil, headerFields: nil) + + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { result in + if case .Failure(let error) = result, + case .ResponseError(let responseError as ResponseError) = error, + case .UnacceptableStatusCode(let statusCode) = responseError { + XCTAssertEqual(statusCode, 400) + } else { + XCTFail() + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + // MARK: Cancel + func testCancel() { + let expectation = expectationWithDescription("wait for response") + let request = TestRequest() + + session.sendRequest(request) { result in + if case .Failure(let error) = result, + case .ConnectionError(let connectionError as NSError) = error { + XCTAssertEqual(connectionError.code, 0) + } else { + XCTFail() + } + + expectation.fulfill() + } + + session.cancelRequest(TestRequest.self) + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testCancelFilter() { + let successExpectation = expectationWithDescription("wait for response") + let successRequest = TestRequest(path: "/success") + + session.sendRequest(successRequest) { result in + if case .Failure = result { + XCTFail() + } + + successExpectation.fulfill() + } + + let failureExpectation = expectationWithDescription("wait for response") + let failureRequest = TestRequest(path: "/failure") + + session.sendRequest(failureRequest) { result in + if case .Success = result { + XCTFail() + } + + failureExpectation.fulfill() + } + + session.cancelRequest(TestRequest.self) { request in + return request.path == failureRequest.path + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } +} diff --git a/Tests/APIKit/TestComponents/TestRequest.swift b/Tests/APIKit/TestComponents/TestRequest.swift new file mode 100644 index 00000000..a9e5ea31 --- /dev/null +++ b/Tests/APIKit/TestComponents/TestRequest.swift @@ -0,0 +1,36 @@ +import Foundation +import APIKit + +struct TestRequest: RequestType { + var absoluteURL: NSURL? { + let URLRequest = try? buildURLRequest() + return URLRequest?.URL + } + + // MARK: RequestType + typealias Response = AnyObject + + init(baseURL: String = "https://example.com", path: String = "/", method: HTTPMethod = .GET, parameters: AnyObject? = [:], headerFields: [String: String] = [:], interceptURLRequest: NSMutableURLRequest throws -> NSMutableURLRequest = { $0 }) { + self.baseURL = NSURL(string: baseURL)! + self.path = path + self.method = method + self.parameters = parameters + self.headerFields = headerFields + self.interceptURLRequest = interceptURLRequest + } + + let baseURL: NSURL + let method: HTTPMethod + let path: String + let parameters: AnyObject? + let headerFields: [String: String] + let interceptURLRequest: NSMutableURLRequest throws -> NSMutableURLRequest + + func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest { + return try interceptURLRequest(URLRequest) + } + + func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { + return object + } +} diff --git a/Tests/APIKit/TestComponents/TestSessionAdapter.swift b/Tests/APIKit/TestComponents/TestSessionAdapter.swift new file mode 100644 index 00000000..76b7b1c6 --- /dev/null +++ b/Tests/APIKit/TestComponents/TestSessionAdapter.swift @@ -0,0 +1,62 @@ +import Foundation +import APIKit + +class TestSessionAdapter: SessionAdapterType { + enum Error: ErrorType { + case Cancelled + } + + var data: NSData? + var URLResponse: NSURLResponse? + var error: ErrorType? + + private class Runner { + weak var adapter: TestSessionAdapter? + + @objc func run() { + adapter?.executeAllTasks() + } + } + + private var tasks = [TestSessionTask]() + private let runner: Runner + private let timer: NSTimer + + init(data: NSData? = NSData(), URLResponse: NSURLResponse? = NSHTTPURLResponse(URL: NSURL(), statusCode: 200, HTTPVersion: nil, headerFields: nil), error: NSError? = nil) { + self.data = data + self.URLResponse = URLResponse + self.error = error + + self.runner = Runner() + self.timer = NSTimer.scheduledTimerWithTimeInterval(0.001, + target: runner, + selector: #selector(Runner.run), + userInfo: nil, + repeats: true) + + self.runner.adapter = self + } + + func executeAllTasks() { + for task in tasks { + if task.cancelled { + task.handler(nil, nil, Error.Cancelled) + } else { + task.handler(data, URLResponse, error) + } + } + + tasks = [] + } + + func createTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) -> SessionTaskType { + let task = TestSessionTask(handler: handler) + tasks.append(task) + + return task + } + + func getTasksWithHandler(handler: [SessionTaskType] -> Void) { + handler(tasks.map { $0 }) + } +} diff --git a/Tests/APIKit/TestComponents/TestSessionTask.swift b/Tests/APIKit/TestComponents/TestSessionTask.swift new file mode 100644 index 00000000..15bf3add --- /dev/null +++ b/Tests/APIKit/TestComponents/TestSessionTask.swift @@ -0,0 +1,20 @@ +import Foundation +import APIKit + +class TestSessionTask: SessionTaskType { + + var handler: (NSData?, NSURLResponse?, ErrorType?) -> Void + var cancelled = false + + init(handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) { + self.handler = handler + } + + func resume() { + + } + + func cancel() { + cancelled = true + } +}