diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9a0800f0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + podspec: + runs-on: macos-12 + name: CocoaPods Lint + env: + DEVELOPER_DIR: "/Applications/Xcode_13.4.1.app/Contents/Developer" + steps: + - uses: actions/checkout@v2 + - run: pod lib lint --allow-warnings + + xcode: + name: ${{ matrix.xcode }} + runs-on: ${{ matrix.runsOn }} + env: + DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" + strategy: + fail-fast: false + matrix: + include: + - xcode: "Xcode_14.0.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 14.0.1, Swift 5.7" + - xcode: "Xcode_13.4.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" + - xcode: "Xcode_12.5.1" + runsOn: macOS-11 + name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" + - xcode: "Xcode_12" + runsOn: macOS-10.15 + name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 2 + - name: ${{ matrix.name }} + run: | + set -o pipefail + xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit | xcpretty -c + xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator -destination "name=iPhone 8" | xcpretty -c + xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator -destination "name=Apple TV" | xcpretty -c + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.2.1 + if: ${{ success() }} + + swiftpm: + name: SPM with ${{ matrix.xcode }} + runs-on: ${{ matrix.runsOn }} + env: + DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" + strategy: + fail-fast: false + matrix: + include: + - xcode: "Xcode_14.0.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 14.0.1, Swift 5.7" + action: swift test -c debug + - xcode: "Xcode_13.4.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" + action: swift test -c debug + - xcode: "Xcode_12.5.1" + runsOn: macOS-11 + name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" + action: swift test -c debug + - xcode: "Xcode_12" + runsOn: macOS-10.15 + name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" + action: swift build -c debug + steps: + - uses: actions/checkout@v2 + - name: ${{ matrix.name }} + run: ${{ matrix.action }} diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 00f2c4d7..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "Carthage/Checkouts/Result"] - path = Carthage/Checkouts/Result - url = https://github.com/antitypical/Result.git diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 5186d070..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.0 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 53d24f72..00000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -env: - global: - - LC_CTYPE=en_US.UTF-8 - -matrix: - include: - - os: osx - language: objective-c - osx_image: xcode9.2 - script: - - set -o pipefail - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release ENABLE_TESTABILITY=YES | xcpretty -c - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release -sdk iphonesimulator -destination "name=iPhone 6s" ENABLE_TESTABILITY=YES | xcpretty -c - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release -sdk appletvsimulator -destination "name=Apple TV" ENABLE_TESTABILITY=YES | xcpretty -c - after_success: - - bash <(curl -s https://codecov.io/bash) - - os: osx - language: objective-c - osx_image: xcode9.4 - script: - - set -o pipefail - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release ENABLE_TESTABILITY=YES | xcpretty -c - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release -sdk iphonesimulator -destination "name=iPhone 6s" ENABLE_TESTABILITY=YES | xcpretty -c - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release -sdk appletvsimulator -destination "name=Apple TV" ENABLE_TESTABILITY=YES | xcpretty -c - after_success: - - bash <(curl -s https://codecov.io/bash) - - os: osx - language: objective-c - osx_image: xcode10 - script: - - set -o pipefail - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release ENABLE_TESTABILITY=YES | xcpretty -c - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release -sdk iphonesimulator -destination "name=iPhone 6s" ENABLE_TESTABILITY=YES | xcpretty -c - - xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit -configuration Release -sdk appletvsimulator -destination "name=Apple TV" ENABLE_TESTABILITY=YES | xcpretty -c - after_success: - - bash <(curl -s https://codecov.io/bash) - - os: osx - language: generic - osx_image: xcode9.2 - script: - - swift build - - swift test - - os: osx - language: generic - osx_image: xcode9.4 - script: - - swift build - - swift test - - os: osx - language: generic - osx_image: xcode10 - script: - - swift build - - swift test - -notifications: - email: false diff --git a/APIKit.podspec b/APIKit.podspec index 4a32f309..f93510f8 100644 --- a/APIKit.podspec +++ b/APIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "APIKit" - s.version = "4.0.0" + s.version = "5.4.0" s.summary = "Type-safe networking abstraction layer that associates request type with response type." s.homepage = "https://github.com/ishkawa/APIKit" @@ -8,7 +8,7 @@ Pod::Spec.new do |s| "Yosuke Ishikawa" => "y@ishkawa.org" } - s.ios.deployment_target = "8.0" + s.ios.deployment_target = "9.0" s.osx.deployment_target = "10.10" if s.respond_to?(:watchos) s.watchos.deployment_target = "2.0" @@ -23,7 +23,7 @@ Pod::Spec.new do |s| :tag => "#{s.version}", } - s.swift_version = "4.0" + s.swift_version = "5.0" s.license = { :type => "MIT", @@ -34,6 +34,4 @@ Pod::Spec.new do |s| 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. LICENSE } - - s.dependency "Result", "~> 4.0" end diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 0e672020..e460ab57 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -7,9 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 141F12201C1C9ABE0026D415 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; - 141F12311C1C9AC70026D415 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; }; - 141F12361C1C9AC70026D415 /* Result.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CD5115241B1FFBA900514240 /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0969AE0E259DEC6D00C498AF /* Combine.swift */; }; + 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0973EE34259E2DDC00879BA2 /* CombineTests.swift */; }; 7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */; }; 7F698E511D9D680C00F1561D /* JSONBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3D1D9D680C00F1561D /* JSONBodyParametersTests.swift */; }; 7F698E521D9D680C00F1561D /* MultipartFormDataParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3E1D9D680C00F1561D /* MultipartFormDataParametersTests.swift */; }; @@ -22,7 +21,6 @@ 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */; }; 7F698E5B1D9D680C00F1561D /* SessionCallbackQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */; }; 7F698E5C1D9D680C00F1561D /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4A1D9D680C00F1561D /* SessionTests.swift */; }; - 7F698E5D1D9D680C00F1561D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = 7F698E4B1D9D680C00F1561D /* test.json */; }; 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4D1D9D680C00F1561D /* TestRequest.swift */; }; 7F698E5F1D9D680C00F1561D /* TestSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */; }; 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */; }; @@ -49,6 +47,9 @@ 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048EE1D9D8A12003C99F6 /* SessionTaskError.swift */; }; 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */; }; 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */; }; + C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5725F4A28D8C36500810D7C /* Concurrency.swift */; }; + C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; }; + C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */; }; ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */; }; ECA8314C1DE4E677004EB1B5 /* ProtobufBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */; }; @@ -72,7 +73,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 141F12361C1C9AC70026D415 /* Result.framework in Copy Frameworks */, ); name = "Copy Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -80,6 +80,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0969AE0E259DEC6D00C498AF /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = ""; }; + 0973EE34259E2DDC00879BA2 /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; 141F120F1C1C96820026D415 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Configurations/Base.xcconfig; sourceTree = ""; }; 141F12101C1C96820026D415 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configurations/Debug.xcconfig; sourceTree = ""; }; 141F12111C1C96820026D415 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = ""; }; @@ -100,7 +102,6 @@ 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionAdapterTests.swift; sourceTree = ""; }; 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionCallbackQueueTests.swift; sourceTree = ""; }; 7F698E4A1D9D680C00F1561D /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; - 7F698E4B1D9D680C00F1561D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; 7F698E4D1D9D680C00F1561D /* TestRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestRequest.swift; sourceTree = ""; }; 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionAdapter.swift; sourceTree = ""; }; 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionTask.swift; sourceTree = ""; }; @@ -129,7 +130,9 @@ 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLEncodedSerialization.swift; path = Sources/APIKit/Serializations/URLEncodedSerialization.swift; sourceTree = SOURCE_ROOT; }; 7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPStub.swift; sourceTree = ""; }; - CD5115241B1FFBA900514240 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C5725F4A28D8C36500810D7C /* Concurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = ""; }; + C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; + C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufDataParser.swift; path = Sources/APIKit/DataParser/ProtobufDataParser.swift; sourceTree = SOURCE_ROOT; }; ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtobufDataParserTests.swift; sourceTree = ""; }; ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufBodyParameters.swift; path = Sources/APIKit/BodyParameters/ProtobufBodyParameters.swift; sourceTree = SOURCE_ROOT; }; @@ -141,7 +144,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 141F12201C1C9ABE0026D415 /* Result.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -149,13 +151,29 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 141F12311C1C9AC70026D415 /* Result.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0969AE0D259DEC3C00C498AF /* Combine */ = { + isa = PBXGroup; + children = ( + 0969AE0E259DEC6D00C498AF /* Combine.swift */, + ); + name = Combine; + path = APIKit/Combine; + sourceTree = ""; + }; + 0973EE33259E2DD000879BA2 /* Combine */ = { + isa = PBXGroup; + children = ( + 0973EE34259E2DDC00879BA2 /* CombineTests.swift */, + ); + path = Combine; + sourceTree = ""; + }; 141F120E1C1C96690026D415 /* Configurations */ = { isa = PBXGroup; children = ( @@ -187,7 +205,8 @@ ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */, 7F7048D81D9D89FB003C99F6 /* AbstractInputStream.m */, ); - path = BodyParameters; + name = BodyParameters; + path = APIKit/BodyParameters; sourceTree = ""; }; 7F18BD161C9730ED003A31DF /* Serializations */ = { @@ -195,7 +214,8 @@ children = ( 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */, ); - path = Serializations; + name = Serializations; + path = APIKit/Serializations; sourceTree = ""; }; 7F45FCD31A94D02C006863BB = { @@ -224,10 +244,13 @@ 7F698E451D9D680C00F1561D /* RequestTests.swift */, 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */, 7F698E4A1D9D680C00F1561D /* SessionTests.swift */, + C5B144D628D8D7D000E30ECD /* Concurrency */, + 0973EE33259E2DD000879BA2 /* Combine */, 7F698E3B1D9D680C00F1561D /* BodyParametersType */, 7F698E401D9D680C00F1561D /* DataParserType */, 7F698E461D9D680C00F1561D /* SessionAdapterType */, 7F698E4C1D9D680C00F1561D /* TestComponents */, + C5FF1DBF28A80FFD0059573D /* Resources */, 7F698E611D9D681500F1561D /* Supporting Files */, ); path = APIKitTests; @@ -280,7 +303,6 @@ isa = PBXGroup; children = ( 7F698E441D9D680C00F1561D /* Info.plist */, - 7F698E4B1D9D680C00F1561D /* test.json */, ); name = "Supporting Files"; sourceTree = ""; @@ -293,6 +315,8 @@ 7F7048CA1D9D89BE003C99F6 /* Request.swift */, 7F7048CB1D9D89BE003C99F6 /* Session.swift */, 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */, + C5725F4928D8C36500810D7C /* Concurrency */, + 0969AE0D259DEC3C00C498AF /* Combine */, 7F85FB8B1C9D317300CEE132 /* SessionAdapter */, 7F18BD0D1C972C38003A31DF /* BodyParameters */, 7FA19A441C9CC9A2005D25AE /* DataParser */, @@ -308,7 +332,6 @@ children = ( 7F7048C61D9D89BE003C99F6 /* APIKit.h */, 7F7048C91D9D89BE003C99F6 /* Info.plist */, - CD5115241B1FFBA900514240 /* Result.framework */, ); name = "Supporting Files"; sourceTree = ""; @@ -319,7 +342,8 @@ 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */, 7F7048D51D9D89F2003C99F6 /* URLSessionAdapter.swift */, ); - path = SessionAdapter; + name = SessionAdapter; + path = APIKit/SessionAdapter; sourceTree = ""; }; 7FA19A3D1C9CBF2A005D25AE /* Error */ = { @@ -329,7 +353,8 @@ 7F7048EC1D9D8A12003C99F6 /* RequestError.swift */, 7F7048ED1D9D8A12003C99F6 /* ResponseError.swift */, ); - path = Error; + name = Error; + path = APIKit/Error; sourceTree = ""; }; 7FA19A441C9CC9A2005D25AE /* DataParser */ = { @@ -341,7 +366,33 @@ ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */, 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */, ); - path = DataParser; + name = DataParser; + path = APIKit/DataParser; + sourceTree = ""; + }; + C5725F4928D8C36500810D7C /* Concurrency */ = { + isa = PBXGroup; + children = ( + C5725F4A28D8C36500810D7C /* Concurrency.swift */, + ); + name = Concurrency; + path = APIKit/Concurrency; + sourceTree = ""; + }; + C5B144D628D8D7D000E30ECD /* Concurrency */ = { + isa = PBXGroup; + children = ( + C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */, + ); + path = Concurrency; + sourceTree = ""; + }; + C5FF1DBF28A80FFD0059573D /* Resources */ = { + isa = PBXGroup; + children = ( + C5FF1DC028A80FFD0059573D /* test.json */, + ); + path = Resources; sourceTree = ""; }; /* End PBXGroup section */ @@ -402,7 +453,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Yosuke Ishikawa"; TargetAttributes = { 141F12161C1C9ABE0026D415 = { @@ -415,7 +466,7 @@ }; buildConfigurationList = 7F45FCD71A94D02C006863BB /* Build configuration list for PBXProject "APIKit" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -444,7 +495,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7F698E5D1D9D680C00F1561D /* test.json in Resources */, + C5FF1DC128A80FFD0059573D /* test.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -467,6 +518,7 @@ 7F7048E01D9D89FB003C99F6 /* Data+InputStream.swift in Sources */, 7F7048DF1D9D89FB003C99F6 /* BodyParameters.swift in Sources */, 7F7048E21D9D89FB003C99F6 /* JSONBodyParameters.swift in Sources */, + C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */, 7F7048D61D9D89F2003C99F6 /* SessionAdapter.swift in Sources */, 7F7048EF1D9D8A12003C99F6 /* RequestError.swift in Sources */, 7F7048E91D9D8A08003C99F6 /* FormURLEncodedDataParser.swift in Sources */, @@ -476,6 +528,7 @@ ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */, 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */, 7F7048D71D9D89F2003C99F6 /* URLSessionAdapter.swift in Sources */, + 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */, 7F7048EB1D9D8A08003C99F6 /* StringDataParser.swift in Sources */, 7F7048CF1D9D89BE003C99F6 /* HTTPMethod.swift in Sources */, ); @@ -492,7 +545,9 @@ 7F698E581D9D680C00F1561D /* RequestTests.swift in Sources */, ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */, 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */, + C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */, 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, + 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */, 7F698E561D9D680C00F1561D /* StringDataParserTests.swift in Sources */, @@ -562,6 +617,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 141F12101C1C96820026D415 /* Debug.xcconfig */; buildSettings = { + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; @@ -575,7 +631,7 @@ ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -583,6 +639,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 141F12111C1C96820026D415 /* Release.xcconfig */; buildSettings = { + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; @@ -594,7 +651,7 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; GCC_NO_COMMON_BLOCKS = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit.xcscheme b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit.xcscheme index 8125e06b..762479b4 100644 --- a/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit.xcscheme +++ b/APIKit.xcodeproj/xcshareddata/xcschemes/APIKit.xcscheme @@ -1,6 +1,6 @@ - - diff --git a/Cartfile b/Cartfile deleted file mode 100644 index e9bc3ad0..00000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -github "antitypical/Result" ~> 4.0 diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index 23aa9709..00000000 --- a/Cartfile.resolved +++ /dev/null @@ -1 +0,0 @@ -github "antitypical/Result" "4.0.0" diff --git a/Carthage/Checkouts/Result b/Carthage/Checkouts/Result deleted file mode 160000 index 8fc088dc..00000000 --- a/Carthage/Checkouts/Result +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8fc088dcf72802801efeecba76ea8fb041fb773d diff --git a/Configurations/APIKit.xcconfig b/Configurations/APIKit.xcconfig index f84c0571..8b0bdbfc 100644 --- a/Configurations/APIKit.xcconfig +++ b/Configurations/APIKit.xcconfig @@ -22,3 +22,4 @@ LD_RUNPATH_SEARCH_PATHS[sdk=watch*] = $(inherited) @executable_path/Frameworks @ LD_RUNPATH_SEARCH_PATHS[sdk=appletv*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks APPLICATION_EXTENSION_API_ONLY = YES; +BUILD_LIBRARY_FOR_DISTRIBUTION = YES; diff --git a/Configurations/Base.xcconfig b/Configurations/Base.xcconfig index cf59d8cd..3a036335 100644 --- a/Configurations/Base.xcconfig +++ b/Configurations/Base.xcconfig @@ -27,6 +27,6 @@ VERSIONING_SYSTEM = apple-generic CODE_SIGNING_REQUIRED = NO CODE_SIGN_IDENTITY = MACOSX_DEPLOYMENT_TARGET = 10.10 -IPHONEOS_DEPLOYMENT_TARGET = 8.0 +IPHONEOS_DEPLOYMENT_TARGET = 9.0 WATCHOS_DEPLOYMENT_TARGET = 2.0 TVOS_DEPLOYMENT_TARGET = 9.0 diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground index b1148fc5..a8211e59 100644 --- a/Demo.playground/contents.xcplayground +++ b/Demo.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index dc09c894..00000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "Result", - "repositoryURL": "https://github.com/antitypical/Result.git", - "state": { - "branch": null, - "revision": "8fc088dcf72802801efeecba76ea8fb041fb773d", - "version": "4.0.0" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 0b9f1c72..e55d20b6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,26 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.3 import PackageDescription let package = Package( name: "APIKit", + platforms: [ + .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) + ], products: [ .library(name: "APIKit", targets: ["APIKit"]), ], - dependencies: [ - .package(url: "https://github.com/antitypical/Result.git", from: "4.0.0"), - ], + dependencies: [], targets: [ .target( name: "APIKit", - dependencies: ["Result"], + dependencies: [], exclude: ["BodyParameters/AbstractInputStream.m"] ), .testTarget( name: "APIKitTests", - dependencies: ["APIKit"] + dependencies: ["APIKit"], + resources: [.process("Resources")] ), ], - swiftLanguageVersions: [4] + swiftLanguageVersions: [.v5] ) diff --git a/Package@swift-4.2.swift b/Package@swift-4.2.swift deleted file mode 100644 index 9021d1eb..00000000 --- a/Package@swift-4.2.swift +++ /dev/null @@ -1,24 +0,0 @@ -// swift-tools-version:4.2 -import PackageDescription - -let package = Package( - name: "APIKit", - products: [ - .library(name: "APIKit", targets: ["APIKit"]), - ], - dependencies: [ - .package(url: "https://github.com/antitypical/Result.git", from: "4.0.0"), - ], - targets: [ - .target( - name: "APIKit", - dependencies: ["Result"], - exclude: ["BodyParameters/AbstractInputStream.m"] - ), - .testTarget( - name: "APIKitTests", - dependencies: ["APIKit"] - ), - ], - swiftLanguageVersions: [.v4, .v4_2] -) diff --git a/README.md b/README.md index 6dd793e3..45c1bed7 100644 --- a/README.md +++ b/README.md @@ -30,28 +30,32 @@ Session.send(request) { result in ## Requirements -- Swift 3.0 or later -- iOS 8.0 or later +- Swift 5.3 or later +- iOS 9.0 or later - Mac OS 10.10 or later - watchOS 2.0 or later - tvOS 9.0 or later If you use Swift 2.2 or 2.3, try [APIKit 2.0.5](https://github.com/ishkawa/APIKit/tree/2.0.5). +If you use Swift 4.2 or before, try [APIKit 4.1.0](https://github.com/ishkawa/APIKit/tree/4.1.0). + +If you use Swift 5.2 or before, try [APIKit 5.3.0](https://github.com/ishkawa/APIKit/tree/5.3.0). + ## Installation #### [Carthage](https://github.com/Carthage/Carthage) -- Insert `github "ishkawa/APIKit" ~> 3.1` to your Cartfile. +- Insert `github "ishkawa/APIKit" ~> 5.0` to your Cartfile. - Run `carthage update`. -- Link your app with `APIKit.framework` and `Result.framework` in `Carthage/Build`. +- Link your app with `APIKit.framework` in `Carthage/Build`. #### [CocoaPods](https://github.com/cocoapods/cocoapods) -- Insert `pod 'APIKit', '~> 3.1'` to your Podfile. +- Insert `pod 'APIKit', '~> 5.0'` to your Podfile. - Run `pod install`. -Note: CocoaPods 1.1.0 is required to install APIKit 3. +Note: CocoaPods 1.4.0 is required to install APIKit 5. ## Documentation diff --git a/Sources/APIKit/BodyParameters/JSONBodyParameters.swift b/Sources/APIKit/BodyParameters/JSONBodyParameters.swift index 3f541917..4388ce34 100644 --- a/Sources/APIKit/BodyParameters/JSONBodyParameters.swift +++ b/Sources/APIKit/BodyParameters/JSONBodyParameters.swift @@ -32,3 +32,12 @@ public struct JSONBodyParameters: BodyParameters { return .data(try JSONSerialization.data(withJSONObject: JSONObject, options: writingOptions)) } } + +extension JSONBodyParameters: ExpressibleByDictionaryLiteral { + public typealias Key = String + public typealias Value = Any + + public init(dictionaryLiteral elements: (Key, Value)...) { + self.init(JSONObject: Dictionary(uniqueKeysWithValues: elements)) + } +} diff --git a/Sources/APIKit/BodyParameters/MultipartFormDataBodyParameters.swift b/Sources/APIKit/BodyParameters/MultipartFormDataBodyParameters.swift index 0b1e2d2d..d1337978 100644 --- a/Sources/APIKit/BodyParameters/MultipartFormDataBodyParameters.swift +++ b/Sources/APIKit/BodyParameters/MultipartFormDataBodyParameters.swift @@ -58,7 +58,7 @@ public struct MultipartFormDataBodyParameters: BodyParameters { public extension MultipartFormDataBodyParameters { /// Part represents single part of multipart/form-data. - public struct Part { + struct Part { public enum Error: Swift.Error { case illegalValue(Any) case illegalFileURL(URL) diff --git a/Sources/APIKit/Combine/Combine.swift b/Sources/APIKit/Combine/Combine.swift new file mode 100644 index 00000000..6e3223c0 --- /dev/null +++ b/Sources/APIKit/Combine/Combine.swift @@ -0,0 +1,90 @@ +#if canImport(Combine) + +import Foundation +import Combine + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public struct SessionTaskPublisher: Publisher { + /// The kind of values published by this publisher. + public typealias Output = Request.Response + + /// The kind of errors this publisher might publish. + public typealias Failure = SessionTaskError + + private let request: Request + private let session: Session + private let callbackQueue: CallbackQueue? + + public init(request: Request, session: Session, callbackQueue: CallbackQueue?) { + self.request = request + self.session = session + self.callbackQueue = callbackQueue + } + + public func receive(subscriber: S) where S: Subscriber, S.Failure == SessionTaskPublisher.Failure, S.Input == SessionTaskPublisher.Output { + subscriber.receive(subscription: SessionTaskSubscription(request: request, + session: session, + callbackQueue: callbackQueue, + downstream: subscriber)) + } + + private final class SessionTaskSubscription: Subscription where Request.Response == Downstream.Input, Downstream.Failure == Failure { + + private let request: Request + private let session: Session + private let callbackQueue: CallbackQueue? + private var downstream: Downstream? + private var task: SessionTask? + + init(request: Request, session: Session, callbackQueue: CallbackQueue?, downstream: Downstream) { + self.request = request + self.session = session + self.callbackQueue = callbackQueue + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + guard let downstream = self.downstream else { return } + self.downstream = nil + task = session.send(request, callbackQueue: callbackQueue) { result in + switch result { + case .success(let response): + _ = downstream.receive(response) + downstream.receive(completion: .finished) + case .failure(let error): + downstream.receive(completion: .failure(error)) + } + } + } + + func cancel() { + task?.cancel() + downstream = nil + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Session { + /// Calls `sessionTaskPublisher(for:callbackQueue:)` of `Session.shared`. + /// + /// - parameter request: The request to be sent. + /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. + /// - returns: A publisher that wraps a session task for the request. + static func sessionTaskPublisher(for request: Request, callbackQueue: CallbackQueue? = nil) -> SessionTaskPublisher { + return SessionTaskPublisher(request: request, session: .shared, callbackQueue: callbackQueue) + } + + /// Returns a publisher that wraps a session task for the request. + /// + /// The publisher publishes `Request.Response` when the task completes, or terminates if the task fails with an error. + /// - parameter request: The request to be sent. + /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. + /// - returns: A publisher that wraps a session task for the request. + func sessionTaskPublisher(for request: Request, callbackQueue: CallbackQueue? = nil) -> SessionTaskPublisher { + return SessionTaskPublisher(request: request, session: self, callbackQueue: callbackQueue) + } +} + +#endif diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift new file mode 100644 index 00000000..c0518e35 --- /dev/null +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -0,0 +1,60 @@ +#if compiler(>=5.5.2) && canImport(_Concurrency) + +import Foundation + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Session { + /// Calls `response(for:callbackQueue:)` of `Session.shared`. + /// + /// - parameter request: The request to be sent. + /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. + /// - returns: `Request.Response` + static func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { + return try await shared.response(for: request, callbackQueue: callbackQueue) + } + + /// Convenience method to load `Request.Response` using an `Request`, creates and resumes an `SessionTask` internally. + /// + /// - parameter request: The request to be sent. + /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. + /// - returns: `Request.Response` + func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { + let cancellationHandler = SessionTaskCancellationHandler() + return try await withTaskCancellationHandler(operation: { + return try await withCheckedThrowingContinuation { continuation in + Task { + let sessionTask = createSessionTask(request, callbackQueue: callbackQueue) { result in + continuation.resume(with: result) + } + await cancellationHandler.register(with: sessionTask) + if await cancellationHandler.isTaskCancelled { + sessionTask?.cancel() + } else { + sessionTask?.resume() + } + } + } + }, onCancel: { + Task { await cancellationHandler.cancel() } + }) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private actor SessionTaskCancellationHandler { + private var sessionTask: SessionTask? + private(set) var isTaskCancelled = false + + func register(with task: SessionTask?) { + guard !isTaskCancelled else { return } + guard sessionTask == nil else { return } + sessionTask = task + } + + func cancel() { + isTaskCancelled = true + sessionTask?.cancel() + } +} + +#endif diff --git a/Sources/APIKit/Info.plist b/Sources/APIKit/Info.plist index 0e9f1f71..9b01e54a 100644 --- a/Sources/APIKit/Info.plist +++ b/Sources/APIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.0.0 + 5.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/APIKit/Request.swift b/Sources/APIKit/Request.swift index e65e3819..ea6c5dae 100644 --- a/Sources/APIKit/Request.swift +++ b/Sources/APIKit/Request.swift @@ -1,5 +1,4 @@ import Foundation -import Result /// `Request` protocol represents a request for Web API. /// Following 5 items must be implemented. @@ -62,11 +61,11 @@ public protocol Request { } public extension Request { - public var parameters: Any? { + var parameters: Any? { return nil } - public var queryParameters: [String: Any]? { + var queryParameters: [String: Any]? { guard let parameters = parameters as? [String: Any], method.prefersQueryParameters else { return nil } @@ -74,7 +73,7 @@ public extension Request { return parameters } - public var bodyParameters: BodyParameters? { + var bodyParameters: BodyParameters? { guard let parameters = parameters, !method.prefersQueryParameters else { return nil } @@ -82,19 +81,19 @@ public extension Request { return JSONBodyParameters(JSONObject: parameters) } - public var headerFields: [String: String] { + var headerFields: [String: String] { return [:] } - public var dataParser: DataParser { + var dataParser: DataParser { return JSONDataParser(readingOptions: []) } - public func intercept(urlRequest: URLRequest) throws -> URLRequest { + func intercept(urlRequest: URLRequest) throws -> URLRequest { return urlRequest } - public func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any { + func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any { guard 200..<300 ~= urlResponse.statusCode else { throw ResponseError.unacceptableStatusCode(urlResponse.statusCode) } @@ -103,7 +102,7 @@ public extension Request { /// Builds `URLRequest` from properties of `self`. /// - Throws: `RequestError`, `Error` - public func buildURLRequest() throws -> URLRequest { + func buildURLRequest() throws -> URLRequest { let url = path.isEmpty ? baseURL : baseURL.appendingPathComponent(path) guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { throw RequestError.invalidBaseURL(baseURL) @@ -140,9 +139,15 @@ public extension Request { /// Builds `Response` from response `Data`. /// - Throws: `ResponseError`, `Error` - public func parse(data: Data, urlResponse: HTTPURLResponse) throws -> Response { + func parse(data: Data, urlResponse: HTTPURLResponse) throws -> Response { let parsedObject = try dataParser.parse(data: data) let passedObject = try intercept(object: parsedObject, urlResponse: urlResponse) return try response(from: passedObject, urlResponse: urlResponse) } } + +public extension Request where Response == Void { + func response(from object: Any, urlResponse: HTTPURLResponse) throws { + return + } +} diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 72a9405e..85fb9615 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -1,5 +1,4 @@ import Foundation -import Result private var taskRequestKey = 0 @@ -31,7 +30,7 @@ open class Session { return privateShared } - /// Calls `send(_:handler:)` of `sharedSession`. + /// Calls `send(_:callbackQueue:handler:)` of `Session.shared`. /// - parameter request: The request to be sent. /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. /// - parameter handler: The closure that receives result of the request. @@ -41,7 +40,7 @@ open class Session { return shared.send(request, callbackQueue: callbackQueue, handler: handler) } - /// Calls `cancelRequests(with:passingTest:)` of `sharedSession`. + /// Calls `cancelRequests(with:passingTest:)` of `Session.shared`. open class func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { shared.cancelRequests(with: requestType, passingTest: test) } @@ -56,8 +55,30 @@ open class Session { /// - returns: The new session task. @discardableResult open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - let callbackQueue = callbackQueue ?? self.callbackQueue + let task = createSessionTask(request, callbackQueue: callbackQueue, handler: handler) + task?.resume() + return task + } + + /// Cancels requests that passes the test. + /// - parameter requestType: The request type to cancel. + /// - parameter test: The test closure that determines if a request should be cancelled or not. + open func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { + adapter.getTasks { [weak self] tasks in + tasks + .filter { task in + if let request = self?.requestForTask(task) as Request? { + return test(request) + } else { + return false + } + } + .forEach { $0.cancel() } + } + } + internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { + let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest do { urlRequest = try request.buildURLRequest() @@ -92,28 +113,10 @@ open class Session { } setRequest(request, forTask: task) - task.resume() return task } - /// Cancels requests that passes the test. - /// - parameter requestType: The request type to cancel. - /// - parameter test: The test closure that determines if a request should be cancelled or not. - open func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { - adapter.getTasks { [weak self] tasks in - return tasks - .filter { task in - if let request = self?.requestForTask(task) as Request? { - return test(request) - } else { - return false - } - } - .forEach { $0.cancel() } - } - } - private func setRequest(_ request: Request, forTask task: SessionTask) { objc_setAssociatedObject(task, &taskRequestKey, request, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index d123dae0..aa71ac5d 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -1,7 +1,7 @@ import Foundation /// `SessionTask` protocol represents a task for a request. -public protocol SessionTask: class { +public protocol SessionTask: AnyObject { func resume() func cancel() } diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index 6caad171..38ac15ef 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -15,7 +15,7 @@ private var taskAssociatedObjectCompletionHandlerKey = 0 /// `URLSession(_:task: didCompleteWithError:)` and `URLSession(_:dataTask:didReceiveData:)`, you have to call /// `super` in these methods if you implement them. open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate { - /// The undelying `URLSession` instance. + /// The underlying `URLSession` instance. open var urlSession: URLSession! /// Returns `URLSessionAdapter` initialized with `URLSessionConfiguration`. @@ -37,11 +37,8 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS /// Aggregates `URLSessionTask` instances in `URLSession` using `getTasksWithCompletionHandler(_:)`. open func getTasks(with handler: @escaping ([SessionTask]) -> Void) { urlSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in - let allTasks = dataTasks as [URLSessionTask] - + uploadTasks as [URLSessionTask] - + downloadTasks as [URLSessionTask] - - handler(allTasks.map { $0 }) + let allTasks: [URLSessionTask] = dataTasks + uploadTasks + downloadTasks + handler(allTasks) } } diff --git a/Sources/APIKit/Unavailable.swift b/Sources/APIKit/Unavailable.swift index 92765230..2c05f7f6 100644 --- a/Sources/APIKit/Unavailable.swift +++ b/Sources/APIKit/Unavailable.swift @@ -6,8 +6,6 @@ // Copyright © 2016 Yosuke Ishikawa. All rights reserved. // -import Result - // MARK: - Protocols @available(*, unavailable, renamed: "Request") diff --git a/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift b/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift index 006c8e04..0bc9b535 100644 --- a/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift @@ -3,37 +3,47 @@ import XCTest import APIKit class JSONBodyParametersTests: XCTestCase { - func testJSONSuccess() { + func testJSONSuccess() throws { let object = ["foo": 1, "bar": 2, "baz": 3] let parameters = JSONBodyParameters(JSONObject: object) XCTAssertEqual(parameters.contentType, "application/json") - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let dictionary = try JSONSerialization.jsonObject(with: data, options: []) - XCTAssertEqual((dictionary as? [String: Int])?["foo"], 1) - XCTAssertEqual((dictionary as? [String: Int])?["bar"], 2) - XCTAssertEqual((dictionary as? [String: Int])?["baz"], 3) - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let dictionary = try JSONSerialization.jsonObject(with: data, options: []) + XCTAssertEqual((dictionary as? [String: Int])?["foo"], 1) + XCTAssertEqual((dictionary as? [String: Int])?["bar"], 2) + XCTAssertEqual((dictionary as? [String: Int])?["baz"], 3) } func testJSONFailure() { let object = NSObject() let parameters = JSONBodyParameters(JSONObject: object) - do { - try _ = parameters.buildEntity() - XCTFail() - } catch { + XCTAssertThrowsError(try parameters.buildEntity()) { error in let nserror = error as NSError XCTAssertEqual(nserror.domain, NSCocoaErrorDomain) XCTAssertEqual(nserror.code, 3840) } } + + func testDictionaryLiteral() throws { + let object = ["foo": 1, "bar": 2, "baz": 3] + + let parameters1: JSONBodyParameters = .init(JSONObject: object) + let parameters2: JSONBodyParameters = ["foo": 1, "bar": 2, "baz": 3] + + guard case .data(let data1) = try parameters1.buildEntity(), + case .data(let data2) = try parameters2.buildEntity() else { + XCTFail() + return + } + let dictionary1 = try JSONSerialization.jsonObject(with: data1, options: []) + let dictionary2 = try JSONSerialization.jsonObject(with: data2, options: []) + XCTAssertEqual((dictionary1 as? [String: Int])?["foo"], (dictionary2 as? [String: Int])?["foo"]) + XCTAssertEqual((dictionary1 as? [String: Int])?["bar"], (dictionary2 as? [String: Int])?["bar"]) + XCTAssertEqual((dictionary1 as? [String: Int])?["baz"], (dictionary2 as? [String: Int])?["baz"]) + } } diff --git a/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift b/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift index 4a0536dc..faf68949 100644 --- a/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift @@ -4,155 +4,130 @@ import XCTest class MultipartFormDataParametersTests: XCTestCase { // MARK: Entity - func testDataEntitySuccess() { - let value1 = "1".data(using: .utf8)! - let value2 = "2".data(using: .utf8)! + func testDataEntitySuccess() throws { + let value1 = try XCTUnwrap("1".data(using: .utf8)) + let value2 = try XCTUnwrap("2".data(using: .utf8)) let parameters = MultipartFormDataBodyParameters(parts: [ MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), ]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let encodedData = String(data: data, encoding:.utf8)! - let returnCode = "\r\n" - - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: parameters.contentType.count) - let match = regexp.matches(in: parameters.contentType, options: [], range: range) - XCTAssertTrue(match.count > 0) - - let boundary = (parameters.contentType as NSString).substring(with: match.first!.range(at: 1)) - XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.count) + let match = regexp.matches(in: parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let firstRange = try XCTUnwrap(match.first?.range(at: 1)) + let boundary = (parameters.contentType as NSString).substring(with: firstRange) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") } - func testInputStreamEntitySuccess() { - let value1 = "1".data(using: .utf8)! - let value2 = "2".data(using: .utf8)! + func testInputStreamEntitySuccess() throws { + let value1 = try XCTUnwrap("1".data(using: .utf8)) + let value2 = try XCTUnwrap("2".data(using: .utf8)) let parameters = MultipartFormDataBodyParameters(parts: [ MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), ], entityType: .inputStream) - do { - guard case .inputStream(let inputStream) = try parameters.buildEntity() else { - XCTFail() - return - } - - let data = try Data(inputStream: inputStream) - let encodedData = String(data: data, encoding:.utf8)! - let returnCode = "\r\n" - - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: parameters.contentType.count) - let match = regexp.matches(in: parameters.contentType, options: [], range: range) - XCTAssertTrue(match.count > 0) - - let boundary = (parameters.contentType as NSString).substring(with: match.first!.range(at: 1)) - XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") - } catch { + guard case .inputStream(let inputStream) = try parameters.buildEntity() else { XCTFail() + return } + let data = try Data(inputStream: inputStream) + let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.count) + let match = regexp.matches(in: parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let firstRange = try XCTUnwrap(match.first?.range(at: 1)) + let boundary = (parameters.contentType as NSString).substring(with: firstRange) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") } // MARK: Values - // Skip test cases that uses files until SwiftPM supports resources. - #if !SWIFT_PACKAGE - func testFileValue() { - let fileURL = Bundle(for: type(of: self)).url(forResource: "test", withExtension: "json")! - let part = try! MultipartFormDataBodyParameters.Part(fileURL: fileURL, name: "test") + func testFileValue() throws { + #if SWIFT_PACKAGE + let bundle = Bundle.module + #else + let bundle = Bundle(for: type(of: self)) + #endif + let fileURL = try XCTUnwrap(bundle.url(forResource: "test", withExtension: "json")) + let part = try MultipartFormDataBodyParameters.Part(fileURL: fileURL, name: "test") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let testData = try! Data(contentsOf: fileURL) - let testString = String(data: testData, encoding: .utf8)! - - let encodedData = String(data: data, encoding:.utf8)! - let returnCode = "\r\n" - - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: parameters.contentType.count) - let match = regexp.matches(in: parameters.contentType, options: [], range: range) - XCTAssertTrue(match.count > 0) - - let boundary = (parameters.contentType as NSString).substring(with: match.first!.range(at: 1)) - XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"test\"; filename=\"test.json\"\r\nContent-Type: application/json\(returnCode)\(returnCode)\(testString)\(returnCode)--\(boundary)--\(returnCode)") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let testData = try Data(contentsOf: fileURL) + let testString = try XCTUnwrap(String(data: testData, encoding: .utf8)) + + let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.count) + let match = regexp.matches(in: parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let firstRange = try XCTUnwrap(match.first?.range(at: 1)) + let boundary = (parameters.contentType as NSString).substring(with: firstRange) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"test\"; filename=\"test.json\"\r\nContent-Type: application/json\(returnCode)\(returnCode)\(testString)\(returnCode)--\(boundary)--\(returnCode)") } - #endif - func testStringValue() { - let part = try! MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") + func testStringValue() throws { + let part = try MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let string = String(data: data, encoding:.utf8)! - XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nabcdef\r\n--\(parameters.boundary)--\r\n") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding:.utf8) + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nabcdef\r\n--\(parameters.boundary)--\r\n") } - func testIntValue() { - let part = try! MultipartFormDataBodyParameters.Part(value: 123, name: "foo") + func testIntValue() throws { + let part = try MultipartFormDataBodyParameters.Part(value: 123, name: "foo") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let string = String(data: data, encoding:.utf8)! - XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n123\r\n--\(parameters.boundary)--\r\n") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding:.utf8) + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n123\r\n--\(parameters.boundary)--\r\n") } - func testDoubleValue() { - let part = try! MultipartFormDataBodyParameters.Part(value: 3.14, name: "foo") + func testDoubleValue() throws { + let part = try MultipartFormDataBodyParameters.Part(value: 3.14, name: "foo") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let string = String(data: data, encoding:.utf8)! - XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--\(parameters.boundary)--\r\n") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding:.utf8) + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--\(parameters.boundary)--\r\n") } } diff --git a/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift b/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift index 844ea00d..ccbc2515 100644 --- a/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift @@ -3,23 +3,16 @@ import XCTest import APIKit class ProtobufBodyParametersTests: XCTestCase { - func testProtobufSuccess() { - let object = NSKeyedArchiver.archivedData(withRootObject: ["foo": 1, "bar": 2, "baz": 3]) + func testProtobufSuccess() throws { + let object = try XCTUnwrap("data".data(using: .utf8)) let parameters = ProtobufBodyParameters(protobufObject: object) XCTAssertEqual(parameters.contentType, "application/protobuf") - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let dictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding: .utf8) + XCTAssertEqual(string, "data") } } diff --git a/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift b/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift index 89702e4d..64d33cf4 100644 --- a/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift +++ b/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift @@ -4,21 +4,18 @@ import APIKit class URLEncodedSerializationTests: XCTestCase { // MARK: NSData -> Any - func testObjectFromData() { - let data = "key1=value1&key2=value2".data(using: .utf8)! + func testObjectFromData() throws { + let data = try XCTUnwrap("key1=value1&key2=value2".data(using: .utf8)) let object = try? URLEncodedSerialization.object(from: data, encoding: .utf8) XCTAssertEqual(object?["key1"], "value1") XCTAssertEqual(object?["key2"], "value2") } - func testInvalidFormatString() { + func testInvalidFormatString() throws { let string = "key==value&" - do { - let data = string.data(using: .utf8)! - try _ = URLEncodedSerialization.object(from: data, encoding: .utf8) - XCTFail() - } catch { + let data = try XCTUnwrap(string.data(using: .utf8)) + XCTAssertThrowsError(try URLEncodedSerialization.object(from: data, encoding: .utf8)) { error in guard let error = error as? URLEncodedSerialization.Error, case .invalidFormatString(let invalidString) = error else { XCTFail() @@ -33,10 +30,7 @@ class URLEncodedSerializationTests: XCTestCase { var bytes = [UInt8]([0xed, 0xa0, 0x80]) // U+D800 (high surrogate) let data = Data(bytes: &bytes, count: bytes.count) - do { - try _ = URLEncodedSerialization.object(from: data, encoding: .utf8) - XCTFail() - } catch { + XCTAssertThrowsError(try URLEncodedSerialization.object(from: data, encoding: .utf8)) { error in guard let error = error as? URLEncodedSerialization.Error, case .cannotGetStringFromData(let invalidData, let encoding) = error else { XCTFail() @@ -59,10 +53,7 @@ class URLEncodedSerializationTests: XCTestCase { func testNonDictionaryObject() { let dictionaries = [["hey": "yo"]] as Any - do { - try _ = URLEncodedSerialization.data(from: dictionaries, encoding: .utf8) - XCTFail() - } catch { + XCTAssertThrowsError(try URLEncodedSerialization.data(from: dictionaries, encoding: .utf8)) { error in guard let error = error as? URLEncodedSerialization.Error, case .cannotCastObjectToDictionary(let object) = error else { XCTFail() diff --git a/Tests/APIKitTests/Combine/CombineTests.swift b/Tests/APIKitTests/Combine/CombineTests.swift new file mode 100644 index 00000000..32c548b9 --- /dev/null +++ b/Tests/APIKitTests/Combine/CombineTests.swift @@ -0,0 +1,124 @@ +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +import APIKit + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +final class CombineTests: XCTestCase { + + var adapter: TestSessionAdapter! + var session: Session! + var cancellables: Set = [] + + override func setUp() { + super.setUp() + adapter = TestSessionAdapter() + session = Session(adapter: adapter) + } + + override func tearDown() { + super.tearDown() + cancellables = [] + } + + func testSuccess() throws { + let dictionary = ["key": "value"] + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest() + + session.sessionTaskPublisher(for: request) + .sink(receiveCompletion: { completion in + switch completion { + case .failure: + XCTFail() + case .finished: + expectation.fulfill() + } + }, receiveValue: { response in + XCTAssertEqual((response as? [String: String])?["key"], "value") + }) + .store(in: &cancellables) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testParseDataError() { + adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest() + + session.sessionTaskPublisher(for: request) + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion, case .responseError(let responseError as NSError) = error { + XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) + XCTAssertEqual(responseError.code, 3840) + expectation.fulfill() + } else { + XCTFail() + } + }, receiveValue: { response in + XCTFail() + }) + .store(in: &cancellables) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testBefore2020OSVersionsCancel() throws { + if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { + throw XCTSkip("Skip on After 2020 OS versions, as Combine cancellation no longer emits a value.") + } + + let cancelExpectation = self.expectation(description: "wait for cancel") + let completionExpectation = self.expectation(description: "wait for response") + let request = TestRequest() + + let cancellable = session.sessionTaskPublisher(for: request) + .handleEvents(receiveCancel: { + cancelExpectation.fulfill() + }) + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion, case .connectionError(let connectionError as NSError) = error { + XCTAssertEqual(connectionError.code, 0) + completionExpectation.fulfill() + } else { + XCTFail() + } + }, receiveValue: { response in + XCTFail() + }) + cancellable.cancel() + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testAfter2020OSVersionsCancel() throws { + guard #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) else { + throw XCTSkip("Skip on Before 2020 OS versions.") + } + + let expectation = self.expectation(description: "wait for cancel") + let request = TestRequest() + + let cancellable = session.sessionTaskPublisher(for: request) + .handleEvents(receiveCancel: { + expectation.fulfill() + }) + .sink(receiveCompletion: { completion in + XCTFail() + }, receiveValue: { response in + XCTFail() + }) + cancellable.cancel() + + waitForExpectations(timeout: 1.0, handler: nil) + } + +} + +#endif diff --git a/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift b/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift new file mode 100644 index 00000000..30052335 --- /dev/null +++ b/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift @@ -0,0 +1,68 @@ +#if compiler(>=5.6.0) && canImport(_Concurrency) + +import XCTest +import APIKit + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +final class ConcurrencyTests: XCTestCase { + var adapter: TestSessionAdapter! + var session: Session! + + override func setUp() { + super.setUp() + adapter = TestSessionAdapter() + session = Session(adapter: adapter) + } + + func testSuccess() async throws { + let dictionary = ["key": "value"] + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) + + let request = TestRequest() + let value = try await session.response(for: request) + XCTAssertEqual((value as? [String: String])?["key"], "value") + } + + func testParseDataError() async throws { + adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) + + let request = TestRequest() + do { + _ = try await session.response(for: request) + XCTFail() + } catch { + let sessionError = try XCTUnwrap(error as? SessionTaskError) + if case .responseError(let responseError as NSError) = sessionError { + XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) + XCTAssertEqual(responseError.code, 3840) + } else { + XCTFail() + } + } + } + + func testCancel() async throws { + let request = TestRequest() + + let task = Task { + do { + _ = try await session.response(for: request) + XCTFail() + } catch { + let sessionError = try XCTUnwrap(error as? SessionTaskError) + if case .connectionError(let connectionError as NSError) = sessionError { + XCTAssertEqual(connectionError.code, 0) + XCTAssertTrue(Task.isCancelled) + } else { + XCTFail() + } + } + } + task.cancel() + _ = try await task.value + + XCTAssertTrue(task.isCancelled) + } +} + +#endif diff --git a/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift b/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift index 8bf66a77..cb2f5357 100644 --- a/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift @@ -8,20 +8,16 @@ class FormURLEncodedDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/x-www-form-urlencoded") } - func testURLSuccess() { + func testURLSuccess() throws { let string = "foo=1&bar=2&baz=3" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = FormURLEncodedDataParser(encoding: .utf8) - do { - let object = try parser.parse(data: data) - let dictionary = object as? [String: String] - XCTAssertEqual(dictionary?["foo"], "1") - XCTAssertEqual(dictionary?["bar"], "2") - XCTAssertEqual(dictionary?["baz"], "3") - } catch { - XCTFail() - } + let object = try parser.parse(data: data) + let dictionary = object as? [String: String] + XCTAssertEqual(dictionary?["foo"], "1") + XCTAssertEqual(dictionary?["bar"], "2") + XCTAssertEqual(dictionary?["baz"], "3") } func testInvalidString() { @@ -29,10 +25,7 @@ class FormURLEncodedDataParserTests: XCTestCase { let data = Data(bytes: &bytes, count: bytes.count) let parser = FormURLEncodedDataParser(encoding: .utf8) - do { - try _ = parser.parse(data: data) - XCTFail() - } catch { + XCTAssertThrowsError(try parser.parse(data: data)) { error in guard let error = error as? FormURLEncodedDataParser.Error, case .cannotGetStringFromData(let invalidData) = error else { XCTFail() diff --git a/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift b/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift index e7faaca4..3cf81427 100644 --- a/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift @@ -8,19 +8,15 @@ class JSONDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/json") } - func testJSONSuccess() { + func testJSONSuccess() throws { let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = JSONDataParser(readingOptions: []) - do { - let object = try parser.parse(data: data) - let dictionary = object as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { - XCTFail() - } + let object = try parser.parse(data: data) + let dictionary = object as? [String: Int] + XCTAssertEqual(dictionary?["foo"], 1) + XCTAssertEqual(dictionary?["bar"], 2) + XCTAssertEqual(dictionary?["baz"], 3) } } diff --git a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift index e517e5c7..63e91853 100644 --- a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift @@ -8,18 +8,12 @@ class ProtobufDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/protobuf") } - func testProtobufSuccess() { - let data = NSKeyedArchiver.archivedData(withRootObject: ["foo": 1, "bar": 2, "baz": 3]) + func testProtobufSuccess() throws { + let data = try XCTUnwrap("data".data(using: .utf8)) let parser = ProtobufDataParser() - do { - let object = try parser.parse(data: data) as! Data - let dictionary = NSKeyedUnarchiver.unarchiveObject(with: object) as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { - XCTFail() - } + let object = try XCTUnwrap(try parser.parse(data: data) as? Data) + let string = String(data: object, encoding: .utf8) + XCTAssertEqual(string, "data") } } diff --git a/Tests/APIKitTests/DataParserType/StringDataParserTests.swift b/Tests/APIKitTests/DataParserType/StringDataParserTests.swift index 0dc0255a..d932501a 100644 --- a/Tests/APIKitTests/DataParserType/StringDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/StringDataParserTests.swift @@ -8,17 +8,13 @@ class StringDataParserTests: XCTestCase { XCTAssertNil(parser.contentType) } - func testParseData() { + func testParseData() throws { let string = "abcdef" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = StringDataParser(encoding: .utf8) - do { - let object = try parser.parse(data: data) - XCTAssertEqual(object as? String, string) - } catch { - XCTFail() - } + let object = try parser.parse(data: data) + XCTAssertEqual(object as? String, string) } func testInvalidString() { @@ -26,10 +22,7 @@ class StringDataParserTests: XCTestCase { let data = Data(bytes: &bytes, count: bytes.count) let parser = StringDataParser(encoding: .utf8) - do { - try _ = parser.parse(data: data) - XCTFail() - } catch { + XCTAssertThrowsError(try parser.parse(data: data)) { error in guard let error = error as? StringDataParser.Error, case .invalidData(let invalidData) = error else { XCTFail() diff --git a/Tests/APIKitTests/RequestTests.swift b/Tests/APIKitTests/RequestTests.swift index bd10942b..88bfa72c 100644 --- a/Tests/APIKitTests/RequestTests.swift +++ b/Tests/APIKitTests/RequestTests.swift @@ -2,33 +2,33 @@ import XCTest import APIKit class RequestTests: XCTestCase { - func testJapanesesQueryParameters() { + func testJapanesesQueryParameters() throws { let request = TestRequest(parameters: ["q": "こんにちは"]) - let urlRequest = try? request.buildURLRequest() - XCTAssertEqual(urlRequest?.url?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") + let urlRequest = try request.buildURLRequest() + XCTAssertEqual(urlRequest.url?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") } - func testSymbolQueryParameters() { + func testSymbolQueryParameters() throws { let request = TestRequest(parameters: ["q": "!\"#$%&'()0=~|`{}*+<>?/_"]) - let urlRequest = try? request.buildURLRequest() - XCTAssertEqual(urlRequest?.url?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?/_") + let urlRequest = try request.buildURLRequest() + XCTAssertEqual(urlRequest.url?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?/_") } - func testNullQueryParameters() { + func testNullQueryParameters() throws { let request = TestRequest(parameters: ["null": NSNull()]) - let urlRequest = try? request.buildURLRequest() - XCTAssertEqual(urlRequest?.url?.query, "null") + let urlRequest = try request.buildURLRequest() + XCTAssertEqual(urlRequest.url?.query, "null") } - func testheaderFields() { + func testheaderFields() throws { let request = TestRequest(headerFields: ["Foo": "f", "Accept": "a", "Content-Type": "c"]) - let urlReqeust = try? request.buildURLRequest() - XCTAssertEqual(urlReqeust?.value(forHTTPHeaderField: "Foo"), "f") - XCTAssertEqual(urlReqeust?.value(forHTTPHeaderField: "Accept"), "a") - XCTAssertEqual(urlReqeust?.value(forHTTPHeaderField: "Content-Type"), "c") + let urlReqeust = try request.buildURLRequest() + XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Foo"), "f") + XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Accept"), "a") + XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Content-Type"), "c") } - func testPOSTJSONRequest() { + func testPOSTJSONRequest() throws { let parameters: [Any] = [ ["id": "1"], ["id": "2"], @@ -38,10 +38,9 @@ class RequestTests: XCTestCase { let request = TestRequest(method: .post, parameters: parameters) XCTAssert((request.parameters as? [Any])?.count == 3) - let urlRequest = try? request.buildURLRequest() - XCTAssertNotNil(urlRequest?.httpBody) + let urlRequest = try request.buildURLRequest() - let json = urlRequest?.httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [AnyObject] + let json = urlRequest.httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [AnyObject] XCTAssertEqual(json?.count, 3) XCTAssertEqual((json?[0] as? [String: String])?["id"], "1") XCTAssertEqual((json?[1] as? [String: String])?["id"], "2") @@ -409,12 +408,12 @@ class RequestTests: XCTestCase { ) } - func testInterceptURLRequest() { - let url = URL(string: "https://example.com/customize")! + func testInterceptURLRequest() throws { + let url = try XCTUnwrap(URL(string: "https://example.com/customize")) let request = TestRequest() { _ in return URLRequest(url: url) } - XCTAssertEqual((try? request.buildURLRequest())?.url, url) + XCTAssertEqual(try request.buildURLRequest().url, url) } } diff --git a/Tests/APIKitTests/test.json b/Tests/APIKitTests/Resources/test.json similarity index 100% rename from Tests/APIKitTests/test.json rename to Tests/APIKitTests/Resources/test.json diff --git a/Tests/APIKitTests/SessionCallbackQueueTests.swift b/Tests/APIKitTests/SessionCallbackQueueTests.swift index 66f2f494..b9d961f9 100644 --- a/Tests/APIKitTests/SessionCallbackQueueTests.swift +++ b/Tests/APIKitTests/SessionCallbackQueueTests.swift @@ -6,11 +6,11 @@ class SessionCallbackQueueTests: XCTestCase { var adapter: TestSessionAdapter! var session: Session! - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() adapter = TestSessionAdapter() - adapter.data = try! JSONSerialization.data(withJSONObject: ["key": "value"], options: []) + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: ["key": "value"], options: [])) session = Session(adapter: adapter, callbackQueue: .main) } @@ -33,7 +33,7 @@ class SessionCallbackQueueTests: XCTestCase { session.send(request, callbackQueue: .sessionQueue) { result in // This depends on implementation of TestSessionAdapter - XCTAssert(Thread.isMainThread) + XCTAssertTrue(Thread.isMainThread) expectation.fulfill() } @@ -60,7 +60,7 @@ class SessionCallbackQueueTests: XCTestCase { session.send(request, callbackQueue: .dispatchQueue(dispatchQueue)) { result in // There is no way to test current dispatch queue. - XCTAssert(!Thread.isMainThread) + XCTAssertFalse(Thread.isMainThread) expectation.fulfill() } diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index 138fdf9f..40b887fc 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -1,7 +1,6 @@ import Foundation import APIKit import XCTest -import Result class SessionTests: XCTestCase { var adapter: TestSessionAdapter! @@ -14,9 +13,9 @@ class SessionTests: XCTestCase { session = Session(adapter: adapter) } - func testSuccess() { + func testSuccess() throws { let dictionary = ["key": "value"] - adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) let expectation = self.expectation(description: "wait for response") let request = TestRequest() @@ -189,10 +188,6 @@ class SessionTests: XCTestCase { var path: String { return "/" } - - func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { - return () - } } func testCancelOtherRequest() { diff --git a/Tests/APIKitTests/TestComponents/HTTPStub.swift b/Tests/APIKitTests/TestComponents/HTTPStub.swift index b56ff6d5..6170d79f 100644 --- a/Tests/APIKitTests/TestComponents/HTTPStub.swift +++ b/Tests/APIKitTests/TestComponents/HTTPStub.swift @@ -1,6 +1,5 @@ import Foundation import Dispatch -import Result class HTTPStub: URLProtocol { static var stubResult: Result = .success(Data())