├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .hound.yml ├── .swift-version ├── .swiftlint.yml ├── LICENSE ├── Package.swift ├── README.md ├── Source ├── BasicNetworkService.swift ├── ContainerNetworkTask.swift ├── HTTPMethod.swift ├── ModifyRequestNetworkService.swift ├── NetworkAccess.swift ├── NetworkError.swift ├── NetworkResponseProcessor.swift ├── NetworkService+Async.swift ├── NetworkService+ResourceWithError.swift ├── NetworkService+Result.swift ├── NetworkService.swift ├── NetworkServiceMock.swift ├── NetworkTask.swift ├── NetworkTaskMock.swift ├── Resource+Decodable.swift ├── Resource+Inspect.swift ├── Resource+Map.swift ├── Resource+Void.swift ├── Resource.swift ├── ResourceWithError.swift ├── RetryNetworkService.swift ├── URL+StaticStringInit.swift ├── URLRequest+Init.swift ├── URLRequest+Modifications.swift ├── URLSession+NetworkAccess.swift └── URLSessionDataTask+NetworkTask.swift ├── Tests ├── ContainerNetworkTaskTest.swift ├── DecodableResoureTest.swift ├── DefaultMocks.swift ├── ModifyRequestNetworkService.swift ├── NetworkAccessMock.swift ├── NetworkErrorTest.swift ├── NetworkResponseProcessorTest.swift ├── NetworkServiceMockTest.swift ├── NetworkServiceTest.swift ├── NetworkServiceWithErrorTest.swift ├── NetworkTaskMockTests.swift ├── ResourceInspectTest.swift ├── ResourceTest.swift ├── ResourceWithErrorTest.swift ├── RetryNetworkserviceTest.swift ├── TrainModel.swift ├── URL+StaticStringInitTest.swift ├── URLRequestTest.swift ├── URLSession+NetworkAccessTest.swift └── URLSessionMock.swift └── codecov.yml /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to submit a pull request with new features, improvements on tests or documentation and bug fixes. 2 | Keep in mind that we welcome code that is well tested and documented. 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes issues: 2 | 3 | New feature: 4 | 5 | #### Make sure to check all boxes before merging 6 | 7 | - [ ] Method/Class documentation 8 | - [ ] README.md documentation 9 | - [ ] Unit tests for new features/regressions 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Build 3 | jobs: 4 | test: 5 | name: Build 6 | runs-on: macOS-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | with: 11 | ref: ${{ github.ref }} 12 | - name: Build and test 13 | run: swift test 14 | -------------------------------------------------------------------------------- /.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 | .DS_Store 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | .build/ 40 | 41 | # CocoaPods 42 | # 43 | # We recommend against adding the Pods directory to your .gitignore. However 44 | # you should judge for yourself, the pros and cons are mentioned at: 45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 46 | # 47 | # Pods/ 48 | 49 | # Carthage 50 | # 51 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 52 | # Carthage/Checkouts 53 | 54 | Carthage/Build 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 62 | 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | swift: 2 | config_file: .swiftlint.yml -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.1 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - trailing_whitespace 3 | - missing_docs 4 | - syntactic_sugar 5 | - vertical_parameter_alignment 6 | opt_in_rules: # some rules are only opt-in 7 | - conditional_returns_on_newline 8 | - force_unwrapping 9 | - empty_count 10 | - vertical_whitespace 11 | - private_outlet 12 | excluded: # paths to ignore during linting. Takes precedence over `included`. 13 | - Carthage 14 | # configurable rules can be customized from this configuration file 15 | # binary rules can set their severity level 16 | force_cast: warning # implicitly 17 | force_try: 18 | severity: warning # explicitly 19 | line_length: 160 20 | function_parameter_count: 21 | warning: 10 22 | error: 15 23 | function_body_length: 24 | warning: 100 25 | error: 150 26 | type_body_length: 27 | waring: 300 28 | error: 400 29 | file_length: 30 | warning: 600 31 | error: 1200 32 | # naming rules can set warnings/errors for min_length and max_length 33 | # additionally they can set excluded names 34 | type_name: 35 | min_length: 4 # only warning 36 | max_length: # warning and error 37 | warning: 40 38 | error: 50 39 | variable_name: 40 | min_length: # only min_length 41 | error: 4 # only error 42 | excluded: # excluded via string array 43 | - id 44 | - URL 45 | - url 46 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 DB Systel GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // 3 | // Package.swift 4 | // 5 | // Copyright © 2016 Lukas Schmidt. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a 8 | // copy of this software and associated documentation files (the "Software"), 9 | // to deal in the Software without restriction, including without limitation 10 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | // and/or sell copies of the Software, and to permit persons to whom the 12 | // Software is furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | // DEALINGS IN THE SOFTWARE. 24 | // 25 | // Created by Lukas Schmidt on 21.07.16. 26 | // 27 | 28 | import PackageDescription 29 | 30 | let package = Package( 31 | name: "DBNetworkStack", 32 | platforms: [ 33 | .iOS(.v9), 34 | .tvOS(.v9), 35 | .watchOS(.v2), 36 | .macOS(.v10_10) 37 | ], 38 | products: [ 39 | .library( 40 | name: "DBNetworkStack", 41 | targets: ["DBNetworkStack"]) 42 | ], 43 | targets: [ 44 | .target( 45 | name: "DBNetworkStack", 46 | dependencies: [], 47 | path: "Source"), 48 | .testTarget( 49 | name: "DBNetworkStackTests", 50 | dependencies: ["DBNetworkStack"], 51 | path: "Tests") 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DBNetworkStack 2 | 3 | [![Build Status](https://travis-ci.org/dbsystel/DBNetworkStack.svg?branch=develop)](https://travis-ci.org/dbsystel/DBNetworkStack) 4 | [![codebeat badge](https://codebeat.co/badges/e438e768-249d-4e9f-8dd8-32928537740e)](https://codebeat.co/projects/github-com-dbsystel-dbnetworkstack-develop) 5 | [![codecov](https://codecov.io/gh/dbsystel/DBNetworkStack/branch/develop/graph/badge.svg)](https://codecov.io/gh/dbsystel/DBNetworkStack) 6 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 7 | 8 | | | Main Features | 9 | | --------- | ------------------------------ | 10 | | 🛡 | Typed network resources | 11 | | 🏠 | Value oriented architecture | 12 | | 🔀 | Exchangeable implementations | 13 | | 🚄 | Extendable API | 14 | | 🎹        | Composable Features           | 15 | | ✅ | Fully unit tested | 16 | | 📕  | [Documented here](https://dbsystel.github.io/DBNetworkStack/)            | 17 | 18 | The idea behind this project comes from this [talk.objc.io article](https://talk.objc.io/episodes/S01E01-networking). 19 | 20 | ## Basic Demo 21 | Lets say you want to fetch a ``html`` string. 22 | 23 | First you have to create a service, by providing a network access. You can use URLSession out of the box or provide your own custom solution by implementing ```NetworkAccess```. 24 | 25 | ```swift 26 | 27 | let networkAccess = URLSession(configuration: .default) 28 | let networkService = BasicNetworkService(networkAccess: networkAccess) 29 | 30 | ``` 31 | 32 | Create a resource with a request to fetch your data. 33 | 34 | ```swift 35 | 36 | let url = URL(staticString: "https://httpbin.org") 37 | let request = URLRequest(path: "/", baseURL: url, HTTPMethod: .GET) 38 | let resource = Resource(request: request, parse: { String(data: $0, encoding: .utf8) }) 39 | 40 | ``` 41 | Request your resource and handle the result 42 | ```swift 43 | networkService.request(resource, onCompletion: { htmlText in 44 | print(htmlText) 45 | }, onError: { error in 46 | //Handle errors 47 | }) 48 | 49 | ``` 50 | 51 | ## Load types conforming to Swift-`Decodable` 52 | ```swift 53 | struct IPOrigin: Decodable { 54 | let origin: String 55 | } 56 | 57 | let url = URL(staticString: "https://www.httpbin.org") 58 | let request = URLRequest(path: "ip", baseURL: url) 59 | 60 | let resource = Resource(request: request, decoder: JSONDecoder()) 61 | 62 | networkService.request(resource, onCompletion: { origin in 63 | print(origin) 64 | }, onError: { error in 65 | //Handle errors 66 | }) 67 | ``` 68 | 69 | ## Accessing HTTPResponse 70 | 71 | Request your resource and handle the result & http response. This is similar to just requesting a resulting model. 72 | ```swift 73 | networkService.request(resource, onCompletionWithResponse: { origin, response in 74 | print(origin, response) 75 | }, onError: { error in 76 | //Handle errors 77 | }) 78 | ``` 79 | 80 | ## Protocol oriented architecture / Exchangability 81 | 82 | The following table shows all the protocols and their default implementations. 83 | 84 | | Protocol | Default Implementation | 85 | | -------------------------------- | ---------------------- | 86 | | ```NetworkAccess``` | ```URLSession``` | 87 | | ```NetworkService``` | ```BasicNetworkService``` | 88 | | ```NetworkTask``` | ```URLSessionTask``` | 89 | 90 | ## Composable Features 91 | 92 | | Class | Feature | 93 | | -------------------------------- | ---------------------- | 94 | | ```RetryNetworkService``` | Retrys requests after a given delay when an error meets given criteria. | 95 | | ```ModifyRequestNetworkService``` | Modify matching requests. Can be used to add auth tokens or API Keys | 96 | | ```NetworkServiceMock``` | Mocks a NetworkService. Can be use during unit tests | 97 | 98 | ## Requirements 99 | 100 | - iOS 9.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ 101 | 102 | ## Installation 103 | 104 | ### Swift Package Manager 105 | 106 | [SPM](https://swift.org/package-manager/) is integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 107 | 108 | Specify the following in your `Package.swift`: 109 | 110 | ```swift 111 | .package(url: "https://github.com/dbsystel/DBNetworkStack", from: "2.1.0"), 112 | ``` 113 | 114 | ## Contributing 115 | Feel free to submit a pull request with new features, improvements on tests or documentation and bug fixes. Keep in mind that we welcome code that is well tested and documented. 116 | 117 | ## Contact 118 | Lukas Schmidt ([Mail](mailto:lukas.la.schmidt@deutschebahn.com), [@lightsprint09](https://twitter.com/lightsprint09)), 119 | Christian Himmelsbach ([Mail](mailto:christian.himmelsbach@deutschebahn.com)) 120 | 121 | ## License 122 | DBNetworkStack is released under the MIT license. See LICENSE for details. 123 | -------------------------------------------------------------------------------- /Source/BasicNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import Dispatch 26 | 27 | /** 28 | `BasicNetworkService` handles network request for resources by using a given `NetworkAccess`. 29 | 30 | **Example**: 31 | ```swift 32 | // Just use an URLSession for the networkAccess. 33 | let basicNetworkService: NetworkService = BasicNetworkService(networkAccess: URLSession(configuration: .default)) 34 | ``` 35 | 36 | - seealso: `NetworkService` 37 | */ 38 | public final class BasicNetworkService: NetworkService { 39 | let networkAccess: NetworkAccess 40 | let networkResponseProcessor: NetworkResponseProcessor 41 | 42 | /** 43 | Creates an `BasicNetworkService` instance with a given network access to execute requests on. 44 | 45 | - parameter networkAccess: provides basic access to the network. 46 | */ 47 | public init(networkAccess: NetworkAccess) { 48 | self.networkAccess = networkAccess 49 | self.networkResponseProcessor = NetworkResponseProcessor() 50 | } 51 | 52 | /** 53 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 54 | Execution happens on no specific queue. It dependes on the network access which queue is used. 55 | Once execution is finished either the completion block or the error block gets called. 56 | You decide on which queue these blocks get executed. 57 | 58 | **Example**: 59 | ```swift 60 | let networkService: NetworkService = // 61 | let resource: Resource = // 62 | 63 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 64 | print(htmlText, response) 65 | }, onError: { error in 66 | // Handle errors 67 | }) 68 | ``` 69 | 70 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 71 | - parameter resource: The resource you want to fetch. 72 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 73 | - parameter onError: Callback which gets called when fetching or transforming fails. 74 | 75 | - returns: a running network task 76 | */ 77 | @discardableResult 78 | public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 79 | onError: @escaping (NetworkError) -> Void) -> NetworkTask { 80 | let request = resource.request 81 | let dataTask = networkAccess.load(request: request, callback: { data, response, error in 82 | self.networkResponseProcessor.processAsyncResponse(queue: queue, response: response, resource: resource, data: data, 83 | error: error, onCompletion: onCompletionWithResponse, onError: onError) 84 | }) 85 | return dataTask 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Source/ContainerNetworkTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2018 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /// A task which contains another task which can be updated in fligh. 27 | /// Use this task to compose a chain of requests during the original request. 28 | /// An oAuth Flow would be a good example for this. 29 | /// 30 | /// - Note: Take look at `RetryNetworkService` to see how to use it in detail. 31 | public final class ContainerNetworkTask: NetworkTask { 32 | 33 | // MARK: - Init 34 | 35 | /// Creates a `ContainerNetworkTask` instance. 36 | public init() { } 37 | 38 | // MARK: - Override 39 | 40 | // MARK: - Protocol NetworkTask 41 | 42 | /** 43 | Resumes a task. 44 | */ 45 | public func resume() { 46 | underlyingTask?.resume() 47 | } 48 | 49 | /** 50 | Cancels the underlying task. 51 | */ 52 | public func cancel() { 53 | isCanceled = true 54 | underlyingTask?.cancel() 55 | } 56 | 57 | /** 58 | Suspends a task. 59 | */ 60 | public func suspend() { 61 | underlyingTask?.suspend() 62 | } 63 | 64 | // MARK: - Public 65 | 66 | /// The underlying task 67 | public var underlyingTask: NetworkTask? 68 | 69 | /// Indicates if the request has been canceled. 70 | /// When composing multiple requests this flag must be respected. 71 | public private(set) var isCanceled = false 72 | } 73 | -------------------------------------------------------------------------------- /Source/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | // swiftlint:disable identifier_name 27 | /** 28 | HTTP Methods 29 | 30 | See [IETF document](https://tools.ietf.org/html/rfc7231#section-4.3) 31 | */ 32 | public enum HTTPMethod: String { 33 | case GET 34 | case POST 35 | case PUT 36 | case DELETE 37 | case OPTIONS 38 | case HEAD 39 | case PATCH 40 | case TRACE 41 | case CONNECT 42 | } 43 | -------------------------------------------------------------------------------- /Source/ModifyRequestNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import Dispatch 26 | 27 | /** 28 | `ModifyRequestNetworkService` can be composed with a network service to modify all outgoing requests. 29 | One could add auth tokens or API keys for specifics URLs. 30 | 31 | **Example**: 32 | ```swift 33 | let networkService: NetworkService = // 34 | let modifyRequestNetworkService = ModifyRequestNetworkService(networkService: networkService, requestModifications: [ { request in 35 | return request.added(HTTPHeaderFields: ["API-Key": "SecretKey"]) 36 | }]) 37 | ``` 38 | 39 | - note: Requests can only be modified syncronously. 40 | - seealso: `NetworkService` 41 | */ 42 | public final class ModifyRequestNetworkService: NetworkService { 43 | 44 | private let requestModifications: [(URLRequest) -> URLRequest] 45 | private let networkService: NetworkService 46 | 47 | /// Creates an insatcne of `ModifyRequestNetworkService`. 48 | /// 49 | /// - Parameters: 50 | /// - networkService: a networkservice. 51 | /// - requestModifications: array of modifications to modify requests. 52 | public init(networkService: NetworkService, requestModifications: [(URLRequest) -> URLRequest]) { 53 | self.networkService = networkService 54 | self.requestModifications = requestModifications 55 | } 56 | 57 | /** 58 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 59 | Execution happens on no specific queue. It dependes on the network access which queue is used. 60 | Once execution is finished either the completion block or the error block gets called. 61 | You decide on which queue these blocks get executed. 62 | 63 | **Example**: 64 | ```swift 65 | let networkService: NetworkService = // 66 | let resource: Resource = // 67 | 68 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 69 | print(htmlText, response) 70 | }, onError: { error in 71 | // Handle errors 72 | }) 73 | ``` 74 | 75 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 76 | - parameter resource: The resource you want to fetch. 77 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 78 | - parameter onError: Callback which gets called when fetching or transforming fails. 79 | 80 | - returns: a running network task 81 | */ 82 | @discardableResult 83 | public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 84 | onError: @escaping (NetworkError) -> Void) -> NetworkTask { 85 | let request = requestModifications.reduce(resource.request, { request, modify in 86 | return modify(request) 87 | }) 88 | let newResource = Resource(request: request, parse: resource.parse) 89 | return networkService.request(queue: queue, resource: newResource, onCompletionWithResponse: onCompletionWithResponse, onError: onError) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/NetworkAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /// `NetworkAccess` provides access to the network. 27 | public protocol NetworkAccess { 28 | 29 | /// Fetches a request asynchrony from remote location. 30 | /// 31 | /// - Parameters: 32 | /// - request: The request one wants to fetch. 33 | /// - callback: Callback which gets called when the request finishes. 34 | /// - Returns: the running network task 35 | func load(request: URLRequest, callback: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) -> NetworkTask 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Source/NetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /// `NetworkError` provides a collection of error types which can occur during execution. 27 | public enum NetworkError: Error { 28 | /// The error is unkonw 29 | case unknownError 30 | /// The request was cancelled before it finished 31 | case cancelled 32 | /// Missing authorization for the request (HTTP Error 401) 33 | case unauthorized(response: HTTPURLResponse, data: Data?) 34 | /// Invalid payload was send to the server (HTTP Error 400...451) 35 | case clientError(response: HTTPURLResponse?, data: Data?) 36 | /// Error on the server (HTTP Error 500...511) 37 | case serverError(response: HTTPURLResponse?, data: Data?) 38 | /// Parsing the body into expected type failed. 39 | case serializationError(error: Error, data: Data?) 40 | /// Complete request failed. 41 | case requestError(error: Error) 42 | 43 | public init?(response: HTTPURLResponse?, data: Data?) { 44 | guard let response = response else { 45 | return nil 46 | } 47 | 48 | switch response.statusCode { 49 | case 200..<300: return nil 50 | case 401: 51 | self = .unauthorized(response: response, data: data) 52 | case 400...451: 53 | self = .clientError(response: response, data: data) 54 | case 500...511: 55 | self = .serverError(response: response, data: data) 56 | default: 57 | return nil 58 | } 59 | } 60 | 61 | } 62 | 63 | extension String { 64 | fileprivate func appendingContentsOf(data: Data?) -> String { 65 | if let data = data, let string = String(data: data, encoding: .utf8) { 66 | return self.appending(string) 67 | } 68 | return self 69 | } 70 | } 71 | 72 | extension NetworkError: CustomDebugStringConvertible { 73 | 74 | /// Details description of the error. 75 | public var debugDescription: String { 76 | switch self { 77 | case .unknownError: 78 | return "Unknown error" 79 | case .cancelled: 80 | return "Request cancelled" 81 | case .unauthorized(let response, let data): 82 | return "Authorization error: \(response), response: ".appendingContentsOf(data: data) 83 | case .clientError(let response, let data): 84 | if let response = response { 85 | return "Client error: \((response)), response: ".appendingContentsOf(data: data) 86 | } 87 | return "Client error, response: ".appendingContentsOf(data: data) 88 | case .serializationError(let description, let data): 89 | return "Serialization error: \(description), response: ".appendingContentsOf(data: data) 90 | case .requestError(let error): 91 | return "Request error: \(error)" 92 | case .serverError(let response, let data): 93 | if let response = response { 94 | return "Server error: \(String(describing: response)), response: ".appendingContentsOf(data: data) 95 | } else { 96 | return "Server error: nil, response: ".appendingContentsOf(data: data) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Source/NetworkResponseProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import Dispatch 26 | 27 | final class NetworkResponseProcessor { 28 | /** 29 | Processes the results of an HTTPRequest and parses the result the matching Model type of the given resource. 30 | 31 | Great error handling should be implemented here as well. 32 | 33 | - parameter response: response from the server. Could be nil 34 | - parameter resource: The resource matching the response. 35 | - parameter data: Returned data. Could be nil. 36 | - parameter error: the return error. Could be nil. 37 | 38 | - returns: the parsed model object. 39 | */ 40 | func process(response: HTTPURLResponse?, resource: Resource, data: Data?, error: Error?) throws -> Result { 41 | if let error = error { 42 | if case URLError.cancelled = error { 43 | throw NetworkError.cancelled 44 | } 45 | 46 | throw NetworkError.requestError(error: error) 47 | } 48 | if let responseError = NetworkError(response: response, data: data) { 49 | throw responseError 50 | } 51 | guard let data = data else { 52 | throw NetworkError.serverError(response: response, data: nil) 53 | } 54 | do { 55 | return try resource.parse(data) 56 | } catch let error { 57 | throw NetworkError.serializationError(error: error, data: data) 58 | } 59 | } 60 | 61 | /// This parseses a `HTTPURLResponse` with a given resource into the result type of the resource or errors. 62 | /// The result will be return via a blocks onCompletion/onError. 63 | /// 64 | /// - Parameters: 65 | /// - queue: The `DispatchQueue` to execute the completion and error block on. 66 | /// - response: the HTTPURLResponse one wants to parse. 67 | /// - resource: the resource. 68 | /// - data: the payload of the response. 69 | /// - error: optional error from net network. 70 | /// - onCompletion: completion block which gets called on the given `queue`. 71 | /// - onError: error block which gets called on the given `queue`. 72 | func processAsyncResponse(queue: DispatchQueue, response: HTTPURLResponse?, resource: Resource, data: Data?, 73 | error: Error?, onCompletion: @escaping (Result, HTTPURLResponse) -> Void, onError: @escaping (NetworkError) -> Void) { 74 | do { 75 | let parsed = try process( 76 | response: response, 77 | resource: resource, 78 | data: data, 79 | error: error 80 | ) 81 | queue.async { 82 | if let response = response { 83 | onCompletion(parsed, response) 84 | } else { 85 | onError(NetworkError.unknownError) 86 | } 87 | } 88 | } catch let genericError { 89 | let dbNetworkError: NetworkError! = genericError as? NetworkError 90 | queue.async { 91 | return onError(dbNetworkError) 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Source/NetworkService+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Lukas Schmidt on 19.12.21. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 11 | public extension NetworkService { 12 | 13 | /** 14 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 15 | 16 | **Example**: 17 | ```swift 18 | let networkService: NetworkService = // 19 | let resource: Resource = // 20 | 21 | let (result, response) = try await networkService.request(resource) 22 | ``` 23 | 24 | - parameter resource: The resource you want to fetch. 25 | 26 | - returns: a touple containing the parsed result and the HTTP response 27 | - Throws: A `NetworkError` 28 | */ 29 | @discardableResult 30 | func request(_ resource: Resource) async throws -> (Result, HTTPURLResponse) { 31 | var task: NetworkTask? 32 | let cancel = { task?.cancel() } 33 | return try await withTaskCancellationHandler(operation: { 34 | try Task.checkCancellation() 35 | return try await withCheckedThrowingContinuation({ coninuation in 36 | task = request(resource: resource, onCompletionWithResponse: { 37 | coninuation.resume(with: $0) 38 | }) 39 | }) 40 | }, onCancel: { 41 | cancel() 42 | }) 43 | } 44 | 45 | /** 46 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 47 | 48 | **Example**: 49 | ```swift 50 | let networkService: NetworkService = // 51 | let resource: ResourceWithError = // 52 | 53 | let (result, response) = try await networkService.request(resource) 54 | ``` 55 | 56 | - parameter resource: The resource you want to fetch. 57 | 58 | - returns: a touple containing the parsed result and the HTTP response 59 | - Throws: Custom Error provided by ResourceWithError 60 | */ 61 | @discardableResult 62 | func request(_ resource: ResourceWithError) async throws -> (Result, HTTPURLResponse) { 63 | var task: NetworkTask? 64 | let cancel = { task?.cancel() } 65 | return try await withTaskCancellationHandler(operation: { 66 | try Task.checkCancellation() 67 | return try await withCheckedThrowingContinuation({ coninuation in 68 | task = request(resource: resource, onCompletionWithResponse: { 69 | coninuation.resume(with: $0) 70 | }) 71 | }) 72 | }, onCancel: { 73 | cancel() 74 | }) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Source/NetworkService+ResourceWithError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2021 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | extension NetworkService { 27 | 28 | /** 29 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 30 | Execution happens on no specific queue. It dependes on the network access which queue is used. 31 | Once execution is finished either the completion block or the error block gets called. 32 | You decide on which queue these blocks get executed. 33 | 34 | **Example**: 35 | ```swift 36 | let networkService: NetworkService = // 37 | let resource: ResourceWithError = // 38 | 39 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 40 | print(htmlText, response) 41 | }, onError: { error in 42 | // Handle errors 43 | }) 44 | ``` 45 | 46 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 47 | - parameter resource: The resource you want to fetch. 48 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 49 | - parameter onError: Callback which gets called with an custom error. 50 | 51 | - returns: a running network task 52 | */ 53 | @discardableResult 54 | public func request( 55 | queue: DispatchQueue, 56 | resource: ResourceWithError, 57 | onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 58 | onError: @escaping (E) -> Void 59 | ) -> NetworkTask { 60 | let resourceWithoutError = Resource(request: resource.request, parse: resource.parse) 61 | return request(queue: queue, resource: resourceWithoutError, onCompletionWithResponse: onCompletionWithResponse) { networkError in 62 | onError(resource.mapError(networkError)) 63 | } 64 | } 65 | 66 | /** 67 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 68 | Execution happens on no specific queue. It dependes on the network access which queue is used. 69 | Once execution is finished either the completion block or the error block gets called. 70 | These blocks are called on the main queue. 71 | 72 | **Example**: 73 | ```swift 74 | let networkService: NetworkService = // 75 | let resource: ResourceWithError = // 76 | 77 | networkService.request(resource, onCompletion: { htmlText in 78 | print(htmlText) 79 | }, onError: { error in 80 | // Handle errors 81 | }) 82 | ``` 83 | 84 | - parameter resource: The resource you want to fetch. 85 | - parameter onComplition: Callback which gets called when fetching and transforming into model succeeds. 86 | - parameter onError: Callback which gets called with an custom error. 87 | 88 | - returns: a running network task 89 | */ 90 | @discardableResult 91 | public func request( 92 | _ resource: ResourceWithError, 93 | onCompletion: @escaping (Result) -> Void, 94 | onError: @escaping (E) -> Void 95 | ) -> NetworkTask { 96 | return request( 97 | queue: .main, 98 | resource: resource, 99 | onCompletionWithResponse: { model, _ in onCompletion(model) }, 100 | onError: onError 101 | ) 102 | } 103 | 104 | /** 105 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 106 | Execution happens on no specific queue. It dependes on the network access which queue is used. 107 | Once execution is finished either the completion block or the error block gets called. 108 | You decide on which queue these blocks get executed. 109 | 110 | **Example**: 111 | ```swift 112 | let networkService: NetworkService = // 113 | let resource: ResourceWithError = // 114 | 115 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 116 | print(htmlText, response) 117 | }, onError: { error in 118 | // Handle errors 119 | }) 120 | ``` 121 | 122 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 123 | - parameter resource: The resource you want to fetch. 124 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 125 | - parameter onError: Callback which gets called with an custom error. 126 | 127 | - returns: a running network task 128 | */ 129 | @discardableResult 130 | func request( 131 | queue: DispatchQueue = .main, 132 | resource: ResourceWithError, 133 | onCompletionWithResponse: @escaping (Swift.Result<(Result, HTTPURLResponse), E>) -> Void 134 | ) -> NetworkTask { 135 | return request( 136 | queue: queue, 137 | resource: resource, 138 | onCompletionWithResponse: { result, response in 139 | onCompletionWithResponse(.success((result, response))) 140 | }, onError: { error in 141 | onCompletionWithResponse(.failure(error)) 142 | } 143 | ) 144 | } 145 | 146 | /** 147 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 148 | Execution happens on no specific queue. It dependes on the network access which queue is used. 149 | Once execution is finished either the completion block or the error block gets called. 150 | These blocks are called on the main queue. 151 | 152 | **Example**: 153 | ```swift 154 | let networkService: NetworkService = // 155 | let resource: ResourceWithError = // 156 | 157 | networkService.request(resource, onCompletion: { htmlText in 158 | print(htmlText) 159 | }, onError: { error in 160 | // Handle errors 161 | }) 162 | ``` 163 | 164 | - parameter resource: The resource you want to fetch. 165 | - parameter onComplition: Callback which gets called when fetching and transforming into model succeeds. 166 | - parameter onError: Callback which gets called with an custom error. 167 | 168 | - returns: a running network task 169 | */ 170 | @discardableResult 171 | public func request( 172 | _ resource: ResourceWithError, 173 | onCompletion: @escaping (Swift.Result) -> Void 174 | ) -> NetworkTask { 175 | return request( 176 | queue: .main, 177 | resource: resource, 178 | onCompletionWithResponse: { model, _ in onCompletion(.success(model)) }, 179 | onError: { onCompletion(.failure($0))} 180 | ) 181 | } 182 | 183 | /** 184 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 185 | Execution happens on no specific queue. It dependes on the network access which queue is used. 186 | Once execution is finished either the completion block or the error block gets called. 187 | These blocks are called on the main queue. 188 | 189 | **Example**: 190 | ```swift 191 | let networkService: NetworkService = // 192 | let resource: Resource = // 193 | 194 | networkService.request(resource, onCompletionWithResponse: { htmlText, httpResponse in 195 | print(htmlText, httpResponse) 196 | }, onError: { error in 197 | // Handle errors 198 | }) 199 | ``` 200 | 201 | - parameter resource: The resource you want to fetch. 202 | - parameter onCompletion: Callback which gets called when fetching and transforming into model succeeds. 203 | - parameter onError: Callback which gets called when fetching or transforming fails. 204 | 205 | - returns: a running network task 206 | */ 207 | @discardableResult 208 | func request(_ resource: ResourceWithError, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 209 | onError: @escaping (E) -> Void) -> NetworkTask { 210 | return request(queue: .main, resource: resource, onCompletionWithResponse: onCompletionWithResponse, onError: onError) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Source/NetworkService+Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService+Result.swift 3 | // DBNetworkStack 4 | // 5 | // Created by Lukas Schmidt on 03.01.19. 6 | // Copyright © 2019 DBSystel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension NetworkService { 12 | 13 | /** 14 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 15 | Execution happens on no specific queue. It dependes on the network access which queue is used. 16 | Once execution is finished the completion block gets called. 17 | You decide on which queue completion gets executed. Defaults to `main`. 18 | 19 | **Example**: 20 | ```swift 21 | let networkService: NetworkService = // 22 | let resource: Resource = // 23 | 24 | networkService.request(resource: resource, onCompletionWithResponse: { result in 25 | print(result) 26 | }) 27 | ``` 28 | 29 | - parameter queue: The `DispatchQueue` to execute the completion block on. Defaults to `main`. 30 | - parameter resource: The resource you want to fetch. 31 | - parameter onCompletionWithResponse: Callback which gets called when request completes. 32 | 33 | - returns: a running network task 34 | */ 35 | @discardableResult 36 | func request(queue: DispatchQueue = .main, 37 | resource: Resource, 38 | onCompletionWithResponse: @escaping (Swift.Result<(Result, HTTPURLResponse), NetworkError>) -> Void) -> NetworkTask { 39 | return request(queue: queue, 40 | resource: resource, 41 | onCompletionWithResponse: { result, response in 42 | onCompletionWithResponse(.success((result, response))) 43 | }, onError: { error in 44 | onCompletionWithResponse(.failure(error)) 45 | }) 46 | } 47 | 48 | /** 49 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 50 | Execution happens on no specific queue. It dependes on the network access which queue is used. 51 | Once execution is finished the completion block gets called. 52 | Completion gets executed on `main` queue. 53 | 54 | **Example**: 55 | ```swift 56 | let networkService: NetworkService = // 57 | let resource: Resource = // 58 | 59 | networkService.request(resource, onCompletion: { result in 60 | print(result) 61 | }) 62 | ``` 63 | 64 | - parameter resource: The resource you want to fetch. 65 | - parameter onComplition: Callback which gets called when request completes. 66 | 67 | - returns: a running network task 68 | */ 69 | @discardableResult 70 | func request(_ resource: Resource, 71 | onCompletion: @escaping (Swift.Result) -> Void) -> NetworkTask { 72 | return request(resource: resource, 73 | onCompletionWithResponse: { result in 74 | switch result { 75 | case .success(let response): 76 | onCompletion(.success(response.0)) 77 | case .failure(let error): 78 | onCompletion(.failure(error)) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Source/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import Dispatch 26 | 27 | /** 28 | `NetworkService` provides access to remote resources. 29 | 30 | - seealso: `BasicNetworkService` 31 | - seealso: `NetworkServiceMock` 32 | */ 33 | public protocol NetworkService { 34 | /** 35 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 36 | Execution happens on no specific queue. It dependes on the network access which queue is used. 37 | Once execution is finished either the completion block or the error block gets called. 38 | You decide on which queue these blocks get executed. 39 | 40 | **Example**: 41 | ```swift 42 | let networkService: NetworkService = // 43 | let resource: Resource = // 44 | 45 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 46 | print(htmlText, response) 47 | }, onError: { error in 48 | // Handle errors 49 | }) 50 | ``` 51 | 52 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 53 | - parameter resource: The resource you want to fetch. 54 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 55 | - parameter onError: Callback which gets called when fetching or transforming fails. 56 | 57 | - returns: a running network task 58 | */ 59 | @discardableResult 60 | func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 61 | onError: @escaping (NetworkError) -> Void) -> NetworkTask 62 | } 63 | 64 | public extension NetworkService { 65 | /** 66 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 67 | Execution happens on no specific queue. It dependes on the network access which queue is used. 68 | Once execution is finished either the completion block or the error block gets called. 69 | These blocks are called on the main queue. 70 | 71 | **Example**: 72 | ```swift 73 | let networkService: NetworkService = // 74 | let resource: Resource = // 75 | 76 | networkService.request(resource, onCompletion: { htmlText in 77 | print(htmlText) 78 | }, onError: { error in 79 | // Handle errors 80 | }) 81 | ``` 82 | 83 | - parameter resource: The resource you want to fetch. 84 | - parameter onComplition: Callback which gets called when fetching and transforming into model succeeds. 85 | - parameter onError: Callback which gets called when fetching or transforming fails. 86 | 87 | - returns: a running network task 88 | */ 89 | @discardableResult 90 | func request(_ resource: Resource, onCompletion: @escaping (Result) -> Void, 91 | onError: @escaping (NetworkError) -> Void) -> NetworkTask { 92 | return request(queue: .main, resource: resource, onCompletionWithResponse: { model, _ in onCompletion(model) }, onError: onError) 93 | } 94 | 95 | /** 96 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 97 | Execution happens on no specific queue. It dependes on the network access which queue is used. 98 | Once execution is finished either the completion block or the error block gets called. 99 | These blocks are called on the main queue. 100 | 101 | **Example**: 102 | ```swift 103 | let networkService: NetworkService = // 104 | let resource: Resource = // 105 | 106 | networkService.request(resource, onCompletionWithResponse: { htmlText, httpResponse in 107 | print(htmlText, httpResponse) 108 | }, onError: { error in 109 | // Handle errors 110 | }) 111 | ``` 112 | 113 | - parameter resource: The resource you want to fetch. 114 | - parameter onCompletion: Callback which gets called when fetching and transforming into model succeeds. 115 | - parameter onError: Callback which gets called when fetching or transforming fails. 116 | 117 | - returns: a running network task 118 | */ 119 | @discardableResult 120 | func request(_ resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 121 | onError: @escaping (NetworkError) -> Void) -> NetworkTask { 122 | return request(queue: .main, resource: resource, onCompletionWithResponse: onCompletionWithResponse, onError: onError) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Source/NetworkServiceMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import Dispatch 26 | 27 | struct NetworkServiceMockCallback { 28 | let onErrorCallback: (NetworkError) -> Void 29 | let onTypedSuccess: (Any, HTTPURLResponse) throws -> Void 30 | 31 | init(resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, onError: @escaping (NetworkError) -> Void) { 32 | onTypedSuccess = { anyResult, response in 33 | guard let typedResult = anyResult as? Result else { 34 | throw NetworkServiceMock.Error.typeMismatch 35 | } 36 | onCompletionWithResponse(typedResult, response) 37 | } 38 | onErrorCallback = { error in 39 | onError(error) 40 | } 41 | } 42 | } 43 | 44 | /** 45 | Mocks a `NetworkService`. 46 | You can configure expected results or errors to have a fully functional mock. 47 | 48 | **Example**: 49 | ```swift 50 | //Given 51 | let networkServiceMock = NetworkServiceMock() 52 | let resource: Resource = // 53 | 54 | //When 55 | networkService.request( 56 | resource, 57 | onCompletion: { string in /*...*/ }, 58 | onError: { error in /*...*/ } 59 | ) 60 | networkService.returnSuccess(with: "Sucess") 61 | 62 | //Then 63 | //Test your expectations 64 | 65 | ``` 66 | 67 | It is possible to start multiple requests at a time. 68 | All requests and responses (or errors) are processed 69 | in order they have been called. So, everything is serial. 70 | 71 | **Example**: 72 | ```swift 73 | //Given 74 | let networkServiceMock = NetworkServiceMock() 75 | let resource: Resource = // 76 | 77 | //When 78 | networkService.request( 79 | resource, 80 | onCompletion: { string in /* Success */ }, 81 | onError: { error in /*...*/ } 82 | ) 83 | networkService.request( 84 | resource, 85 | onCompletion: { string in /*...*/ }, 86 | onError: { error in /*. cancel error .*/ } 87 | ) 88 | 89 | networkService.returnSuccess(with: "Sucess") 90 | networkService.returnError(with: .cancelled) 91 | 92 | //Then 93 | //Test your expectations 94 | 95 | ``` 96 | 97 | - seealso: `NetworkService` 98 | */ 99 | public final class NetworkServiceMock: NetworkService { 100 | 101 | public enum Error: Swift.Error, CustomDebugStringConvertible { 102 | case missingRequest 103 | case typeMismatch 104 | 105 | public var debugDescription: String { 106 | switch self { 107 | case .missingRequest: 108 | return "Could not return because no request" 109 | case .typeMismatch: 110 | return "Return type does not match requested type" 111 | } 112 | } 113 | } 114 | 115 | /// Count of all started requests 116 | public var requestCount: Int { 117 | return lastRequests.count 118 | } 119 | 120 | /// Last executed request 121 | public var lastRequest: URLRequest? { 122 | return lastRequests.last 123 | } 124 | 125 | public var pendingRequestCount: Int { 126 | return callbacks.count 127 | } 128 | 129 | /// All executed requests. 130 | public private(set) var lastRequests: [URLRequest] = [] 131 | 132 | /// Set this to hava a custom networktask returned by the mock 133 | public var nextNetworkTask: NetworkTask? 134 | 135 | private var callbacks: [NetworkServiceMockCallback] = [] 136 | 137 | /// Creates an instace of `NetworkServiceMock` 138 | public init() {} 139 | 140 | /** 141 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 142 | Execution happens on no specific queue. It dependes on the network access which queue is used. 143 | Once execution is finished either the completion block or the error block gets called. 144 | You decide on which queue these blocks get executed. 145 | 146 | **Example**: 147 | ```swift 148 | let networkService: NetworkService = // 149 | let resource: Resource = // 150 | 151 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 152 | print(htmlText, response) 153 | }, onError: { error in 154 | // Handle errors 155 | }) 156 | ``` 157 | 158 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 159 | - parameter resource: The resource you want to fetch. 160 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 161 | - parameter onError: Callback which gets called when fetching or transforming fails. 162 | 163 | - returns: a running network task 164 | */ 165 | @discardableResult 166 | public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 167 | onError: @escaping (NetworkError) -> Void) -> NetworkTask { 168 | lastRequests.append(resource.request) 169 | callbacks.append(NetworkServiceMockCallback( 170 | resource: resource, 171 | onCompletionWithResponse: onCompletionWithResponse, 172 | onError: onError 173 | )) 174 | 175 | return nextNetworkTask ?? NetworkTaskMock() 176 | } 177 | 178 | /// Will return an error to the current waiting request. 179 | /// 180 | /// - Parameters: 181 | /// - error: the error which gets passed to the caller 182 | /// 183 | /// - Throws: An error of type `NetworkServiceMock.Error` 184 | public func returnError(with error: NetworkError) throws { 185 | guard !callbacks.isEmpty else { 186 | throw Error.missingRequest 187 | } 188 | callbacks.removeFirst().onErrorCallback(error) 189 | } 190 | 191 | /// Will return a successful request, by using the given type `T` as serialized result of a request. 192 | /// 193 | /// - Parameters: 194 | /// - data: the mock response from the server. `Data()` by default 195 | /// - httpResponse: the mock `HTTPURLResponse` from the server. `HTTPURLResponse()` by default 196 | /// 197 | /// - Throws: An error of type `NetworkServiceMock.Error` 198 | public func returnSuccess(with serializedResponse: T, httpResponse: HTTPURLResponse = HTTPURLResponse()) throws { 199 | guard !callbacks.isEmpty else { 200 | throw Error.missingRequest 201 | } 202 | try callbacks.removeFirst().onTypedSuccess(serializedResponse, httpResponse) 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /Source/NetworkTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /** 27 | `NetworkTaskRepresenting` is a task which runs async to fetch data. 28 | */ 29 | public protocol NetworkTask: AnyObject { 30 | /** 31 | Cancels a task. 32 | */ 33 | func cancel() 34 | 35 | /** 36 | Resumes a task. 37 | */ 38 | func resume() 39 | 40 | /** 41 | Suspends a task. 42 | */ 43 | func suspend() 44 | } 45 | -------------------------------------------------------------------------------- /Source/NetworkTaskMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /// Mock implementation for `NetworkTask`. 27 | public class NetworkTaskMock: NetworkTask { 28 | 29 | /// Mock state of the network task 30 | public enum State { 31 | case canceled, resumed, suspended 32 | } 33 | 34 | /// Creates an `NetworkTaskMock` instance 35 | public init() {} 36 | 37 | /// State of the network taks. Can be used to assert. 38 | public private(set) var state: State? 39 | 40 | /// Cancel the request. Sets state to cancled. 41 | public func cancel() { 42 | state = .canceled 43 | } 44 | 45 | /// Resumes the request. Sets state to resumed. 46 | public func resume() { 47 | state = .resumed 48 | } 49 | 50 | /// Suspends the request. Sets state to suspended. 51 | public func suspend() { 52 | state = .suspended 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/Resource+Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | extension Resource where Model: Decodable { 27 | 28 | /// Creates an instace of Resource where the result type is `Decodable` and 29 | /// can be decoded with the given decoder 30 | /// 31 | /// - Parameters: 32 | /// - request: The request to get the remote data payload 33 | /// - decoder: a decoder which can decode the payload into the model type 34 | public init(request: URLRequest, decoder: JSONDecoder) { 35 | self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }) 36 | } 37 | } 38 | 39 | extension ResourceWithError where Model: Decodable { 40 | 41 | /// Creates an instace of Resource where the result type is `Decodable` and 42 | /// can be decoded with the given decoder 43 | /// 44 | /// - Parameters: 45 | /// - request: The request to get the remote data payload 46 | /// - decoder: a decoder which can decode the payload into the model type 47 | /// - mapError: a closure which maps to Error 48 | public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping (_ networkError: NetworkError) -> E) { 49 | self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Source/Resource+Inspect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) DB Systel GmbH. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a 5 | // copy of this software and associated documentation files (the "Software"), 6 | // to deal in the Software without restriction, including without limitation 7 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | // and/or sell copies of the Software, and to permit persons to whom the 9 | // Software is furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | // DEALINGS IN THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | extension Resource { 26 | /** 27 | This lets one inspect the data payload before data gets parsed. 28 | 29 | ```swift 30 | let resource: Resource = // 31 | resource.inspectData { data in 32 | print(String(bytes: data, encoding: .utf8)) 33 | } 34 | ``` 35 | 36 | - parameter inspector: closure which gets passed the data 37 | - returns: a new resource which gets instepcted before parsing 38 | */ 39 | public func inspectData(_ inspector: @escaping (Data) -> Void) -> Resource { 40 | return Resource(request: request, parse: { data in 41 | inspector(data) 42 | return try self.parse(data) 43 | }) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Source/Resource+Map.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | extension Resource { 25 | 26 | /// Maps a resource result to a different resource. This is useful when you have result of R which contains T and your API request a resource of T, 27 | /// 28 | /// - Parameter transform: transforms the original result of the resource 29 | /// - Returns: the transformed resource 30 | public func map(transform: @escaping (Model) throws -> T) -> Resource { 31 | return Resource(request: request, parse: { data in 32 | return try transform(try self.parse(data)) 33 | }) 34 | } 35 | } 36 | 37 | extension ResourceWithError { 38 | 39 | /// Maps a resource result to a different resource. This is useful when you have result of R which contains T and your API request a resource of T, 40 | /// 41 | /// Error parsing is not changed 42 | /// 43 | /// - Parameter transform: transforms the original result of the resource 44 | /// - Returns: the transformed resource 45 | public func map(transform: @escaping (Model) throws -> T) -> ResourceWithError { 46 | return ResourceWithError( 47 | request: request, 48 | parse: { data in 49 | return try transform(try self.parse(data)) 50 | }, 51 | mapError: mapError 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/Resource+Void.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Lukas Schmidt on 12.04.21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Resource where Model == Void { 11 | 12 | /// Creates an instace of Resource where the result type is `Void` 13 | /// 14 | /// - Parameters: 15 | /// - request: The request to get the remote data payload 16 | init(request: URLRequest) { 17 | self.init(request: request, parse: { _ in }) 18 | } 19 | } 20 | 21 | extension ResourceWithError where Model == Void { 22 | 23 | /// Creates an instace of Resource where the result type is `Void` 24 | /// 25 | /// - Parameters: 26 | /// - request: The request to get the remote data payload 27 | /// - mapError: a closure which maps to Error 28 | public init(request: URLRequest, mapError: @escaping (_ networkError: NetworkError) -> E) { 29 | self.init(request: request, parse: { _ in }, mapError: mapError) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Source/Resource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /** 27 | `Resource` describes a remote resource of generic type. 28 | The type can be fetched via HTTP(S) and parsed into the coresponding model object. 29 | 30 | **Example**: 31 | ```swift 32 | let request: URLRequest = // 33 | let resource: Resource = Resource(request: request, parse: { data in 34 | String(data: data, encoding: .utf8) 35 | }) 36 | ``` 37 | */ 38 | public struct Resource { 39 | /// The request to fetch the resource remote payload 40 | public let request: URLRequest 41 | 42 | /// Parses data into given model. 43 | public let parse: (_ data: Data) throws -> Model 44 | 45 | /// Creates a type safe resource, which can be used to fetch it with `NetworkService` 46 | /// 47 | /// - Parameters: 48 | /// - request: The request to get the remote data payload 49 | /// - parse: Parses data fetched with the request into given Model 50 | public init(request: URLRequest, parse: @escaping (Data) throws -> Model) { 51 | self.request = request 52 | self.parse = parse 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/ResourceWithError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2021 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /** 27 | `ResourceWithError` describes a remote resource of generic type and generic error. 28 | The type can be fetched via HTTP(S) and parsed into the coresponding model object. 29 | 30 | **Example**: 31 | ```swift 32 | let request: URLRequest = // 33 | let resource: ResourceWithError = Resource(request: request, parse: { data in 34 | String(data: data, encoding: .utf8) 35 | }, mapError: { networkError in 36 | return CustomError(networkError) 37 | }) 38 | ``` 39 | */ 40 | public struct ResourceWithError { 41 | /// The request to fetch the resource remote payload 42 | public let request: URLRequest 43 | 44 | /// Parses data into given model. 45 | public let parse: (_ data: Data) throws -> Model 46 | public let mapError: (_ networkError: NetworkError) -> E 47 | 48 | /// Creates a type safe resource, which can be used to fetch it with NetworkService 49 | /// 50 | /// - Parameters: 51 | /// - request: The request to get the remote data payload 52 | /// - parse: Parses data fetched with the request into given Model 53 | 54 | public init(request: URLRequest, parse: @escaping (Data) throws -> Model, mapError: @escaping (_ networkError: NetworkError) -> E) { 55 | self.request = request 56 | self.parse = parse 57 | self.mapError = mapError 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Source/RetryNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import Dispatch 26 | 27 | /** 28 | `RetryNetworkService` can request resource. When a request fails with a given condtion it can retry the request after a given time interval. 29 | The count of retry attemps can be configured as well. 30 | 31 | - seealso: `NetworkService` 32 | */ 33 | public final class RetryNetworkService: NetworkService { 34 | private let networkService: NetworkService 35 | private let numberOfRetries: Int 36 | private let idleTimeInterval: TimeInterval 37 | private let dispatchRetry: (_ deadline: DispatchTime, _ execute: @escaping () -> Void ) -> Void 38 | private let shouldRetry: (NetworkError) -> Bool 39 | 40 | /// Creates an instance of `RetryNetworkService` 41 | /// 42 | /// - Parameters: 43 | /// - networkService: a networkservice 44 | /// - numberOfRetries: the number of retrys before final error 45 | /// - idleTimeInterval: time between error and retry 46 | /// - shouldRetry: closure which evaluated if error should be retry 47 | /// - dispatchRetry: closure where to dispatch the waiting 48 | public init(networkService: NetworkService, numberOfRetries: Int, 49 | idleTimeInterval: TimeInterval, shouldRetry: @escaping (NetworkError) -> Bool, 50 | dispatchRetry: @escaping (_ deadline: DispatchTime, _ execute: @escaping () -> Void ) -> Void = { deadline, execute in 51 | DispatchQueue.global(qos: .utility).asyncAfter(deadline: deadline, execute: execute) 52 | }) { 53 | self.networkService = networkService 54 | self.numberOfRetries = numberOfRetries 55 | self.idleTimeInterval = idleTimeInterval 56 | self.shouldRetry = shouldRetry 57 | self.dispatchRetry = dispatchRetry 58 | } 59 | 60 | /** 61 | Fetches a resource asynchronously from remote location. Execution of the requests starts immediately. 62 | Execution happens on no specific queue. It dependes on the network access which queue is used. 63 | Once execution is finished either the completion block or the error block gets called. 64 | You decide on which queue these blocks get executed. 65 | 66 | **Example**: 67 | ```swift 68 | let networkService: NetworkService = // 69 | let resource: Resource = // 70 | 71 | networkService.request(queue: .main, resource: resource, onCompletionWithResponse: { htmlText, response in 72 | print(htmlText, response) 73 | }, onError: { error in 74 | // Handle errors 75 | }) 76 | ``` 77 | 78 | - parameter queue: The `DispatchQueue` to execute the completion and error block on. 79 | - parameter resource: The resource you want to fetch. 80 | - parameter onCompletionWithResponse: Callback which gets called when fetching and transforming into model succeeds. 81 | - parameter onError: Callback which gets called when fetching or transforming fails. 82 | 83 | - returns: a running network task 84 | */ 85 | @discardableResult 86 | public func request(queue: DispatchQueue, resource: Resource, onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 87 | onError: @escaping (NetworkError) -> Void) -> NetworkTask { 88 | let containerTask = ContainerNetworkTask() 89 | let retryOnError = customOnError( 90 | containerTask: containerTask, 91 | numberOfRetriesLeft: numberOfRetries, 92 | queue: queue, 93 | resource: resource, 94 | onCompletionWithResponse: onCompletionWithResponse, 95 | onError: onError 96 | ) 97 | containerTask.underlyingTask = networkService.request( 98 | queue: queue, 99 | resource: resource, 100 | onCompletionWithResponse: onCompletionWithResponse, 101 | onError: retryOnError 102 | ) 103 | 104 | return containerTask 105 | } 106 | 107 | private func customOnError(containerTask: ContainerNetworkTask, 108 | numberOfRetriesLeft: Int, 109 | queue: DispatchQueue, 110 | resource: Resource, 111 | onCompletionWithResponse: @escaping (Result, HTTPURLResponse) -> Void, 112 | onError: @escaping (NetworkError) -> Void) -> (NetworkError) -> Void { 113 | return { error in 114 | if self.shouldRetry(error), numberOfRetriesLeft > 0 { 115 | guard !containerTask.isCanceled else { 116 | return 117 | } 118 | self.dispatchRetry(.now() + self.idleTimeInterval, { 119 | let newOnError = self.customOnError( 120 | containerTask: containerTask, 121 | numberOfRetriesLeft: numberOfRetriesLeft - 1, 122 | queue: queue, 123 | resource: resource, 124 | onCompletionWithResponse: onCompletionWithResponse, 125 | onError: onError 126 | ) 127 | 128 | containerTask.underlyingTask = self.networkService.request( 129 | queue: queue, 130 | resource: resource, 131 | onCompletionWithResponse: onCompletionWithResponse, 132 | onError: newOnError 133 | ) 134 | }) 135 | } else { 136 | onError(error) 137 | } 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Source/URL+StaticStringInit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2018 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public extension URL { 27 | 28 | /// Create a URL with a compile time constant string. 29 | /// You guarantee that the string can be transformed into a valid `URL` 30 | /// 31 | /// - Parameter staticString: the string which gets transformed into `URL`. 32 | init(staticString: StaticString) { 33 | guard let newUrl = URL(string: "\(staticString)") else { 34 | fatalError("Could not create url with string: \(staticString)") 35 | } 36 | 37 | self = newUrl 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Source/URLRequest+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | extension URLRequest { 27 | 28 | /// Convience initializer for easy request creation. 29 | /// 30 | /// - Parameters: 31 | /// - path: path to the resource. 32 | /// - baseURL: the base url of the resource. 33 | /// - HTTPMethod: the HTTP method for the request. Defaults to `.GET` 34 | /// - parameters: url parameters for the request. Defaults to `nil` 35 | /// - body: body data payload. Defaults to `nil` 36 | /// - allHTTPHeaderFields: HTTP request header fields. Defaults to `nil` 37 | /// 38 | /// - Important: path must not start with a `/` 39 | public init(path: String, baseURL: URL, 40 | HTTPMethod: HTTPMethod = .GET, parameters: [String: String]? = nil, 41 | body: Data? = nil, allHTTPHeaderFields: Dictionary? = nil) { 42 | guard let url = URL(string: path, relativeTo: baseURL) else { 43 | fatalError("Error creating absolute URL from path: \(path), with baseURL: \(baseURL)") 44 | } 45 | 46 | let urlWithParameters: URL 47 | if let parameters = parameters, !parameters.isEmpty { 48 | urlWithParameters = url.appending(queryParameters: parameters) 49 | } else { 50 | urlWithParameters = url 51 | } 52 | 53 | self.init(url: urlWithParameters) 54 | self.httpBody = body 55 | self.httpMethod = HTTPMethod.rawValue 56 | self.allHTTPHeaderFields = allHTTPHeaderFields 57 | } 58 | } 59 | 60 | extension Array where Element == URLQueryItem { 61 | 62 | func appending(queryItems: [URLQueryItem], overrideExisting: Bool = true) -> [URLQueryItem] { 63 | var items = overrideExisting ? [URLQueryItem]() : self 64 | 65 | var itemsToAppend = queryItems 66 | 67 | if overrideExisting { 68 | for item in self { 69 | var itemFound = false 70 | for (index, value) in itemsToAppend.enumerated() where value.name == item.name { 71 | itemFound = true 72 | items.append(value) 73 | itemsToAppend.remove(at: index) 74 | break 75 | } 76 | if itemFound == false { 77 | items.append(item) 78 | } 79 | } 80 | } 81 | 82 | items.append(contentsOf: itemsToAppend) 83 | 84 | return items 85 | } 86 | 87 | } 88 | 89 | extension Dictionary where Key == String, Value == String { 90 | func asURLQueryItems() -> [URLQueryItem] { 91 | return map { URLQueryItem(name: $0.0, value: $0.1) } 92 | } 93 | } 94 | 95 | extension URL { 96 | 97 | func modifyingComponents(using block:(inout URLComponents) -> Void) -> URL { 98 | guard var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: true) else { 99 | fatalError("Could not create url components from \(self.absoluteString)") 100 | } 101 | 102 | block(&urlComponents) 103 | 104 | guard let absoluteURL = urlComponents.url else { 105 | fatalError("Error creating absolute URL from path: \(path), with baseURL: \(baseURL?.absoluteString ?? "No BaseURL found")") 106 | } 107 | return absoluteURL 108 | } 109 | 110 | func appending(queryItems: [URLQueryItem], overrideExisting: Bool = true) -> URL { 111 | return modifyingComponents { urlComponents in 112 | let items = urlComponents.queryItems ?? [URLQueryItem]() 113 | urlComponents.queryItems = items.appending(queryItems: queryItems, overrideExisting: overrideExisting) 114 | } 115 | } 116 | 117 | func appending(queryParameters: [String: String], overrideExisting: Bool = true) -> URL { 118 | return appending(queryItems: queryParameters.asURLQueryItems(), overrideExisting: overrideExisting) 119 | } 120 | 121 | func replacingAllQueryItems(with queryItems: [URLQueryItem]) -> URL { 122 | return modifyingComponents { urlComonents in 123 | urlComonents.queryItems = queryItems 124 | } 125 | } 126 | 127 | func replacingAllQueryParameters(with queryParameters: [String: String]) -> URL { 128 | return replacingAllQueryItems(with: queryParameters.asURLQueryItems()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Source/URLRequest+Modifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public extension URLRequest { 27 | 28 | /// Creates a new `URLRequest` with HTTPHeaderFields added into the new request. 29 | /// Keep in mind that this overrides header fields which are already contained. 30 | /// 31 | /// - Parameter HTTPHeaderFields: the header fileds to add to the request 32 | /// - Returns: a new `URLRequest` 33 | func added(HTTPHeaderFields: [String: String]) -> URLRequest { 34 | var request = self 35 | let headerFiels = (request.allHTTPHeaderFields ?? [:]).merging(HTTPHeaderFields, uniquingKeysWith: { $1 }) 36 | request.allHTTPHeaderFields = headerFiels 37 | 38 | return request 39 | } 40 | 41 | /// Creates a new `URLRequest` with query items appended to the new request. 42 | /// 43 | /// - Parameter queryItems: the query items to append to the request 44 | /// - Parameter overrideExisting: if `true existing items with the same name will be overridden 45 | /// - Returns: a new `URLRequest` 46 | func appending(queryItems: [URLQueryItem], overrideExisting: Bool = true) -> URLRequest { 47 | var request = self 48 | guard let url = request.url else { 49 | return self 50 | } 51 | request.url = url.appending(queryItems: queryItems) 52 | return request 53 | } 54 | 55 | /// Creates a new `URLRequest` with query parameters appended to the new request. 56 | /// 57 | /// - Parameter queryParameters: the parameters to append to the request 58 | /// - Parameter overrideExisting: if `true existing items with the same name will be overridden 59 | /// - Returns: a new `URLRequest` 60 | func appending(queryParameters: [String: String], overrideExisting: Bool = true) -> URLRequest { 61 | return appending(queryItems: queryParameters.asURLQueryItems() ) 62 | } 63 | 64 | /// Creates a new `URLRequest` with all existing query items replaced with new ones. 65 | /// 66 | /// - Parameter queryItems: the query items to add to the request 67 | /// - Returns: a new `URLRequest` 68 | func replacingAllQueryItems(with queryItems: [URLQueryItem]) -> URLRequest { 69 | var request = self 70 | guard let url = request.url else { 71 | return self 72 | } 73 | request.url = url.replacingAllQueryItems(with: queryItems) 74 | return request 75 | } 76 | 77 | /// Creates a new `URLRequest` with all existing query items replaced with new ones. 78 | /// 79 | /// - Parameter parameters: the parameters to add to the request 80 | /// - Returns: a new `URLRequest` 81 | func replacingAllQueryItems(with parameters: [String: String]) -> URLRequest { 82 | return replacingAllQueryItems(with: parameters.asURLQueryItems() ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Source/URLSession+NetworkAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /// Adds conformens to `NetworkAccess`. `URLSession` can be used as a network access. 27 | extension URLSession: NetworkAccess { 28 | /** 29 | Fetches a request asynchrony from remote location. 30 | 31 | - parameter request: The request you want to fetch. 32 | - parameter callback: Callback which gets called when the request finishes. 33 | 34 | - returns: the running network task 35 | */ 36 | public func load(request: URLRequest, callback: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) -> NetworkTask { 37 | let task = dataTask(with: request, completionHandler: { data, response, error in 38 | callback(data, response as? HTTPURLResponse, error) 39 | }) 40 | 41 | task.resume() 42 | 43 | return task 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/URLSessionDataTask+NetworkTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | /// URLSessionDataTask conforms to NetworkTask 27 | extension URLSessionDataTask: NetworkTask { } 28 | -------------------------------------------------------------------------------- /Tests/ContainerNetworkTaskTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Christian Himmelsbach on 23.07.18. 3 | // 4 | 5 | import XCTest 6 | import DBNetworkStack 7 | 8 | class ContainerNetworkTaskTest: XCTestCase { 9 | 10 | func testGIVEN_AuthenticatorNetworkTask_WHEN_ResumeTask_THEN_UnderlyingTaskShouldBeResumed() { 11 | //Given 12 | let taskMock = NetworkTaskMock() 13 | let task = ContainerNetworkTask() 14 | task.underlyingTask = taskMock 15 | 16 | //When 17 | task.resume() 18 | 19 | //Then 20 | XCTAssert(taskMock.state == .resumed) 21 | } 22 | 23 | func testGIVEN_AuthenticatorNetworkTask_WHEN_SuspendTask_THEN_UnderlyingTaskShouldBeSuspened() { 24 | //Given 25 | let taskMock = NetworkTaskMock() 26 | let task = ContainerNetworkTask() 27 | task.underlyingTask = taskMock 28 | 29 | //When 30 | task.suspend() 31 | 32 | //Then 33 | XCTAssert(taskMock.state == .suspended) 34 | } 35 | 36 | func testGIVEN_AuthenticatorNetworkTask_WHEN_CancelTask_THEN_UnderlyingTaskShouldBeCanceled() { 37 | // Given 38 | let taskMock = NetworkTaskMock() 39 | let task = ContainerNetworkTask() 40 | task.underlyingTask = taskMock 41 | 42 | // When 43 | task.cancel() 44 | 45 | // Then 46 | XCTAssert(taskMock.state == .canceled) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Tests/DecodableResoureTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import XCTest 26 | @testable import DBNetworkStack 27 | 28 | class DecodableResoureTest: XCTestCase { 29 | var resource: Resource { 30 | let request = URLRequest(path: "/train", baseURL: .defaultMock) 31 | return Resource(request: request, decoder: JSONDecoder()) 32 | } 33 | 34 | func testResource_withValidData() { 35 | //When 36 | let fetchedTrain = try? resource.parse(Train.validJSONData) 37 | 38 | //Then 39 | XCTAssertEqual(fetchedTrain?.name, "ICE") 40 | } 41 | 42 | func testResource_withMAppedResult() { 43 | //When 44 | let nameResource = resource.map { $0.name } 45 | let fetchedTrainName = try? nameResource.parse(Train.validJSONData) 46 | 47 | //Then 48 | XCTAssertEqual(fetchedTrainName, "ICE") 49 | } 50 | 51 | func testResource_WithInvalidData() throws { 52 | //When 53 | do { 54 | _ = try resource.parse(Train.invalidJSONData) 55 | XCTFail("Expected method to throws") 56 | } catch { } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Tests/DefaultMocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | extension URL { 27 | static let defaultMock = URL(staticString: "https://bahn.de") 28 | } 29 | 30 | extension URLRequest { 31 | static let defaultMock = URLRequest(url: .defaultMock) 32 | } 33 | 34 | extension HTTPURLResponse { 35 | static let defaultMock = HTTPURLResponse(url: .defaultMock, mimeType: nil, expectedContentLength: 1, textEncodingName: nil) 36 | } 37 | -------------------------------------------------------------------------------- /Tests/ModifyRequestNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import XCTest 25 | import Foundation 26 | @testable import DBNetworkStack 27 | 28 | class ModifyRequestNetworkServiceTest: XCTestCase { 29 | 30 | var networkServiceMock: NetworkServiceMock! 31 | 32 | override func setUp() { 33 | super.setUp() 34 | networkServiceMock = NetworkServiceMock() 35 | } 36 | 37 | func testRequest_withModifedRequest() { 38 | //Given 39 | let modification: [(URLRequest) -> URLRequest] = [ { request in 40 | return request.appending(queryParameters: ["key": "1"]) 41 | } ] 42 | let networkService: NetworkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification) 43 | let request = URLRequest(path: "/trains", baseURL: .defaultMock) 44 | let resource = Resource(request: request, parse: { _ in return 1 }) 45 | 46 | //When 47 | networkService.request(resource, onCompletion: { _ in }, onError: { _ in }) 48 | 49 | //Then 50 | XCTAssert(networkServiceMock.lastRequest?.url?.absoluteString.contains("key=1") ?? false) 51 | } 52 | 53 | func testAddHTTPHeaderToRequest() { 54 | //Given 55 | let request = URLRequest(url: .defaultMock) 56 | let header = ["header": "head"] 57 | 58 | //When 59 | let newRequest = request.added(HTTPHeaderFields: header) 60 | 61 | //Then 62 | XCTAssertEqual(newRequest.allHTTPHeaderFields?["header"], "head") 63 | } 64 | 65 | func testAddDuplicatedQueryToRequest() { 66 | //Given 67 | let url = URL(staticString: "bahn.de?test=test&bool=true") 68 | let request = URLRequest(url: url) 69 | 70 | let parameters = ["test": "test2"] 71 | 72 | //When 73 | let newRequest = request.appending(queryParameters: parameters) 74 | 75 | //Then 76 | let newURL: URL! = newRequest.url 77 | let query = URLComponents(url: newURL, resolvingAgainstBaseURL: true)?.queryItems 78 | XCTAssertEqual(query?.count, 2) 79 | XCTAssert(query?.contains(where: { $0.name == "test" && $0.value == "test2" }) ?? false) 80 | XCTAssert(query?.contains(where: { $0.name == "bool" && $0.value == "true" }) ?? false) 81 | } 82 | 83 | func testReplaceAllQueryItemsFromRequest() { 84 | //Given 85 | let url = URL(staticString: "bahn.de?test=test&bool=true") 86 | let request = URLRequest(url: url) 87 | 88 | let parameters = ["test5": "test2"] 89 | 90 | //When 91 | let newRequest = request.replacingAllQueryItems(with: parameters) 92 | 93 | //Then 94 | let newURL: URL! = newRequest.url 95 | let query = URLComponents(url: newURL, resolvingAgainstBaseURL: true)?.queryItems 96 | XCTAssertEqual(query?.count, 1) 97 | XCTAssert(query?.contains(where: { $0.name == "test5" && $0.value == "test2" }) ?? false) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/NetworkAccessMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import DBNetworkStack 26 | 27 | class NetworkAccessMock: NetworkAccess { 28 | fileprivate(set) var data: Data? 29 | fileprivate(set) var response: HTTPURLResponse? 30 | fileprivate(set) var error: NSError? 31 | 32 | fileprivate(set) var request: URLRequest? 33 | 34 | func load(request: URLRequest, callback: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) -> NetworkTask { 35 | self.request = request 36 | 37 | callback(data, response, error) 38 | 39 | return NetworkTaskMock() 40 | } 41 | 42 | func changeMock(data: Data?, response: HTTPURLResponse?, error: NSError?) { 43 | self.data = data 44 | self.response = response 45 | self.error = error 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/NetworkErrorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import XCTest 26 | @testable import DBNetworkStack 27 | 28 | class NetworkErrorTest: XCTestCase { 29 | 30 | func urlResponseWith(statusCode: Int) -> HTTPURLResponse? { 31 | return HTTPURLResponse(url: .defaultMock, statusCode: statusCode, httpVersion: nil, headerFields: nil) 32 | } 33 | 34 | private let testData: Data! = "test_string".data(using: .utf8) 35 | 36 | func testInit_WithHTTPStatusCode400() { 37 | //Given 38 | let expectedResponse = urlResponseWith(statusCode: 400) 39 | 40 | //When 41 | let error = NetworkError(response: expectedResponse, data: testData) 42 | 43 | //Then 44 | switch error { 45 | case .clientError(let response, let data)?: 46 | XCTAssertEqual(response, expectedResponse) 47 | XCTAssertEqual(data, testData) 48 | default: 49 | XCTFail("Expects clientError") 50 | } 51 | } 52 | 53 | func testInit_WithHTTPStatusCode401() { 54 | //Given 55 | let expectedResponse = urlResponseWith(statusCode: 401) 56 | 57 | //When 58 | let error = NetworkError(response: expectedResponse, data: testData) 59 | 60 | //Then 61 | switch error { 62 | case .unauthorized(let response, let data)?: 63 | XCTAssertEqual(response, expectedResponse) 64 | XCTAssertEqual(data, testData) 65 | default: 66 | XCTFail("Expects unauthorized") 67 | } 68 | } 69 | 70 | func testInit_WithHTTPStatusCode200() { 71 | //Given 72 | let response = urlResponseWith(statusCode: 200) 73 | 74 | //When 75 | let error = NetworkError(response: response, data: nil) 76 | 77 | //Then 78 | XCTAssertNil(error) 79 | } 80 | 81 | func testInit_WithHTTPStatusCode511() { 82 | //Given 83 | let expectedResponse = urlResponseWith(statusCode: 511) 84 | 85 | //When 86 | let error = NetworkError(response: expectedResponse, data: testData) 87 | 88 | //Then 89 | switch error { 90 | case .serverError(let response, let data)?: 91 | XCTAssertEqual(response, expectedResponse) 92 | XCTAssertEqual(data, testData) 93 | default: 94 | XCTFail("Expects serverError") 95 | } 96 | } 97 | 98 | func testInit_WithInvalidHTTPStatusCode900() { 99 | //Given 100 | let response = urlResponseWith(statusCode: 900) 101 | 102 | //When 103 | let error = NetworkError(response: response, data: nil) 104 | 105 | //Then 106 | XCTAssertNil(error) 107 | } 108 | 109 | func testUnknownError_debug_description() { 110 | //Given 111 | let error: NetworkError = .unknownError 112 | 113 | //When 114 | let debugDescription = error.debugDescription 115 | 116 | //Then 117 | XCTAssertEqual(debugDescription, "Unknown error") 118 | } 119 | 120 | func testUnknownError_cancelled_description() { 121 | //Given 122 | let error: NetworkError = .cancelled 123 | 124 | //When 125 | let debugDescription = error.debugDescription 126 | 127 | //Then 128 | XCTAssertEqual(debugDescription, "Request cancelled") 129 | } 130 | 131 | func testUnknownError_unauthorized_description() { 132 | //Given 133 | let response: HTTPURLResponse! = HTTPURLResponse(url: .defaultMock, statusCode: 0, httpVersion: "1.1", headerFields: nil) 134 | let data = "dataString".data(using: .utf8) 135 | let error: NetworkError = .unauthorized(response: response, data: data) 136 | 137 | //When 138 | let debugDescription = error.debugDescription 139 | 140 | //Then 141 | XCTAssert(debugDescription.hasPrefix("Authorization error: Int in 65 | throw NetworkError.unknownError }) 66 | let data: Data! = "Data".data(using: .utf8) 67 | 68 | // When 69 | do { 70 | _ = try processor.process(response: .defaultMock, resource: resource, data: data, error: nil) 71 | } catch let error as NetworkError { 72 | // Then 73 | switch error { 74 | case .serializationError(let error, let recievedData): // Excpected 75 | switch error as? NetworkError { 76 | case .unknownError?: 77 | XCTAssert(true) 78 | default: 79 | XCTFail("Expects unknownError") 80 | } 81 | 82 | XCTAssertEqual(recievedData, data) 83 | default: 84 | XCTFail("Expected cancelled error (got \(error)") 85 | } 86 | } catch let error { 87 | XCTFail("Expected NetworkError (got \(type(of: error)))") 88 | } 89 | } 90 | 91 | func testParseSucessFullWithNilResponse() { 92 | //Given 93 | let resource = Resource(request: URLRequest.defaultMock, parse: { _ in return 0 }) 94 | 95 | //When 96 | do { 97 | _ = try processor.process(response: nil, resource: resource, data: Data(), error: nil) 98 | } catch let error as NetworkError { 99 | // Then 100 | switch error { 101 | case .unknownError: // Excpected 102 | break 103 | default: 104 | XCTFail("Expected cancelled error (got \(error)") 105 | } 106 | } catch let error { 107 | XCTFail("Expected NetworkError (got \(type(of: error)))") 108 | } 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/NetworkServiceMockTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import XCTest 25 | import DBNetworkStack 26 | 27 | class NetworkServiceMockTest: XCTestCase { 28 | 29 | var networkServiceMock: NetworkServiceMock! 30 | 31 | let resource = Resource(request: URLRequest(path: "/trains", baseURL: .defaultMock), parse: { _ in return 1 }) 32 | 33 | override func setUp() { 34 | networkServiceMock = NetworkServiceMock() 35 | } 36 | 37 | func testRequestCount() { 38 | //When 39 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) 40 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) 41 | 42 | //Then 43 | XCTAssertEqual(networkServiceMock.requestCount, 2) 44 | } 45 | 46 | func testLastRequests() { 47 | //When 48 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) 49 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) 50 | 51 | //Then 52 | XCTAssertEqual(networkServiceMock.lastRequests, [resource.request, resource.request]) 53 | } 54 | 55 | func testReturnSuccessWithData() throws { 56 | //Given 57 | var capturedResult: Int? 58 | var executionCount: Int = 0 59 | 60 | //When 61 | networkServiceMock.request(resource, onCompletion: { result in 62 | capturedResult = result 63 | executionCount += 1 64 | }, onError: { _ in }) 65 | try networkServiceMock.returnSuccess(with: 1) 66 | 67 | //Then 68 | XCTAssertEqual(capturedResult, 1) 69 | XCTAssertEqual(executionCount, 1) 70 | } 71 | 72 | func testCorrectOrderOfReturnSuccessWithDataForMultipleRequests() throws { 73 | //Given 74 | var called1First = false 75 | var called2First = false 76 | 77 | //When 78 | networkServiceMock.request(resource, onCompletion: { _ in 79 | if !called2First { 80 | called1First = true 81 | } 82 | }, onError: { _ in }) 83 | networkServiceMock.request(resource, onCompletion: { _ in 84 | if !called1First { 85 | called2First = true 86 | } 87 | }, onError: { _ in }) 88 | try networkServiceMock.returnSuccess(with: 0) 89 | try networkServiceMock.returnSuccess(with: 0) 90 | 91 | //Then 92 | XCTAssertTrue(called1First) 93 | XCTAssertFalse(called2First) 94 | } 95 | 96 | func testRequestSuccessWithDataChaining() throws { 97 | //Given 98 | var executionCount1: Int = 0 99 | var executionCount2: Int = 0 100 | 101 | //When 102 | networkServiceMock.request(resource, onCompletion: { _ in 103 | executionCount1 += 1 104 | self.networkServiceMock.request(self.resource, onCompletion: { _ in 105 | executionCount2 += 1 106 | }, onError: { _ in }) 107 | }, onError: { _ in }) 108 | try networkServiceMock.returnSuccess(with: 0) 109 | try networkServiceMock.returnSuccess(with: 0) 110 | 111 | //Then 112 | XCTAssertEqual(executionCount1, 1) 113 | XCTAssertEqual(executionCount2, 1) 114 | } 115 | 116 | func testReturnSuccessWithDataForAllRequests() throws { 117 | //Given 118 | var executionCount1: Int = 0 119 | var executionCount2: Int = 0 120 | 121 | //When 122 | networkServiceMock.request(resource, onCompletion: { _ in 123 | executionCount1 += 1 124 | }, onError: { _ in }) 125 | networkServiceMock.request(resource, onCompletion: { _ in 126 | executionCount2 += 1 127 | }, onError: { _ in }) 128 | try networkServiceMock.returnSuccess(with: 0) 129 | try networkServiceMock.returnSuccess(with: 0) 130 | 131 | //Then 132 | XCTAssertEqual(executionCount1, 1) 133 | XCTAssertEqual(executionCount2, 1) 134 | } 135 | 136 | func testReturnSuccessWithSerializedData() throws { 137 | //Given 138 | var capturedResult: Int? 139 | var executionCount: Int = 0 140 | 141 | //When 142 | networkServiceMock.request(resource, onCompletion: { result in 143 | capturedResult = result 144 | executionCount += 1 145 | }, onError: { _ in }) 146 | try networkServiceMock.returnSuccess(with: 10) 147 | 148 | //Then 149 | XCTAssertEqual(capturedResult, 10) 150 | XCTAssertEqual(executionCount, 1) 151 | } 152 | 153 | func testCorrectOrderOfReturnSuccessWithSerializedDataForMultipleRequests() throws { 154 | //Given 155 | var capturedResult1: Int? 156 | var capturedResult2: Int? 157 | 158 | //When 159 | networkServiceMock.request(resource, onCompletion: { result in 160 | capturedResult1 = result 161 | }, onError: { _ in }) 162 | networkServiceMock.request(resource, onCompletion: { result in 163 | capturedResult2 = result 164 | }, onError: { _ in }) 165 | try networkServiceMock.returnSuccess(with: 10) 166 | try networkServiceMock.returnSuccess(with: 20) 167 | 168 | //Then 169 | XCTAssertEqual(capturedResult1, 10) 170 | XCTAssertEqual(capturedResult2, 20) 171 | } 172 | 173 | func testRequestSuccessWithSerializedDataChaining() throws { 174 | //Given 175 | var executionCount1: Int = 0 176 | var executionCount2: Int = 0 177 | 178 | //When 179 | networkServiceMock.request(resource, onCompletion: { _ in 180 | executionCount1 += 1 181 | self.networkServiceMock.request(self.resource, onCompletion: { _ in 182 | executionCount2 += 1 183 | }, onError: { _ in }) 184 | }, onError: { _ in }) 185 | try networkServiceMock.returnSuccess(with: 10) 186 | try networkServiceMock.returnSuccess(with: 20) 187 | 188 | //Then 189 | XCTAssertEqual(executionCount1, 1) 190 | XCTAssertEqual(executionCount2, 1) 191 | } 192 | 193 | func testReturnSuccessWithSerializedDataForAllRequests() throws { 194 | //Given 195 | var executionCount1: Int = 0 196 | var executionCount2: Int = 0 197 | 198 | //When 199 | networkServiceMock.request(resource, onCompletion: { _ in 200 | executionCount1 += 1 201 | }, onError: { _ in }) 202 | networkServiceMock.request(resource, onCompletion: { _ in 203 | executionCount2 += 1 204 | }, onError: { _ in }) 205 | try networkServiceMock.returnSuccess(with: 10) 206 | try networkServiceMock.returnSuccess(with: 10) 207 | 208 | //Then 209 | XCTAssertEqual(executionCount1, 1) 210 | XCTAssertEqual(executionCount2, 1) 211 | } 212 | 213 | func testReturnError() throws { 214 | //Given 215 | var capturedError: NetworkError? 216 | var executionCount: Int = 0 217 | 218 | //When 219 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { error in 220 | capturedError = error 221 | executionCount += 1 222 | }) 223 | try networkServiceMock.returnError(with: .unknownError) 224 | 225 | //Then 226 | if let error = capturedError, case .unknownError = error { 227 | 228 | } else { 229 | XCTFail("Wrong error type") 230 | } 231 | XCTAssertEqual(executionCount, 1) 232 | } 233 | 234 | func testCorrectOrderOfReturnErrorForMultipleRequests() throws { 235 | //Given 236 | var capturedError1: NetworkError? 237 | var capturedError2: NetworkError? 238 | 239 | //When 240 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { error in 241 | capturedError1 = error 242 | }) 243 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { error in 244 | capturedError2 = error 245 | }) 246 | try networkServiceMock.returnError(with: .unknownError) 247 | try networkServiceMock.returnError(with: .cancelled) 248 | 249 | //Then 250 | if case .unknownError? = capturedError1, case .cancelled? = capturedError2 { 251 | 252 | } else { 253 | XCTFail("Wrong order of error responses") 254 | } 255 | } 256 | 257 | func testRequestErrorChaining() throws { 258 | //Given 259 | var executionCount1: Int = 0 260 | var executionCount2: Int = 0 261 | 262 | //When 263 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in 264 | executionCount1 += 1 265 | self.networkServiceMock.request(self.resource, onCompletion: { _ in }, onError: { _ in 266 | executionCount2 += 1 267 | }) 268 | }) 269 | 270 | try networkServiceMock.returnError(with: .unknownError) 271 | try networkServiceMock.returnError(with: .unknownError) 272 | 273 | //Then 274 | XCTAssertEqual(executionCount1, 1) 275 | XCTAssertEqual(executionCount2, 1) 276 | } 277 | 278 | func testReturnErrorsForAllRequests() throws { 279 | //Given 280 | var executionCount1: Int = 0 281 | var executionCount2: Int = 0 282 | 283 | //When 284 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in 285 | executionCount1 += 1 286 | }) 287 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in 288 | executionCount2 += 1 289 | }) 290 | try networkServiceMock.returnError(with: .unknownError) 291 | try networkServiceMock.returnError(with: .unknownError) 292 | 293 | //Then 294 | XCTAssertEqual(executionCount1, 1) 295 | XCTAssertEqual(executionCount2, 1) 296 | } 297 | 298 | func testReturnSuccessMismatchType() { 299 | //When 300 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) 301 | 302 | //Then 303 | XCTAssertThrowsError(try networkServiceMock.returnSuccess(with: "Mismatch Type")) 304 | } 305 | 306 | func testReturnSuccessMissingRequest() { 307 | //Then 308 | XCTAssertThrowsError(try networkServiceMock.returnSuccess(with: 1)) 309 | } 310 | 311 | func testReturnErrorMissingRequest() { 312 | //Then 313 | XCTAssertThrowsError(try networkServiceMock.returnError(with: .unknownError)) 314 | } 315 | 316 | func testPendingRequestCountEmpty() { 317 | XCTAssertEqual(networkServiceMock.pendingRequestCount, 0) 318 | } 319 | 320 | func testPendingRequestCountNotEmpty() { 321 | //When 322 | networkServiceMock.request(resource, onCompletion: { _ in }, onError: { _ in }) 323 | 324 | //Then 325 | XCTAssertEqual(networkServiceMock.pendingRequestCount, 1) 326 | } 327 | 328 | } 329 | -------------------------------------------------------------------------------- /Tests/NetworkServiceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import XCTest 26 | @testable import DBNetworkStack 27 | 28 | class NetworkServiceTest: XCTestCase { 29 | 30 | var networkService: NetworkService! 31 | 32 | var networkAccess = NetworkAccessMock() 33 | 34 | let trainName = "ICE" 35 | 36 | var resource: Resource { 37 | let request = URLRequest(path: "train", baseURL: .defaultMock) 38 | return Resource(request: request, decoder: JSONDecoder()) 39 | } 40 | 41 | override func setUp() { 42 | networkService = BasicNetworkService(networkAccess: networkAccess) 43 | } 44 | 45 | func testRequest_withValidResponse() { 46 | //Given 47 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 48 | let expection = expectation(description: "loadValidRequest") 49 | 50 | //When 51 | networkService.request(resource, onCompletionWithResponse: { train, response in 52 | XCTAssertEqual(train.name, self.trainName) 53 | XCTAssertEqual(response, .defaultMock) 54 | expection.fulfill() 55 | }, onError: { _ in 56 | XCTFail("Should not call error block") 57 | }) 58 | 59 | waitForExpectations(timeout: 1, handler: nil) 60 | 61 | //Then 62 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 63 | } 64 | 65 | func testRequest_withNoDataResponse() { 66 | //Given 67 | networkAccess.changeMock(data: nil, response: nil, error: nil) 68 | let expection = expectation(description: "testNoData") 69 | 70 | //When 71 | var capturedError: NetworkError? 72 | networkService.request(resource, onCompletion: { _ in 73 | XCTFail("Should not call success block") 74 | }, onError: { error in 75 | capturedError = error 76 | expection.fulfill() 77 | }) 78 | 79 | //Then 80 | waitForExpectations(timeout: 1, handler: nil) 81 | 82 | switch capturedError { 83 | case .serverError(let response, let data)?: 84 | XCTAssertNil(response) 85 | XCTAssertNil(data) 86 | default: 87 | XCTFail("Expect serverError") 88 | } 89 | } 90 | 91 | func testRequest_withFailingSerialization() { 92 | //Given 93 | networkAccess.changeMock(data: Train.JSONDataWithInvalidKey, response: nil, error: nil) 94 | let expection = expectation(description: "testRequest_withFailingSerialization") 95 | 96 | //When 97 | networkService.request(resource, onCompletion: { _ in 98 | XCTFail("Should not call success block") 99 | }, onError: { (error: NetworkError) in 100 | if case .serializationError(_, _) = error { 101 | expection.fulfill() 102 | } else { 103 | XCTFail("Expects serializationError") 104 | } 105 | }) 106 | 107 | waitForExpectations(timeout: 1, handler: nil) 108 | } 109 | 110 | func testRequest_withErrorResponse() { 111 | //Given 112 | let error = NSError(domain: "", code: 0, userInfo: nil) 113 | networkAccess.changeMock(data: nil, response: nil, error: error) 114 | let expection = expectation(description: "testOnError") 115 | 116 | //When 117 | networkService.request(resource, onCompletion: { _ in 118 | }, onError: { resultError in 119 | //Then 120 | switch resultError { 121 | case .requestError: 122 | expection.fulfill() 123 | default: 124 | XCTFail("Expects requestError") 125 | } 126 | }) 127 | 128 | waitForExpectations(timeout: 1, handler: nil) 129 | } 130 | 131 | private lazy var testData: Data! = { 132 | return "test_string".data(using: .utf8) 133 | }() 134 | 135 | func testRequest_withStatusCode401Response() { 136 | //Given 137 | let expectedResponse = HTTPURLResponse(url: .defaultMock, statusCode: 401, httpVersion: nil, headerFields: nil) 138 | networkAccess.changeMock(data: testData, response: expectedResponse, error: nil) 139 | let expection = expectation(description: "testOnError") 140 | 141 | //When 142 | networkService.request(resource, onCompletion: { _ in 143 | }, onError: { resultError in 144 | //Then 145 | switch resultError { 146 | case .unauthorized(let response, let data): 147 | XCTAssertEqual(response, expectedResponse) 148 | XCTAssertEqual(data, self.testData) 149 | expection.fulfill() 150 | default: 151 | XCTFail("Expects unauthorized") 152 | } 153 | }) 154 | 155 | waitForExpectations(timeout: 1, handler: nil) 156 | } 157 | 158 | func testGIVEN_aRequest_WHEN_requestWithResultResponse_THEN_ShouldRespond() { 159 | // GIVEN 160 | 161 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 162 | let expection = expectation(description: "loadValidRequest") 163 | var expectedResult: Result? 164 | 165 | //When 166 | networkService.request(resource, onCompletion: { result in 167 | expectedResult = result 168 | expection.fulfill() 169 | }) 170 | 171 | waitForExpectations(timeout: 1, handler: nil) 172 | 173 | //Then 174 | switch expectedResult { 175 | case .success(let train)?: 176 | XCTAssertEqual(train.name, self.trainName) 177 | case .failure?: 178 | XCTFail("Should be an error") 179 | case nil: 180 | XCTFail("Result should not be nil") 181 | } 182 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 183 | } 184 | 185 | func testGIVEN_aRequest_WHEN_requestWithResultErrorResponse_THEN_ShouldError() { 186 | //Given 187 | networkAccess.changeMock(data: nil, response: nil, error: nil) 188 | var expectedResult: Result? 189 | let expection = expectation(description: "testNoData") 190 | 191 | //When 192 | 193 | networkService.request(resource, onCompletion: { result in 194 | expectedResult = result 195 | expection.fulfill() 196 | }) 197 | 198 | //Then 199 | waitForExpectations(timeout: 1, handler: nil) 200 | 201 | switch expectedResult { 202 | case .failure(let error)?: 203 | if case .serverError(let response, let data) = error { 204 | XCTAssertNil(response) 205 | XCTAssertNil(data) 206 | } else { 207 | XCTFail("Expect serverError") 208 | } 209 | default: 210 | XCTFail("Expect serverError") 211 | } 212 | } 213 | 214 | func testGIVEN_aRequest_WHEN_requestWithResultAndResponse_THEN_ShouldRespond() { 215 | // GIVEN 216 | 217 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 218 | let expection = expectation(description: "loadValidRequest") 219 | var expectedResult: Result<(Train, HTTPURLResponse), NetworkError>? 220 | 221 | //When 222 | networkService.request(resource: resource) { (result) in 223 | expectedResult = result 224 | expection.fulfill() 225 | } 226 | 227 | waitForExpectations(timeout: 1, handler: nil) 228 | 229 | //Then 230 | switch expectedResult { 231 | case .success(let result)?: 232 | XCTAssertEqual(result.0.name, self.trainName) 233 | XCTAssertEqual(result.1, .defaultMock) 234 | case .failure?: 235 | XCTFail("Should be an error") 236 | case nil: 237 | XCTFail("Result should not be nil") 238 | } 239 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 240 | } 241 | 242 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 243 | func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldRespond() async throws { 244 | // GIVEN 245 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 246 | 247 | //When 248 | let (result, response) = try await networkService.request(resource) 249 | 250 | 251 | //Then 252 | XCTAssertEqual(result.name, self.trainName) 253 | XCTAssertEqual(response, .defaultMock) 254 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 255 | } 256 | 257 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 258 | func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldThwo() async { 259 | // GIVEN 260 | let error = NSError(domain: "", code: 0, userInfo: nil) 261 | networkAccess.changeMock(data: nil, response: nil, error: error) 262 | 263 | //When 264 | do { 265 | try await networkService.request(resource) 266 | XCTFail("Schould throw") 267 | } catch let error { 268 | XCTAssertTrue(error is NetworkError) 269 | } 270 | } 271 | 272 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 273 | func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponseAndCancel_THEN_ShouldThwo() async { 274 | // GIVEN 275 | let error = NSError(domain: "", code: 0, userInfo: nil) 276 | networkAccess.changeMock(data: nil, response: nil, error: error) 277 | 278 | //When 279 | let task = Task { 280 | try await networkService.request(resource) 281 | } 282 | task.cancel() 283 | let result = await task.result 284 | if case .failure(let error) = result, let networkError = error as? CancellationError { 285 | 286 | } else { 287 | XCTFail("Schould throw") 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Tests/NetworkServiceWithErrorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import XCTest 26 | @testable import DBNetworkStack 27 | 28 | enum CustomError: Error { 29 | case error 30 | 31 | init(networkError: NetworkError) { 32 | self = .error 33 | } 34 | } 35 | 36 | class NetworkServiceWithErrorTest: XCTestCase { 37 | 38 | var networkService: NetworkService! 39 | 40 | var networkAccess = NetworkAccessMock() 41 | 42 | let trainName = "ICE" 43 | 44 | var resource: ResourceWithError { 45 | let request = URLRequest(path: "train", baseURL: .defaultMock) 46 | return ResourceWithError(request: request, decoder: JSONDecoder(), mapError: { CustomError(networkError: $0) }) 47 | } 48 | 49 | override func setUp() { 50 | networkService = BasicNetworkService(networkAccess: networkAccess) 51 | } 52 | 53 | func testRequest_withValidResponse() { 54 | //Given 55 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 56 | let expection = expectation(description: "loadValidRequest") 57 | 58 | //When 59 | networkService.request(resource, onCompletionWithResponse: { train, response in 60 | XCTAssertEqual(train.name, self.trainName) 61 | XCTAssertEqual(response, .defaultMock) 62 | expection.fulfill() 63 | }, onError: { _ in 64 | XCTFail("Should not call error block") 65 | }) 66 | 67 | waitForExpectations(timeout: 1, handler: nil) 68 | 69 | //Then 70 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 71 | } 72 | 73 | func testRequest_withError() { 74 | //Given 75 | networkAccess.changeMock(data: nil, response: nil, error: nil) 76 | let expection = expectation(description: "testNoData") 77 | 78 | //When 79 | var capturedError: CustomError? 80 | networkService.request(resource, onCompletion: { _ in 81 | XCTFail("Should not call success block") 82 | }, onError: { error in 83 | capturedError = error 84 | expection.fulfill() 85 | }) 86 | 87 | //Then 88 | waitForExpectations(timeout: 1, handler: nil) 89 | 90 | XCTAssertEqual(capturedError, .error) 91 | } 92 | 93 | 94 | func testGIVEN_aRequest_WHEN_requestWithResultResponse_THEN_ShouldRespond() { 95 | // GIVEN 96 | 97 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 98 | let expection = expectation(description: "loadValidRequest") 99 | var expectedResult: Result? 100 | 101 | //When 102 | networkService.request(resource, onCompletion: { result in 103 | expectedResult = result 104 | expection.fulfill() 105 | }) 106 | 107 | waitForExpectations(timeout: 1, handler: nil) 108 | 109 | //Then 110 | switch expectedResult { 111 | case .success(let train)?: 112 | XCTAssertEqual(train.name, self.trainName) 113 | case .failure?: 114 | XCTFail("Should be an error") 115 | case nil: 116 | XCTFail("Result should not be nil") 117 | } 118 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 119 | } 120 | 121 | func testGIVEN_aRequest_WHEN_requestWithResultErrorResponse_THEN_ShouldError() { 122 | //Given 123 | networkAccess.changeMock(data: nil, response: nil, error: nil) 124 | var expectedResult: Result? 125 | let expection = expectation(description: "testNoData") 126 | 127 | //When 128 | 129 | networkService.request(resource, onCompletion: { result in 130 | expectedResult = result 131 | expection.fulfill() 132 | }) 133 | 134 | //Then 135 | waitForExpectations(timeout: 1, handler: nil) 136 | 137 | XCTAssertEqual(expectedResult, .failure(.error)) 138 | } 139 | 140 | func testGIVEN_aRequest_WHEN_requestWithResultAndResponse_THEN_ShouldRespond() { 141 | // GIVEN 142 | 143 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 144 | let expection = expectation(description: "loadValidRequest") 145 | var expectedResult: Result<(Train, HTTPURLResponse), CustomError>? 146 | 147 | //When 148 | networkService.request(resource: resource) { (result) in 149 | expectedResult = result 150 | expection.fulfill() 151 | } 152 | 153 | waitForExpectations(timeout: 1, handler: nil) 154 | 155 | //Then 156 | switch expectedResult { 157 | case .success(let result)?: 158 | XCTAssertEqual(result.0.name, self.trainName) 159 | XCTAssertEqual(result.1, .defaultMock) 160 | case .failure?: 161 | XCTFail("Should be an error") 162 | case nil: 163 | XCTFail("Result should not be nil") 164 | } 165 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 166 | } 167 | 168 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 169 | func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldRespond() async throws { 170 | // GIVEN 171 | networkAccess.changeMock(data: Train.validJSONData, response: .defaultMock, error: nil) 172 | 173 | //When 174 | let (result, response) = try await networkService.request(resource) 175 | 176 | 177 | //Then 178 | XCTAssertEqual(result.name, self.trainName) 179 | XCTAssertEqual(response, .defaultMock) 180 | XCTAssertEqual(networkAccess.request?.url?.absoluteString, "https://bahn.de/train") 181 | } 182 | 183 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 184 | func testGIVEN_aRequest_WHEN_requestWithAsyncResultAndResponse_THEN_ShouldThwo() async { 185 | // GIVEN 186 | let error = NSError(domain: "", code: 0, userInfo: nil) 187 | networkAccess.changeMock(data: nil, response: nil, error: error) 188 | 189 | //When 190 | do { 191 | try await networkService.request(resource) 192 | XCTFail("Schould throw") 193 | } catch let error { 194 | XCTAssertTrue(error is CustomError) 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Tests/NetworkTaskMockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import XCTest 25 | import DBNetworkStack 26 | 27 | class NetworkTaskMockTests: XCTestCase { 28 | 29 | func testCancled() { 30 | //Given 31 | let task = NetworkTaskMock() 32 | 33 | //When 34 | task.cancel() 35 | 36 | //Then 37 | XCTAssertEqual(task.state, .canceled) 38 | } 39 | 40 | func testResumed() { 41 | //Given 42 | let task = NetworkTaskMock() 43 | 44 | //When 45 | task.resume() 46 | 47 | //Then 48 | XCTAssertEqual(task.state, .resumed) 49 | } 50 | 51 | func testSuspended() { 52 | //Given 53 | let task = NetworkTaskMock() 54 | 55 | //When 56 | task.suspend() 57 | 58 | //Then 59 | XCTAssertEqual(task.state, .suspended) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/ResourceInspectTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) DB Systel GmbH. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a 5 | // copy of this software and associated documentation files (the "Software"), 6 | // to deal in the Software without restriction, including without limitation 7 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | // and/or sell copies of the Software, and to permit persons to whom the 9 | // Software is furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | // DEALINGS IN THE SOFTWARE. 21 | // 22 | 23 | import XCTest 24 | import DBNetworkStack 25 | 26 | final class ResourceInspectTest: XCTestCase { 27 | func testInspect() { 28 | //Given 29 | let data = Data() 30 | var capuredParsingData: Data? 31 | var capturedInspectedData: Data? 32 | let resource = Resource(request: URLRequest.defaultMock, parse: { data in 33 | capuredParsingData = data 34 | return 1 35 | }) 36 | 37 | //When 38 | let inspectedResource = resource.inspectData({ data in 39 | capturedInspectedData = data 40 | }) 41 | let result = try? inspectedResource.parse(data) 42 | 43 | //Then 44 | XCTAssertNotNil(result) 45 | XCTAssertEqual(capuredParsingData, capturedInspectedData) 46 | XCTAssertEqual(data, capturedInspectedData) 47 | XCTAssertEqual(capuredParsingData, data) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/ResourceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import XCTest 26 | import DBNetworkStack 27 | 28 | class ResourceTest: XCTestCase { 29 | 30 | func testResource() { 31 | //Given 32 | let validData: Data! = "ICE".data(using: .utf8) 33 | 34 | let resource = Resource(request: URLRequest.defaultMock, parse: { String(data: $0, encoding: .utf8) }) 35 | 36 | //When 37 | let name = try? resource.parse(validData) 38 | 39 | //Then 40 | XCTAssertEqual(name ?? nil, "ICE") 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Tests/ResourceWithErrorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2021 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import XCTest 26 | import DBNetworkStack 27 | 28 | class ResourceWithErrorTest: XCTestCase { 29 | 30 | func testResource() { 31 | //Given 32 | let validData: Data! = "ICE".data(using: .utf8) 33 | 34 | let resource = ResourceWithError( 35 | request: URLRequest.defaultMock, 36 | parse: { String(data: $0, encoding: .utf8) }, 37 | mapError: { $0 } 38 | ) 39 | 40 | //When 41 | let name = try? resource.parse(validData) 42 | 43 | //Then 44 | XCTAssertEqual(name ?? nil, "ICE") 45 | } 46 | 47 | func testResourceMapError() { 48 | //Given 49 | enum CustomError: Error{ 50 | case error 51 | } 52 | let resource = ResourceWithError( 53 | request: URLRequest.defaultMock, 54 | mapError: { _ in return .error } 55 | ) 56 | 57 | //When 58 | let mappedError = resource.mapError(.unknownError) 59 | 60 | //Then 61 | XCTAssertEqual(mappedError, .error) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Tests/RetryNetworkserviceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017 DB Systel GmbH. 3 | // DB Systel GmbH; Jürgen-Ponto-Platz 1; D-60329 Frankfurt am Main; Germany; http://www.dbsystel.de/ 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | import XCTest 25 | @testable import DBNetworkStack 26 | 27 | class RetryNetworkserviceTest: XCTestCase { 28 | var networkServiceMock: NetworkServiceMock! 29 | var resource: Resource { 30 | let request = URLRequest(path: "/train", baseURL: .defaultMock) 31 | return Resource(request: request, parse: { _ in return 1}) 32 | } 33 | 34 | override func setUp() { 35 | super.setUp() 36 | networkServiceMock = NetworkServiceMock() 37 | } 38 | 39 | override func tearDown() { 40 | networkServiceMock = nil 41 | super.tearDown() 42 | } 43 | 44 | func testRetryRequest_shouldRetry() throws { 45 | //Given 46 | let errorCount = 2 47 | let numberOfRetries = 2 48 | var executedRetrys = 0 49 | 50 | let retryService = RetryNetworkService(networkService: networkServiceMock, numberOfRetries: numberOfRetries, 51 | idleTimeInterval: 0, shouldRetry: { _ in return true }, dispatchRetry: { _, block in 52 | executedRetrys += 1 53 | block() 54 | }) 55 | 56 | //When 57 | weak var task = retryService.request(resource, onCompletion: { _ in 58 | XCTAssertEqual(executedRetrys, numberOfRetries) 59 | }, onError: { _ in 60 | XCTFail("Expects to not call error block") 61 | }) 62 | try (0.. Void)? 43 | 44 | override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 45 | lastOutgoingRequest = request 46 | currentDataTask = URLSessionDataTaskMock() 47 | completeRequest = completionHandler 48 | return currentDataTask 49 | } 50 | 51 | func completeWith(data: Data?, response: URLResponse?, error: Error?) { 52 | completeRequest?(data, response, error) 53 | completeRequest = nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - Tests/* 4 | --------------------------------------------------------------------------------