├── .github └── workflows │ └── ci.yml ├── .gitignore ├── APIKit.podspec ├── APIKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── APIKit.xcscheme ├── APIKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Configurations ├── APIKit.xcconfig ├── Base.xcconfig ├── Debug.xcconfig ├── Release.xcconfig └── Tests.xcconfig ├── Demo.playground ├── Contents.swift └── contents.xcplayground ├── Documentation ├── APIKit2MigrationGuide.md ├── APIKit3MigrationGuide.md ├── ConvenienceParametersAndActualParameters.md ├── CustomizingNetworkingBackend.md ├── DefiningRequestProtocolForWebService.md └── GettingStarted.md ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── APIKit │ ├── APIKit.h │ ├── BodyParameters │ ├── AbstractInputStream.m │ ├── BodyParameters.swift │ ├── Data+InputStream.swift │ ├── FormURLEncodedBodyParameters.swift │ ├── JSONBodyParameters.swift │ ├── MultipartFormDataBodyParameters.swift │ └── ProtobufBodyParameters.swift │ ├── CallbackQueue.swift │ ├── Combine │ └── Combine.swift │ ├── Concurrency │ └── Concurrency.swift │ ├── DataParser │ ├── DataParser.swift │ ├── FormURLEncodedDataParser.swift │ ├── JSONDataParser.swift │ ├── ProtobufDataParser.swift │ └── StringDataParser.swift │ ├── Error │ ├── RequestError.swift │ ├── ResponseError.swift │ └── SessionTaskError.swift │ ├── HTTPMethod.swift │ ├── Info.plist │ ├── Request.swift │ ├── Serializations │ └── URLEncodedSerialization.swift │ ├── Session.swift │ ├── SessionAdapter │ ├── SessionAdapter.swift │ └── URLSessionAdapter.swift │ └── Unavailable.swift ├── Tests └── APIKitTests │ ├── BodyParametersType │ ├── FormURLEncodedBodyParametersTests.swift │ ├── JSONBodyParametersTests.swift │ ├── MultipartFormDataParametersTests.swift │ ├── ProtobufBodyParametersTests.swift │ └── URLEncodedSerializationTests.swift │ ├── Combine │ └── CombineTests.swift │ ├── Concurrency │ └── ConcurrencyTests.swift │ ├── DataParserType │ ├── FormURLEncodedDataParserTests.swift │ ├── JSONDataParserTests.swift │ ├── ProtobufDataParserTests.swift │ └── StringDataParserTests.swift │ ├── Info.plist │ ├── RequestTests.swift │ ├── Resources │ └── test.json │ ├── SessionAdapterType │ ├── URLSessionAdapterSubclassTests.swift │ └── URLSessionAdapterTests.swift │ ├── SessionCallbackQueueTests.swift │ ├── SessionTests.swift │ └── TestComponents │ ├── HTTPStub.swift │ ├── TestRequest.swift │ ├── TestSessionAdapter.swift │ └── TestSessionTask.swift └── codecov.yml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | podspec: 10 | runs-on: macos-12 11 | name: CocoaPods Lint 12 | env: 13 | DEVELOPER_DIR: "/Applications/Xcode_13.4.1.app/Contents/Developer" 14 | steps: 15 | - uses: actions/checkout@v2 16 | - run: pod lib lint --allow-warnings 17 | 18 | xcode: 19 | name: ${{ matrix.xcode }} 20 | runs-on: ${{ matrix.runsOn }} 21 | env: 22 | DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | include: 27 | - xcode: "Xcode_14.0.1" 28 | runsOn: macOS-12 29 | name: "macOS 12, Xcode 14.0.1, Swift 5.7" 30 | - xcode: "Xcode_13.4.1" 31 | runsOn: macOS-12 32 | name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" 33 | - xcode: "Xcode_12.5.1" 34 | runsOn: macOS-11 35 | name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" 36 | - xcode: "Xcode_12" 37 | runsOn: macOS-10.15 38 | name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" 39 | steps: 40 | - uses: actions/checkout@v2 41 | with: 42 | fetch-depth: 2 43 | - name: ${{ matrix.name }} 44 | run: | 45 | set -o pipefail 46 | xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit | xcpretty -c 47 | xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator -destination "name=iPhone 8" | xcpretty -c 48 | xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator -destination "name=Apple TV" | xcpretty -c 49 | - name: Upload coverage to Codecov 50 | uses: codecov/codecov-action@v1.2.1 51 | if: ${{ success() }} 52 | 53 | swiftpm: 54 | name: SPM with ${{ matrix.xcode }} 55 | runs-on: ${{ matrix.runsOn }} 56 | env: 57 | DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | include: 62 | - xcode: "Xcode_14.0.1" 63 | runsOn: macOS-12 64 | name: "macOS 12, Xcode 14.0.1, Swift 5.7" 65 | action: swift test -c debug 66 | - xcode: "Xcode_13.4.1" 67 | runsOn: macOS-12 68 | name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" 69 | action: swift test -c debug 70 | - xcode: "Xcode_12.5.1" 71 | runsOn: macOS-11 72 | name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" 73 | action: swift test -c debug 74 | - xcode: "Xcode_12" 75 | runsOn: macOS-10.15 76 | name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" 77 | action: swift build -c debug 78 | steps: 79 | - uses: actions/checkout@v2 80 | - name: ${{ matrix.name }} 81 | run: ${{ matrix.action }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | Packages/ 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | # Pods/ 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 60 | 61 | fastlane/report.xml 62 | fastlane/screenshots 63 | -------------------------------------------------------------------------------- /APIKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "APIKit" 3 | s.version = "5.4.0" 4 | s.summary = "Type-safe networking abstraction layer that associates request type with response type." 5 | s.homepage = "https://github.com/ishkawa/APIKit" 6 | 7 | s.author = { 8 | "Yosuke Ishikawa" => "y@ishkawa.org" 9 | } 10 | 11 | s.ios.deployment_target = "9.0" 12 | s.osx.deployment_target = "10.10" 13 | if s.respond_to?(:watchos) 14 | s.watchos.deployment_target = "2.0" 15 | end 16 | if s.respond_to?(:tvos) 17 | s.tvos.deployment_target = "9.0" 18 | end 19 | 20 | s.source_files = "Sources/**/*.{swift,h,m}" 21 | s.source = { 22 | :git => "https://github.com/ishkawa/APIKit.git", 23 | :tag => "#{s.version}", 24 | } 25 | 26 | s.swift_version = "5.0" 27 | 28 | s.license = { 29 | :type => "MIT", 30 | :text => <<-LICENSE 31 | Copyright (c) 2015 - 2016 Yosuke Ishikawa 32 | 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: 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 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. 35 | LICENSE 36 | } 37 | end 38 | -------------------------------------------------------------------------------- /APIKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0969AE0E259DEC6D00C498AF /* Combine.swift */; }; 11 | 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0973EE34259E2DDC00879BA2 /* CombineTests.swift */; }; 12 | 7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */; }; 13 | 7F698E511D9D680C00F1561D /* JSONBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3D1D9D680C00F1561D /* JSONBodyParametersTests.swift */; }; 14 | 7F698E521D9D680C00F1561D /* MultipartFormDataParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3E1D9D680C00F1561D /* MultipartFormDataParametersTests.swift */; }; 15 | 7F698E531D9D680C00F1561D /* URLEncodedSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3F1D9D680C00F1561D /* URLEncodedSerializationTests.swift */; }; 16 | 7F698E541D9D680C00F1561D /* FormURLEncodedDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E411D9D680C00F1561D /* FormURLEncodedDataParserTests.swift */; }; 17 | 7F698E551D9D680C00F1561D /* JSONDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E421D9D680C00F1561D /* JSONDataParserTests.swift */; }; 18 | 7F698E561D9D680C00F1561D /* StringDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E431D9D680C00F1561D /* StringDataParserTests.swift */; }; 19 | 7F698E581D9D680C00F1561D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E451D9D680C00F1561D /* RequestTests.swift */; }; 20 | 7F698E591D9D680C00F1561D /* URLSessionAdapterSubclassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E471D9D680C00F1561D /* URLSessionAdapterSubclassTests.swift */; }; 21 | 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */; }; 22 | 7F698E5B1D9D680C00F1561D /* SessionCallbackQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */; }; 23 | 7F698E5C1D9D680C00F1561D /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4A1D9D680C00F1561D /* SessionTests.swift */; }; 24 | 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4D1D9D680C00F1561D /* TestRequest.swift */; }; 25 | 7F698E5F1D9D680C00F1561D /* TestSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */; }; 26 | 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */; }; 27 | 7F7048CD1D9D89BE003C99F6 /* APIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F7048C61D9D89BE003C99F6 /* APIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 28 | 7F7048CE1D9D89BE003C99F6 /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048C71D9D89BE003C99F6 /* CallbackQueue.swift */; }; 29 | 7F7048CF1D9D89BE003C99F6 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048C81D9D89BE003C99F6 /* HTTPMethod.swift */; }; 30 | 7F7048D11D9D89BE003C99F6 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048CA1D9D89BE003C99F6 /* Request.swift */; }; 31 | 7F7048D21D9D89BE003C99F6 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048CB1D9D89BE003C99F6 /* Session.swift */; }; 32 | 7F7048D31D9D89BE003C99F6 /* Unavailable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */; }; 33 | 7F7048D61D9D89F2003C99F6 /* SessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */; }; 34 | 7F7048D71D9D89F2003C99F6 /* URLSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048D51D9D89F2003C99F6 /* URLSessionAdapter.swift */; }; 35 | 7F7048DE1D9D89FB003C99F6 /* AbstractInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048D81D9D89FB003C99F6 /* AbstractInputStream.m */; }; 36 | 7F7048DF1D9D89FB003C99F6 /* BodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048D91D9D89FB003C99F6 /* BodyParameters.swift */; }; 37 | 7F7048E01D9D89FB003C99F6 /* Data+InputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048DA1D9D89FB003C99F6 /* Data+InputStream.swift */; }; 38 | 7F7048E11D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048DB1D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift */; }; 39 | 7F7048E21D9D89FB003C99F6 /* JSONBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048DC1D9D89FB003C99F6 /* JSONBodyParameters.swift */; }; 40 | 7F7048E31D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048DD1D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift */; }; 41 | 7F7048E81D9D8A08003C99F6 /* DataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048E41D9D8A08003C99F6 /* DataParser.swift */; }; 42 | 7F7048E91D9D8A08003C99F6 /* FormURLEncodedDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048E51D9D8A08003C99F6 /* FormURLEncodedDataParser.swift */; }; 43 | 7F7048EA1D9D8A08003C99F6 /* JSONDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048E61D9D8A08003C99F6 /* JSONDataParser.swift */; }; 44 | 7F7048EB1D9D8A08003C99F6 /* StringDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */; }; 45 | 7F7048EF1D9D8A12003C99F6 /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048EC1D9D8A12003C99F6 /* RequestError.swift */; }; 46 | 7F7048F01D9D8A12003C99F6 /* ResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048ED1D9D8A12003C99F6 /* ResponseError.swift */; }; 47 | 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048EE1D9D8A12003C99F6 /* SessionTaskError.swift */; }; 48 | 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */; }; 49 | 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */; }; 50 | C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5725F4A28D8C36500810D7C /* Concurrency.swift */; }; 51 | C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; }; 52 | C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; 53 | ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */; }; 54 | ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */; }; 55 | ECA8314C1DE4E677004EB1B5 /* ProtobufBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */; }; 56 | ECA8314E1DE4E739004EB1B5 /* ProtobufBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA8314D1DE4E739004EB1B5 /* ProtobufBodyParametersTests.swift */; }; 57 | /* End PBXBuildFile section */ 58 | 59 | /* Begin PBXContainerItemProxy section */ 60 | 7F4A73AC1CA839AE002554B1 /* PBXContainerItemProxy */ = { 61 | isa = PBXContainerItemProxy; 62 | containerPortal = 7F45FCD41A94D02C006863BB /* Project object */; 63 | proxyType = 1; 64 | remoteGlobalIDString = 141F12161C1C9ABE0026D415; 65 | remoteInfo = APIKit; 66 | }; 67 | /* End PBXContainerItemProxy section */ 68 | 69 | /* Begin PBXCopyFilesBuildPhase section */ 70 | 141F12351C1C9AC70026D415 /* Copy Frameworks */ = { 71 | isa = PBXCopyFilesBuildPhase; 72 | buildActionMask = 2147483647; 73 | dstPath = ""; 74 | dstSubfolderSpec = 10; 75 | files = ( 76 | ); 77 | name = "Copy Frameworks"; 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | /* End PBXCopyFilesBuildPhase section */ 81 | 82 | /* Begin PBXFileReference section */ 83 | 0969AE0E259DEC6D00C498AF /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = ""; }; 84 | 0973EE34259E2DDC00879BA2 /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; 85 | 141F120F1C1C96820026D415 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Configurations/Base.xcconfig; sourceTree = ""; }; 86 | 141F12101C1C96820026D415 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configurations/Debug.xcconfig; sourceTree = ""; }; 87 | 141F12111C1C96820026D415 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = ""; }; 88 | 141F12261C1C9ABE0026D415 /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | 141F123C1C1C9AC70026D415 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = APIKit.xcconfig; path = Configurations/APIKit.xcconfig; sourceTree = ""; }; 91 | 141F12401C1C9EA30026D415 /* Tests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Tests.xcconfig; path = Configurations/Tests.xcconfig; sourceTree = ""; }; 92 | 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormURLEncodedBodyParametersTests.swift; sourceTree = ""; }; 93 | 7F698E3D1D9D680C00F1561D /* JSONBodyParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONBodyParametersTests.swift; sourceTree = ""; }; 94 | 7F698E3E1D9D680C00F1561D /* MultipartFormDataParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataParametersTests.swift; sourceTree = ""; }; 95 | 7F698E3F1D9D680C00F1561D /* URLEncodedSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerializationTests.swift; sourceTree = ""; }; 96 | 7F698E411D9D680C00F1561D /* FormURLEncodedDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormURLEncodedDataParserTests.swift; sourceTree = ""; }; 97 | 7F698E421D9D680C00F1561D /* JSONDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONDataParserTests.swift; sourceTree = ""; }; 98 | 7F698E431D9D680C00F1561D /* StringDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringDataParserTests.swift; sourceTree = ""; }; 99 | 7F698E441D9D680C00F1561D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 100 | 7F698E451D9D680C00F1561D /* RequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; 101 | 7F698E471D9D680C00F1561D /* URLSessionAdapterSubclassTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionAdapterSubclassTests.swift; sourceTree = ""; }; 102 | 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionAdapterTests.swift; sourceTree = ""; }; 103 | 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionCallbackQueueTests.swift; sourceTree = ""; }; 104 | 7F698E4A1D9D680C00F1561D /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; 105 | 7F698E4D1D9D680C00F1561D /* TestRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestRequest.swift; sourceTree = ""; }; 106 | 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionAdapter.swift; sourceTree = ""; }; 107 | 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionTask.swift; sourceTree = ""; }; 108 | 7F7048C61D9D89BE003C99F6 /* APIKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = APIKit.h; path = APIKit/APIKit.h; sourceTree = ""; }; 109 | 7F7048C71D9D89BE003C99F6 /* CallbackQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = APIKit/CallbackQueue.swift; sourceTree = ""; }; 110 | 7F7048C81D9D89BE003C99F6 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = APIKit/HTTPMethod.swift; sourceTree = ""; }; 111 | 7F7048C91D9D89BE003C99F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = APIKit/Info.plist; sourceTree = ""; }; 112 | 7F7048CA1D9D89BE003C99F6 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Request.swift; path = APIKit/Request.swift; sourceTree = ""; }; 113 | 7F7048CB1D9D89BE003C99F6 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = APIKit/Session.swift; sourceTree = ""; }; 114 | 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Unavailable.swift; path = APIKit/Unavailable.swift; sourceTree = ""; }; 115 | 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionAdapter.swift; path = Sources/APIKit/SessionAdapter/SessionAdapter.swift; sourceTree = SOURCE_ROOT; }; 116 | 7F7048D51D9D89F2003C99F6 /* URLSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLSessionAdapter.swift; path = Sources/APIKit/SessionAdapter/URLSessionAdapter.swift; sourceTree = SOURCE_ROOT; }; 117 | 7F7048D81D9D89FB003C99F6 /* AbstractInputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AbstractInputStream.m; path = Sources/APIKit/BodyParameters/AbstractInputStream.m; sourceTree = SOURCE_ROOT; }; 118 | 7F7048D91D9D89FB003C99F6 /* BodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BodyParameters.swift; path = Sources/APIKit/BodyParameters/BodyParameters.swift; sourceTree = SOURCE_ROOT; }; 119 | 7F7048DA1D9D89FB003C99F6 /* Data+InputStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Data+InputStream.swift"; path = "Sources/APIKit/BodyParameters/Data+InputStream.swift"; sourceTree = SOURCE_ROOT; }; 120 | 7F7048DB1D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FormURLEncodedBodyParameters.swift; path = Sources/APIKit/BodyParameters/FormURLEncodedBodyParameters.swift; sourceTree = SOURCE_ROOT; }; 121 | 7F7048DC1D9D89FB003C99F6 /* JSONBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JSONBodyParameters.swift; path = Sources/APIKit/BodyParameters/JSONBodyParameters.swift; sourceTree = SOURCE_ROOT; }; 122 | 7F7048DD1D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultipartFormDataBodyParameters.swift; path = Sources/APIKit/BodyParameters/MultipartFormDataBodyParameters.swift; sourceTree = SOURCE_ROOT; }; 123 | 7F7048E41D9D8A08003C99F6 /* DataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DataParser.swift; path = Sources/APIKit/DataParser/DataParser.swift; sourceTree = SOURCE_ROOT; }; 124 | 7F7048E51D9D8A08003C99F6 /* FormURLEncodedDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FormURLEncodedDataParser.swift; path = Sources/APIKit/DataParser/FormURLEncodedDataParser.swift; sourceTree = SOURCE_ROOT; }; 125 | 7F7048E61D9D8A08003C99F6 /* JSONDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JSONDataParser.swift; path = Sources/APIKit/DataParser/JSONDataParser.swift; sourceTree = SOURCE_ROOT; }; 126 | 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringDataParser.swift; path = Sources/APIKit/DataParser/StringDataParser.swift; sourceTree = SOURCE_ROOT; }; 127 | 7F7048EC1D9D8A12003C99F6 /* RequestError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RequestError.swift; path = Sources/APIKit/Error/RequestError.swift; sourceTree = SOURCE_ROOT; }; 128 | 7F7048ED1D9D8A12003C99F6 /* ResponseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ResponseError.swift; path = Sources/APIKit/Error/ResponseError.swift; sourceTree = SOURCE_ROOT; }; 129 | 7F7048EE1D9D8A12003C99F6 /* SessionTaskError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionTaskError.swift; path = Sources/APIKit/Error/SessionTaskError.swift; sourceTree = SOURCE_ROOT; }; 130 | 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLEncodedSerialization.swift; path = Sources/APIKit/Serializations/URLEncodedSerialization.swift; sourceTree = SOURCE_ROOT; }; 131 | 7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 132 | 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPStub.swift; sourceTree = ""; }; 133 | C5725F4A28D8C36500810D7C /* Concurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = ""; }; 134 | C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; 135 | C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; 136 | ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufDataParser.swift; path = Sources/APIKit/DataParser/ProtobufDataParser.swift; sourceTree = SOURCE_ROOT; }; 137 | ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtobufDataParserTests.swift; sourceTree = ""; }; 138 | ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufBodyParameters.swift; path = Sources/APIKit/BodyParameters/ProtobufBodyParameters.swift; sourceTree = SOURCE_ROOT; }; 139 | ECA8314D1DE4E739004EB1B5 /* ProtobufBodyParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtobufBodyParametersTests.swift; sourceTree = ""; }; 140 | /* End PBXFileReference section */ 141 | 142 | /* Begin PBXFrameworksBuildPhase section */ 143 | 141F121F1C1C9ABE0026D415 /* Frameworks */ = { 144 | isa = PBXFrameworksBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | 141F12301C1C9AC70026D415 /* Frameworks */ = { 151 | isa = PBXFrameworksBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXFrameworksBuildPhase section */ 158 | 159 | /* Begin PBXGroup section */ 160 | 0969AE0D259DEC3C00C498AF /* Combine */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 0969AE0E259DEC6D00C498AF /* Combine.swift */, 164 | ); 165 | name = Combine; 166 | path = APIKit/Combine; 167 | sourceTree = ""; 168 | }; 169 | 0973EE33259E2DD000879BA2 /* Combine */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 0973EE34259E2DDC00879BA2 /* CombineTests.swift */, 173 | ); 174 | path = Combine; 175 | sourceTree = ""; 176 | }; 177 | 141F120E1C1C96690026D415 /* Configurations */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 141F120F1C1C96820026D415 /* Base.xcconfig */, 181 | 141F12101C1C96820026D415 /* Debug.xcconfig */, 182 | 141F12111C1C96820026D415 /* Release.xcconfig */, 183 | 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */, 184 | 141F12401C1C9EA30026D415 /* Tests.xcconfig */, 185 | ); 186 | name = Configurations; 187 | sourceTree = ""; 188 | }; 189 | 7F09BF751C8AE64200F4A59A /* Tests */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 7F698E3A1D9D680C00F1561D /* APIKitTests */, 193 | ); 194 | path = Tests; 195 | sourceTree = ""; 196 | }; 197 | 7F18BD0D1C972C38003A31DF /* BodyParameters */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | 7F7048D91D9D89FB003C99F6 /* BodyParameters.swift */, 201 | 7F7048DA1D9D89FB003C99F6 /* Data+InputStream.swift */, 202 | 7F7048DB1D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift */, 203 | 7F7048DC1D9D89FB003C99F6 /* JSONBodyParameters.swift */, 204 | 7F7048DD1D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift */, 205 | ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */, 206 | 7F7048D81D9D89FB003C99F6 /* AbstractInputStream.m */, 207 | ); 208 | name = BodyParameters; 209 | path = APIKit/BodyParameters; 210 | sourceTree = ""; 211 | }; 212 | 7F18BD161C9730ED003A31DF /* Serializations */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */, 216 | ); 217 | name = Serializations; 218 | path = APIKit/Serializations; 219 | sourceTree = ""; 220 | }; 221 | 7F45FCD31A94D02C006863BB = { 222 | isa = PBXGroup; 223 | children = ( 224 | 7F8ECDFD1B6A799E00234E04 /* Demo.playground */, 225 | 7F7E8F091C8AD4B1008A13A9 /* Sources */, 226 | 7F09BF751C8AE64200F4A59A /* Tests */, 227 | 141F120E1C1C96690026D415 /* Configurations */, 228 | 7F45FCDE1A94D02C006863BB /* Products */, 229 | ); 230 | sourceTree = ""; 231 | }; 232 | 7F45FCDE1A94D02C006863BB /* Products */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | 141F12261C1C9ABE0026D415 /* APIKit.framework */, 236 | 141F123C1C1C9AC70026D415 /* APIKitTests.xctest */, 237 | ); 238 | name = Products; 239 | sourceTree = ""; 240 | }; 241 | 7F698E3A1D9D680C00F1561D /* APIKitTests */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | 7F698E451D9D680C00F1561D /* RequestTests.swift */, 245 | 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */, 246 | 7F698E4A1D9D680C00F1561D /* SessionTests.swift */, 247 | C5B144D628D8D7D000E30ECD /* Concurrency */, 248 | 0973EE33259E2DD000879BA2 /* Combine */, 249 | 7F698E3B1D9D680C00F1561D /* BodyParametersType */, 250 | 7F698E401D9D680C00F1561D /* DataParserType */, 251 | 7F698E461D9D680C00F1561D /* SessionAdapterType */, 252 | 7F698E4C1D9D680C00F1561D /* TestComponents */, 253 | C5FF1DBF28A80FFD0059573D /* Resources */, 254 | 7F698E611D9D681500F1561D /* Supporting Files */, 255 | ); 256 | path = APIKitTests; 257 | sourceTree = ""; 258 | }; 259 | 7F698E3B1D9D680C00F1561D /* BodyParametersType */ = { 260 | isa = PBXGroup; 261 | children = ( 262 | 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */, 263 | 7F698E3D1D9D680C00F1561D /* JSONBodyParametersTests.swift */, 264 | 7F698E3E1D9D680C00F1561D /* MultipartFormDataParametersTests.swift */, 265 | ECA8314D1DE4E739004EB1B5 /* ProtobufBodyParametersTests.swift */, 266 | 7F698E3F1D9D680C00F1561D /* URLEncodedSerializationTests.swift */, 267 | ); 268 | path = BodyParametersType; 269 | sourceTree = ""; 270 | }; 271 | 7F698E401D9D680C00F1561D /* DataParserType */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | 7F698E411D9D680C00F1561D /* FormURLEncodedDataParserTests.swift */, 275 | 7F698E421D9D680C00F1561D /* JSONDataParserTests.swift */, 276 | ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */, 277 | 7F698E431D9D680C00F1561D /* StringDataParserTests.swift */, 278 | ); 279 | path = DataParserType; 280 | sourceTree = ""; 281 | }; 282 | 7F698E461D9D680C00F1561D /* SessionAdapterType */ = { 283 | isa = PBXGroup; 284 | children = ( 285 | 7F698E471D9D680C00F1561D /* URLSessionAdapterSubclassTests.swift */, 286 | 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */, 287 | ); 288 | path = SessionAdapterType; 289 | sourceTree = ""; 290 | }; 291 | 7F698E4C1D9D680C00F1561D /* TestComponents */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */, 295 | 7F698E4D1D9D680C00F1561D /* TestRequest.swift */, 296 | 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */, 297 | 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */, 298 | ); 299 | path = TestComponents; 300 | sourceTree = ""; 301 | }; 302 | 7F698E611D9D681500F1561D /* Supporting Files */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | 7F698E441D9D680C00F1561D /* Info.plist */, 306 | ); 307 | name = "Supporting Files"; 308 | sourceTree = ""; 309 | }; 310 | 7F7E8F091C8AD4B1008A13A9 /* Sources */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | 7F7048C71D9D89BE003C99F6 /* CallbackQueue.swift */, 314 | 7F7048C81D9D89BE003C99F6 /* HTTPMethod.swift */, 315 | 7F7048CA1D9D89BE003C99F6 /* Request.swift */, 316 | 7F7048CB1D9D89BE003C99F6 /* Session.swift */, 317 | 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */, 318 | C5725F4928D8C36500810D7C /* Concurrency */, 319 | 0969AE0D259DEC3C00C498AF /* Combine */, 320 | 7F85FB8B1C9D317300CEE132 /* SessionAdapter */, 321 | 7F18BD0D1C972C38003A31DF /* BodyParameters */, 322 | 7FA19A441C9CC9A2005D25AE /* DataParser */, 323 | 7F18BD161C9730ED003A31DF /* Serializations */, 324 | 7FA19A3D1C9CBF2A005D25AE /* Error */, 325 | 7F7E8F1E1C8AD4E6008A13A9 /* Supporting Files */, 326 | ); 327 | path = Sources; 328 | sourceTree = ""; 329 | }; 330 | 7F7E8F1E1C8AD4E6008A13A9 /* Supporting Files */ = { 331 | isa = PBXGroup; 332 | children = ( 333 | 7F7048C61D9D89BE003C99F6 /* APIKit.h */, 334 | 7F7048C91D9D89BE003C99F6 /* Info.plist */, 335 | ); 336 | name = "Supporting Files"; 337 | sourceTree = ""; 338 | }; 339 | 7F85FB8B1C9D317300CEE132 /* SessionAdapter */ = { 340 | isa = PBXGroup; 341 | children = ( 342 | 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */, 343 | 7F7048D51D9D89F2003C99F6 /* URLSessionAdapter.swift */, 344 | ); 345 | name = SessionAdapter; 346 | path = APIKit/SessionAdapter; 347 | sourceTree = ""; 348 | }; 349 | 7FA19A3D1C9CBF2A005D25AE /* Error */ = { 350 | isa = PBXGroup; 351 | children = ( 352 | 7F7048EE1D9D8A12003C99F6 /* SessionTaskError.swift */, 353 | 7F7048EC1D9D8A12003C99F6 /* RequestError.swift */, 354 | 7F7048ED1D9D8A12003C99F6 /* ResponseError.swift */, 355 | ); 356 | name = Error; 357 | path = APIKit/Error; 358 | sourceTree = ""; 359 | }; 360 | 7FA19A441C9CC9A2005D25AE /* DataParser */ = { 361 | isa = PBXGroup; 362 | children = ( 363 | 7F7048E41D9D8A08003C99F6 /* DataParser.swift */, 364 | 7F7048E51D9D8A08003C99F6 /* FormURLEncodedDataParser.swift */, 365 | 7F7048E61D9D8A08003C99F6 /* JSONDataParser.swift */, 366 | ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */, 367 | 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */, 368 | ); 369 | name = DataParser; 370 | path = APIKit/DataParser; 371 | sourceTree = ""; 372 | }; 373 | C5725F4928D8C36500810D7C /* Concurrency */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | C5725F4A28D8C36500810D7C /* Concurrency.swift */, 377 | ); 378 | name = Concurrency; 379 | path = APIKit/Concurrency; 380 | sourceTree = ""; 381 | }; 382 | C5B144D628D8D7D000E30ECD /* Concurrency */ = { 383 | isa = PBXGroup; 384 | children = ( 385 | C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */, 386 | ); 387 | path = Concurrency; 388 | sourceTree = ""; 389 | }; 390 | C5FF1DBF28A80FFD0059573D /* Resources */ = { 391 | isa = PBXGroup; 392 | children = ( 393 | C5FF1DC028A80FFD0059573D /* test.json */, 394 | ); 395 | path = Resources; 396 | sourceTree = ""; 397 | }; 398 | /* End PBXGroup section */ 399 | 400 | /* Begin PBXHeadersBuildPhase section */ 401 | 141F12211C1C9ABE0026D415 /* Headers */ = { 402 | isa = PBXHeadersBuildPhase; 403 | buildActionMask = 2147483647; 404 | files = ( 405 | 7F7048CD1D9D89BE003C99F6 /* APIKit.h in Headers */, 406 | ); 407 | runOnlyForDeploymentPostprocessing = 0; 408 | }; 409 | /* End PBXHeadersBuildPhase section */ 410 | 411 | /* Begin PBXNativeTarget section */ 412 | 141F12161C1C9ABE0026D415 /* APIKit */ = { 413 | isa = PBXNativeTarget; 414 | buildConfigurationList = 141F12231C1C9ABE0026D415 /* Build configuration list for PBXNativeTarget "APIKit" */; 415 | buildPhases = ( 416 | 141F12171C1C9ABE0026D415 /* Sources */, 417 | 141F121F1C1C9ABE0026D415 /* Frameworks */, 418 | 141F12211C1C9ABE0026D415 /* Headers */, 419 | 141F12221C1C9ABE0026D415 /* Resources */, 420 | ); 421 | buildRules = ( 422 | ); 423 | dependencies = ( 424 | ); 425 | name = APIKit; 426 | productName = APIKit; 427 | productReference = 141F12261C1C9ABE0026D415 /* APIKit.framework */; 428 | productType = "com.apple.product-type.framework"; 429 | }; 430 | 141F12281C1C9AC70026D415 /* Tests */ = { 431 | isa = PBXNativeTarget; 432 | buildConfigurationList = 141F12391C1C9AC70026D415 /* Build configuration list for PBXNativeTarget "Tests" */; 433 | buildPhases = ( 434 | 141F122B1C1C9AC70026D415 /* Sources */, 435 | 141F12301C1C9AC70026D415 /* Frameworks */, 436 | 141F12341C1C9AC70026D415 /* Resources */, 437 | 141F12351C1C9AC70026D415 /* Copy Frameworks */, 438 | ); 439 | buildRules = ( 440 | ); 441 | dependencies = ( 442 | 7F4A73AD1CA839AE002554B1 /* PBXTargetDependency */, 443 | ); 444 | name = Tests; 445 | productName = APIKitTests; 446 | productReference = 141F123C1C1C9AC70026D415 /* APIKitTests.xctest */; 447 | productType = "com.apple.product-type.bundle.unit-test"; 448 | }; 449 | /* End PBXNativeTarget section */ 450 | 451 | /* Begin PBXProject section */ 452 | 7F45FCD41A94D02C006863BB /* Project object */ = { 453 | isa = PBXProject; 454 | attributes = { 455 | LastSwiftUpdateCheck = 0730; 456 | LastUpgradeCheck = 1020; 457 | ORGANIZATIONNAME = "Yosuke Ishikawa"; 458 | TargetAttributes = { 459 | 141F12161C1C9ABE0026D415 = { 460 | LastSwiftMigration = 0800; 461 | }; 462 | 141F12281C1C9AC70026D415 = { 463 | LastSwiftMigration = 0800; 464 | }; 465 | }; 466 | }; 467 | buildConfigurationList = 7F45FCD71A94D02C006863BB /* Build configuration list for PBXProject "APIKit" */; 468 | compatibilityVersion = "Xcode 3.2"; 469 | developmentRegion = en; 470 | hasScannedForEncodings = 0; 471 | knownRegions = ( 472 | en, 473 | Base, 474 | ); 475 | mainGroup = 7F45FCD31A94D02C006863BB; 476 | productRefGroup = 7F45FCDE1A94D02C006863BB /* Products */; 477 | projectDirPath = ""; 478 | projectRoot = ""; 479 | targets = ( 480 | 141F12161C1C9ABE0026D415 /* APIKit */, 481 | 141F12281C1C9AC70026D415 /* Tests */, 482 | ); 483 | }; 484 | /* End PBXProject section */ 485 | 486 | /* Begin PBXResourcesBuildPhase section */ 487 | 141F12221C1C9ABE0026D415 /* Resources */ = { 488 | isa = PBXResourcesBuildPhase; 489 | buildActionMask = 2147483647; 490 | files = ( 491 | ); 492 | runOnlyForDeploymentPostprocessing = 0; 493 | }; 494 | 141F12341C1C9AC70026D415 /* Resources */ = { 495 | isa = PBXResourcesBuildPhase; 496 | buildActionMask = 2147483647; 497 | files = ( 498 | C5FF1DC128A80FFD0059573D /* test.json in Resources */, 499 | ); 500 | runOnlyForDeploymentPostprocessing = 0; 501 | }; 502 | /* End PBXResourcesBuildPhase section */ 503 | 504 | /* Begin PBXSourcesBuildPhase section */ 505 | 141F12171C1C9ABE0026D415 /* Sources */ = { 506 | isa = PBXSourcesBuildPhase; 507 | buildActionMask = 2147483647; 508 | files = ( 509 | 7F7048D31D9D89BE003C99F6 /* Unavailable.swift in Sources */, 510 | 7F7048D11D9D89BE003C99F6 /* Request.swift in Sources */, 511 | 7F7048E81D9D8A08003C99F6 /* DataParser.swift in Sources */, 512 | 7F7048CE1D9D89BE003C99F6 /* CallbackQueue.swift in Sources */, 513 | 7F7048DE1D9D89FB003C99F6 /* AbstractInputStream.m in Sources */, 514 | 7F7048E31D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift in Sources */, 515 | 7F7048F01D9D8A12003C99F6 /* ResponseError.swift in Sources */, 516 | 7F7048EA1D9D8A08003C99F6 /* JSONDataParser.swift in Sources */, 517 | 7F7048D21D9D89BE003C99F6 /* Session.swift in Sources */, 518 | 7F7048E01D9D89FB003C99F6 /* Data+InputStream.swift in Sources */, 519 | 7F7048DF1D9D89FB003C99F6 /* BodyParameters.swift in Sources */, 520 | 7F7048E21D9D89FB003C99F6 /* JSONBodyParameters.swift in Sources */, 521 | C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */, 522 | 7F7048D61D9D89F2003C99F6 /* SessionAdapter.swift in Sources */, 523 | 7F7048EF1D9D8A12003C99F6 /* RequestError.swift in Sources */, 524 | 7F7048E91D9D8A08003C99F6 /* FormURLEncodedDataParser.swift in Sources */, 525 | ECA8314C1DE4E677004EB1B5 /* ProtobufBodyParameters.swift in Sources */, 526 | 7F7048E11D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift in Sources */, 527 | 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */, 528 | ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */, 529 | 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */, 530 | 7F7048D71D9D89F2003C99F6 /* URLSessionAdapter.swift in Sources */, 531 | 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */, 532 | 7F7048EB1D9D8A08003C99F6 /* StringDataParser.swift in Sources */, 533 | 7F7048CF1D9D89BE003C99F6 /* HTTPMethod.swift in Sources */, 534 | ); 535 | runOnlyForDeploymentPostprocessing = 0; 536 | }; 537 | 141F122B1C1C9AC70026D415 /* Sources */ = { 538 | isa = PBXSourcesBuildPhase; 539 | buildActionMask = 2147483647; 540 | files = ( 541 | 7F698E5F1D9D680C00F1561D /* TestSessionAdapter.swift in Sources */, 542 | 7F698E5C1D9D680C00F1561D /* SessionTests.swift in Sources */, 543 | 7F698E5B1D9D680C00F1561D /* SessionCallbackQueueTests.swift in Sources */, 544 | 7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */, 545 | 7F698E581D9D680C00F1561D /* RequestTests.swift in Sources */, 546 | ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */, 547 | 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */, 548 | C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */, 549 | 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, 550 | 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, 551 | 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, 552 | 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */, 553 | 7F698E561D9D680C00F1561D /* StringDataParserTests.swift in Sources */, 554 | 7F698E541D9D680C00F1561D /* FormURLEncodedDataParserTests.swift in Sources */, 555 | 7F698E591D9D680C00F1561D /* URLSessionAdapterSubclassTests.swift in Sources */, 556 | 7F698E551D9D680C00F1561D /* JSONDataParserTests.swift in Sources */, 557 | ECA8314E1DE4E739004EB1B5 /* ProtobufBodyParametersTests.swift in Sources */, 558 | 7F698E511D9D680C00F1561D /* JSONBodyParametersTests.swift in Sources */, 559 | 7F698E521D9D680C00F1561D /* MultipartFormDataParametersTests.swift in Sources */, 560 | 7F698E531D9D680C00F1561D /* URLEncodedSerializationTests.swift in Sources */, 561 | ); 562 | runOnlyForDeploymentPostprocessing = 0; 563 | }; 564 | /* End PBXSourcesBuildPhase section */ 565 | 566 | /* Begin PBXTargetDependency section */ 567 | 7F4A73AD1CA839AE002554B1 /* PBXTargetDependency */ = { 568 | isa = PBXTargetDependency; 569 | target = 141F12161C1C9ABE0026D415 /* APIKit */; 570 | targetProxy = 7F4A73AC1CA839AE002554B1 /* PBXContainerItemProxy */; 571 | }; 572 | /* End PBXTargetDependency section */ 573 | 574 | /* Begin XCBuildConfiguration section */ 575 | 141F12241C1C9ABE0026D415 /* Debug */ = { 576 | isa = XCBuildConfiguration; 577 | baseConfigurationReference = 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */; 578 | buildSettings = { 579 | CLANG_ENABLE_MODULES = YES; 580 | DEFINES_MODULE = YES; 581 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 582 | SWIFT_OBJC_BRIDGING_HEADER = ""; 583 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 584 | }; 585 | name = Debug; 586 | }; 587 | 141F12251C1C9ABE0026D415 /* Release */ = { 588 | isa = XCBuildConfiguration; 589 | baseConfigurationReference = 141F123F1C1C9EA30026D415 /* APIKit.xcconfig */; 590 | buildSettings = { 591 | CLANG_ENABLE_MODULES = YES; 592 | DEFINES_MODULE = YES; 593 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 594 | SWIFT_OBJC_BRIDGING_HEADER = ""; 595 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 596 | }; 597 | name = Release; 598 | }; 599 | 141F123A1C1C9AC70026D415 /* Debug */ = { 600 | isa = XCBuildConfiguration; 601 | baseConfigurationReference = 141F12401C1C9EA30026D415 /* Tests.xcconfig */; 602 | buildSettings = { 603 | INFOPLIST_FILE = Tests/APIKitTests/Info.plist; 604 | }; 605 | name = Debug; 606 | }; 607 | 141F123B1C1C9AC70026D415 /* Release */ = { 608 | isa = XCBuildConfiguration; 609 | baseConfigurationReference = 141F12401C1C9EA30026D415 /* Tests.xcconfig */; 610 | buildSettings = { 611 | INFOPLIST_FILE = Tests/APIKitTests/Info.plist; 612 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 613 | }; 614 | name = Release; 615 | }; 616 | 7F45FCF11A94D02C006863BB /* Debug */ = { 617 | isa = XCBuildConfiguration; 618 | baseConfigurationReference = 141F12101C1C96820026D415 /* Debug.xcconfig */; 619 | buildSettings = { 620 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 621 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 622 | CLANG_WARN_COMMA = YES; 623 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 624 | CLANG_WARN_INFINITE_RECURSION = YES; 625 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 626 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 627 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 628 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 629 | CLANG_WARN_STRICT_PROTOTYPES = YES; 630 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 631 | ENABLE_TESTABILITY = YES; 632 | GCC_NO_COMMON_BLOCKS = YES; 633 | ONLY_ACTIVE_ARCH = YES; 634 | SWIFT_VERSION = 5.0; 635 | }; 636 | name = Debug; 637 | }; 638 | 7F45FCF21A94D02C006863BB /* Release */ = { 639 | isa = XCBuildConfiguration; 640 | baseConfigurationReference = 141F12111C1C96820026D415 /* Release.xcconfig */; 641 | buildSettings = { 642 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 643 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 644 | CLANG_WARN_COMMA = YES; 645 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 646 | CLANG_WARN_INFINITE_RECURSION = YES; 647 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 648 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 649 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 650 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 651 | CLANG_WARN_STRICT_PROTOTYPES = YES; 652 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 653 | GCC_NO_COMMON_BLOCKS = YES; 654 | SWIFT_VERSION = 5.0; 655 | }; 656 | name = Release; 657 | }; 658 | /* End XCBuildConfiguration section */ 659 | 660 | /* Begin XCConfigurationList section */ 661 | 141F12231C1C9ABE0026D415 /* Build configuration list for PBXNativeTarget "APIKit" */ = { 662 | isa = XCConfigurationList; 663 | buildConfigurations = ( 664 | 141F12241C1C9ABE0026D415 /* Debug */, 665 | 141F12251C1C9ABE0026D415 /* Release */, 666 | ); 667 | defaultConfigurationIsVisible = 0; 668 | defaultConfigurationName = Release; 669 | }; 670 | 141F12391C1C9AC70026D415 /* Build configuration list for PBXNativeTarget "Tests" */ = { 671 | isa = XCConfigurationList; 672 | buildConfigurations = ( 673 | 141F123A1C1C9AC70026D415 /* Debug */, 674 | 141F123B1C1C9AC70026D415 /* Release */, 675 | ); 676 | defaultConfigurationIsVisible = 0; 677 | defaultConfigurationName = Release; 678 | }; 679 | 7F45FCD71A94D02C006863BB /* Build configuration list for PBXProject "APIKit" */ = { 680 | isa = XCConfigurationList; 681 | buildConfigurations = ( 682 | 7F45FCF11A94D02C006863BB /* Debug */, 683 | 7F45FCF21A94D02C006863BB /* Release */, 684 | ); 685 | defaultConfigurationIsVisible = 0; 686 | defaultConfigurationName = Release; 687 | }; 688 | /* End XCConfigurationList section */ 689 | }; 690 | rootObject = 7F45FCD41A94D02C006863BB /* Project object */; 691 | } 692 | -------------------------------------------------------------------------------- /APIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /APIKit.xcodeproj/xcshareddata/xcschemes/APIKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /APIKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /APIKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Configurations/APIKit.xcconfig: -------------------------------------------------------------------------------- 1 | COMBINE_HIDPI_IMAGES = YES 2 | DEFINES_MODULE = YES 3 | DYLIB_COMPATIBILITY_VERSION = 1 4 | DYLIB_CURRENT_VERSION = 1 5 | DYLIB_INSTALL_NAME_BASE = @rpath 6 | FRAMEWORK_VERSION = A 7 | INFOPLIST_FILE = Sources/APIKit/Info.plist 8 | PRODUCT_BUNDLE_IDENTIFIER = org.ishkawa.$(PRODUCT_NAME:rfc1034identifier) 9 | PRODUCT_NAME = $(PROJECT_NAME) 10 | SKIP_INSTALL = YES 11 | 12 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator appletvos appletvsimulator 13 | TARGETED_DEVICE_FAMILY = 1,2,3,4 14 | 15 | ENABLE_BITCODE[sdk=iphone*] = YES; 16 | ENABLE_BITCODE[sdk=watch*] = YES; 17 | ENABLE_BITCODE[sdk=appletv*] = YES; 18 | 19 | LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks 20 | LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 21 | LD_RUNPATH_SEARCH_PATHS[sdk=watch*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 22 | LD_RUNPATH_SEARCH_PATHS[sdk=appletv*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 23 | 24 | APPLICATION_EXTENSION_API_ONLY = YES; 25 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 26 | -------------------------------------------------------------------------------- /Configurations/Base.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_SEARCH_USER_PATHS = NO 2 | CLANG_CXX_LANGUAGE_STANDARD = gnu++0x 3 | CLANG_CXX_LIBRARY = libc++ 4 | CLANG_ENABLE_MODULES = YES 5 | CLANG_ENABLE_OBJC_ARC = YES 6 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 7 | CLANG_WARN_BOOL_CONVERSION = YES 8 | CLANG_WARN_CONSTANT_CONVERSION = YES 9 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 10 | CLANG_WARN_EMPTY_BODY = YES 11 | CLANG_WARN_ENUM_CONVERSION = YES 12 | CLANG_WARN_INT_CONVERSION = YES 13 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 14 | CLANG_WARN_UNREACHABLE_CODE = YES 15 | CURRENT_PROJECT_VERSION = 1 16 | ENABLE_STRICT_OBJC_MSGSEND = YES 17 | GCC_C_LANGUAGE_STANDARD = gnu99 18 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 19 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 20 | GCC_WARN_UNDECLARED_SELECTOR = YES 21 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 22 | GCC_WARN_UNUSED_FUNCTION = YES 23 | GCC_WARN_UNUSED_VARIABLE = YES 24 | VERSION_INFO_PREFIX = 25 | VERSIONING_SYSTEM = apple-generic 26 | 27 | CODE_SIGNING_REQUIRED = NO 28 | CODE_SIGN_IDENTITY = 29 | MACOSX_DEPLOYMENT_TARGET = 10.10 30 | IPHONEOS_DEPLOYMENT_TARGET = 9.0 31 | WATCHOS_DEPLOYMENT_TARGET = 2.0 32 | TVOS_DEPLOYMENT_TARGET = 9.0 33 | -------------------------------------------------------------------------------- /Configurations/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Base.xcconfig" 2 | 3 | COPY_PHASE_STRIP = NO 4 | ENABLE_TESTABILITY = YES 5 | GCC_DYNAMIC_NO_PIC = NO 6 | GCC_OPTIMIZATION_LEVEL = 0 7 | GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited) 8 | GCC_SYMBOLS_PRIVATE_EXTERN = NO 9 | MTL_ENABLE_DEBUG_INFO = YES 10 | ONLY_ACTIVE_ARCH = YES 11 | SWIFT_OPTIMIZATION_LEVEL = -Onone 12 | BITCODE_GENERATION_MODE = marker 13 | -------------------------------------------------------------------------------- /Configurations/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Base.xcconfig" 2 | 3 | COPY_PHASE_STRIP = YES 4 | ENABLE_NS_ASSERTIONS = NO 5 | MTL_ENABLE_DEBUG_INFO = NO 6 | VALIDATE_PRODUCT = YES 7 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 8 | BITCODE_GENERATION_MODE = bitcode 9 | -------------------------------------------------------------------------------- /Configurations/Tests.xcconfig: -------------------------------------------------------------------------------- 1 | COMBINE_HIDPI_IMAGES = YES 2 | INFOPLIST_FILE = Tests/APIKit/Info.plist 3 | PRODUCT_BUNDLE_IDENTIFIER = org.ishkawa.$(PRODUCT_NAME:rfc1034identifier) 4 | PRODUCT_NAME = $(PROJECT_NAME)Tests 5 | 6 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator 7 | 8 | LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks 9 | LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 10 | LD_RUNPATH_SEARCH_PATHS[sdk=appletv*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 11 | -------------------------------------------------------------------------------- /Demo.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import Foundation 3 | import APIKit 4 | 5 | PlaygroundPage.current.needsIndefiniteExecution = true 6 | 7 | //: Step 1: Define request protocol 8 | protocol GitHubRequest: Request { 9 | 10 | } 11 | 12 | extension GitHubRequest { 13 | var baseURL: URL { 14 | return URL(string: "https://api.github.com")! 15 | } 16 | } 17 | 18 | //: Step 2: Create model object 19 | struct RateLimit { 20 | let count: Int 21 | let resetDate: Date 22 | 23 | init?(dictionary: [String: AnyObject]) { 24 | guard let count = dictionary["rate"]?["limit"] as? Int else { 25 | return nil 26 | } 27 | 28 | guard let resetDateString = dictionary["rate"]?["reset"] as? TimeInterval else { 29 | return nil 30 | } 31 | 32 | self.count = count 33 | self.resetDate = Date(timeIntervalSince1970: resetDateString) 34 | } 35 | } 36 | 37 | //: Step 3: Define request type conforming to created request protocol 38 | // https://developer.github.com/v3/rate_limit/ 39 | struct GetRateLimitRequest: GitHubRequest { 40 | typealias Response = RateLimit 41 | 42 | var method: HTTPMethod { 43 | return .get 44 | } 45 | 46 | var path: String { 47 | return "/rate_limit" 48 | } 49 | 50 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { 51 | guard let dictionary = object as? [String: AnyObject], 52 | let rateLimit = RateLimit(dictionary: dictionary) else { 53 | throw ResponseError.unexpectedObject(object) 54 | } 55 | 56 | return rateLimit 57 | } 58 | } 59 | 60 | //: Step 4: Send request 61 | let request = GetRateLimitRequest() 62 | 63 | Session.send(request) { result in 64 | switch result { 65 | case .success(let rateLimit): 66 | print("count: \(rateLimit.count)") 67 | print("reset: \(rateLimit.resetDate)") 68 | 69 | case .failure(let error): 70 | print("error: \(error)") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Demo.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Documentation/APIKit2MigrationGuide.md: -------------------------------------------------------------------------------- 1 | # APIKit 2 Migration Guide 2 | 3 | APIKit 2.0 introduces several breaking changes to add functionality and to improve modeling of web API. 4 | 5 | - Abstraction of backend 6 | - Improved error handling modeling 7 | - Separation of convenience parameters and type-safe parameters 8 | 9 | ## Errors 10 | 11 | - [**Deleted**] `APIError` 12 | - [**Added**] `SessionTaskError` 13 | 14 | Errors cases of `Session.sendRequest(_:handler:)` is reduced to 3 cases listed below: 15 | 16 | ```swift 17 | public enum SessionTaskError: ErrorType { 18 | /// Error of networking backend such as `NSURLSession`. 19 | case ConnectionError(ErrorType) 20 | 21 | /// Error while creating `NSURLRequest` from `Request`. 22 | case RequestError(ErrorType) 23 | 24 | /// Error while creating `RequestType.Response` from `(NSData, NSURLResponse)`. 25 | case ResponseError(ErrorType) 26 | } 27 | ``` 28 | 29 | 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))`. 30 | 31 | ## RequestType 32 | 33 | ### Converting AnyObject to Response 34 | 35 | - [**Deleted**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response?` 36 | - [**Added**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response` 37 | 38 | ### Handling response errors 39 | 40 | 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. 41 | 42 | - [**Deleted**] `var acceptableStatusCodes: Set { get }` 43 | - [**Deleted**] `func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType?` 44 | - [**Added**] `func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject` 45 | 46 | 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. 47 | 48 | ```swift 49 | func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject { 50 | guard 200..<300 ~= URLResponse.statusCode else { 51 | // https://developer.github.com/v3/#client-errors 52 | throw GitHubError(object: object) 53 | } 54 | 55 | return object 56 | } 57 | ``` 58 | 59 | ### Parameters 60 | 61 | 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`. 62 | 63 | - [**Deleted**] `var parameters: [String: AnyObject]` 64 | - [**Deleted**] `var objectParameters: AnyObject` 65 | - [**Deleted**] `var requestBodyBuilder: RequestBodyBuilder` 66 | - [**Added**] `var parameters: AnyObject?` (convenience property) 67 | - [**Added**] `var bodyParameters: BodyParametersType?` (actual property) 68 | - [**Added**] `var queryParameters: [String: AnyObject]?` (actual property) 69 | 70 | Related types: 71 | 72 | - [**Deleted**] `enum RequestBodyBuilder` 73 | - [**Added**] `protocol BodyParametersType` 74 | 75 | APIKit provides 3 parameters types that conform to `BodyParametersType`: 76 | 77 | - [**Added**] `class JSONBodyParameters` 78 | - [**Added**] `class FormURLEncodedBodyParameters` 79 | - [**Added**] `class MultipartFormDataBodyParameters` 80 | 81 | ### HTTP Headers 82 | 83 | - [**Deleted**] `var HTTPHeaderFields: [String: String]` 84 | - [**Added**] `var headerFields: [String: String]` 85 | 86 | ### Data parsers 87 | 88 | - [**Deleted**] `var responseBodyParser: ResponseBodyParser` 89 | - [**Added**] `var dataParser: DataParserType` 90 | 91 | Related types: 92 | 93 | - [**Deleted**] `enum ResponseBodyParser` 94 | - [**Added**] `protocol DataParserType` 95 | - [**Added**] `class JSONDataParser` 96 | - [**Added**] `class FormURLEncodedDataParser` 97 | - [**Added**] `class StringDataParser` 98 | 99 | ### Configuring NSURLRequest 100 | 101 | `configureURLRequest()` in 1.x is renamed to `interceptURLRequest()` for the consistency with `interceptObject()`. 102 | 103 | - [**Deleted**] `func configureURLRequest(URLRequest: NSMutableURLRequest) -> NSMutableURLRequest` 104 | - [**Added**] `func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest` 105 | 106 | ## NSURLSession 107 | 108 | - [**Deleted**] `class URLSessionDelegate` 109 | - [**Added**] `protocol SessionTaskType` 110 | - [**Added**] `protocol SessionAdapterType` 111 | - [**Added**] `class NSURLSessionAdapter` 112 | -------------------------------------------------------------------------------- /Documentation/APIKit3MigrationGuide.md: -------------------------------------------------------------------------------- 1 | # APIKit 3 Migration Guide 2 | 3 | APIs of APIKit are redesigned to follow [Swift 3 API design guidelines](https://swift.org/documentation/api-design-guidelines/). This major version changes interface only, and all functionalities are same as APIKit 2. 4 | 5 | **NOTE:** Make sure that all old protocol methods are replaced by the new method. Especially, methods which has default implementation such as `interceptURLRequest(_:)` and `interceptObject(_:URLResponse:)`, because Swift compiler cannot warn that existing method is no longer a member of any protocol. To find this kind of old methods, search project with keyword `interceptURLRequest` and `interceptObject`. 6 | 7 | ## Name of protocols 8 | 9 | - [**Renamed**] `RequestType` → `Request` 10 | - [**Renamed**] `SessionAdapterType` → `SessionAdapter` 11 | - [**Renamed**] `SessionTaskType` → `SessionTask` 12 | - [**Renamed**] `BodyParametersType` → `BodyParameters` 13 | - [**Renamed**] `DataParserType` → `DataParser` 14 | 15 | ## Request 16 | 17 | - [**Renamed**] `interceptURLRequest(_:)` → `intercept(urlRequest:)` 18 | - [**Renamed**] `interceptObject(_:URLResponse:)` → `intercept(object:urlResponse:)` 19 | - [**Renamed**] `responseFromObject(_:URLResponse:)` → `response(from:urlResponse:)` 20 | 21 | ## Session 22 | 23 | - [**Renamed**] `sharedSession` → `shared` 24 | - [**Renamed**] `sendRequest(_:callbackQueue:handler:)` → `send(_:callbackQueue:handler:)` 25 | - [**Renamed**] `cancelRequest(_:passingTest:)` → `cancelRequests(with:passingTest:)` 26 | 27 | ## HTTPMethod 28 | 29 | - [**Renamed**] `GET` → `get` 30 | - [**Renamed**] `POST` → `post` 31 | - [**Renamed**] `PUT` → `put` 32 | - [**Renamed**] `HEAD` → `head` 33 | - [**Renamed**] `DELETE` → `delete` 34 | - [**Renamed**] `PATCH` → `patch` 35 | - [**Renamed**] `TRACE` → `trace` 36 | - [**Renamed**] `OPTIONS` → `options` 37 | - [**Renamed**] `CONNECT` → `connect` 38 | 39 | ## CallbackQueue 40 | 41 | - [**Renamed**] `Main` → `main` 42 | - [**Renamed**] `SessionQueue` → `sessionQueue` 43 | - [**Renamed**] `OperationQueue` → `operationQueue` 44 | - [**Renamed**] `DispatchQueue` → `dispatchQueue` 45 | 46 | ## SessionAdapter 47 | 48 | - [**Renamed**] `createTaskWithURLRequest(_:handler:)` → `createTask(with:handler:)` 49 | - [**Renamed**] `getTasksWithHandler(_:)` → `getTasks(with:)` 50 | 51 | ## DataParser 52 | 53 | - [**Renamed**] `parseData(_:)` → `parse(data:)` 54 | 55 | ## SessionTaskError 56 | 57 | - [**Renamed**] `ConnectionError` → `connectionError` 58 | - [**Renamed**] `RequestError` → `requestError` 59 | - [**Renamed**] `ResponseError` → `responseError` 60 | 61 | ## RequestError 62 | 63 | - [**Renamed**] `InvalidBaseURL` → `invalidBaseURL` 64 | - [**Renamed**] `UnexpectedURLRequest` → `unexpectedURLRequest` 65 | 66 | ## ResponseError 67 | 68 | - [**Renamed**] `NonHTTPURLResponse` → `nonHTTPURLResponse` 69 | - [**Renamed**] `UnacceptableStatusCode` → `unacceptableStatusCode` 70 | - [**Renamed**] `UnexpectedObject` → `unexpectedObject` 71 | -------------------------------------------------------------------------------- /Documentation/ConvenienceParametersAndActualParameters.md: -------------------------------------------------------------------------------- 1 | # Convenience Parameters and Actual Parameters 2 | 3 | To satisfy both ease and accuracy, `Request` 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 `Request`. 4 | 5 | 1. [Convenience parameters](#convenience-parameters) 6 | 2. [Actual parameters](#actual-parameters) 7 | 8 | ## Convenience parameters 9 | 10 | Most documentations of web APIs express parameters in dictionary-like notation: 11 | 12 | |Name |Type |Description | 13 | |-------|--------|-------------------------------------------------------------------------------------------------| 14 | |`q` |`string`|The search keywords, as well as any qualifiers. | 15 | |`sort` |`string`|The sort field. One of `stars`, `forks`, or `updated`. Default: results are sorted by best match.| 16 | |`order`|`string`|The sort order if `sort` parameter is provided. One of `asc` or `desc`. Default: `desc` | 17 | 18 | `Request` has a property `var parameter: Any?` to express parameters in this kind of notation. That is the convenience parameters. 19 | 20 | ```swift 21 | struct SomeRequest: Request { 22 | ... 23 | 24 | var parameters: Any? { 25 | return [ 26 | "q": "Swift", 27 | "sort": "stars", 28 | "order": "desc", 29 | ] 30 | } 31 | } 32 | ``` 33 | 34 | `Request` provides default implementation of `parameters` `nil`. 35 | 36 | ```swift 37 | public extension Request { 38 | public var parameters: Any? { 39 | return nil 40 | } 41 | } 42 | ``` 43 | 44 | ## Actual parameters 45 | 46 | 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. `Request` has interface to express them, `var queryParameters: [String: Any]?` and `var bodyParameters: BodyParameters?`. Those are the actual parameters. 47 | 48 | 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: 49 | 50 | ```swift 51 | public extension Request { 52 | public var queryParameters: [String: Any]? { 53 | guard let parameters = parameters as? [String: Any], method.prefersQueryParameters else { 54 | return nil 55 | } 56 | 57 | return parameters 58 | } 59 | 60 | public var bodyParameters: BodyParameters? { 61 | guard let parameters = parameters, !method.prefersQueryParameters else { 62 | return nil 63 | } 64 | 65 | return JSONBodyParameters(JSONObject: parameters) 66 | } 67 | } 68 | ``` 69 | 70 | If you implement actual parameters for the HTTP method, the convenience parameters will be ignored. 71 | 72 | ### BodyParameters 73 | 74 | 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 `BodyParameters`. 75 | 76 | `BodyParameters` defines 2 components, `contentType` and `buildEntity()`. You can create custom body parameters type that conforms to `BodyParameters`. 77 | 78 | ```swift 79 | public enum RequestBodyEntity { 80 | case data(Data) 81 | case inputStream(InputStream) 82 | } 83 | 84 | public protocol BodyParameters { 85 | var contentType: String { get } 86 | func buildEntity() throws -> RequestBodyEntity 87 | } 88 | ``` 89 | 90 | APIKit provides 3 body parameters type listed below: 91 | 92 | |Name |Parameters Type | 93 | |---------------------------------|----------------------------------------| 94 | |`JSONBodyParameters` |`Any` | 95 | |`FormURLEncodedBodyParameters` |`[String: Any]` | 96 | |`MultipartFormDataBodyParameters`|`[MultipartFormDataBodyParameters.Part]`| 97 | -------------------------------------------------------------------------------- /Documentation/CustomizingNetworkingBackend.md: -------------------------------------------------------------------------------- 1 | # Customizing Networking Backend 2 | 3 | APIKit uses `URLSession` as networking backend by default. Since `Session` has abstraction layer of backend called `SessionAdapter`, you can change the backend of `Session` like below: 4 | 5 | - Third party HTTP client like [Alamofire](https://github.com/Alamofire/Alamofire) 6 | - Mock backend like [`TestSessionAdapter`](../Tests/APIKitTests/TestComponents/TestSessionAdapter.swift) 7 | - `URLSession` with custom configuration and delegate 8 | 9 | Demo implementation of Alamofire adapter is available [here](https://github.com/ishkawa/APIKit-AlamofireAdapter). 10 | 11 | ## SessionAdapter 12 | 13 | `SessionAdapter` provides an interface to get `(Data?, URLResponse?, Error?)` from `URLRequest` and returns `SessionTask` for cancellation. 14 | 15 | ```swift 16 | public protocol SessionAdapter { 17 | func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask 18 | func getTasks(with handler: @escaping ([SessionTask]) -> Void) 19 | } 20 | 21 | public protocol SessionTask: class { 22 | func resume() 23 | func cancel() 24 | } 25 | ``` 26 | 27 | 28 | ## How Session works with SessionAdapter 29 | 30 | `Session` takes an instance of type that conforms `SessionAdapter` as a parameter of initializer. 31 | 32 | ```swift 33 | open class Session { 34 | public let adapter: SessionAdapter 35 | public let callbackQueue: CallbackQueue 36 | 37 | public init(adapter: SessionAdapter, callbackQueue: CallbackQueue = .main) { 38 | self.adapter = adapter 39 | self.callbackQueue = callbackQueue 40 | } 41 | 42 | ... 43 | } 44 | ``` 45 | 46 | Once it is initialized with a session adapter, it sends `URLRequest` and receives `(Data?, URLResponse?, Error?)` via the interfaces which are defined in `SessionAdapter`. 47 | 48 | ```swift 49 | open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { 50 | let urlRequest: URLRequest = ... 51 | let task = adapter.createTask(with: urlRequest) { data, urlResponse, error in 52 | ... 53 | } 54 | 55 | task.resume() 56 | 57 | return task 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /Documentation/DefiningRequestProtocolForWebService.md: -------------------------------------------------------------------------------- 1 | # Defining Request Protocol for Web Service 2 | 3 | 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. 4 | 5 | We define `GitHubRequest` to give common configuration for example. 6 | 7 | 1. [Giving default implementation to Request components](#giving-default-implementation-to-request-components) 8 | 2. [Throwing custom errors web API returns](#throwing-custom-errors-web-api-returns) 9 | 10 | ## Giving default implementation to Request components 11 | 12 | ### Base URL 13 | 14 | First of all, we give default implementation for `baseURL`. 15 | 16 | ```swift 17 | import APIKit 18 | 19 | protocol GitHubRequest: Request { 20 | 21 | } 22 | 23 | extension GitHubRequest { 24 | var baseURL: URL { 25 | return URL(string: "https://api.github.com")! 26 | } 27 | } 28 | ``` 29 | 30 | ### JSON Mapping 31 | 32 | 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 `Any` into JSON model type. If you adopt one of them, you can give default implementation to `response(from:urlResponse:)`. Here is an example of default implementation with Himotoki: 33 | 34 | ```swift 35 | import Himotoki 36 | 37 | extension GitHubRequest where Response: Decodable { 38 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { 39 | return try Response.decodeValue(object) 40 | } 41 | } 42 | ``` 43 | 44 | ### Defining request types 45 | 46 | Since `GitHubRequest` has default implementations of `baseURL` and `response(from:urlResponse:)`, all you have to implement to conform to `GitHubRequest` are 3 components, `Response`, `method` and `path`. 47 | 48 | ```swift 49 | import APIKit 50 | import Himotoki 51 | 52 | final class GitHubAPI { 53 | struct RateLimitRequest: GitHubRequest { 54 | typealias Response = RateLimit 55 | 56 | var method: HTTPMethod { 57 | return .get 58 | } 59 | 60 | var path: String { 61 | return "/rate_limit" 62 | } 63 | } 64 | 65 | struct SearchRepositoriesRequest: GitHubRequest { 66 | let query: String 67 | 68 | // MARK: Request 69 | typealias Response = SearchResponse 70 | 71 | var method: HTTPMethod { 72 | return .get 73 | } 74 | 75 | var path: String { 76 | return "/search/repositories" 77 | } 78 | 79 | var parameters: Any? { 80 | return ["q": query] 81 | } 82 | } 83 | } 84 | 85 | struct RateLimit: Decodable { 86 | let limit: Int 87 | let remaining: Int 88 | 89 | static func decode(_ e: Extractor) throws -> RateLimit { 90 | return try RateLimit( 91 | limit: e.value(["rate", "limit"]), 92 | remaining: e.value(["rate", "remaining"])) 93 | } 94 | } 95 | 96 | struct Repository: Decodable { 97 | let id: Int64 98 | let name: String 99 | 100 | static func decode(_ e: Extractor) throws -> Repository { 101 | return try Repository( 102 | id: e.value("id"), 103 | name: e.value("name")) 104 | } 105 | } 106 | 107 | struct SearchResponse: Decodable { 108 | let items: [Item] 109 | let totalCount: Int 110 | 111 | static func decode(_ e: Extractor) throws -> SearchResponse { 112 | return try SearchResponse( 113 | items: e.array("items"), 114 | totalCount: e.value("total_count")) 115 | } 116 | } 117 | ``` 118 | 119 | It is useful for code completion to nest request types in a utility class like `GitHubAPI` above. 120 | 121 | ## Throwing custom errors web API returns 122 | 123 | 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 `Request` 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:)`. 124 | 125 | Here is an example of handling [GitHub API errors](https://developer.github.com/v3/#client-errors): 126 | 127 | ```swift 128 | // https://developer.github.com/v3/#client-errors 129 | struct GitHubError: Error { 130 | let message: String 131 | 132 | init(object: Any) { 133 | let dictionary = object as? [String: Any] 134 | message = dictionary?["message"] as? String ?? "Unknown error occurred" 135 | } 136 | } 137 | 138 | extension GitHubRequest { 139 | func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any { 140 | guard 200..<300 ~= urlResponse.statusCode else { 141 | throw GitHubError(object: object) 142 | } 143 | 144 | return object 145 | } 146 | } 147 | ``` 148 | 149 | The custom error you throw in `intercept(object:urlResponse:)` can be retrieved from call-site as `.failure(.responseError(GitHubError))`. 150 | 151 | ```swift 152 | let request = GitHubAPI.SearchRepositoriesRequest(query: "swift") 153 | 154 | Session.send(request) { result in 155 | switch result { 156 | case .success(let response): 157 | print(response) 158 | 159 | case .failure(let error): 160 | self.printError(error) 161 | } 162 | } 163 | 164 | func printError(_ error: SessionTaskError) { 165 | switch error { 166 | case .responseError(let error as GitHubError): 167 | print(error.message) // Prints message from GitHub API 168 | 169 | case .connectionError(let error): 170 | print("Connection error: \(error)") 171 | 172 | default: 173 | print("System error :bow:") 174 | } 175 | } 176 | ``` 177 | -------------------------------------------------------------------------------- /Documentation/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | 1. [Library overview](#library-overview) 4 | 2. [Defining request type](#defining-request-type) 5 | 3. [Sending request](#sending-request) 6 | 4. [Canceling request](#canceling-request) 7 | 8 | ## Library overview 9 | 10 | The main units of APIKit are `Request` protocol and `Session` class. `Request` has properties that represent components of HTTP/HTTPS request. `Session` receives an instance of a type that conforms to `Request`, 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. 11 | 12 | ```swift 13 | // SearchRepositoriesRequest conforms to Request protocol. 14 | let request = SearchRepositoriesRequest(query: "swift") 15 | 16 | // Session receives an instance of a type that conforms to Request. 17 | Session.send(request) { result in 18 | switch result { 19 | case .success(let response): 20 | // Type of `response` is `[Repository]`, 21 | // which is inferred from `SearchRepositoriesRequest`. 22 | print(response) 23 | 24 | case .failure(let error): 25 | self.printError(error) 26 | } 27 | } 28 | ``` 29 | 30 | ## Defining request type 31 | 32 | `Request` defines several properties and methods. Since many of them have default implementation, components which is necessary for conforming to `Request` are following 5 components: 33 | 34 | - `typealias Response` 35 | - `var baseURL: URL` 36 | - `var method: HTTPMethod` 37 | - `var path: String` 38 | - `func response(from object: Any, urlResponse: HTTPURLResponse) throws -> RateLimit` 39 | 40 | ```swift 41 | struct RateLimitRequest: Request { 42 | typealias Response = RateLimit 43 | 44 | var baseURL: URL { 45 | return URL(string: "https://api.github.com")! 46 | } 47 | 48 | var method: HTTPMethod { 49 | return .get 50 | } 51 | 52 | var path: String { 53 | return "/rate_limit" 54 | } 55 | 56 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> RateLimit { 57 | return try RateLimit(object: object) 58 | } 59 | } 60 | 61 | struct RateLimit { 62 | let limit: Int 63 | let remaining: Int 64 | 65 | init(object: Any) throws { 66 | guard let dictionary = object as? [String: Any], 67 | let rateDictionary = dictionary["rate"] as? [String: Any], 68 | let limit = rateDictionary["limit"] as? Int, 69 | let remaining = rateDictionary["remaining"] as? Int else { 70 | throw ResponseError.unexpectedObject(object) 71 | } 72 | 73 | self.limit = limit 74 | self.remaining = remaining 75 | } 76 | } 77 | ``` 78 | 79 | ## Sending request 80 | 81 | `Session.send(_:handler:)` is a method to send a request that conforms to `Request`. 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.send(_:handler:)` which conforms to `Request` protocol. 82 | 83 | For example, when `Session.send(_:handler:)` receives `RateLimitRequest` as a type parameter `Request`, the result type will be `Result`. 84 | 85 | ```swift 86 | let request = RateLimitRequest() 87 | 88 | Session.send(request) { result in 89 | switch result { 90 | case .success(let rateLimit): 91 | // Type of `rateLimit` is inferred as `RateLimit`, 92 | // which is also known as `RateLimitRequest.Response`. 93 | print("limit: \(rateLimit.limit)") 94 | print("remaining: \(rateLimit.remaining)") 95 | 96 | case .failure(let error): 97 | print("error: \(error)") 98 | } 99 | } 100 | ``` 101 | 102 | `SessionTaskError` is an error enumeration that has 3 cases: 103 | 104 | - `connectionError`: Error of networking backend stack. 105 | - `requestError`: Error while creating `URLRequest` from `Request`. 106 | - `responseError`: Error while creating `Request.Response` from `(Data, URLResponse)`. 107 | 108 | ## Canceling request 109 | 110 | `Session.cancelRequests(with:passingTest:)` also has a type parameter `Request` that conforms to `Request`. This method 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. 111 | 112 | For example, when `Session.cancelRequests(with:passingTest:)` 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. 113 | 114 | ```swift 115 | Session.cancelRequests(with: RateLimitRequest.self) { request in 116 | return true 117 | } 118 | ``` 119 | 120 | `Session.cancelRequests(with:passingTest:)` has default parameter for predicate closure, so you can omit the predicate closure like below: 121 | 122 | ```swift 123 | Session.cancelRequests(with: RateLimitRequest.self) 124 | ``` 125 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2015 - 2016 Yosuke Ishikawa** 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "APIKit", 6 | platforms: [ 7 | .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) 8 | ], 9 | products: [ 10 | .library(name: "APIKit", targets: ["APIKit"]), 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target( 15 | name: "APIKit", 16 | dependencies: [], 17 | exclude: ["BodyParameters/AbstractInputStream.m"] 18 | ), 19 | .testTarget( 20 | name: "APIKitTests", 21 | dependencies: ["APIKit"], 22 | resources: [.process("Resources")] 23 | ), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | APIKit 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/ishkawa/APIKit.svg?branch=master)](https://travis-ci.org/ishkawa/APIKit) 5 | [![codecov](https://codecov.io/gh/ishkawa/APIKit/branch/master/graph/badge.svg)](https://codecov.io/gh/ishkawa/APIKit) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Version](https://img.shields.io/cocoapods/v/APIKit.svg?style=flat)](http://cocoadocs.org/docsets/APIKit) 8 | [![Platform](https://img.shields.io/cocoapods/p/APIKit.svg?style=flat)](http://cocoadocs.org/docsets/APIKit) 9 | [![Swift Package Manager](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 10 | 11 | APIKit is a type-safe networking abstraction layer that associates request type with response type. 12 | 13 | ```swift 14 | // SearchRepositoriesRequest conforms to Request protocol. 15 | let request = SearchRepositoriesRequest(query: "swift") 16 | 17 | // Session receives an instance of a type that conforms to Request. 18 | Session.send(request) { result in 19 | switch result { 20 | case .success(let response): 21 | // Type of `response` is `[Repository]`, 22 | // which is inferred from `SearchRepositoriesRequest`. 23 | print(response) 24 | 25 | case .failure(let error): 26 | self.printError(error) 27 | } 28 | } 29 | ``` 30 | 31 | ## Requirements 32 | 33 | - Swift 5.3 or later 34 | - iOS 9.0 or later 35 | - Mac OS 10.10 or later 36 | - watchOS 2.0 or later 37 | - tvOS 9.0 or later 38 | 39 | If you use Swift 2.2 or 2.3, try [APIKit 2.0.5](https://github.com/ishkawa/APIKit/tree/2.0.5). 40 | 41 | If you use Swift 4.2 or before, try [APIKit 4.1.0](https://github.com/ishkawa/APIKit/tree/4.1.0). 42 | 43 | If you use Swift 5.2 or before, try [APIKit 5.3.0](https://github.com/ishkawa/APIKit/tree/5.3.0). 44 | 45 | ## Installation 46 | 47 | #### [Carthage](https://github.com/Carthage/Carthage) 48 | 49 | - Insert `github "ishkawa/APIKit" ~> 5.0` to your Cartfile. 50 | - Run `carthage update`. 51 | - Link your app with `APIKit.framework` in `Carthage/Build`. 52 | 53 | #### [CocoaPods](https://github.com/cocoapods/cocoapods) 54 | 55 | - Insert `pod 'APIKit', '~> 5.0'` to your Podfile. 56 | - Run `pod install`. 57 | 58 | Note: CocoaPods 1.4.0 is required to install APIKit 5. 59 | 60 | ## Documentation 61 | 62 | - [Getting started](Documentation/GettingStarted.md) 63 | - [Defining Request Protocol for Web Service](Documentation/DefiningRequestProtocolForWebService.md) 64 | - [Convenience Parameters and Actual Parameters](Documentation/ConvenienceParametersAndActualParameters.md) 65 | 66 | ### Advanced Guides 67 | 68 | - [Customizing Networking Backend](Documentation/CustomizingNetworkingBackend.md) 69 | 70 | ### Migration Guides 71 | 72 | - [APIKit 3 Migration Guide](Documentation/APIKit3MigrationGuide.md) 73 | - [APIKit 2 Migration Guide](Documentation/APIKit2MigrationGuide.md) 74 | -------------------------------------------------------------------------------- /Sources/APIKit/APIKit.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double APIKitVersionNumber; 4 | FOUNDATION_EXPORT const unsigned char APIKitVersionString[]; 5 | 6 | @interface AbstractInputStream : NSInputStream 7 | 8 | // Workaround for http://www.openradar.me/19809067 9 | // This issue only occurs on iOS 8 10 | - (instancetype)init; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/AbstractInputStream.m: -------------------------------------------------------------------------------- 1 | #import "APIKit.h" 2 | 3 | @implementation AbstractInputStream 4 | 5 | - (instancetype)init 6 | { 7 | return [super init]; 8 | } 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/BodyParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `RequestBodyEntity` represents entity of HTTP body. 4 | public enum RequestBodyEntity { 5 | /// Expresses entity as `Data`. The associated value will be set to `URLRequest.httpBody`. 6 | case data(Data) 7 | 8 | /// Expresses entity as `InputStream`. The associated value will be set to `URLRequest.httpBodyStream`. 9 | case inputStream(InputStream) 10 | } 11 | 12 | /// `BodyParameters` provides interface to parse HTTP response body and to state `Content-Type` to accept. 13 | public protocol BodyParameters { 14 | /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. 15 | var contentType: String { get } 16 | 17 | /// Builds `RequestBodyEntity`. 18 | /// Throws: `Error` 19 | func buildEntity() throws -> RequestBodyEntity 20 | } 21 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/Data+InputStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum InputStreamError: Error { 4 | case invalidDataCapacity(Int) 5 | case unreadableStream(InputStream) 6 | } 7 | 8 | extension Data { 9 | init(inputStream: InputStream, capacity: Int = Int(UInt16.max)) throws { 10 | var data = Data(capacity: capacity) 11 | 12 | let bufferSize = Swift.min(Int(UInt16.max), capacity) 13 | let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) 14 | 15 | var readSize: Int 16 | 17 | repeat { 18 | readSize = inputStream.read(buffer, maxLength: bufferSize) 19 | 20 | switch readSize { 21 | case let x where x > 0: 22 | data.append(buffer, count: readSize) 23 | 24 | case let x where x < 0: 25 | throw InputStreamError.unreadableStream(inputStream) 26 | 27 | default: 28 | break 29 | } 30 | } while readSize > 0 31 | 32 | #if swift(>=4.1) 33 | buffer.deallocate() 34 | #else 35 | buffer.deallocate(capacity: bufferSize) 36 | #endif 37 | 38 | self.init(data) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/FormURLEncodedBodyParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `FormURLEncodedBodyParameters` serializes form object for HTTP body and states its content type is form. 4 | public struct FormURLEncodedBodyParameters: BodyParameters { 5 | /// The form object to be serialized. 6 | public let form: [String: Any] 7 | 8 | /// The string encoding of the serialized form. 9 | public let encoding: String.Encoding 10 | 11 | /// Returns `FormURLEncodedBodyParameters` that is initialized with form object and encoding. 12 | public init(formObject: [String: Any], encoding: String.Encoding = .utf8) { 13 | self.form = formObject 14 | self.encoding = encoding 15 | } 16 | 17 | // MARK: - BodyParameters 18 | 19 | /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. 20 | public var contentType: String { 21 | return "application/x-www-form-urlencoded" 22 | } 23 | 24 | /// Builds `RequestBodyEntity.data` that represents `form`. 25 | /// - Throws: `URLEncodedSerialization.Error` if `URLEncodedSerialization` fails to serialize form object. 26 | public func buildEntity() throws -> RequestBodyEntity { 27 | return .data(try URLEncodedSerialization.data(from: form, encoding: encoding)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/JSONBodyParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `JSONBodyParameters` serializes JSON object for HTTP body and states its content type is JSON. 4 | public struct JSONBodyParameters: BodyParameters { 5 | /// The JSON object to be serialized. 6 | public let JSONObject: Any 7 | 8 | /// The writing options for serialization. 9 | public let writingOptions: JSONSerialization.WritingOptions 10 | 11 | /// Returns `JSONBodyParameters` that is initialized with JSON object and writing options. 12 | public init(JSONObject: Any, writingOptions: JSONSerialization.WritingOptions = []) { 13 | self.JSONObject = JSONObject 14 | self.writingOptions = writingOptions 15 | } 16 | 17 | // MARK: - BodyParameters 18 | 19 | /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. 20 | public var contentType: String { 21 | return "application/json" 22 | } 23 | 24 | /// Builds `RequestBodyEntity.data` that represents `JSONObject`. 25 | /// - Throws: `NSError` if `JSONSerialization` fails to serialize `JSONObject`. 26 | public func buildEntity() throws -> RequestBodyEntity { 27 | // If isValidJSONObject(_:) is false, dataWithJSONObject(_:options:) throws NSException. 28 | guard JSONSerialization.isValidJSONObject(JSONObject) else { 29 | throw NSError(domain: NSCocoaErrorDomain, code: 3840, userInfo: nil) 30 | } 31 | 32 | return .data(try JSONSerialization.data(withJSONObject: JSONObject, options: writingOptions)) 33 | } 34 | } 35 | 36 | extension JSONBodyParameters: ExpressibleByDictionaryLiteral { 37 | public typealias Key = String 38 | public typealias Value = Any 39 | 40 | public init(dictionaryLiteral elements: (Key, Value)...) { 41 | self.init(JSONObject: Dictionary(uniqueKeysWithValues: elements)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/MultipartFormDataBodyParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(iOS) || os(watchOS) || os(tvOS) 4 | import MobileCoreServices 5 | #elseif os(OSX) 6 | import CoreServices 7 | #endif 8 | 9 | #if SWIFT_PACKAGE 10 | class AbstractInputStream: InputStream { 11 | init() { 12 | super.init(data: Data()) 13 | } 14 | } 15 | #endif 16 | 17 | /// `FormURLEncodedBodyParameters` serializes array of `Part` for HTTP body and states its content type is multipart/form-data. 18 | public struct MultipartFormDataBodyParameters: BodyParameters { 19 | /// `EntityType` represents whether the entity is expressed as `Data` or `InputStream`. 20 | public enum EntityType { 21 | /// Expresses the entity as `Data`, which has faster upload speed and larger memory usage. 22 | case data 23 | 24 | /// Expresses the entity as `InputStream`, which has smaller memory usage and slower upload speed. 25 | case inputStream 26 | } 27 | 28 | public let parts: [Part] 29 | public let boundary: String 30 | public let entityType: EntityType 31 | 32 | public init(parts: [Part], boundary: String = String(format: "%08x%08x", arc4random(), arc4random()), entityType: EntityType = .data) { 33 | self.parts = parts 34 | self.boundary = boundary 35 | self.entityType = entityType 36 | } 37 | 38 | // MARK: BodyParameters 39 | 40 | /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. 41 | public var contentType: String { 42 | return "multipart/form-data; boundary=\(boundary)" 43 | } 44 | 45 | /// Builds `RequestBodyEntity.data` that represents `form`. 46 | public func buildEntity() throws -> RequestBodyEntity { 47 | let inputStream = MultipartInputStream(parts: parts, boundary: boundary) 48 | 49 | switch entityType { 50 | case .inputStream: 51 | return .inputStream(inputStream) 52 | 53 | case .data: 54 | return .data(try Data(inputStream: inputStream)) 55 | } 56 | } 57 | } 58 | 59 | public extension MultipartFormDataBodyParameters { 60 | /// Part represents single part of multipart/form-data. 61 | struct Part { 62 | public enum Error: Swift.Error { 63 | case illegalValue(Any) 64 | case illegalFileURL(URL) 65 | case cannotGetFileSize(URL) 66 | } 67 | 68 | public let inputStream: InputStream 69 | public let name: String 70 | public let mimeType: String? 71 | public let fileName: String? 72 | public let count: Int 73 | 74 | /// Returns Part instance that has data presentation of passed value. 75 | /// `value` will be converted via `String(_:)` and serialized via `String.dataUsingEncoding(_:)`. 76 | /// If `mimeType` or `fileName` are `nil`, the fields will be omitted. 77 | public init(value: Any, name: String, mimeType: String? = nil, fileName: String? = nil, encoding: String.Encoding = .utf8) throws { 78 | guard let data = String(describing: value).data(using: encoding) else { 79 | throw Error.illegalValue(value) 80 | } 81 | 82 | self.inputStream = InputStream(data: data) 83 | self.name = name 84 | self.mimeType = mimeType 85 | self.fileName = fileName 86 | self.count = data.count 87 | } 88 | 89 | /// Returns Part instance that has input stream of specified data. 90 | /// If `mimeType` or `fileName` are `nil`, the fields will be omitted. 91 | public init(data: Data, name: String, mimeType: String? = nil, fileName: String? = nil) { 92 | self.inputStream = InputStream(data: data) 93 | self.name = name 94 | self.mimeType = mimeType 95 | self.fileName = fileName 96 | self.count = data.count 97 | } 98 | 99 | /// Returns Part instance that has input stream of specified file URL. 100 | /// If `mimeType` or `fileName` are `nil`, values for the fields will be detected from URL. 101 | public init(fileURL: URL, name: String, mimeType: String? = nil, fileName: String? = nil) throws { 102 | guard let inputStream = InputStream(url: fileURL) else { 103 | throw Error.illegalFileURL(fileURL) 104 | } 105 | 106 | let fileSize = (try? FileManager.default.attributesOfItem(atPath: fileURL.path)) 107 | .flatMap { $0[FileAttributeKey.size] as? NSNumber } 108 | .map { $0.intValue } 109 | 110 | guard let bodyLength = fileSize else { 111 | throw Error.cannotGetFileSize(fileURL) 112 | } 113 | 114 | let detectedMimeType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileURL.pathExtension as CFString, nil) 115 | .map { $0.takeRetainedValue() } 116 | .flatMap { UTTypeCopyPreferredTagWithClass($0, kUTTagClassMIMEType)?.takeRetainedValue() } 117 | .map { $0 as String } 118 | 119 | self.inputStream = inputStream 120 | self.name = name 121 | self.mimeType = mimeType ?? detectedMimeType ?? "application/octet-stream" 122 | self.fileName = fileName ?? fileURL.lastPathComponent 123 | self.count = bodyLength 124 | } 125 | } 126 | 127 | internal class PartInputStream: AbstractInputStream { 128 | let headerData: Data 129 | let footerData: Data 130 | let bodyPart: Part 131 | 132 | let totalLength: Int 133 | var totalSentLength: Int 134 | 135 | init(part: Part, boundary: String) { 136 | let header: String 137 | switch (part.mimeType, part.fileName) { 138 | case (let mimeType?, let fileName?): 139 | header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(fileName)\"\r\nContent-Type: \(mimeType)\r\n\r\n" 140 | 141 | case (let mimeType?, _): 142 | header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(part.name)\"; \r\nContent-Type: \(mimeType)\r\n\r\n" 143 | 144 | default: 145 | header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(part.name)\"\r\n\r\n" 146 | } 147 | 148 | headerData = header.data(using: .utf8)! 149 | footerData = "\r\n".data(using: .utf8)! 150 | bodyPart = part 151 | totalLength = headerData.count + bodyPart.count + footerData.count 152 | totalSentLength = 0 153 | 154 | super.init() 155 | } 156 | 157 | var headerRange: Range { 158 | return 0.. { 162 | return headerRange.upperBound..<(headerRange.upperBound + bodyPart.count) 163 | } 164 | 165 | var footerRange: Range { 166 | return bodyRange.upperBound..<(bodyRange.upperBound + footerData.count) 167 | } 168 | 169 | // MARK: InputStream 170 | override var hasBytesAvailable: Bool { 171 | return totalSentLength < totalLength 172 | } 173 | 174 | override func read(_ buffer: UnsafeMutablePointer, maxLength: Int) -> Int { 175 | var sentLength = 0 176 | 177 | while sentLength < maxLength && totalSentLength < totalLength { 178 | let offsetBuffer = buffer + sentLength 179 | let availableLength = maxLength - sentLength 180 | 181 | switch totalSentLength { 182 | case headerRange: 183 | let readLength = min(headerRange.upperBound - totalSentLength, availableLength) 184 | let readRange = NSRange(location: totalSentLength - headerRange.lowerBound, length: readLength) 185 | (headerData as NSData).getBytes(offsetBuffer, range: readRange) 186 | sentLength += readLength 187 | totalSentLength += sentLength 188 | 189 | case bodyRange: 190 | if bodyPart.inputStream.streamStatus == .notOpen { 191 | bodyPart.inputStream.open() 192 | } 193 | 194 | let readLength = bodyPart.inputStream.read(offsetBuffer, maxLength: availableLength) 195 | sentLength += readLength 196 | totalSentLength += readLength 197 | 198 | case footerRange: 199 | let readLength = min(footerRange.upperBound - totalSentLength, availableLength) 200 | let range = NSRange(location: totalSentLength - footerRange.lowerBound, length: readLength) 201 | (footerData as NSData).getBytes(offsetBuffer, range: range) 202 | sentLength += readLength 203 | totalSentLength += readLength 204 | 205 | default: 206 | print("Illegal range access: \(totalSentLength) is out of \(headerRange.lowerBound)..<\(footerRange.upperBound)") 207 | return -1 208 | } 209 | } 210 | 211 | return sentLength; 212 | } 213 | } 214 | 215 | internal class MultipartInputStream: AbstractInputStream { 216 | let boundary: String 217 | let partStreams: [PartInputStream] 218 | let footerData: Data 219 | 220 | let totalLength: Int 221 | var totalSentLength: Int 222 | 223 | private var privateStreamStatus = Stream.Status.notOpen 224 | 225 | init(parts: [Part], boundary: String) { 226 | self.boundary = boundary 227 | self.partStreams = parts.map { PartInputStream(part: $0, boundary: boundary) } 228 | self.footerData = "--\(boundary)--\r\n".data(using: .utf8)! 229 | self.totalLength = partStreams.reduce(footerData.count) { $0 + $1.totalLength } 230 | self.totalSentLength = 0 231 | super.init() 232 | } 233 | 234 | var partsRange: Range { 235 | return 0.. { 239 | return partsRange.upperBound..<(partsRange.upperBound + footerData.count) 240 | } 241 | 242 | var currentPartInputStream: PartInputStream? { 243 | var currentOffset = 0 244 | 245 | for partStream in partStreams { 246 | let partStreamRange = currentOffset..<(currentOffset + partStream.totalLength) 247 | if partStreamRange.contains(totalSentLength) { 248 | return partStream 249 | } 250 | 251 | currentOffset += partStream.totalLength 252 | } 253 | 254 | return nil 255 | } 256 | 257 | // MARK: InputStream 258 | // NOTE: InputStream does not have its own implementation because it is a class cluster. 259 | override var streamStatus: Stream.Status { 260 | return privateStreamStatus 261 | } 262 | 263 | override var hasBytesAvailable: Bool { 264 | return totalSentLength < totalLength 265 | } 266 | 267 | override func open() { 268 | privateStreamStatus = .open 269 | } 270 | 271 | override func close() { 272 | privateStreamStatus = .closed 273 | } 274 | 275 | override func read(_ buffer: UnsafeMutablePointer, maxLength: Int) -> Int { 276 | privateStreamStatus = .reading 277 | 278 | var sentLength = 0 279 | 280 | while sentLength < maxLength && totalSentLength < totalLength { 281 | let offsetBuffer = buffer + sentLength 282 | let availableLength = maxLength - sentLength 283 | 284 | switch totalSentLength { 285 | case partsRange: 286 | guard let partStream = currentPartInputStream else { 287 | print("Illegal offset \(totalLength) for part streams \(partsRange)") 288 | return -1 289 | } 290 | 291 | let readLength = partStream.read(offsetBuffer, maxLength: availableLength) 292 | sentLength += readLength 293 | totalSentLength += readLength 294 | 295 | case footerRange: 296 | let readLength = min(footerRange.upperBound - totalSentLength, availableLength) 297 | let range = NSRange(location: totalSentLength - footerRange.lowerBound, length: readLength) 298 | (footerData as NSData).getBytes(offsetBuffer, range: range) 299 | sentLength += readLength 300 | totalSentLength += readLength 301 | 302 | default: 303 | print("Illegal range access: \(totalSentLength) is out of \(partsRange.lowerBound)..<\(footerRange.upperBound)") 304 | return -1 305 | } 306 | 307 | if privateStreamStatus != .closed && !hasBytesAvailable { 308 | privateStreamStatus = .atEnd 309 | } 310 | } 311 | 312 | return sentLength 313 | } 314 | 315 | override var delegate: StreamDelegate? { 316 | get { return nil } 317 | set { } 318 | } 319 | 320 | override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) { 321 | 322 | } 323 | 324 | override func remove(from aRunLoop: RunLoop, forMode mode: RunLoop.Mode) { 325 | 326 | } 327 | } 328 | } 329 | 330 | #if !swift(>=4.2) 331 | extension RunLoop { 332 | internal typealias Mode = RunLoopMode 333 | } 334 | #endif 335 | -------------------------------------------------------------------------------- /Sources/APIKit/BodyParameters/ProtobufBodyParameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `ProtobufBodyParameters` serializes Protobuf object for HTTP body and states its content type is Protobuf. 4 | public struct ProtobufBodyParameters: BodyParameters { 5 | /// The Protobuf object to be serialized. 6 | public let protobufObject: Data 7 | 8 | /// Returns `ProtobufBodyParameters`. 9 | public init(protobufObject: Data) { 10 | self.protobufObject = protobufObject 11 | } 12 | 13 | // MARK: - BodyParameters 14 | 15 | /// `Content-Type` to send. The value for this property will be set to `Accept` HTTP header field. 16 | public var contentType: String { 17 | return "application/protobuf" 18 | } 19 | 20 | /// Builds `RequestBodyEntity.data` that represents `ProtobufObject`. 21 | public func buildEntity() throws -> RequestBodyEntity { 22 | return .data(protobufObject) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/APIKit/CallbackQueue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `CallbackQueue` represents queue where `handler` of `Session.send(_:handler:)` runs. 4 | public enum CallbackQueue { 5 | /// Dispatches callback closure on main queue asynchronously. 6 | case main 7 | 8 | /// Dispatches callback closure on the queue where backend adapter callback runs. 9 | case sessionQueue 10 | 11 | /// Dispatches callback closure on associated operation queue. 12 | case operationQueue(OperationQueue) 13 | 14 | /// Dispatches callback closure on associated dispatch queue. 15 | case dispatchQueue(DispatchQueue) 16 | 17 | public func execute(closure: @escaping () -> Void) { 18 | switch self { 19 | case .main: 20 | DispatchQueue.main.async { 21 | closure() 22 | } 23 | 24 | case .sessionQueue: 25 | closure() 26 | 27 | case .operationQueue(let operationQueue): 28 | operationQueue.addOperation { 29 | closure() 30 | } 31 | 32 | case .dispatchQueue(let dispatchQueue): 33 | dispatchQueue.async { 34 | closure() 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/APIKit/Combine/Combine.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Combine) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 7 | public struct SessionTaskPublisher: Publisher { 8 | /// The kind of values published by this publisher. 9 | public typealias Output = Request.Response 10 | 11 | /// The kind of errors this publisher might publish. 12 | public typealias Failure = SessionTaskError 13 | 14 | private let request: Request 15 | private let session: Session 16 | private let callbackQueue: CallbackQueue? 17 | 18 | public init(request: Request, session: Session, callbackQueue: CallbackQueue?) { 19 | self.request = request 20 | self.session = session 21 | self.callbackQueue = callbackQueue 22 | } 23 | 24 | public func receive(subscriber: S) where S: Subscriber, S.Failure == SessionTaskPublisher.Failure, S.Input == SessionTaskPublisher.Output { 25 | subscriber.receive(subscription: SessionTaskSubscription(request: request, 26 | session: session, 27 | callbackQueue: callbackQueue, 28 | downstream: subscriber)) 29 | } 30 | 31 | private final class SessionTaskSubscription: Subscription where Request.Response == Downstream.Input, Downstream.Failure == Failure { 32 | 33 | private let request: Request 34 | private let session: Session 35 | private let callbackQueue: CallbackQueue? 36 | private var downstream: Downstream? 37 | private var task: SessionTask? 38 | 39 | init(request: Request, session: Session, callbackQueue: CallbackQueue?, downstream: Downstream) { 40 | self.request = request 41 | self.session = session 42 | self.callbackQueue = callbackQueue 43 | self.downstream = downstream 44 | } 45 | 46 | func request(_ demand: Subscribers.Demand) { 47 | assert(demand > 0) 48 | guard let downstream = self.downstream else { return } 49 | self.downstream = nil 50 | task = session.send(request, callbackQueue: callbackQueue) { result in 51 | switch result { 52 | case .success(let response): 53 | _ = downstream.receive(response) 54 | downstream.receive(completion: .finished) 55 | case .failure(let error): 56 | downstream.receive(completion: .failure(error)) 57 | } 58 | } 59 | } 60 | 61 | func cancel() { 62 | task?.cancel() 63 | downstream = nil 64 | } 65 | } 66 | } 67 | 68 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 69 | public extension Session { 70 | /// Calls `sessionTaskPublisher(for:callbackQueue:)` of `Session.shared`. 71 | /// 72 | /// - parameter request: The request to be sent. 73 | /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. 74 | /// - returns: A publisher that wraps a session task for the request. 75 | static func sessionTaskPublisher(for request: Request, callbackQueue: CallbackQueue? = nil) -> SessionTaskPublisher { 76 | return SessionTaskPublisher(request: request, session: .shared, callbackQueue: callbackQueue) 77 | } 78 | 79 | /// Returns a publisher that wraps a session task for the request. 80 | /// 81 | /// The publisher publishes `Request.Response` when the task completes, or terminates if the task fails with an error. 82 | /// - parameter request: The request to be sent. 83 | /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. 84 | /// - returns: A publisher that wraps a session task for the request. 85 | func sessionTaskPublisher(for request: Request, callbackQueue: CallbackQueue? = nil) -> SessionTaskPublisher { 86 | return SessionTaskPublisher(request: request, session: self, callbackQueue: callbackQueue) 87 | } 88 | } 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/APIKit/Concurrency/Concurrency.swift: -------------------------------------------------------------------------------- 1 | #if compiler(>=5.5.2) && canImport(_Concurrency) 2 | 3 | import Foundation 4 | 5 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 6 | public extension Session { 7 | /// Calls `response(for:callbackQueue:)` of `Session.shared`. 8 | /// 9 | /// - parameter request: The request to be sent. 10 | /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. 11 | /// - returns: `Request.Response` 12 | static func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { 13 | return try await shared.response(for: request, callbackQueue: callbackQueue) 14 | } 15 | 16 | /// Convenience method to load `Request.Response` using an `Request`, creates and resumes an `SessionTask` internally. 17 | /// 18 | /// - parameter request: The request to be sent. 19 | /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. 20 | /// - returns: `Request.Response` 21 | func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { 22 | let cancellationHandler = SessionTaskCancellationHandler() 23 | return try await withTaskCancellationHandler(operation: { 24 | return try await withCheckedThrowingContinuation { continuation in 25 | Task { 26 | let sessionTask = createSessionTask(request, callbackQueue: callbackQueue) { result in 27 | continuation.resume(with: result) 28 | } 29 | await cancellationHandler.register(with: sessionTask) 30 | if await cancellationHandler.isTaskCancelled { 31 | sessionTask?.cancel() 32 | } else { 33 | sessionTask?.resume() 34 | } 35 | } 36 | } 37 | }, onCancel: { 38 | Task { await cancellationHandler.cancel() } 39 | }) 40 | } 41 | } 42 | 43 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 44 | private actor SessionTaskCancellationHandler { 45 | private var sessionTask: SessionTask? 46 | private(set) var isTaskCancelled = false 47 | 48 | func register(with task: SessionTask?) { 49 | guard !isTaskCancelled else { return } 50 | guard sessionTask == nil else { return } 51 | sessionTask = task 52 | } 53 | 54 | func cancel() { 55 | isTaskCancelled = true 56 | sessionTask?.cancel() 57 | } 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/APIKit/DataParser/DataParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `DataParser` protocol provides interface to parse HTTP response body and to state Content-Type to accept. 4 | public protocol DataParser { 5 | /// Value for `Accept` header field of HTTP request. 6 | var contentType: String? { get } 7 | 8 | /// Return `Any` that expresses structure of response such as JSON and XML. 9 | /// - Throws: `Error` when parser encountered invalid format data. 10 | func parse(data: Data) throws -> Any 11 | } 12 | -------------------------------------------------------------------------------- /Sources/APIKit/DataParser/FormURLEncodedDataParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `FormURLEncodedDataParser` parses form URL encoded response data. 4 | public class FormURLEncodedDataParser: DataParser { 5 | public enum Error: Swift.Error { 6 | case cannotGetStringFromData(Data) 7 | } 8 | 9 | /// The string encoding of the data. 10 | public let encoding: String.Encoding 11 | 12 | /// Returns `FormURLEncodedDataParser` with the string encoding. 13 | public init(encoding: String.Encoding) { 14 | self.encoding = encoding 15 | } 16 | 17 | // MARK: - DataParser 18 | 19 | /// Value for `Accept` header field of HTTP request. 20 | public var contentType: String? { 21 | return "application/x-www-form-urlencoded" 22 | } 23 | 24 | /// Return `Any` that expresses structure of response. 25 | /// - Throws: `FormURLEncodedDataParser.Error` when the parser fails to initialize `String` from `Data`. 26 | public func parse(data: Data) throws -> Any { 27 | guard let string = String(data: data, encoding: encoding) else { 28 | throw Error.cannotGetStringFromData(data) 29 | } 30 | 31 | var components = URLComponents() 32 | components.percentEncodedQuery = string 33 | 34 | let queryItems = components.queryItems ?? [] 35 | var dictionary = [String: Any]() 36 | 37 | for queryItem in queryItems { 38 | dictionary[queryItem.name] = queryItem.value 39 | } 40 | 41 | return dictionary 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/APIKit/DataParser/JSONDataParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `JSONDataParser` response JSON data. 4 | public class JSONDataParser: DataParser { 5 | /// Options for reading the JSON data and creating the objects. 6 | public let readingOptions: JSONSerialization.ReadingOptions 7 | 8 | /// Returns `JSONDataParser` with the reading options. 9 | public init(readingOptions: JSONSerialization.ReadingOptions) { 10 | self.readingOptions = readingOptions 11 | } 12 | 13 | // MARK: - DataParser 14 | 15 | /// Value for `Accept` header field of HTTP request. 16 | public var contentType: String? { 17 | return "application/json" 18 | } 19 | 20 | /// Return `Any` that expresses structure of JSON response. 21 | /// - Throws: `NSError` when `JSONSerialization` fails to deserialize `Data` into `Any`. 22 | public func parse(data: Data) throws -> Any { 23 | guard data.count > 0 else { 24 | return [:] 25 | } 26 | 27 | return try JSONSerialization.jsonObject(with: data, options: readingOptions) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/APIKit/DataParser/ProtobufDataParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `ProtobufDataParser` response Data data. 4 | public class ProtobufDataParser: DataParser { 5 | /// Returns `ProtobufDataParser`. 6 | public init() {} 7 | 8 | // MARK: - DataParser 9 | 10 | /// Value for `Accept` header field of HTTP request. 11 | public var contentType: String? { 12 | return "application/protobuf" 13 | } 14 | 15 | /// Return `Any` that expresses structure of Data response. 16 | public func parse(data: Data) throws -> Any { 17 | return data 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/APIKit/DataParser/StringDataParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `StringDataParser` parses data and convert it to string. 4 | public class StringDataParser: DataParser { 5 | public enum Error: Swift.Error { 6 | case invalidData(Data) 7 | } 8 | 9 | /// The string encoding of the data. 10 | public let encoding: String.Encoding 11 | 12 | /// Returns `StringDataParser` with the string encoding. 13 | public init(encoding: String.Encoding = .utf8) { 14 | self.encoding = encoding 15 | } 16 | 17 | // MARK: - DataParser 18 | 19 | /// Value for `Accept` header field of HTTP request. 20 | public var contentType: String? { 21 | return nil 22 | } 23 | 24 | /// Return `String` that converted from `Data`. 25 | /// - Throws: `StringDataParser.Error` when the parser fails to initialize `String` from `Data`. 26 | public func parse(data: Data) throws -> Any { 27 | guard let string = String(data: data, encoding: encoding) else { 28 | throw Error.invalidData(data) 29 | } 30 | 31 | return string 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/APIKit/Error/RequestError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `RequestError` represents a common error that occurs while building `URLRequest` from `Request`. 4 | public enum RequestError: Error { 5 | /// Indicates `baseURL` of a type that conforms `Request` is invalid. 6 | case invalidBaseURL(URL) 7 | 8 | /// Indicates `URLRequest` built by `Request.buildURLRequest` is unexpected. 9 | case unexpectedURLRequest(URLRequest) 10 | } 11 | -------------------------------------------------------------------------------- /Sources/APIKit/Error/ResponseError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `ResponseError` represents a common error that occurs while getting `Request.Response` 4 | /// from raw result tuple `(Data?, URLResponse?, Error?)`. 5 | public enum ResponseError: Error { 6 | /// Indicates the session adapter returned `URLResponse` that fails to down-cast to `HTTPURLResponse`. 7 | case nonHTTPURLResponse(URLResponse?) 8 | 9 | /// Indicates `HTTPURLResponse.statusCode` is not acceptable. 10 | /// In most cases, *acceptable* means the value is in `200..<300`. 11 | case unacceptableStatusCode(Int) 12 | 13 | /// Indicates `Any` that represents the response is unexpected. 14 | case unexpectedObject(Any) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/APIKit/Error/SessionTaskError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `SessionTaskError` represents an error that occurs while task for a request. 4 | public enum SessionTaskError: Error { 5 | /// Error of `URLSession`. 6 | case connectionError(Error) 7 | 8 | /// Error while creating `URLRequest` from `Request`. 9 | case requestError(Error) 10 | 11 | /// Error while creating `Request.Response` from `(Data, URLResponse)`. 12 | case responseError(Error) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/APIKit/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `HTTPMethod` represents HTTP methods. 4 | public enum HTTPMethod: String { 5 | case get = "GET" 6 | case post = "POST" 7 | case put = "PUT" 8 | case head = "HEAD" 9 | case delete = "DELETE" 10 | case patch = "PATCH" 11 | case trace = "TRACE" 12 | case options = "OPTIONS" 13 | case connect = "CONNECT" 14 | 15 | /// Indicates if the query parameters are suitable for parameters. 16 | public var prefersQueryParameters: Bool { 17 | switch self { 18 | case .get, .head, .delete: 19 | return true 20 | 21 | default: 22 | return false 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/APIKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 5.4.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 Yosuke Ishikawa. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/APIKit/Request.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `Request` protocol represents a request for Web API. 4 | /// Following 5 items must be implemented. 5 | /// - `typealias Response` 6 | /// - `var baseURL: URL` 7 | /// - `var method: HTTPMethod` 8 | /// - `var path: String` 9 | /// - `func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response` 10 | public protocol Request { 11 | /// The response type associated with the request type. 12 | associatedtype Response 13 | 14 | /// The base URL. 15 | var baseURL: URL { get } 16 | 17 | /// The HTTP request method. 18 | var method: HTTPMethod { get } 19 | 20 | /// The path URL component. 21 | var path: String { get } 22 | 23 | /// The convenience property for `queryParameters` and `bodyParameters`. If the implementation of 24 | /// `queryParameters` and `bodyParameters` are not provided, the values for them will be computed 25 | /// from this property depending on `method`. 26 | var parameters: Any? { get } 27 | 28 | /// The actual parameters for the URL query. The values of this property will be escaped using `URLEncodedSerialization`. 29 | /// If this property is not implemented and `method.prefersQueryParameter` is `true`, the value of this property 30 | /// will be computed from `parameters`. 31 | var queryParameters: [String: Any]? { get } 32 | 33 | /// The actual parameters for the HTTP body. If this property is not implemented and `method.prefersQueryParameter` is `false`, 34 | /// the value of this property will be computed from `parameters` using `JSONBodyParameters`. 35 | var bodyParameters: BodyParameters? { get } 36 | 37 | /// The HTTP header fields. In addition to fields defined in this property, `Accept` and `Content-Type` 38 | /// fields will be added by `dataParser` and `bodyParameters`. If you define `Accept` and `Content-Type` 39 | /// in this property, the values in this property are preferred. 40 | var headerFields: [String: String] { get } 41 | 42 | /// The parser object that states `Content-Type` to accept and parses response body. 43 | var dataParser: DataParser { get } 44 | 45 | /// Intercepts `URLRequest` which is created by `Request.buildURLRequest()`. If an error is 46 | /// thrown in this method, the result of `Session.send()` turns `.failure(.requestError(error))`. 47 | /// - Throws: `Error` 48 | func intercept(urlRequest: URLRequest) throws -> URLRequest 49 | 50 | /// Intercepts response `Any` and `HTTPURLResponse`. If an error is thrown in this method, 51 | /// the result of `Session.send()` turns `.failure(.responseError(error))`. 52 | /// The default implementation of this method is provided to throw `ResponseError.unacceptableStatusCode` 53 | /// if the HTTP status code is not in `200..<300`. 54 | /// - Throws: `Error` 55 | func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any 56 | 57 | /// Build `Response` instance from raw response object. This method is called after 58 | /// `intercept(object:urlResponse:)` if it does not throw any error. 59 | /// - Throws: `Error` 60 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response 61 | } 62 | 63 | public extension Request { 64 | var parameters: Any? { 65 | return nil 66 | } 67 | 68 | var queryParameters: [String: Any]? { 69 | guard let parameters = parameters as? [String: Any], method.prefersQueryParameters else { 70 | return nil 71 | } 72 | 73 | return parameters 74 | } 75 | 76 | var bodyParameters: BodyParameters? { 77 | guard let parameters = parameters, !method.prefersQueryParameters else { 78 | return nil 79 | } 80 | 81 | return JSONBodyParameters(JSONObject: parameters) 82 | } 83 | 84 | var headerFields: [String: String] { 85 | return [:] 86 | } 87 | 88 | var dataParser: DataParser { 89 | return JSONDataParser(readingOptions: []) 90 | } 91 | 92 | func intercept(urlRequest: URLRequest) throws -> URLRequest { 93 | return urlRequest 94 | } 95 | 96 | func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any { 97 | guard 200..<300 ~= urlResponse.statusCode else { 98 | throw ResponseError.unacceptableStatusCode(urlResponse.statusCode) 99 | } 100 | return object 101 | } 102 | 103 | /// Builds `URLRequest` from properties of `self`. 104 | /// - Throws: `RequestError`, `Error` 105 | func buildURLRequest() throws -> URLRequest { 106 | let url = path.isEmpty ? baseURL : baseURL.appendingPathComponent(path) 107 | guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { 108 | throw RequestError.invalidBaseURL(baseURL) 109 | } 110 | 111 | var urlRequest = URLRequest(url: url) 112 | 113 | if let queryParameters = queryParameters, !queryParameters.isEmpty { 114 | components.percentEncodedQuery = URLEncodedSerialization.string(from: queryParameters) 115 | } 116 | 117 | if let bodyParameters = bodyParameters { 118 | urlRequest.setValue(bodyParameters.contentType, forHTTPHeaderField: "Content-Type") 119 | 120 | switch try bodyParameters.buildEntity() { 121 | case .data(let data): 122 | urlRequest.httpBody = data 123 | 124 | case .inputStream(let inputStream): 125 | urlRequest.httpBodyStream = inputStream 126 | } 127 | } 128 | 129 | urlRequest.url = components.url 130 | urlRequest.httpMethod = method.rawValue 131 | urlRequest.setValue(dataParser.contentType, forHTTPHeaderField: "Accept") 132 | 133 | headerFields.forEach { key, value in 134 | urlRequest.setValue(value, forHTTPHeaderField: key) 135 | } 136 | 137 | return (try intercept(urlRequest: urlRequest) as URLRequest) 138 | } 139 | 140 | /// Builds `Response` from response `Data`. 141 | /// - Throws: `ResponseError`, `Error` 142 | func parse(data: Data, urlResponse: HTTPURLResponse) throws -> Response { 143 | let parsedObject = try dataParser.parse(data: data) 144 | let passedObject = try intercept(object: parsedObject, urlResponse: urlResponse) 145 | return try response(from: passedObject, urlResponse: urlResponse) 146 | } 147 | } 148 | 149 | public extension Request where Response == Void { 150 | func response(from object: Any, urlResponse: HTTPURLResponse) throws { 151 | return 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/APIKit/Serializations/URLEncodedSerialization.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private func escape(_ string: String) -> String { 4 | // Reserved characters defined by RFC 3986 5 | // Reference: https://www.ietf.org/rfc/rfc3986.txt 6 | let generalDelimiters = ":#[]@" 7 | let subDelimiters = "!$&'()*+,;=" 8 | let reservedCharacters = generalDelimiters + subDelimiters 9 | 10 | var allowedCharacterSet = CharacterSet() 11 | allowedCharacterSet.formUnion(.urlQueryAllowed) 12 | allowedCharacterSet.remove(charactersIn: reservedCharacters) 13 | 14 | // Crashes due to internal bug in iOS 7 ~ iOS 8.2. 15 | // References: 16 | // - https://github.com/Alamofire/Alamofire/issues/206 17 | // - https://github.com/AFNetworking/AFNetworking/issues/3028 18 | // return string.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? string 19 | 20 | let batchSize = 50 21 | var index = string.startIndex 22 | 23 | var escaped = "" 24 | 25 | while index != string.endIndex { 26 | let startIndex = index 27 | let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex 28 | let range = startIndex.. String { 41 | return CFURLCreateStringByReplacingPercentEscapes(nil, string as CFString, nil) as String 42 | } 43 | 44 | /// `URLEncodedSerialization` parses `Data` and `String` as urlencoded, 45 | /// and returns dictionary that represents the data or the string. 46 | public final class URLEncodedSerialization { 47 | public enum Error: Swift.Error { 48 | case cannotGetStringFromData(Data, String.Encoding) 49 | case cannotGetDataFromString(String, String.Encoding) 50 | case cannotCastObjectToDictionary(Any) 51 | case invalidFormatString(String) 52 | } 53 | 54 | /// Returns `[String: String]` that represents urlencoded `Data`. 55 | /// - Throws: URLEncodedSerialization.Error 56 | public static func object(from data: Data, encoding: String.Encoding) throws -> [String: String] { 57 | guard let string = String(data: data, encoding: encoding) else { 58 | throw Error.cannotGetStringFromData(data, encoding) 59 | } 60 | 61 | var dictionary = [String: String]() 62 | for pair in string.components(separatedBy: "&") { 63 | let contents = pair.components(separatedBy: "=") 64 | 65 | guard contents.count == 2 else { 66 | throw Error.invalidFormatString(string) 67 | } 68 | 69 | dictionary[contents[0]] = unescape(contents[1]) 70 | } 71 | 72 | return dictionary 73 | } 74 | 75 | /// Returns urlencoded `Data` from the object. 76 | /// - Throws: URLEncodedSerialization.Error 77 | public static func data(from object: Any, encoding: String.Encoding) throws -> Data { 78 | guard let dictionary = object as? [String: Any] else { 79 | throw Error.cannotCastObjectToDictionary(object) 80 | } 81 | 82 | let string = self.string(from: dictionary) 83 | guard let data = string.data(using: encoding, allowLossyConversion: false) else { 84 | throw Error.cannotGetDataFromString(string, encoding) 85 | } 86 | 87 | return data 88 | } 89 | 90 | /// Returns urlencoded `String` from the dictionary. 91 | public static func string(from dictionary: [String: Any]) -> String { 92 | let pairs = dictionary.map { key, value -> String in 93 | if value is NSNull { 94 | return "\(escape(key))" 95 | } 96 | 97 | let valueAsString = (value as? String) ?? "\(value)" 98 | return "\(escape(key))=\(escape(valueAsString))" 99 | } 100 | 101 | return pairs.joined(separator: "&") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/APIKit/Session.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private var taskRequestKey = 0 4 | 5 | /// `Session` manages tasks for HTTP/HTTPS requests. 6 | open class Session { 7 | /// The adapter that connects `Session` instance and lower level backend. 8 | public let adapter: SessionAdapter 9 | 10 | /// The default callback queue for `send(_:handler:)`. 11 | public let callbackQueue: CallbackQueue 12 | 13 | /// Returns `Session` instance that is initialized with `adapter`. 14 | /// - parameter adapter: The adapter that connects lower level backend with Session interface. 15 | /// - parameter callbackQueue: The default callback queue for `send(_:handler:)`. 16 | public init(adapter: SessionAdapter, callbackQueue: CallbackQueue = .main) { 17 | self.adapter = adapter 18 | self.callbackQueue = callbackQueue 19 | } 20 | 21 | // Shared session for class methods 22 | private static let privateShared: Session = { 23 | let configuration = URLSessionConfiguration.default 24 | let adapter = URLSessionAdapter(configuration: configuration) 25 | return Session(adapter: adapter) 26 | }() 27 | 28 | /// The shared `Session` instance for class methods, `Session.send(_:handler:)` and `Session.cancelRequests(with:passingTest:)`. 29 | open class var shared: Session { 30 | return privateShared 31 | } 32 | 33 | /// Calls `send(_:callbackQueue:handler:)` of `Session.shared`. 34 | /// - parameter request: The request to be sent. 35 | /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. 36 | /// - parameter handler: The closure that receives result of the request. 37 | /// - returns: The new session task. 38 | @discardableResult 39 | open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { 40 | return shared.send(request, callbackQueue: callbackQueue, handler: handler) 41 | } 42 | 43 | /// Calls `cancelRequests(with:passingTest:)` of `Session.shared`. 44 | open class func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { 45 | shared.cancelRequests(with: requestType, passingTest: test) 46 | } 47 | 48 | /// Sends a request and receives the result as the argument of `handler` closure. This method takes 49 | /// a type parameter `Request` that conforms to `Request` protocol. The result of passed request is 50 | /// expressed as `Result`. Since the response type 51 | /// `Request.Response` is inferred from `Request` type parameter, the it changes depending on the request type. 52 | /// - parameter request: The request to be sent. 53 | /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. 54 | /// - parameter handler: The closure that receives result of the request. 55 | /// - returns: The new session task. 56 | @discardableResult 57 | open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { 58 | let task = createSessionTask(request, callbackQueue: callbackQueue, handler: handler) 59 | task?.resume() 60 | return task 61 | } 62 | 63 | /// Cancels requests that passes the test. 64 | /// - parameter requestType: The request type to cancel. 65 | /// - parameter test: The test closure that determines if a request should be cancelled or not. 66 | open func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { 67 | adapter.getTasks { [weak self] tasks in 68 | tasks 69 | .filter { task in 70 | if let request = self?.requestForTask(task) as Request? { 71 | return test(request) 72 | } else { 73 | return false 74 | } 75 | } 76 | .forEach { $0.cancel() } 77 | } 78 | } 79 | 80 | internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { 81 | let callbackQueue = callbackQueue ?? self.callbackQueue 82 | let urlRequest: URLRequest 83 | do { 84 | urlRequest = try request.buildURLRequest() 85 | } catch { 86 | callbackQueue.execute { 87 | handler(.failure(.requestError(error))) 88 | } 89 | return nil 90 | } 91 | 92 | let task = adapter.createTask(with: urlRequest) { data, urlResponse, error in 93 | let result: Result 94 | 95 | switch (data, urlResponse, error) { 96 | case (_, _, let error?): 97 | result = .failure(.connectionError(error)) 98 | 99 | case (let data?, let urlResponse as HTTPURLResponse, _): 100 | do { 101 | result = .success(try request.parse(data: data as Data, urlResponse: urlResponse)) 102 | } catch { 103 | result = .failure(.responseError(error)) 104 | } 105 | 106 | default: 107 | result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse))) 108 | } 109 | 110 | callbackQueue.execute { 111 | handler(result) 112 | } 113 | } 114 | 115 | setRequest(request, forTask: task) 116 | 117 | return task 118 | } 119 | 120 | private func setRequest(_ request: Request, forTask task: SessionTask) { 121 | objc_setAssociatedObject(task, &taskRequestKey, request, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 122 | } 123 | 124 | private func requestForTask(_ task: SessionTask) -> Request? { 125 | return objc_getAssociatedObject(task, &taskRequestKey) as? Request 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/APIKit/SessionAdapter/SessionAdapter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `SessionTask` protocol represents a task for a request. 4 | public protocol SessionTask: AnyObject { 5 | func resume() 6 | func cancel() 7 | } 8 | 9 | /// `SessionAdapter` protocol provides interface to connect lower level networking backend with `Session`. 10 | /// APIKit provides `URLSessionAdapter`, which conforms to `SessionAdapter`, to connect `URLSession` 11 | /// with `Session`. 12 | public protocol SessionAdapter { 13 | /// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure. 14 | func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask 15 | 16 | /// Collects tasks from backend networking stack. `handler` must be called after collecting. 17 | func getTasks(with handler: @escaping ([SessionTask]) -> Void) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/APIKit/SessionAdapter/URLSessionAdapter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URLSessionTask: SessionTask { 4 | 5 | } 6 | 7 | private var dataTaskResponseBufferKey = 0 8 | private var taskAssociatedObjectCompletionHandlerKey = 0 9 | 10 | /// `URLSessionAdapter` connects `URLSession` with `Session`. 11 | /// 12 | /// If you want to add custom behavior of `URLSession` by implementing delegate methods defined in 13 | /// `URLSessionDelegate` and related protocols, define a subclass of `URLSessionAdapter` and implement 14 | /// delegate methods that you want to implement. Since `URLSessionAdapter` also implements delegate methods 15 | /// `URLSession(_:task: didCompleteWithError:)` and `URLSession(_:dataTask:didReceiveData:)`, you have to call 16 | /// `super` in these methods if you implement them. 17 | open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate { 18 | /// The underlying `URLSession` instance. 19 | open var urlSession: URLSession! 20 | 21 | /// Returns `URLSessionAdapter` initialized with `URLSessionConfiguration`. 22 | public init(configuration: URLSessionConfiguration) { 23 | super.init() 24 | self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) 25 | } 26 | 27 | /// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. 28 | open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { 29 | let task = urlSession.dataTask(with: URLRequest) 30 | 31 | setBuffer(NSMutableData(), forTask: task) 32 | setHandler(handler, forTask: task) 33 | 34 | return task 35 | } 36 | 37 | /// Aggregates `URLSessionTask` instances in `URLSession` using `getTasksWithCompletionHandler(_:)`. 38 | open func getTasks(with handler: @escaping ([SessionTask]) -> Void) { 39 | urlSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in 40 | let allTasks: [URLSessionTask] = dataTasks + uploadTasks + downloadTasks 41 | handler(allTasks) 42 | } 43 | } 44 | 45 | private func setBuffer(_ buffer: NSMutableData, forTask task: URLSessionTask) { 46 | objc_setAssociatedObject(task, &dataTaskResponseBufferKey, buffer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 47 | } 48 | 49 | private func buffer(for task: URLSessionTask) -> NSMutableData? { 50 | return objc_getAssociatedObject(task, &dataTaskResponseBufferKey) as? NSMutableData 51 | } 52 | 53 | private func setHandler(_ handler: @escaping (Data?, URLResponse?, Error?) -> Void, forTask task: URLSessionTask) { 54 | objc_setAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey, handler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 55 | } 56 | 57 | private func handler(for task: URLSessionTask) -> ((Data?, URLResponse?, Error?) -> Void)? { 58 | return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void 59 | } 60 | 61 | // MARK: URLSessionTaskDelegate 62 | open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 63 | handler(for: task)?(buffer(for: task) as Data?, task.response, error) 64 | } 65 | 66 | // MARK: URLSessionDataDelegate 67 | open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 68 | buffer(for: dataTask)?.append(data) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/APIKit/Unavailable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Unavailable.swift 3 | // APIKit 4 | // 5 | // Created by Yosuke Ishikawa on 9/19/16. 6 | // Copyright © 2016 Yosuke Ishikawa. All rights reserved. 7 | // 8 | 9 | // MARK: - Protocols 10 | 11 | @available(*, unavailable, renamed: "Request") 12 | public typealias RequestType = Request 13 | 14 | @available(*, unavailable, renamed: "SessionAdapter") 15 | public typealias SessionAdapterType = SessionAdapter 16 | 17 | @available(*, unavailable, renamed: "SessionTask") 18 | public typealias SessionTaskType = SessionTask 19 | 20 | @available(*, unavailable, renamed: "BodyParameters") 21 | public typealias BodyParametersType = BodyParameters 22 | 23 | @available(*, unavailable, renamed: "DataParser") 24 | public typealias DataParserType = DataParser 25 | 26 | // MARK: Session 27 | 28 | extension Session { 29 | @available(*, unavailable, renamed: "shared") 30 | public class var sharedSession: Session { 31 | fatalError("\(#function) is no longer available") 32 | } 33 | 34 | @available(*, unavailable, renamed: "send(_:callbackQueue:handler:)") 35 | public class func sendRequest(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { 36 | fatalError("\(#function) is no longer available") 37 | } 38 | 39 | @available(*, unavailable, renamed: "send(_:callbackQueue:handler:)") 40 | public func sendRequest(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { 41 | fatalError("\(#function) is no longer available") 42 | } 43 | 44 | @available(*, unavailable, renamed: "cancelRequests(with:passingTest:)") 45 | public class func cancelRequest(_ requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { 46 | fatalError("\(#function) is no longer available") 47 | } 48 | 49 | @available(*, unavailable, renamed: "cancelRequests(with:passingTest:)") 50 | public func cancelRequest(_ requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { 51 | fatalError("\(#function) is no longer available") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/APIKitTests/BodyParametersType/FormURLEncodedBodyParametersTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import APIKit 4 | 5 | class FormURLEncodedBodyParametersTests: XCTestCase { 6 | func testURLSuccess() { 7 | let object = ["foo": 1, "bar": 2, "baz": 3] 8 | let parameters = FormURLEncodedBodyParameters(formObject: object) 9 | XCTAssertEqual(parameters.contentType, "application/x-www-form-urlencoded") 10 | 11 | do { 12 | guard case .data(let data) = try parameters.buildEntity() else { 13 | XCTFail() 14 | return 15 | } 16 | 17 | let createdObject = try URLEncodedSerialization.object(from: data, encoding: .utf8) 18 | XCTAssertEqual(createdObject["foo"], "1") 19 | XCTAssertEqual(createdObject["bar"], "2") 20 | XCTAssertEqual(createdObject["baz"], "3") 21 | } catch { 22 | XCTFail() 23 | } 24 | } 25 | 26 | // NSURLComponents crashes on iOS 8.2 or earlier while escaping long CJK string. 27 | // This test ensures that FormURLEncodedBodyParameters avoids this issue correctly. 28 | func testLongCJKString() { 29 | let key = "key" 30 | let value = "一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十" 31 | let parameters = FormURLEncodedBodyParameters(formObject: [key: value]) 32 | _ = try? parameters.buildEntity() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import APIKit 4 | 5 | class JSONBodyParametersTests: XCTestCase { 6 | func testJSONSuccess() throws { 7 | let object = ["foo": 1, "bar": 2, "baz": 3] 8 | let parameters = JSONBodyParameters(JSONObject: object) 9 | XCTAssertEqual(parameters.contentType, "application/json") 10 | 11 | guard case .data(let data) = try parameters.buildEntity() else { 12 | XCTFail() 13 | return 14 | } 15 | let dictionary = try JSONSerialization.jsonObject(with: data, options: []) 16 | XCTAssertEqual((dictionary as? [String: Int])?["foo"], 1) 17 | XCTAssertEqual((dictionary as? [String: Int])?["bar"], 2) 18 | XCTAssertEqual((dictionary as? [String: Int])?["baz"], 3) 19 | } 20 | 21 | func testJSONFailure() { 22 | let object = NSObject() 23 | let parameters = JSONBodyParameters(JSONObject: object) 24 | 25 | XCTAssertThrowsError(try parameters.buildEntity()) { error in 26 | let nserror = error as NSError 27 | XCTAssertEqual(nserror.domain, NSCocoaErrorDomain) 28 | XCTAssertEqual(nserror.code, 3840) 29 | } 30 | } 31 | 32 | func testDictionaryLiteral() throws { 33 | let object = ["foo": 1, "bar": 2, "baz": 3] 34 | 35 | let parameters1: JSONBodyParameters = .init(JSONObject: object) 36 | let parameters2: JSONBodyParameters = ["foo": 1, "bar": 2, "baz": 3] 37 | 38 | guard case .data(let data1) = try parameters1.buildEntity(), 39 | case .data(let data2) = try parameters2.buildEntity() else { 40 | XCTFail() 41 | return 42 | } 43 | let dictionary1 = try JSONSerialization.jsonObject(with: data1, options: []) 44 | let dictionary2 = try JSONSerialization.jsonObject(with: data2, options: []) 45 | XCTAssertEqual((dictionary1 as? [String: Int])?["foo"], (dictionary2 as? [String: Int])?["foo"]) 46 | XCTAssertEqual((dictionary1 as? [String: Int])?["bar"], (dictionary2 as? [String: Int])?["bar"]) 47 | XCTAssertEqual((dictionary1 as? [String: Int])?["baz"], (dictionary2 as? [String: Int])?["baz"]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import APIKit 4 | 5 | class MultipartFormDataParametersTests: XCTestCase { 6 | // MARK: Entity 7 | func testDataEntitySuccess() throws { 8 | let value1 = try XCTUnwrap("1".data(using: .utf8)) 9 | let value2 = try XCTUnwrap("2".data(using: .utf8)) 10 | 11 | let parameters = MultipartFormDataBodyParameters(parts: [ 12 | MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), 13 | MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), 14 | ]) 15 | 16 | guard case .data(let data) = try parameters.buildEntity() else { 17 | XCTFail() 18 | return 19 | } 20 | let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) 21 | let returnCode = "\r\n" 22 | 23 | let pattern = "^multipart/form-data; boundary=([\\w.]+)$" 24 | let regexp = try NSRegularExpression(pattern: pattern, options: []) 25 | let range = NSRange(location: 0, length: parameters.contentType.count) 26 | let match = regexp.matches(in: parameters.contentType, options: [], range: range) 27 | XCTAssertTrue(match.count > 0) 28 | 29 | let firstRange = try XCTUnwrap(match.first?.range(at: 1)) 30 | let boundary = (parameters.contentType as NSString).substring(with: firstRange) 31 | XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") 32 | 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)") 33 | } 34 | 35 | func testInputStreamEntitySuccess() throws { 36 | let value1 = try XCTUnwrap("1".data(using: .utf8)) 37 | let value2 = try XCTUnwrap("2".data(using: .utf8)) 38 | 39 | let parameters = MultipartFormDataBodyParameters(parts: [ 40 | MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), 41 | MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), 42 | ], entityType: .inputStream) 43 | 44 | guard case .inputStream(let inputStream) = try parameters.buildEntity() else { 45 | XCTFail() 46 | return 47 | } 48 | let data = try Data(inputStream: inputStream) 49 | let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) 50 | let returnCode = "\r\n" 51 | 52 | let pattern = "^multipart/form-data; boundary=([\\w.]+)$" 53 | let regexp = try NSRegularExpression(pattern: pattern, options: []) 54 | let range = NSRange(location: 0, length: parameters.contentType.count) 55 | let match = regexp.matches(in: parameters.contentType, options: [], range: range) 56 | XCTAssertTrue(match.count > 0) 57 | 58 | let firstRange = try XCTUnwrap(match.first?.range(at: 1)) 59 | let boundary = (parameters.contentType as NSString).substring(with: firstRange) 60 | XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") 61 | 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)") 62 | } 63 | 64 | // MARK: Values 65 | 66 | func testFileValue() throws { 67 | #if SWIFT_PACKAGE 68 | let bundle = Bundle.module 69 | #else 70 | let bundle = Bundle(for: type(of: self)) 71 | #endif 72 | let fileURL = try XCTUnwrap(bundle.url(forResource: "test", withExtension: "json")) 73 | let part = try MultipartFormDataBodyParameters.Part(fileURL: fileURL, name: "test") 74 | let parameters = MultipartFormDataBodyParameters(parts: [part]) 75 | 76 | guard case .data(let data) = try parameters.buildEntity() else { 77 | XCTFail() 78 | return 79 | } 80 | let testData = try Data(contentsOf: fileURL) 81 | let testString = try XCTUnwrap(String(data: testData, encoding: .utf8)) 82 | 83 | let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) 84 | let returnCode = "\r\n" 85 | 86 | let pattern = "^multipart/form-data; boundary=([\\w.]+)$" 87 | let regexp = try NSRegularExpression(pattern: pattern, options: []) 88 | let range = NSRange(location: 0, length: parameters.contentType.count) 89 | let match = regexp.matches(in: parameters.contentType, options: [], range: range) 90 | XCTAssertTrue(match.count > 0) 91 | 92 | let firstRange = try XCTUnwrap(match.first?.range(at: 1)) 93 | let boundary = (parameters.contentType as NSString).substring(with: firstRange) 94 | XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") 95 | XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"test\"; filename=\"test.json\"\r\nContent-Type: application/json\(returnCode)\(returnCode)\(testString)\(returnCode)--\(boundary)--\(returnCode)") 96 | } 97 | 98 | func testStringValue() throws { 99 | let part = try MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") 100 | let parameters = MultipartFormDataBodyParameters(parts: [part]) 101 | 102 | guard case .data(let data) = try parameters.buildEntity() else { 103 | XCTFail() 104 | return 105 | } 106 | let string = String(data: data, encoding:.utf8) 107 | XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nabcdef\r\n--\(parameters.boundary)--\r\n") 108 | } 109 | 110 | func testIntValue() throws { 111 | let part = try MultipartFormDataBodyParameters.Part(value: 123, name: "foo") 112 | let parameters = MultipartFormDataBodyParameters(parts: [part]) 113 | 114 | guard case .data(let data) = try parameters.buildEntity() else { 115 | XCTFail() 116 | return 117 | } 118 | let string = String(data: data, encoding:.utf8) 119 | XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n123\r\n--\(parameters.boundary)--\r\n") 120 | } 121 | 122 | func testDoubleValue() throws { 123 | let part = try MultipartFormDataBodyParameters.Part(value: 3.14, name: "foo") 124 | let parameters = MultipartFormDataBodyParameters(parts: [part]) 125 | 126 | guard case .data(let data) = try parameters.buildEntity() else { 127 | XCTFail() 128 | return 129 | } 130 | let string = String(data: data, encoding:.utf8) 131 | XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--\(parameters.boundary)--\r\n") 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import APIKit 4 | 5 | class ProtobufBodyParametersTests: XCTestCase { 6 | func testProtobufSuccess() throws { 7 | let object = try XCTUnwrap("data".data(using: .utf8)) 8 | let parameters = ProtobufBodyParameters(protobufObject: object) 9 | XCTAssertEqual(parameters.contentType, "application/protobuf") 10 | 11 | guard case .data(let data) = try parameters.buildEntity() else { 12 | XCTFail() 13 | return 14 | } 15 | let string = String(data: data, encoding: .utf8) 16 | XCTAssertEqual(string, "data") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import APIKit 4 | 5 | class URLEncodedSerializationTests: XCTestCase { 6 | // MARK: NSData -> Any 7 | func testObjectFromData() throws { 8 | let data = try XCTUnwrap("key1=value1&key2=value2".data(using: .utf8)) 9 | let object = try? URLEncodedSerialization.object(from: data, encoding: .utf8) 10 | XCTAssertEqual(object?["key1"], "value1") 11 | XCTAssertEqual(object?["key2"], "value2") 12 | } 13 | 14 | func testInvalidFormatString() throws { 15 | let string = "key==value&" 16 | 17 | let data = try XCTUnwrap(string.data(using: .utf8)) 18 | XCTAssertThrowsError(try URLEncodedSerialization.object(from: data, encoding: .utf8)) { error in 19 | guard let error = error as? URLEncodedSerialization.Error, 20 | case .invalidFormatString(let invalidString) = error else { 21 | XCTFail() 22 | return 23 | } 24 | 25 | XCTAssertEqual(string, invalidString) 26 | } 27 | } 28 | 29 | func testInvalidString() { 30 | var bytes = [UInt8]([0xed, 0xa0, 0x80]) // U+D800 (high surrogate) 31 | let data = Data(bytes: &bytes, count: bytes.count) 32 | 33 | XCTAssertThrowsError(try URLEncodedSerialization.object(from: data, encoding: .utf8)) { error in 34 | guard let error = error as? URLEncodedSerialization.Error, 35 | case .cannotGetStringFromData(let invalidData, let encoding) = error else { 36 | XCTFail() 37 | return 38 | } 39 | 40 | XCTAssertEqual(data, invalidData) 41 | XCTAssertEqual(encoding, .utf8) 42 | } 43 | } 44 | 45 | // MARK: Any -> NSData 46 | func testDataFromObject() { 47 | let object = ["hey": "yo"] as Any 48 | let data = try? URLEncodedSerialization.data(from: object, encoding: .utf8) 49 | let string = data.flatMap { String(data: $0, encoding: .utf8) } 50 | XCTAssertEqual(string, "hey=yo") 51 | } 52 | 53 | func testNonDictionaryObject() { 54 | let dictionaries = [["hey": "yo"]] as Any 55 | 56 | XCTAssertThrowsError(try URLEncodedSerialization.data(from: dictionaries, encoding: .utf8)) { error in 57 | guard let error = error as? URLEncodedSerialization.Error, 58 | case .cannotCastObjectToDictionary(let object) = error else { 59 | XCTFail() 60 | return 61 | } 62 | 63 | XCTAssertEqual((object as AnyObject)["hey"], (dictionaries as AnyObject)["hey"]) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/APIKitTests/Combine/CombineTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Combine) 2 | 3 | import Foundation 4 | import XCTest 5 | import Combine 6 | import APIKit 7 | 8 | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) 9 | final class CombineTests: XCTestCase { 10 | 11 | var adapter: TestSessionAdapter! 12 | var session: Session! 13 | var cancellables: Set = [] 14 | 15 | override func setUp() { 16 | super.setUp() 17 | adapter = TestSessionAdapter() 18 | session = Session(adapter: adapter) 19 | } 20 | 21 | override func tearDown() { 22 | super.tearDown() 23 | cancellables = [] 24 | } 25 | 26 | func testSuccess() throws { 27 | let dictionary = ["key": "value"] 28 | adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) 29 | 30 | let expectation = self.expectation(description: "wait for response") 31 | let request = TestRequest() 32 | 33 | session.sessionTaskPublisher(for: request) 34 | .sink(receiveCompletion: { completion in 35 | switch completion { 36 | case .failure: 37 | XCTFail() 38 | case .finished: 39 | expectation.fulfill() 40 | } 41 | }, receiveValue: { response in 42 | XCTAssertEqual((response as? [String: String])?["key"], "value") 43 | }) 44 | .store(in: &cancellables) 45 | 46 | waitForExpectations(timeout: 1.0, handler: nil) 47 | } 48 | 49 | func testParseDataError() { 50 | adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) 51 | 52 | let expectation = self.expectation(description: "wait for response") 53 | let request = TestRequest() 54 | 55 | session.sessionTaskPublisher(for: request) 56 | .sink(receiveCompletion: { completion in 57 | if case .failure(let error) = completion, case .responseError(let responseError as NSError) = error { 58 | XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) 59 | XCTAssertEqual(responseError.code, 3840) 60 | expectation.fulfill() 61 | } else { 62 | XCTFail() 63 | } 64 | }, receiveValue: { response in 65 | XCTFail() 66 | }) 67 | .store(in: &cancellables) 68 | 69 | waitForExpectations(timeout: 1.0, handler: nil) 70 | } 71 | 72 | func testBefore2020OSVersionsCancel() throws { 73 | if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { 74 | throw XCTSkip("Skip on After 2020 OS versions, as Combine cancellation no longer emits a value.") 75 | } 76 | 77 | let cancelExpectation = self.expectation(description: "wait for cancel") 78 | let completionExpectation = self.expectation(description: "wait for response") 79 | let request = TestRequest() 80 | 81 | let cancellable = session.sessionTaskPublisher(for: request) 82 | .handleEvents(receiveCancel: { 83 | cancelExpectation.fulfill() 84 | }) 85 | .sink(receiveCompletion: { completion in 86 | if case .failure(let error) = completion, case .connectionError(let connectionError as NSError) = error { 87 | XCTAssertEqual(connectionError.code, 0) 88 | completionExpectation.fulfill() 89 | } else { 90 | XCTFail() 91 | } 92 | }, receiveValue: { response in 93 | XCTFail() 94 | }) 95 | cancellable.cancel() 96 | 97 | waitForExpectations(timeout: 1.0, handler: nil) 98 | } 99 | 100 | func testAfter2020OSVersionsCancel() throws { 101 | guard #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) else { 102 | throw XCTSkip("Skip on Before 2020 OS versions.") 103 | } 104 | 105 | let expectation = self.expectation(description: "wait for cancel") 106 | let request = TestRequest() 107 | 108 | let cancellable = session.sessionTaskPublisher(for: request) 109 | .handleEvents(receiveCancel: { 110 | expectation.fulfill() 111 | }) 112 | .sink(receiveCompletion: { completion in 113 | XCTFail() 114 | }, receiveValue: { response in 115 | XCTFail() 116 | }) 117 | cancellable.cancel() 118 | 119 | waitForExpectations(timeout: 1.0, handler: nil) 120 | } 121 | 122 | } 123 | 124 | #endif 125 | -------------------------------------------------------------------------------- /Tests/APIKitTests/Concurrency/ConcurrencyTests.swift: -------------------------------------------------------------------------------- 1 | #if compiler(>=5.6.0) && canImport(_Concurrency) 2 | 3 | import XCTest 4 | import APIKit 5 | 6 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 7 | final class ConcurrencyTests: XCTestCase { 8 | var adapter: TestSessionAdapter! 9 | var session: Session! 10 | 11 | override func setUp() { 12 | super.setUp() 13 | adapter = TestSessionAdapter() 14 | session = Session(adapter: adapter) 15 | } 16 | 17 | func testSuccess() async throws { 18 | let dictionary = ["key": "value"] 19 | adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) 20 | 21 | let request = TestRequest() 22 | let value = try await session.response(for: request) 23 | XCTAssertEqual((value as? [String: String])?["key"], "value") 24 | } 25 | 26 | func testParseDataError() async throws { 27 | adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) 28 | 29 | let request = TestRequest() 30 | do { 31 | _ = try await session.response(for: request) 32 | XCTFail() 33 | } catch { 34 | let sessionError = try XCTUnwrap(error as? SessionTaskError) 35 | if case .responseError(let responseError as NSError) = sessionError { 36 | XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) 37 | XCTAssertEqual(responseError.code, 3840) 38 | } else { 39 | XCTFail() 40 | } 41 | } 42 | } 43 | 44 | func testCancel() async throws { 45 | let request = TestRequest() 46 | 47 | let task = Task { 48 | do { 49 | _ = try await session.response(for: request) 50 | XCTFail() 51 | } catch { 52 | let sessionError = try XCTUnwrap(error as? SessionTaskError) 53 | if case .connectionError(let connectionError as NSError) = sessionError { 54 | XCTAssertEqual(connectionError.code, 0) 55 | XCTAssertTrue(Task.isCancelled) 56 | } else { 57 | XCTFail() 58 | } 59 | } 60 | } 61 | task.cancel() 62 | _ = try await task.value 63 | 64 | XCTAssertTrue(task.isCancelled) 65 | } 66 | } 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import APIKit 3 | import XCTest 4 | 5 | class FormURLEncodedDataParserTests: XCTestCase { 6 | func testURLAcceptHeader() { 7 | let parser = FormURLEncodedDataParser(encoding: .utf8) 8 | XCTAssertEqual(parser.contentType, "application/x-www-form-urlencoded") 9 | } 10 | 11 | func testURLSuccess() throws { 12 | let string = "foo=1&bar=2&baz=3" 13 | let data = string.data(using: .utf8, allowLossyConversion: false)! 14 | let parser = FormURLEncodedDataParser(encoding: .utf8) 15 | 16 | let object = try parser.parse(data: data) 17 | let dictionary = object as? [String: String] 18 | XCTAssertEqual(dictionary?["foo"], "1") 19 | XCTAssertEqual(dictionary?["bar"], "2") 20 | XCTAssertEqual(dictionary?["baz"], "3") 21 | } 22 | 23 | func testInvalidString() { 24 | var bytes = [UInt8]([0xed, 0xa0, 0x80]) // U+D800 (high surrogate) 25 | let data = Data(bytes: &bytes, count: bytes.count) 26 | let parser = FormURLEncodedDataParser(encoding: .utf8) 27 | 28 | XCTAssertThrowsError(try parser.parse(data: data)) { error in 29 | guard let error = error as? FormURLEncodedDataParser.Error, 30 | case .cannotGetStringFromData(let invalidData) = error else { 31 | XCTFail() 32 | return 33 | } 34 | 35 | XCTAssertEqual(data, invalidData) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/APIKitTests/DataParserType/JSONDataParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import APIKit 3 | import XCTest 4 | 5 | class JSONDataParserTests: XCTestCase { 6 | func testContentType() { 7 | let parser = JSONDataParser(readingOptions: []) 8 | XCTAssertEqual(parser.contentType, "application/json") 9 | } 10 | 11 | func testJSONSuccess() throws { 12 | let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" 13 | let data = string.data(using: .utf8, allowLossyConversion: false)! 14 | let parser = JSONDataParser(readingOptions: []) 15 | 16 | let object = try parser.parse(data: data) 17 | let dictionary = object as? [String: Int] 18 | XCTAssertEqual(dictionary?["foo"], 1) 19 | XCTAssertEqual(dictionary?["bar"], 2) 20 | XCTAssertEqual(dictionary?["baz"], 3) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import APIKit 3 | import XCTest 4 | 5 | class ProtobufDataParserTests: XCTestCase { 6 | func testContentType() { 7 | let parser = ProtobufDataParser() 8 | XCTAssertEqual(parser.contentType, "application/protobuf") 9 | } 10 | 11 | func testProtobufSuccess() throws { 12 | let data = try XCTUnwrap("data".data(using: .utf8)) 13 | let parser = ProtobufDataParser() 14 | 15 | let object = try XCTUnwrap(try parser.parse(data: data) as? Data) 16 | let string = String(data: object, encoding: .utf8) 17 | XCTAssertEqual(string, "data") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/APIKitTests/DataParserType/StringDataParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import APIKit 4 | 5 | class StringDataParserTests: XCTestCase { 6 | func testAcceptHeader() { 7 | let parser = StringDataParser(encoding: .utf8) 8 | XCTAssertNil(parser.contentType) 9 | } 10 | 11 | func testParseData() throws { 12 | let string = "abcdef" 13 | let data = string.data(using: .utf8, allowLossyConversion: false)! 14 | let parser = StringDataParser(encoding: .utf8) 15 | 16 | let object = try parser.parse(data: data) 17 | XCTAssertEqual(object as? String, string) 18 | } 19 | 20 | func testInvalidString() { 21 | var bytes = [UInt8]([0xed, 0xa0, 0x80]) // U+D800 (high surrogate) 22 | let data = Data(bytes: &bytes, count: bytes.count) 23 | let parser = StringDataParser(encoding: .utf8) 24 | 25 | XCTAssertThrowsError(try parser.parse(data: data)) { error in 26 | guard let error = error as? StringDataParser.Error, 27 | case .invalidData(let invalidData) = error else { 28 | XCTFail() 29 | return 30 | } 31 | 32 | XCTAssertEqual(data, invalidData) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/APIKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/APIKitTests/RequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import APIKit 3 | 4 | class RequestTests: XCTestCase { 5 | func testJapanesesQueryParameters() throws { 6 | let request = TestRequest(parameters: ["q": "こんにちは"]) 7 | let urlRequest = try request.buildURLRequest() 8 | XCTAssertEqual(urlRequest.url?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") 9 | } 10 | 11 | func testSymbolQueryParameters() throws { 12 | let request = TestRequest(parameters: ["q": "!\"#$%&'()0=~|`{}*+<>?/_"]) 13 | let urlRequest = try request.buildURLRequest() 14 | XCTAssertEqual(urlRequest.url?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?/_") 15 | } 16 | 17 | func testNullQueryParameters() throws { 18 | let request = TestRequest(parameters: ["null": NSNull()]) 19 | let urlRequest = try request.buildURLRequest() 20 | XCTAssertEqual(urlRequest.url?.query, "null") 21 | } 22 | 23 | func testheaderFields() throws { 24 | let request = TestRequest(headerFields: ["Foo": "f", "Accept": "a", "Content-Type": "c"]) 25 | let urlReqeust = try request.buildURLRequest() 26 | XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Foo"), "f") 27 | XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Accept"), "a") 28 | XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Content-Type"), "c") 29 | } 30 | 31 | func testPOSTJSONRequest() throws { 32 | let parameters: [Any] = [ 33 | ["id": "1"], 34 | ["id": "2"], 35 | ["hello", "yellow"] 36 | ] 37 | 38 | let request = TestRequest(method: .post, parameters: parameters) 39 | XCTAssert((request.parameters as? [Any])?.count == 3) 40 | 41 | let urlRequest = try request.buildURLRequest() 42 | 43 | let json = urlRequest.httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [AnyObject] 44 | XCTAssertEqual(json?.count, 3) 45 | XCTAssertEqual((json?[0] as? [String: String])?["id"], "1") 46 | XCTAssertEqual((json?[1] as? [String: String])?["id"], "2") 47 | 48 | let array = json?[2] as? [String] 49 | XCTAssertEqual(array?[0], "hello") 50 | XCTAssertEqual(array?[1], "yellow") 51 | } 52 | 53 | func testPOSTInvalidJSONRequest() { 54 | let request = TestRequest(method: .post, parameters: "foo") 55 | let urlRequest = try? request.buildURLRequest() 56 | XCTAssertNil(urlRequest?.httpBody) 57 | } 58 | 59 | func testBuildURL() { 60 | // MARK: - baseURL = https://example.com 61 | XCTAssertEqual( 62 | TestRequest(baseURL: "https://example.com", path: "").absoluteURL, 63 | URL(string: "https://example.com") 64 | ) 65 | 66 | XCTAssertEqual( 67 | TestRequest(baseURL: "https://example.com", path: "/").absoluteURL, 68 | URL(string: "https://example.com/") 69 | ) 70 | 71 | XCTAssertEqual( 72 | TestRequest(baseURL: "https://example.com", path: "/", parameters: ["p": 1]).absoluteURL, 73 | URL(string: "https://example.com/?p=1") 74 | ) 75 | 76 | XCTAssertEqual( 77 | TestRequest(baseURL: "https://example.com", path: "foo").absoluteURL, 78 | URL(string: "https://example.com/foo") 79 | ) 80 | 81 | XCTAssertEqual( 82 | TestRequest(baseURL: "https://example.com", path: "/foo", parameters: ["p": 1]).absoluteURL, 83 | URL(string: "https://example.com/foo?p=1") 84 | ) 85 | 86 | XCTAssertEqual( 87 | TestRequest(baseURL: "https://example.com", path: "/foo/").absoluteURL, 88 | URL(string: "https://example.com/foo/") 89 | ) 90 | 91 | XCTAssertEqual( 92 | TestRequest(baseURL: "https://example.com", path: "/foo/", parameters: ["p": 1]).absoluteURL, 93 | URL(string: "https://example.com/foo/?p=1") 94 | ) 95 | 96 | XCTAssertEqual( 97 | TestRequest(baseURL: "https://example.com", path: "foo/bar").absoluteURL, 98 | URL(string: "https://example.com/foo/bar") 99 | ) 100 | 101 | XCTAssertEqual( 102 | TestRequest(baseURL: "https://example.com", path: "/foo/bar").absoluteURL, 103 | URL(string: "https://example.com/foo/bar") 104 | ) 105 | 106 | XCTAssertEqual( 107 | TestRequest(baseURL: "https://example.com", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, 108 | URL(string: "https://example.com/foo/bar?p=1") 109 | ) 110 | 111 | XCTAssertEqual( 112 | TestRequest(baseURL: "https://example.com", path: "/foo/bar/").absoluteURL, 113 | URL(string: "https://example.com/foo/bar/") 114 | ) 115 | 116 | XCTAssertEqual( 117 | TestRequest(baseURL: "https://example.com", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, 118 | URL(string: "https://example.com/foo/bar/?p=1") 119 | ) 120 | 121 | XCTAssertEqual( 122 | TestRequest(baseURL: "https://example.com", path: "/foo/bar//").absoluteURL, 123 | URL(string: "https://example.com/foo/bar//") 124 | ) 125 | 126 | // MARK: - baseURL = https://example.com/ 127 | XCTAssertEqual( 128 | TestRequest(baseURL: "https://example.com/", path: "").absoluteURL, 129 | URL(string: "https://example.com/") 130 | ) 131 | 132 | XCTAssertEqual( 133 | TestRequest(baseURL: "https://example.com/", path: "/").absoluteURL, 134 | URL(string: "https://example.com//") 135 | ) 136 | 137 | XCTAssertEqual( 138 | TestRequest(baseURL: "https://example.com/", path: "/", parameters: ["p": 1]).absoluteURL, 139 | URL(string: "https://example.com//?p=1") 140 | ) 141 | 142 | XCTAssertEqual( 143 | TestRequest(baseURL: "https://example.com/", path: "foo").absoluteURL, 144 | URL(string: "https://example.com/foo") 145 | ) 146 | 147 | XCTAssertEqual( 148 | TestRequest(baseURL: "https://example.com/", path: "/foo").absoluteURL, 149 | URL(string: "https://example.com//foo") 150 | ) 151 | 152 | XCTAssertEqual( 153 | TestRequest(baseURL: "https://example.com/", path: "/foo", parameters: ["p": 1]).absoluteURL, 154 | URL(string: "https://example.com//foo?p=1") 155 | ) 156 | 157 | XCTAssertEqual( 158 | TestRequest(baseURL: "https://example.com/", path: "/foo/").absoluteURL, 159 | URL(string: "https://example.com//foo/") 160 | ) 161 | 162 | XCTAssertEqual( 163 | TestRequest(baseURL: "https://example.com/", path: "/foo/", parameters: ["p": 1]).absoluteURL, 164 | URL(string: "https://example.com//foo/?p=1") 165 | ) 166 | 167 | XCTAssertEqual( 168 | TestRequest(baseURL: "https://example.com/", path: "foo/bar").absoluteURL, 169 | URL(string: "https://example.com/foo/bar") 170 | ) 171 | 172 | XCTAssertEqual( 173 | TestRequest(baseURL: "https://example.com/", path: "/foo/bar").absoluteURL, 174 | URL(string: "https://example.com//foo/bar") 175 | ) 176 | 177 | XCTAssertEqual( 178 | TestRequest(baseURL: "https://example.com/", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, 179 | URL(string: "https://example.com//foo/bar?p=1") 180 | ) 181 | 182 | XCTAssertEqual( 183 | TestRequest(baseURL: "https://example.com/", path: "/foo/bar/").absoluteURL, 184 | URL(string: "https://example.com//foo/bar/") 185 | ) 186 | 187 | XCTAssertEqual( 188 | TestRequest(baseURL: "https://example.com/", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, 189 | URL(string: "https://example.com//foo/bar/?p=1") 190 | ) 191 | 192 | XCTAssertEqual( 193 | TestRequest(baseURL: "https://example.com/", path: "foo//bar//").absoluteURL, 194 | URL(string: "https://example.com/foo//bar//") 195 | ) 196 | 197 | // MARK: - baseURL = https://example.com/api 198 | XCTAssertEqual( 199 | TestRequest(baseURL: "https://example.com/api", path: "").absoluteURL, 200 | URL(string: "https://example.com/api") 201 | ) 202 | 203 | XCTAssertEqual( 204 | TestRequest(baseURL: "https://example.com/api", path: "/").absoluteURL, 205 | URL(string: "https://example.com/api/") 206 | ) 207 | 208 | XCTAssertEqual( 209 | TestRequest(baseURL: "https://example.com/api", path: "/", parameters: ["p": 1]).absoluteURL, 210 | URL(string: "https://example.com/api/?p=1") 211 | ) 212 | 213 | XCTAssertEqual( 214 | TestRequest(baseURL: "https://example.com/api", path: "foo").absoluteURL, 215 | URL(string: "https://example.com/api/foo") 216 | ) 217 | 218 | XCTAssertEqual( 219 | TestRequest(baseURL: "https://example.com/api", path: "/foo").absoluteURL, 220 | URL(string: "https://example.com/api/foo") 221 | ) 222 | 223 | XCTAssertEqual( 224 | TestRequest(baseURL: "https://example.com/api", path: "/foo", parameters: ["p": 1]).absoluteURL, 225 | URL(string: "https://example.com/api/foo?p=1") 226 | ) 227 | 228 | XCTAssertEqual( 229 | TestRequest(baseURL: "https://example.com/api", path: "/foo/").absoluteURL, 230 | URL(string: "https://example.com/api/foo/") 231 | ) 232 | 233 | XCTAssertEqual( 234 | TestRequest(baseURL: "https://example.com/api", path: "/foo/", parameters: ["p": 1]).absoluteURL, 235 | URL(string: "https://example.com/api/foo/?p=1") 236 | ) 237 | 238 | XCTAssertEqual( 239 | TestRequest(baseURL: "https://example.com/api", path: "foo/bar").absoluteURL, 240 | URL(string: "https://example.com/api/foo/bar") 241 | ) 242 | 243 | XCTAssertEqual( 244 | TestRequest(baseURL: "https://example.com/api", path: "/foo/bar").absoluteURL, 245 | URL(string: "https://example.com/api/foo/bar") 246 | ) 247 | 248 | XCTAssertEqual( 249 | TestRequest(baseURL: "https://example.com/api", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, 250 | URL(string: "https://example.com/api/foo/bar?p=1") 251 | ) 252 | 253 | XCTAssertEqual( 254 | TestRequest(baseURL: "https://example.com/api", path: "/foo/bar/").absoluteURL, 255 | URL(string: "https://example.com/api/foo/bar/") 256 | ) 257 | 258 | XCTAssertEqual( 259 | TestRequest(baseURL: "https://example.com/api", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, 260 | URL(string: "https://example.com/api/foo/bar/?p=1") 261 | ) 262 | 263 | XCTAssertEqual( 264 | TestRequest(baseURL: "https://example.com/api", path: "foo//bar//").absoluteURL, 265 | URL(string: "https://example.com/api/foo//bar//") 266 | ) 267 | 268 | // MARK: - baseURL = https://example.com/api/ 269 | XCTAssertEqual( 270 | TestRequest(baseURL: "https://example.com/api/", path: "").absoluteURL, 271 | URL(string: "https://example.com/api/") 272 | ) 273 | 274 | XCTAssertEqual( 275 | TestRequest(baseURL: "https://example.com/api/", path: "/").absoluteURL, 276 | URL(string: "https://example.com/api//") 277 | ) 278 | 279 | XCTAssertEqual( 280 | TestRequest(baseURL: "https://example.com/api/", path: "/", parameters: ["p": 1]).absoluteURL, 281 | URL(string: "https://example.com/api//?p=1") 282 | ) 283 | 284 | XCTAssertEqual( 285 | TestRequest(baseURL: "https://example.com/api/", path: "foo").absoluteURL, 286 | URL(string: "https://example.com/api/foo") 287 | ) 288 | 289 | XCTAssertEqual( 290 | TestRequest(baseURL: "https://example.com/api/", path: "/foo").absoluteURL, 291 | URL(string: "https://example.com/api//foo") 292 | ) 293 | 294 | XCTAssertEqual( 295 | TestRequest(baseURL: "https://example.com/api/", path: "/foo", parameters: ["p": 1]).absoluteURL, 296 | URL(string: "https://example.com/api//foo?p=1") 297 | ) 298 | 299 | XCTAssertEqual( 300 | TestRequest(baseURL: "https://example.com/api/", path: "/foo/").absoluteURL, 301 | URL(string: "https://example.com/api//foo/") 302 | ) 303 | 304 | XCTAssertEqual( 305 | TestRequest(baseURL: "https://example.com/api/", path: "/foo/", parameters: ["p": 1]).absoluteURL, 306 | URL(string: "https://example.com/api//foo/?p=1") 307 | ) 308 | 309 | XCTAssertEqual( 310 | TestRequest(baseURL: "https://example.com/api/", path: "foo/bar").absoluteURL, 311 | URL(string: "https://example.com/api/foo/bar") 312 | ) 313 | 314 | XCTAssertEqual( 315 | TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar").absoluteURL, 316 | URL(string: "https://example.com/api//foo/bar") 317 | ) 318 | 319 | XCTAssertEqual( 320 | TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, 321 | URL(string: "https://example.com/api//foo/bar?p=1") 322 | ) 323 | 324 | XCTAssertEqual( 325 | TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar/").absoluteURL, 326 | URL(string: "https://example.com/api//foo/bar/") 327 | ) 328 | 329 | XCTAssertEqual( 330 | TestRequest(baseURL: "https://example.com/api/", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, 331 | URL(string: "https://example.com/api//foo/bar/?p=1") 332 | ) 333 | 334 | XCTAssertEqual( 335 | TestRequest(baseURL: "https://example.com/api/", path: "foo//bar//").absoluteURL, 336 | URL(string: "https://example.com/api/foo//bar//") 337 | ) 338 | 339 | // MARK: - baseURL = https://example.com/// 340 | XCTAssertEqual( 341 | TestRequest(baseURL: "https://example.com///", path: "").absoluteURL, 342 | URL(string: "https://example.com///") 343 | ) 344 | 345 | XCTAssertEqual( 346 | TestRequest(baseURL: "https://example.com///", path: "/").absoluteURL, 347 | URL(string: "https://example.com////") 348 | ) 349 | 350 | XCTAssertEqual( 351 | TestRequest(baseURL: "https://example.com///", path: "/", parameters: ["p": 1]).absoluteURL, 352 | URL(string: "https://example.com////?p=1") 353 | ) 354 | 355 | XCTAssertEqual( 356 | TestRequest(baseURL: "https://example.com///", path: "foo").absoluteURL, 357 | URL(string: "https://example.com///foo") 358 | ) 359 | 360 | XCTAssertEqual( 361 | TestRequest(baseURL: "https://example.com///", path: "/foo").absoluteURL, 362 | URL(string: "https://example.com////foo") 363 | ) 364 | 365 | XCTAssertEqual( 366 | TestRequest(baseURL: "https://example.com///", path: "/foo", parameters: ["p": 1]).absoluteURL, 367 | URL(string: "https://example.com////foo?p=1") 368 | ) 369 | 370 | XCTAssertEqual( 371 | TestRequest(baseURL: "https://example.com///", path: "/foo/").absoluteURL, 372 | URL(string: "https://example.com////foo/") 373 | ) 374 | 375 | XCTAssertEqual( 376 | TestRequest(baseURL: "https://example.com///", path: "/foo/", parameters: ["p": 1]).absoluteURL, 377 | URL(string: "https://example.com////foo/?p=1") 378 | ) 379 | 380 | XCTAssertEqual( 381 | TestRequest(baseURL: "https://example.com///", path: "foo/bar").absoluteURL, 382 | URL(string: "https://example.com///foo/bar") 383 | ) 384 | 385 | XCTAssertEqual( 386 | TestRequest(baseURL: "https://example.com///", path: "/foo/bar").absoluteURL, 387 | URL(string: "https://example.com////foo/bar") 388 | ) 389 | 390 | XCTAssertEqual( 391 | TestRequest(baseURL: "https://example.com///", path: "/foo/bar", parameters: ["p": 1]).absoluteURL, 392 | URL(string: "https://example.com////foo/bar?p=1") 393 | ) 394 | 395 | XCTAssertEqual( 396 | TestRequest(baseURL: "https://example.com///", path: "/foo/bar/").absoluteURL, 397 | URL(string: "https://example.com////foo/bar/") 398 | ) 399 | 400 | XCTAssertEqual( 401 | TestRequest(baseURL: "https://example.com///", path: "/foo/bar/", parameters: ["p": 1]).absoluteURL, 402 | URL(string: "https://example.com////foo/bar/?p=1") 403 | ) 404 | 405 | XCTAssertEqual( 406 | TestRequest(baseURL: "https://example.com///", path: "foo//bar//").absoluteURL, 407 | URL(string: "https://example.com///foo//bar//") 408 | ) 409 | } 410 | 411 | func testInterceptURLRequest() throws { 412 | let url = try XCTUnwrap(URL(string: "https://example.com/customize")) 413 | let request = TestRequest() { _ in 414 | return URLRequest(url: url) 415 | } 416 | 417 | XCTAssertEqual(try request.buildURLRequest().url, url) 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import APIKit 4 | 5 | class URLSessionAdapterSubclassTests: XCTestCase { 6 | class SessionAdapter: URLSessionAdapter { 7 | var functionCallFlags = [String: Bool]() 8 | 9 | override func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 10 | functionCallFlags[(#function)] = true 11 | super.urlSession(session, task: task, didCompleteWithError: error) 12 | } 13 | 14 | override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 15 | functionCallFlags[(#function)] = true 16 | super.urlSession(session, dataTask: dataTask, didReceive: data) 17 | } 18 | } 19 | 20 | var adapter: SessionAdapter! 21 | var session: Session! 22 | 23 | override func setUp() { 24 | super.setUp() 25 | 26 | let configuration = URLSessionConfiguration.default 27 | configuration.protocolClasses = [HTTPStub.self] 28 | 29 | adapter = SessionAdapter(configuration: configuration) 30 | session = Session(adapter: adapter) 31 | } 32 | 33 | func testDelegateMethodCall() { 34 | let data = "{}".data(using: .utf8)! 35 | HTTPStub.stubResult = .success(data) 36 | 37 | let expectation = self.expectation(description: "wait for response") 38 | let request = TestRequest() 39 | 40 | session.send(request) { result in 41 | if case .failure = result { 42 | XCTFail() 43 | } 44 | 45 | expectation.fulfill() 46 | } 47 | 48 | waitForExpectations(timeout: 10.0, handler: nil) 49 | 50 | XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didCompleteWithError:)"], true) 51 | XCTAssertEqual(adapter.functionCallFlags["urlSession(_:dataTask:didReceive:)"], true) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/APIKitTests/SessionAdapterType/URLSessionAdapterTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | import XCTest 4 | 5 | class URLSessionAdapterTests: XCTestCase { 6 | var session: Session! 7 | 8 | override func setUp() { 9 | super.setUp() 10 | 11 | let configuration = URLSessionConfiguration.default 12 | configuration.protocolClasses = [HTTPStub.self] 13 | 14 | let adapter = URLSessionAdapter(configuration: configuration) 15 | session = Session(adapter: adapter) 16 | } 17 | 18 | // MARK: - integration tests 19 | func testSuccess() { 20 | let dictionary = ["key": "value"] 21 | let data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) 22 | HTTPStub.stubResult = .success(data) 23 | 24 | let expectation = self.expectation(description: "wait for response") 25 | let request = TestRequest() 26 | 27 | session.send(request) { response in 28 | switch response { 29 | case .success(let dictionary): 30 | XCTAssertEqual((dictionary as? [String: String])?["key"], "value") 31 | 32 | case .failure: 33 | XCTFail() 34 | } 35 | 36 | expectation.fulfill() 37 | } 38 | 39 | waitForExpectations(timeout: 10.0, handler: nil) 40 | } 41 | 42 | func testConnectionError() { 43 | let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil) 44 | HTTPStub.stubResult = .failure(error) 45 | 46 | let expectation = self.expectation(description: "wait for response") 47 | let request = TestRequest() 48 | 49 | session.send(request) { response in 50 | switch response { 51 | case .success: 52 | XCTFail() 53 | 54 | case .failure(let error): 55 | switch error { 56 | case .connectionError(let error as NSError): 57 | XCTAssertEqual(error.domain, NSURLErrorDomain) 58 | 59 | default: 60 | XCTFail() 61 | } 62 | } 63 | 64 | expectation.fulfill() 65 | } 66 | 67 | waitForExpectations(timeout: 10.0, handler: nil) 68 | } 69 | 70 | func testCancel() { 71 | let data = "{}".data(using: .utf8)! 72 | HTTPStub.stubResult = .success(data) 73 | 74 | let expectation = self.expectation(description: "wait for response") 75 | let request = TestRequest() 76 | 77 | session.send(request) { result in 78 | guard case .failure(let error) = result, 79 | case .connectionError(let connectionError as NSError) = error else { 80 | XCTFail() 81 | return 82 | } 83 | 84 | XCTAssertEqual(connectionError.code, NSURLErrorCancelled) 85 | 86 | expectation.fulfill() 87 | } 88 | 89 | DispatchQueue.main.async { 90 | self.session.cancelRequests(with: TestRequest.self) 91 | } 92 | 93 | waitForExpectations(timeout: 10.0, handler: nil) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/APIKitTests/SessionCallbackQueueTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | import XCTest 4 | 5 | class SessionCallbackQueueTests: XCTestCase { 6 | var adapter: TestSessionAdapter! 7 | var session: Session! 8 | 9 | override func setUpWithError() throws { 10 | try super.setUpWithError() 11 | 12 | adapter = TestSessionAdapter() 13 | adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: ["key": "value"], options: [])) 14 | 15 | session = Session(adapter: adapter, callbackQueue: .main) 16 | } 17 | 18 | func testMain() { 19 | let expectation = self.expectation(description: "wait for response") 20 | let request = TestRequest() 21 | 22 | session.send(request, callbackQueue: .main) { result in 23 | XCTAssert(Thread.isMainThread) 24 | expectation.fulfill() 25 | } 26 | 27 | waitForExpectations(timeout: 1.0, handler: nil) 28 | } 29 | 30 | func testSessionQueue() { 31 | let expectation = self.expectation(description: "wait for response") 32 | let request = TestRequest() 33 | 34 | session.send(request, callbackQueue: .sessionQueue) { result in 35 | // This depends on implementation of TestSessionAdapter 36 | XCTAssertTrue(Thread.isMainThread) 37 | expectation.fulfill() 38 | } 39 | 40 | waitForExpectations(timeout: 1.0, handler: nil) 41 | } 42 | 43 | func testOperationQueue() { 44 | let operationQueue = OperationQueue() 45 | let expectation = self.expectation(description: "wait for response") 46 | let request = TestRequest() 47 | 48 | session.send(request, callbackQueue: .operationQueue(operationQueue)) { result in 49 | XCTAssertEqual(OperationQueue.current, operationQueue) 50 | expectation.fulfill() 51 | } 52 | 53 | waitForExpectations(timeout: 1.0, handler: nil) 54 | } 55 | 56 | func testDispatchQueue() { 57 | let dispatchQueue = DispatchQueue.global(qos: .default) 58 | let expectation = self.expectation(description: "wait for response") 59 | let request = TestRequest() 60 | 61 | session.send(request, callbackQueue: .dispatchQueue(dispatchQueue)) { result in 62 | // There is no way to test current dispatch queue. 63 | XCTAssertFalse(Thread.isMainThread) 64 | expectation.fulfill() 65 | } 66 | 67 | waitForExpectations(timeout: 1.0, handler: nil) 68 | } 69 | 70 | // MARK: Test Session.callbackQueue 71 | func testImplicitSessionCallbackQueue() { 72 | let operationQueue = OperationQueue() 73 | let session = Session(adapter: adapter, callbackQueue: .operationQueue(operationQueue)) 74 | 75 | let expectation = self.expectation(description: "wait for response") 76 | let request = TestRequest() 77 | 78 | session.send(request) { result in 79 | XCTAssertEqual(OperationQueue.current, operationQueue) 80 | expectation.fulfill() 81 | } 82 | 83 | waitForExpectations(timeout: 1.0, handler: nil) 84 | } 85 | 86 | func testExplicitSessionCallbackQueue() { 87 | let operationQueue = OperationQueue() 88 | let session = Session(adapter: adapter, callbackQueue: .operationQueue(operationQueue)) 89 | 90 | let expectation = self.expectation(description: "wait for response") 91 | let request = TestRequest() 92 | 93 | session.send(request, callbackQueue: nil) { result in 94 | XCTAssertEqual(OperationQueue.current, operationQueue) 95 | expectation.fulfill() 96 | } 97 | 98 | waitForExpectations(timeout: 1.0, handler: nil) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/APIKitTests/SessionTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | import XCTest 4 | 5 | class SessionTests: XCTestCase { 6 | var adapter: TestSessionAdapter! 7 | var session: Session! 8 | 9 | override func setUp() { 10 | super.setUp() 11 | 12 | adapter = TestSessionAdapter() 13 | session = Session(adapter: adapter) 14 | } 15 | 16 | func testSuccess() throws { 17 | let dictionary = ["key": "value"] 18 | adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) 19 | 20 | let expectation = self.expectation(description: "wait for response") 21 | let request = TestRequest() 22 | 23 | session.send(request) { response in 24 | switch response { 25 | case .success(let dictionary): 26 | XCTAssertEqual((dictionary as? [String: String])?["key"], "value") 27 | 28 | case .failure: 29 | XCTFail() 30 | } 31 | 32 | expectation.fulfill() 33 | } 34 | 35 | waitForExpectations(timeout: 1.0, handler: nil) 36 | } 37 | 38 | // MARK: Response error 39 | func testParseDataError() { 40 | adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) 41 | 42 | let expectation = self.expectation(description: "wait for response") 43 | let request = TestRequest() 44 | 45 | session.send(request) { result in 46 | if case .failure(let error) = result, 47 | case .responseError(let responseError as NSError) = error { 48 | XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) 49 | XCTAssertEqual(responseError.code, 3840) 50 | } else { 51 | XCTFail() 52 | } 53 | 54 | expectation.fulfill() 55 | } 56 | 57 | waitForExpectations(timeout: 1.0, handler: nil) 58 | } 59 | 60 | func testUnacceptableStatusCodeError() { 61 | adapter.urlResponse = HTTPURLResponse(url: NSURL(string: "")! as URL, statusCode: 400, httpVersion: nil, headerFields: nil) 62 | 63 | let expectation = self.expectation(description: "wait for response") 64 | let request = TestRequest() 65 | 66 | session.send(request) { result in 67 | if case .failure(let error) = result, 68 | case .responseError(let responseError as ResponseError) = error, 69 | case .unacceptableStatusCode(let statusCode) = responseError { 70 | XCTAssertEqual(statusCode, 400) 71 | } else { 72 | XCTFail() 73 | } 74 | 75 | expectation.fulfill() 76 | } 77 | 78 | waitForExpectations(timeout: 1.0, handler: nil) 79 | } 80 | 81 | func testNonHTTPURLResponseError() { 82 | adapter.urlResponse = URLResponse() 83 | 84 | let expectation = self.expectation(description: "wait for response") 85 | let request = TestRequest() 86 | 87 | session.send(request) { result in 88 | if case .failure(let error) = result, 89 | case .responseError(let responseError as ResponseError) = error, 90 | case .nonHTTPURLResponse(let urlResponse) = responseError { 91 | XCTAssert(urlResponse === self.adapter.urlResponse) 92 | } else { 93 | XCTFail() 94 | } 95 | 96 | expectation.fulfill() 97 | } 98 | 99 | waitForExpectations(timeout: 1.0, handler: nil) 100 | } 101 | 102 | // MARK: Request error 103 | func testRequestError() { 104 | struct Error: Swift.Error {} 105 | 106 | let expectation = self.expectation(description: "wait for response") 107 | let request = TestRequest() { urlRequest in 108 | throw Error() 109 | } 110 | 111 | session.send(request) { result in 112 | if case .failure(let error) = result, 113 | case .requestError(let requestError) = error { 114 | XCTAssert(requestError is Error) 115 | } else { 116 | XCTFail() 117 | } 118 | 119 | expectation.fulfill() 120 | } 121 | 122 | waitForExpectations(timeout: 1.0, handler: nil) 123 | 124 | } 125 | 126 | // MARK: Cancel 127 | func testCancel() { 128 | let expectation = self.expectation(description: "wait for response") 129 | let request = TestRequest() 130 | 131 | session.send(request) { result in 132 | if case .failure(let error) = result, 133 | case .connectionError(let connectionError as NSError) = error { 134 | XCTAssertEqual(connectionError.code, 0) 135 | } else { 136 | XCTFail() 137 | } 138 | 139 | expectation.fulfill() 140 | } 141 | 142 | session.cancelRequests(with: TestRequest.self) 143 | 144 | waitForExpectations(timeout: 1.0, handler: nil) 145 | } 146 | 147 | func testCancelFilter() { 148 | let successExpectation = expectation(description: "wait for response") 149 | let successRequest = TestRequest(path: "/success") 150 | 151 | session.send(successRequest) { result in 152 | if case .failure = result { 153 | XCTFail() 154 | } 155 | 156 | successExpectation.fulfill() 157 | } 158 | 159 | let failureExpectation = expectation(description: "wait for response") 160 | let failureRequest = TestRequest(path: "/failure") 161 | 162 | session.send(failureRequest) { result in 163 | if case .success = result { 164 | XCTFail() 165 | } 166 | 167 | failureExpectation.fulfill() 168 | } 169 | 170 | session.cancelRequests(with: TestRequest.self) { request in 171 | return request.path == failureRequest.path 172 | } 173 | 174 | waitForExpectations(timeout: 1.0, handler: nil) 175 | } 176 | 177 | struct AnotherTestRequest: Request { 178 | typealias Response = Void 179 | 180 | var baseURL: URL { 181 | return URL(string: "https://example.com")! 182 | } 183 | 184 | var method: HTTPMethod { 185 | return .get 186 | } 187 | 188 | var path: String { 189 | return "/" 190 | } 191 | } 192 | 193 | func testCancelOtherRequest() { 194 | let successExpectation = expectation(description: "wait for response") 195 | let successRequest = AnotherTestRequest() 196 | 197 | session.send(successRequest) { result in 198 | if case .failure = result { 199 | XCTFail() 200 | } 201 | 202 | successExpectation.fulfill() 203 | } 204 | 205 | let failureExpectation = expectation(description: "wait for response") 206 | let failureRequest = TestRequest() 207 | 208 | session.send(failureRequest) { result in 209 | if case .success = result { 210 | XCTFail() 211 | } 212 | 213 | failureExpectation.fulfill() 214 | } 215 | 216 | session.cancelRequests(with: TestRequest.self) 217 | 218 | waitForExpectations(timeout: 1.0, handler: nil) 219 | } 220 | 221 | // MARK: Class methods 222 | func testSharedSession() { 223 | XCTAssert(Session.shared === Session.shared) 224 | } 225 | 226 | func testSubclassClassMethods() { 227 | class SessionSubclass: Session { 228 | static let testSesssion = SessionSubclass(adapter: TestSessionAdapter()) 229 | 230 | var functionCallFlags = [String: Bool]() 231 | 232 | override class var shared: Session { 233 | return testSesssion 234 | } 235 | 236 | override func send(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { 237 | functionCallFlags[(#function)] = true 238 | return super.send(request) 239 | } 240 | 241 | override func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) { 242 | functionCallFlags[(#function)] = true 243 | } 244 | } 245 | 246 | let testSession = SessionSubclass.testSesssion 247 | SessionSubclass.send(TestRequest()) 248 | SessionSubclass.cancelRequests(with: TestRequest.self) 249 | 250 | XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:handler:)"], true) 251 | XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Tests/APIKitTests/TestComponents/HTTPStub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | 4 | class HTTPStub: URLProtocol { 5 | static var stubResult: Result = .success(Data()) 6 | 7 | private var isCancelled = false 8 | 9 | // MARK: URLProtocol - 10 | 11 | override class func canInit(with: URLRequest) -> Bool { 12 | return true 13 | } 14 | 15 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 16 | return request 17 | } 18 | 19 | override func startLoading() { 20 | let response = HTTPURLResponse( 21 | url: request.url!, 22 | statusCode: 200, 23 | httpVersion: "1.1", 24 | headerFields: nil) 25 | 26 | let queue = DispatchQueue.global(qos: .default) 27 | 28 | queue.asyncAfter(deadline: .now() + 0.01) { 29 | guard !self.isCancelled else { 30 | return 31 | } 32 | 33 | self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed) 34 | 35 | switch HTTPStub.stubResult { 36 | case .success(let data): 37 | self.client?.urlProtocol(self, didLoad: data) 38 | 39 | case .failure(let error): 40 | self.client?.urlProtocol(self, didFailWithError: error) 41 | } 42 | 43 | self.client?.urlProtocolDidFinishLoading(self) 44 | } 45 | } 46 | 47 | override func stopLoading() { 48 | isCancelled = true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/APIKitTests/TestComponents/TestRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | 4 | struct TestRequest: Request { 5 | var absoluteURL: URL? { 6 | let urlRequest = try? buildURLRequest() 7 | return urlRequest?.url 8 | } 9 | 10 | // MARK: Request 11 | typealias Response = Any 12 | 13 | init(baseURL: String = "https://example.com", path: String = "/", method: HTTPMethod = .get, parameters: Any? = [:], headerFields: [String: String] = [:], interceptURLRequest: @escaping (URLRequest) throws -> URLRequest = { $0 }) { 14 | self.baseURL = URL(string: baseURL)! 15 | self.path = path 16 | self.method = method 17 | self.parameters = parameters 18 | self.headerFields = headerFields 19 | self.interceptURLRequest = interceptURLRequest 20 | } 21 | 22 | let baseURL: URL 23 | let method: HTTPMethod 24 | let path: String 25 | let parameters: Any? 26 | let headerFields: [String: String] 27 | let interceptURLRequest: (URLRequest) throws -> URLRequest 28 | 29 | func intercept(urlRequest: URLRequest) throws -> URLRequest { 30 | return try interceptURLRequest(urlRequest) 31 | } 32 | 33 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { 34 | return object 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/APIKitTests/TestComponents/TestSessionAdapter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | 4 | class TestSessionAdapter: SessionAdapter { 5 | enum Error: Swift.Error { 6 | case cancelled 7 | } 8 | 9 | var data: Data? 10 | var urlResponse: URLResponse? 11 | var error: Error? 12 | 13 | private class Runner { 14 | weak var adapter: TestSessionAdapter? 15 | 16 | @objc func run() { 17 | adapter?.executeAllTasks() 18 | } 19 | } 20 | 21 | private var tasks = [TestSessionTask]() 22 | private let runner: Runner 23 | private let timer: Timer 24 | 25 | init(data: Data? = Data(), urlResponse: URLResponse? = HTTPURLResponse(url: NSURL(string: "")! as URL, statusCode: 200, httpVersion: nil, headerFields: nil), error: Error? = nil) { 26 | self.data = data 27 | self.urlResponse = urlResponse 28 | self.error = error 29 | 30 | self.runner = Runner() 31 | self.timer = Timer.scheduledTimer(timeInterval: 0.001, 32 | target: runner, 33 | selector: #selector(Runner.run), 34 | userInfo: nil, 35 | repeats: true) 36 | 37 | self.runner.adapter = self 38 | } 39 | 40 | func executeAllTasks() { 41 | for task in tasks { 42 | if task.cancelled { 43 | task.handler(nil, nil, Error.cancelled) 44 | } else { 45 | task.handler(data, urlResponse, error) 46 | } 47 | } 48 | 49 | tasks = [] 50 | } 51 | 52 | func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { 53 | let task = TestSessionTask(handler: handler) 54 | tasks.append(task) 55 | 56 | return task 57 | } 58 | 59 | func getTasks(with handler: @escaping ([SessionTask]) -> Void) { 60 | handler(tasks.map { $0 }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/APIKitTests/TestComponents/TestSessionTask.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | 4 | class TestSessionTask: SessionTask { 5 | 6 | var handler: (Data?, URLResponse?, Error?) -> Void 7 | var cancelled = false 8 | 9 | init(handler: @escaping (Data?, URLResponse?, Error?) -> Void) { 10 | self.handler = handler 11 | } 12 | 13 | func resume() { 14 | 15 | } 16 | 17 | func cancel() { 18 | cancelled = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: Tests 3 | status: 4 | patch: false 5 | changes: false 6 | project: 7 | default: 8 | target: '80' 9 | 10 | comment: false 11 | 12 | --------------------------------------------------------------------------------