├── .gitignore ├── .slather.yml ├── .swift-version ├── .swiftlint.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── SwiftErrorHandler │ ├── Generic │ │ ├── Actions │ │ │ ├── ActionHandler.swift │ │ │ ├── ConfirmableAlert.swift │ │ │ └── RejectableAlert.swift │ │ ├── Alert │ │ │ ├── AlertAction.swift │ │ │ └── AlertController.swift │ │ ├── Extension │ │ │ ├── Error+String.swift │ │ │ └── Result+ErrorHandler.swift │ │ ├── Manager │ │ │ └── ErrorHandler.swift │ │ ├── Matchers │ │ │ └── ErrorMatcher.swift │ │ └── Protocol │ │ │ ├── ErrorAlert.swift │ │ │ └── ErrorHandlerView.swift │ └── iOS │ │ └── Extension │ │ ├── AlertController+UIKit.swift │ │ └── ErrorHandlerView+UIKit.swift └── ios.xcconfig ├── SwiftErrorHandler.podspec └── Tests ├── LinuxMain.swift └── SwiftErrorHandlerTests ├── ActionHandlerTests.swift ├── ErrorHandlerTests.swift ├── ErrorMatcherTests.swift ├── Helpers ├── DispatchGroup+Enter.swift ├── MockedAlert.swift └── MockedView.swift ├── ResultErrorHandlerTests.swift └── XCTestManifests.swift /.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 | SwiftErrorHandler.xcodeproj/* 20 | cobertura.xml 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | Pods/ 47 | Podfile.lock 48 | ErrorHandler.xcworkspace 49 | 50 | 51 | # fastlane 52 | # 53 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 54 | # screenshots whenever they are needed. 55 | # For more information about the recommended setup visit: 56 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 57 | 58 | fastlane/report.xml 59 | fastlane/Preview.html 60 | fastlane/screenshots/**/*.png 61 | fastlane/test_output 62 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | ci_service: travis_ci 2 | coverage_service: cobertura_xml 3 | xcodeproj: SwiftErrorHandler.xcodeproj 4 | scheme: SwiftErrorHandler-Package 5 | source_directory: Sources/SwiftErrorHandler 6 | ignore: 7 | - Tests/* -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - line_length 4 | - identifier_name 5 | 6 | included: 7 | - Sources 8 | - Tests 9 | 10 | reporter: "xcode" 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | os: osx 3 | osx_image: xcode11.3 4 | env: 5 | global: 6 | - LC_CTYPE=en_US.UTF-8 7 | - LANG=en_US.UTF-8 8 | - PROJECT=SwiftErrorHandler.xcodeproj 9 | - SCHEME=SwiftErrorHandler-Package 10 | - EXPANDED_CODE_SIGN_IDENTITY="" 11 | - EXPANDED_CODE_SIGN_IDENTITY_NAME="" 12 | - EXPANDED_PROVISIONING_PROFILE="" 13 | - DESTINATION="platform=iOS Simulator,name=iPhone 11" 14 | - CC_TEST_REPORTER_ID=f9052cf7f98bd46742d8f3f5d140a053f8cfb4031046eb238d1e5de9b460ac6b 15 | install: 16 | - git config --add diff.renamelimit 0 17 | 18 | jobs: 19 | include: 20 | - stage: lint 21 | script: 22 | - swiftlint 23 | - pod lib lint 24 | - stage: test 25 | before_script: 26 | - gem install slather 27 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-darwin-amd64 > ./cc-test-reporter 28 | - chmod +x ./cc-test-reporter 29 | - ./cc-test-reporter before-build 30 | script: 31 | - set -o pipefail 32 | - xcodebuild -version 33 | - xcodebuild -showsdks 34 | - swift package generate-xcodeproj --xcconfig-overrides ./Sources/ios.xcconfig 35 | - xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -enableCodeCoverage YES clean build test | xcpretty 36 | after_script: 37 | - slather 38 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 39 | - stage: deploy 40 | if: tag IS present 41 | script: 42 | - swift package generate-xcodeproj --xcconfig-overrides ./Sources/ios.xcconfig 43 | - carthage version 44 | - carthage build --no-skip-current 45 | - carthage archive SwiftErrorHandler 46 | deploy: 47 | - provider: releases 48 | api_key: 49 | secure: lYOBEL27WJ8OG8L+IFXLZFrYYAxdPReq6NS1p8nwZs7O6Oc+namqbEp31fBl5XJLpCiq33DiCHLAQHEK7UTavGuvINADgPRlRn45Ieyd+CF8EAVsbK1cYS6+bXXt6RLcrBOS9wrGbpFt18HE1QdVrPtOztRjSvaCf+jMiS4jv1R8HOmWsy4Lxf6UtpPhcpwNqQ4aBt2C61Uwbt406pqVoxIZj/berN2i495LO52/S2eu+4E8wjoeaJSQKZgmMFNoZMpbwIrZIC0ShXRGTjyV/TuY2BSJucCXtxXtsxwF+JgGW+a8UyeMxltLv19DWyfMi2p+xttkZM7LgIFXPEj0+3DZeCwa3rHw/EWuqzXUsnBddZEyh6J9pc4i5ZlVI+uBmbbc0KxZsdSncRl6XL5cr3IHdlDCskqHSEYmW56qnhCqyGSyOZfn1QzM/1SsQIDQZ0S++VoHUEKrD+hhgY9ss7Ecc+cwEITGOGiLuLy6JEJtERTR5c0ovc6Sp/134sYHSeyX3OBvCBnoAPLqfdLtPX35Z5kSjSFuCLNnFY4oVDo1Ye57ypMUICu13WE2iiBPhUmQsJrJnWsy6aqes2OqFFM0tbMi8zu11EOQNSWIGWVWhW8On+7gnIiTiS8+vwAUM++dO/bcLFWZ2ixwt4djuF5lSl6Fh6UK1ow6JSjQFio= 50 | file: SwiftErrorHandler.framework.zip 51 | on: 52 | tags: true 53 | - provider: script 54 | script: 55 | - pod trunk push --allow-warnings 56 | on: 57 | tags: true 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | --- 5 | ## [5.0.4](https://github.com/stefanrenne/SwiftErrorHandler/releases/tag/5.0.4) 6 | 7 | * Platform seperation, preparation to support MacOS & Linux 8 | 9 | ## [5.0.3](https://github.com/stefanrenne/SwiftErrorHandler/releases/tag/5.0.3) 10 | 11 | * Added Swift Result extension for error handling 12 | * Refactored matcher logic 13 | 14 | ## [5.0.2](https://github.com/stefanrenne/SwiftErrorHandler/releases/tag/5.0.2) 15 | 16 | * Added Carthage support 17 | 18 | ## [5.0.1](https://github.com/stefanrenne/SwiftErrorHandler/releases/tag/5.0.1) 19 | 20 | * Introduces `always` to perform actions for all errors 21 | * Match Errors on NSError `Domain` name 22 | 23 | ## [5.0.0](https://github.com/stefanrenne/SwiftErrorHandler/releases/tag/5.0.0) 24 | 25 | SwiftErrorHandler 5.0.0 is the first release targeting the Swift 5 compiler. 26 | 27 | **Xcode 10.2 is the minimum supported version.** 28 | 29 | * Initial release 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftErrorHandler", 6 | platforms: [ 7 | .iOS(.v8), .tvOS(.v9) 8 | ], 9 | products: [ 10 | .library( 11 | name: "SwiftErrorHandler", 12 | type: .static, 13 | targets: ["SwiftErrorHandler"]) 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target( 18 | name: "SwiftErrorHandler", 19 | dependencies: []), 20 | .testTarget( 21 | name: "SwiftErrorHandlerTests", 22 | dependencies: ["SwiftErrorHandler"]) 23 | ], 24 | swiftLanguageVersions: [.v5] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftErrorHandler 2 | 3 | [![Swift 5.0](https://img.shields.io/badge/swift-5.0-orange.svg?style=flat)](https://swift.org) 4 | [![Travis Status](https://travis-ci.org/stefanrenne/SwiftErrorHandler.svg?branch=master)](https://travis-ci.org/stefanrenne/SwiftErrorHandler) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/13a3613bc81ed631e9e2/maintainability)](https://codeclimate.com/github/stefanrenne/SwiftErrorHandler/maintainability) 6 | [![CocoaPods Version Badge](https://img.shields.io/cocoapods/v/SwiftErrorHandler.svg)](https://cocoapods.org/pods/SwiftErrorHandler) 7 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | [![License Badge](https://img.shields.io/cocoapods/l/SwiftErrorHandler.svg)](LICENSE) 9 | [![Platform](https://img.shields.io/cocoapods/p/SwiftErrorHandler.svg?style=flat)](http://cocoapods.org/pods/SwiftErrorHandler) 10 | 11 | SwiftErrorHandler enables expressing complex error handling logic with a few lines of code using a memorable fluent API. 12 | 13 | 14 | ## Installation 15 | 16 | ### CocoaPods 17 | 18 | ``` 19 | pod 'SwiftErrorHandler', '~> 5.0' 20 | ``` 21 | 22 | ### Carthage 23 | 24 | ``` 25 | github "stefanrenne/SwiftErrorHandler" ~> 5.0 26 | ``` 27 | 28 | ### Swift Package Manager (SPM) 29 | 30 | ``` 31 | import PackageDescription 32 | 33 | let package = Package( 34 | name: "My App", 35 | dependencies: [ 36 | .package(url: "https://github.com/stefanrenne/SwiftErrorHandler.git", from: "5.0.0") 37 | ] 38 | ) 39 | ``` 40 | 41 | 42 | ## Usage 43 | 44 | Let's say we're building a account based iOS app that can throw errors in the networking layer. 45 | 46 | We need to: 47 | 48 | ### Setup a default ErrorHandler once 49 | 50 | The default ErrorHandler will contain the error handling logic that is common across your application that you don't want to duplicate. You can create a factory that creates it so that you can get new instance with common handling logic from anywhere in your app. 51 | 52 | ```swift 53 | extension ErrorHandler { 54 | class func `default`(for view: ErrorHandlerView) -> ErrorHandler { 55 | return ErrorHandler(for: view) 56 | .on(error: .code(NSURLErrorTimedOut), then: .present(alert: ConfirmableAlert(title: "Timeout occurred", confirmTitle: "Retry", confirmAction: { error in print("retry network call") }))) 57 | .on(error: .type(NetworkError.noInternet), then: .present(alert: ConfirmableAlert(title: "Did you turn off the internet?", confirmTitle: "No"))) 58 | .on(error: .type(NetworkError.logout), then: .present(alert: RejectableAlert(title: "Are you sure you want to logout?", confirmTitle: "Yes", rejectTitle: "No"))) 59 | .always(.perform(action: AnalyticsService.track)) 60 | .onNoMatch(.present(alert: ConfirmableAlert(title: "Something went wrong", confirmTitle: "Ok"))) 61 | } 62 | } 63 | ``` 64 | 65 | ### Use the default handler to handle common cases 66 | 67 | Often the cases the default handler knows about will be good enough. 68 | 69 | ```swift 70 | do { 71 | try saveStatus() 72 | } catch { 73 | ErrorHandler.default(for: self).handle(error: error) 74 | } 75 | ``` 76 | 77 | ### Customize the error handler when needed. 78 | 79 | In cases where extra context is available you can add more cases or override the ones provided already. 80 | 81 | For example in a LoginViewController 82 | 83 | ```swift 84 | class LoginViewController: UIViewController { 85 | 86 | private lazy var errorHandler = ErrorHandler.default(for: self) 87 | .on(error: .type(NetworkError.authenticate), then: .perform(action: startAuthentication)) 88 | 89 | func performLogin() { 90 | do { 91 | try login() 92 | } catch { 93 | errorHandler.handle(error: error) 94 | } 95 | } 96 | 97 | private func startAuthentication(for error: Error, onCompleted: OnErrorHandled) { 98 | print("start authentication ...") 99 | onCompleted?() 100 | return true 101 | } 102 | } 103 | ``` 104 | 105 | 106 | ### Bonus: RxSwift Support 107 | 108 | ```swift 109 | let errorHandler = ErrorHandler.default(for: self) 110 | Observable 111 | .error(NetworkError.authenticate) 112 | .subscribe(onNext: { result in 113 | print("User loggedin") 114 | }, 115 | onError: errorHandler.handle) 116 | .disposed(by: disposeBag) 117 | ``` 118 | 119 | 120 | ### Bonus: Result Support 121 | 122 | ```swift 123 | let errorHandler = ErrorHandler.default(for: self) 124 | let result: Result = .failure(NetworkError.authenticate) 125 | let user: User? = result.get(onError: errorHandler) 126 | ``` 127 | 128 | 129 | ## Customization options 130 | 131 | ### The way actions are performed for errors 132 | 133 | - Performs actions for specific errors 134 | 135 | `errorHandler.on(error: .code(404), then: .present(Alert))` 136 | 137 | - Performs actions when no specific error matcher can be found 138 | 139 | `errorHandler.onNoMatch(.present(Alert))` 140 | 141 | - Actions that need to be performed for all errors 142 | 143 | `errorHandler.always(.perform(action: analyticsService.track))` 144 | 145 | 146 | ### Error Matchers 147 | 148 | **Match on specific error type** 149 | 150 | `errorHandler.on(error: .type(NetworkError.authenticate), then: .doNothing)` 151 | 152 | #### Match on NSError code 153 | 154 | ``errorHandler.on(error: .code(404), then: .doNothing)` 155 | 156 | #### Match on NSError domain 157 | 158 | `errorHandler.on(error: .domain("remote"), then: .doNothing)` 159 | 160 | #### Custom matching 161 | 162 | ``` 163 | extension ErrorMatcher { 164 | static func onCustomMatch() -> ErrorMatcher { 165 | .init(matcher: { error in 166 | ... 167 | return true 168 | }) 169 | } 170 | } 171 | 172 | .on(error: .onCustomMatch()), then: .doNothing) 173 | ``` 174 | 175 | ### Error Handling 176 | 177 | #### Do nothing 178 | 179 | It mainly exists to make documentation & unit tests easier to understand. 180 | 181 | `errorHandler.on(error: .code(404), then: .doNothing)` 182 | 183 | 184 | #### Present Alert 185 | 186 | The Alert is presented on the View provided in the ErrorHandler init 187 | 188 | `errorHandler.on(error: .code(404), then: .present(alert: ErrorAlert))` 189 | 190 | By default there are two alert types you can present: 191 | 192 | - **ConfirmableAlert**: An alert with one action button 193 | - **RejectableAlert**: An alert with two action buttons 194 | 195 | Would you like to use different alerts? 196 | 197 | 1. Create a struct that conforms to the **ErrorAlert** protocol 198 | 2. Implement the function that builds your custom UIAlertController 199 | `func build(for error: Error, onCompleted: OnErrorHandled) -> UIAlertController` 200 | 3. Make sure the optional `onCompleted` completionblock has been performed in all `UIAlertAction` completion blocks 201 | 202 | #### Custom Action 203 | 204 | The only limitation is your mind. 205 | 206 | `errorHandler.on(error: .code(404), then: .perform(action: CustomActionHandler)` 207 | 208 | The **CustomActionHandler** provides the `Error` and an optional `onCompleted` completionblock that needs to be executed when your custom action has been performed. 209 | 210 | 211 | #### Implementing the ErrorHandler outside the ViewController 212 | 213 | In larger apps it makes sense to implement the ErrorHandler in a different class than the ViewController. To make this work you need to provide a view on which alerts can be presented. This can be done by conforming to the ErrorHandlerView protocol. 214 | 215 | ```swift 216 | public protocol ErrorHandlerView { 217 | func present(alert: UIAlertController) 218 | } 219 | 220 | extension UIViewController: ErrorHandlerView { 221 | public func present(alert: UIAlertController) { 222 | present(alert, animated: true, completion: nil) 223 | } 224 | } 225 | 226 | ``` 227 | 228 | 229 | ## Contribute? 230 | 231 | **Build your xcode project using the Swift Package Manager** 232 | 233 | ``` 234 | swift package generate-xcodeproj --xcconfig-overrides ./Sources/ios.xcconfig 235 | ``` 236 | 237 | 238 | **Quick checklist summary before submitting a PR** 239 | 240 | - 🔎 Make sure tests are added or updated to accomodate your changes. We do not accept any addition that comes without tests. When possible, add tests to verify bug fixes and prevent future regressions. 241 | - 📖 Check that you provided a CHANGELOG entry documenting your changes (except for documentation improvements) 242 | - 👌 Verify that tests pass 243 | - 👍 Push it! 244 | 245 | 246 | ## Why? 247 | 248 | When designing for errors, we usually need to: 249 | 250 | 1. have a **default** handler for **expected** errors 251 | // i.e. network, db errors etc. 252 | 2. handle **specific** errors **in a custom manner** given **the context** of where and when they occur 253 | // i.e. a network error while uploading a file, invalid login 254 | 3. have **unspecific** handlers that get executed on every error 255 | // i.e. log errors to Fabric or any other analytics service 256 | 4. have a **catch-all** handler for **unknown** errors 257 | // i.e. errors we don't have custom handling for 258 | 5. keep our code **DRY** 259 | 260 | Swift has a well thought error handling model that balances between convenience ([automatic propagation](https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst#automatic-propagation)) and clarity-safety ([Typed propagation](https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst#id3), [Marked propagation](https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst#id4)). As a result, the compiler warns of errors that need to be handled, while making it relatively easy to propagate errors and handle them higher up the stack. 261 | 262 | However, even with this help from the language, achieving the goals listed above in an **ad-hoc** manner in an application of a reasonable size can lead to a lot of **boilerplate** which is **tedious** to write and reason about. Because of this friction, developers quite often choose to swallow errors or handle them all in the same generic way. 263 | 264 | This library addresses these issues by providing an abstraction to define flexible error handling rules with an opinionated, fluent API. 265 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Actions/ActionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionHandler.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias CustomActionHandler = (_ error: Error, _ onCompleted: OnErrorHandled) -> Void 12 | 13 | public enum ActionHandler { 14 | case doNothing 15 | case present(alert: ErrorAlert) 16 | case perform(action: CustomActionHandler) 17 | } 18 | 19 | extension ActionHandler { 20 | func perform(on view: ErrorHandlerView, for error: Error, onCompleted: OnErrorHandled) -> () -> Void { 21 | switch self { 22 | case .doNothing: 23 | return { onCompleted?() } 24 | case .present(let alert): 25 | let alertController = alert.build(for: error, onCompleted: onCompleted) 26 | return { view.present(alert: alertController) } 27 | case .perform(let action): 28 | return { action(error, onCompleted) } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Actions/ConfirmableAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfirmableAlert.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ConfirmableAlert: ErrorAlert { 12 | let title: String 13 | let message: String? 14 | let confirmTitle: String 15 | let confirmAction: ((Error) -> Void)? 16 | 17 | public init(title: String, message: String? = nil, confirmTitle: String, confirmAction: ((Error) -> Void)? = nil) { 18 | self.title = title 19 | self.message = message 20 | self.confirmTitle = confirmTitle 21 | self.confirmAction = confirmAction 22 | } 23 | 24 | public func build(for error: Error, onCompleted: OnErrorHandled) -> AlertController { 25 | let controller = AlertController(title: title, message: message, preferredStyle: .alert) 26 | let confirmButton = AlertAction(title: confirmTitle, style: .default) { _ in 27 | self.confirmAction?(error) 28 | onCompleted?() 29 | } 30 | controller.addAction(confirmButton) 31 | return controller 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Actions/RejectableAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RejectableAlert.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct RejectableAlert: ErrorAlert { 12 | let title: String 13 | let message: String? 14 | let confirmTitle: String 15 | let confirmAction: ((Error) -> Void)? 16 | let rejectTitle: String 17 | let rejectAction: ((Error) -> Void)? 18 | 19 | public init(title: String, message: String? = nil, confirmTitle: String, rejectTitle: String, confirmAction: ((Error) -> Void)? = nil, rejectAction: ((Error) -> Void)? = nil) { 20 | self.title = title 21 | self.message = message 22 | self.confirmTitle = confirmTitle 23 | self.rejectTitle = rejectTitle 24 | self.confirmAction = confirmAction 25 | self.rejectAction = rejectAction 26 | } 27 | 28 | public func build(for error: Error, onCompleted: OnErrorHandled) -> AlertController { 29 | let controller = AlertController(title: title, message: message, preferredStyle: .alert) 30 | 31 | let confirmButton = AlertAction(title: confirmTitle, style: .default) { _ in 32 | self.confirmAction?(error) 33 | onCompleted?() 34 | } 35 | let rejectButton = AlertAction(title: rejectTitle, style: .cancel) { _ in 36 | self.rejectAction?(error) 37 | onCompleted?() 38 | } 39 | 40 | controller.addAction(confirmButton) 41 | controller.addAction(rejectButton) 42 | return controller 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Alert/AlertAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertAction.swift 3 | // SwiftErrorHandler 4 | // 5 | // Created by Stefan Renne on 31/12/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum AlertActionStyle { 11 | 12 | case `default` 13 | 14 | case cancel 15 | } 16 | 17 | public class AlertAction { 18 | 19 | internal let title: String? 20 | 21 | internal let style: AlertActionStyle 22 | 23 | internal let handler: ((AlertAction) -> Void)? 24 | 25 | public init(title: String?, style: AlertActionStyle, handler: ((AlertAction) -> Void)?) { 26 | self.title = title 27 | self.style = style 28 | self.handler = handler 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Alert/AlertController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertController.swift 3 | // SwiftErrorHandler 4 | // 5 | // Created by Stefan Renne on 31/12/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum AlertControllerStyle { 11 | 12 | case actionSheet 13 | 14 | case alert 15 | } 16 | 17 | public class AlertController { 18 | 19 | internal let title: String? 20 | 21 | internal let message: String? 22 | 23 | internal let preferredStyle: AlertControllerStyle 24 | 25 | internal var actions = [AlertAction]() 26 | 27 | public init(title: String?, message: String?, preferredStyle: AlertControllerStyle) { 28 | self.title = title 29 | self.message = message 30 | self.preferredStyle = preferredStyle 31 | } 32 | 33 | public func addAction(_ action: AlertAction) { 34 | actions.append(action) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Extension/Error+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error+String.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 05/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Error { 12 | var reflectedString: String { 13 | return String(reflecting: self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Extension/Result+ErrorHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+ErrorHandler.swift 3 | // SwiftErrorHandler 4 | // 5 | // Created by Stefan Renne on 30/12/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Result { 11 | 12 | func get(onError handler: ErrorHandler, onErrorCompleted: OnErrorHandled = nil) -> Success? { 13 | do { 14 | return try get() 15 | } catch { 16 | handler.handle(error: error, onCompleted: onErrorCompleted) 17 | return nil 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Manager/ErrorHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorHandler.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 05/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias OnErrorHandled = (() -> Void)? 12 | 13 | open class ErrorHandler { 14 | 15 | private let view: ErrorHandlerView 16 | public init(for errorHandlerView: ErrorHandlerView) { 17 | self.view = errorHandlerView 18 | } 19 | 20 | private var specificErrorActions = [(matcher: ErrorMatcher, action: ActionHandler)]() 21 | private var alwaysActions = [ActionHandler]() 22 | private var defaultActions = [ActionHandler]() 23 | 24 | /// adds an error handler for a specific error to the ErrorHandler 25 | /// 26 | /// - Parameters: 27 | /// - error: The error matcher 28 | /// - then action: The action that needs to be performed when the error matches 29 | /// - Returns: an instance of self (for chaining purposes) 30 | @discardableResult 31 | public func on(error errors: ErrorMatcher..., then action: ActionHandler) -> ErrorHandler { 32 | errors.forEach { (error) in 33 | specificErrorActions.append((error, action)) 34 | } 35 | return self 36 | } 37 | /// adds a default error handler to the ErrorHandler 38 | /// 39 | /// - Parameters: 40 | /// - action: the catch-all action that needs to be performed when no other match can be found 41 | /// - Returns: an instance of self (for chaining purposes) 42 | @discardableResult 43 | public func onNoMatch(_ action: ActionHandler) -> ErrorHandler { 44 | defaultActions.append(action) 45 | return self 46 | } 47 | 48 | /// adds a error handler that will be executed on every error 49 | /// 50 | /// - Parameters: 51 | /// - action: The action that always needs to be performed 52 | /// - Returns: an instance of self (for chaining purposes) 53 | @discardableResult 54 | public func always(_ action: ActionHandler) -> ErrorHandler { 55 | alwaysActions.append(action) 56 | return self 57 | } 58 | 59 | /// This function is called to handle an error 60 | /// 61 | /// - Parameter error: The error that should be handled 62 | /// - Parameter onCompleted: The optional block that gets executed after the error has been handled successfully 63 | /// - Returns: an boolean inidication if the error was handled successfully 64 | @discardableResult 65 | public func handle(error: Error, onCompleted: OnErrorHandled) -> Bool { 66 | 67 | // Check if we have a handler for this error: 68 | let specificErrorHandlers: [ActionHandler] = specificErrorActions 69 | .filter({ $0.matcher.catches(error) }) 70 | .map({ $0.action }) 71 | 72 | let actions: [ActionHandler] 73 | if specificErrorHandlers.count > 0 { 74 | actions = specificErrorHandlers + alwaysActions 75 | } else { 76 | actions = defaultActions + alwaysActions 77 | } 78 | 79 | // Chain the on completion actions to trigger the next error handler 80 | let chainedActions = actions 81 | .reversed() 82 | .reduce(into: [() -> Void]()) { (result, action) in 83 | let previousAction = result.last ?? onCompleted 84 | let actionRow = action.perform(on: view, for: error, onCompleted: previousAction) 85 | result.append(actionRow) 86 | } 87 | .reversed() 88 | 89 | // Perform First Action 90 | chainedActions.first?() 91 | 92 | let handled = chainedActions.count > 0 93 | 94 | return handled 95 | } 96 | 97 | /// This convenience function is called to handle an error 98 | /// The main reason for this function is to handle RxSwift errors inline 99 | /// Example: subscribe(onError: errorHandler.handle) 100 | /// 101 | /// - Parameter error: The error that should be handled 102 | public func handle(error: Error) { 103 | handle(error: error, onCompleted: nil) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Matchers/ErrorMatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorMatcher.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ErrorMatcher { 12 | 13 | private let matcher: ((Error) -> Bool) 14 | 15 | public init(matcher: @escaping ((Error) -> Bool)) { 16 | self.matcher = matcher 17 | } 18 | 19 | public func catches(_ error: Error) -> Bool { 20 | return matcher(error) 21 | } 22 | } 23 | 24 | public extension ErrorMatcher { 25 | 26 | static func type(_ error: Error) -> ErrorMatcher { 27 | return .init(matcher: { $0.reflectedString == error.reflectedString }) 28 | } 29 | 30 | static func code(_ code: Int) -> ErrorMatcher { 31 | return .init(matcher: { $0._code == code }) 32 | } 33 | 34 | static func domain(_ domain: String) -> ErrorMatcher { 35 | return .init(matcher: { $0._domain == domain }) 36 | } 37 | 38 | @available(*, deprecated, message: "Create an extension on ErrorMatcher instead") 39 | static func match(_ matcher: @escaping ((Error) -> Bool)) -> ErrorMatcher { 40 | return .init(matcher: matcher) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Protocol/ErrorAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorAlert.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ErrorAlert { 12 | func build(for error: Error, onCompleted: OnErrorHandled) -> AlertController 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/Generic/Protocol/ErrorHandlerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorHandlerView.swift 3 | // ErrorHandler 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ErrorHandlerView { 12 | func present(alert: AlertController) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/iOS/Extension/AlertController+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertController+UIKit.swift 3 | // SwiftErrorHandler 4 | // 5 | // Created by Stefan Renne on 31/12/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) || os(watchOS) 9 | import UIKit 10 | 11 | extension AlertControllerStyle { 12 | func build() -> UIAlertController.Style { 13 | switch self { 14 | case .actionSheet: 15 | return .actionSheet 16 | case .alert: 17 | return .alert 18 | } 19 | } 20 | } 21 | 22 | extension AlertActionStyle { 23 | func build() -> UIAlertAction.Style { 24 | switch self { 25 | case .default: 26 | return .default 27 | case .cancel: 28 | return .cancel 29 | } 30 | } 31 | } 32 | 33 | extension AlertController { 34 | func build() -> UIAlertController { 35 | let preferredStyle: UIAlertController.Style = self.preferredStyle.build() 36 | let controller: UIAlertController = UIAlertController(title: self.title, message: self.message, preferredStyle: preferredStyle) 37 | 38 | self.actions 39 | .map({ $0.build() }) 40 | .forEach { (action) in 41 | controller.addAction(action) 42 | } 43 | 44 | return controller 45 | } 46 | } 47 | 48 | extension AlertAction { 49 | func build() -> UIAlertAction { 50 | let style: UIAlertAction.Style = self.style.build() 51 | return UIAlertAction(title: self.title, style: style, handler: { [weak self] _ in 52 | guard let `self` = self else { return } 53 | self.handler?(self) 54 | }) 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/SwiftErrorHandler/iOS/Extension/ErrorHandlerView+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorHandlerView+UIKit.swift 3 | // SwiftErrorHandler 4 | // 5 | // Created by Stefan Renne on 31/12/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) || os(watchOS) 9 | import UIKit 10 | 11 | extension UIViewController: ErrorHandlerView { 12 | public func present(alert: AlertController) { 13 | present(alert.build(), animated: true, completion: nil) 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /Sources/ios.xcconfig: -------------------------------------------------------------------------------- 1 | SDKROOT = iphoneos 2 | SUPPORTED_PLATFORMS = iphonesimulator iphoneos 3 | IPHONEOS_DEPLOYMENT_TARGET = 12.0 4 | ARCHS = $(ARCHS_STANDARD) 5 | VALID_ARCHS = $(ARCHS_STANDARD) 6 | VALIDATE_PRODUCT = YES 7 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks 8 | TARGETED_DEVICE_FAMILY = 1, 2 9 | -------------------------------------------------------------------------------- /SwiftErrorHandler.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'SwiftErrorHandler' 3 | spec.version = '5.0.4' 4 | spec.license = { :type => 'Apache-2.0' } 5 | spec.homepage = 'https://github.com/stefanrenne/SwiftErrorHandler' 6 | spec.authors = { 'Stefan Renne' => 'info@stefanrenne.nl' } 7 | spec.summary = 'Flexible library for handling Swift Errors' 8 | spec.source = { :git => 'https://github.com/stefanrenne/SwiftErrorHandler.git', :tag => spec.version.to_s } 9 | spec.swift_version = '5.0' 10 | spec.ios.deployment_target = '8.0' 11 | spec.tvos.deployment_target = '9.0' 12 | spec.requires_arc = true 13 | spec.source_files = 'Sources/**/*.swift' 14 | spec.ios.framework = 'UIKit' 15 | spec.tvos.framework = 'UIKit' 16 | end 17 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftErrorHandlerTests 4 | 5 | let tests: [XCTestCaseEntry] = SwiftErrorHandlerTests.allTests() 6 | XCTMain(tests) 7 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/ActionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionHandlerTests.swift 3 | // ErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 22/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftErrorHandler 11 | 12 | class ActionHandlerTests: XCTestCase { 13 | 14 | func testItCanHandleTheEmptyAction() throws { 15 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 0, numberExpectedonCompleted: 1) 16 | 17 | let didHandleAction = ActionHandler.doNothing.perform(on: view, for: SimpleError.error1, onCompleted: view.onCompleted) 18 | didHandleAction() 19 | 20 | XCTAssertTrue(view.didHandleResult()) 21 | } 22 | 23 | func testItCanHandleTheAlertAction() throws { 24 | let view = MockedView(numberExpectedPresentedAlerts: 1, numberExpectedCustomHandlers: 0, numberExpectedonCompleted: 0) 25 | let alert = MockedAlert() 26 | 27 | let didHandleAction = ActionHandler.present(alert: alert).perform(on: view, for: SimpleError.error1, onCompleted: nil) 28 | didHandleAction() 29 | 30 | XCTAssertTrue(view.didHandleResult()) 31 | 32 | XCTAssertEqual(view.lastResult?.title, "title") 33 | XCTAssertEqual(view.lastResult?.message, "message") 34 | XCTAssertEqual(view.lastResult?.actions.count, 1) 35 | XCTAssertEqual(view.lastResult?.actions[0].title, "cancel") 36 | XCTAssertEqual(view.lastResult?.actions[0].style, .cancel) 37 | } 38 | 39 | func testItCanHandleAConfirmableAlertAction() throws { 40 | let view = MockedView(numberExpectedPresentedAlerts: 1, numberExpectedCustomHandlers: 0, numberExpectedonCompleted: 0) 41 | let alert = ConfirmableAlert(title: "title", message: "message", confirmTitle: "confirm", confirmAction: nil) 42 | 43 | let didHandleAction = ActionHandler.present(alert: alert).perform(on: view, for: SimpleError.error1, onCompleted: nil) 44 | didHandleAction() 45 | 46 | XCTAssertTrue(view.didHandleResult()) 47 | 48 | XCTAssertEqual(view.lastResult?.title, "title") 49 | XCTAssertEqual(view.lastResult?.message, "message") 50 | XCTAssertEqual(view.lastResult?.actions.count, 1) 51 | XCTAssertEqual(view.lastResult?.actions[0].title, "confirm") 52 | XCTAssertEqual(view.lastResult?.actions[0].style, .default) 53 | } 54 | 55 | func testItCanHandleARejectableAlertAction() throws { 56 | let view = MockedView(numberExpectedPresentedAlerts: 1, numberExpectedCustomHandlers: 0, numberExpectedonCompleted: 0) 57 | let alert = RejectableAlert(title: "title", message: "message", confirmTitle: "YES", rejectTitle: "NO", confirmAction: nil, rejectAction: nil) 58 | 59 | let didHandleAction = ActionHandler.present(alert: alert).perform(on: view, for: SimpleError.error1, onCompleted: nil) 60 | didHandleAction() 61 | 62 | XCTAssertTrue(view.didHandleResult()) 63 | 64 | XCTAssertEqual(view.lastResult?.title, "title") 65 | XCTAssertEqual(view.lastResult?.message, "message") 66 | XCTAssertEqual(view.lastResult?.actions.count, 2) 67 | XCTAssertEqual(view.lastResult?.actions[0].title, "YES") 68 | XCTAssertEqual(view.lastResult?.actions[0].style, .default) 69 | XCTAssertEqual(view.lastResult?.actions[1].title, "NO") 70 | XCTAssertEqual(view.lastResult?.actions[1].style, .cancel) 71 | } 72 | 73 | func testItCanHandleTheCustomAction() throws { 74 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 75 | 76 | let didHandleAction = ActionHandler.perform(action: view.customHandler).perform(on: view, for: SimpleError.error1, onCompleted: view.onCompleted) 77 | didHandleAction() 78 | 79 | XCTAssertTrue(view.didHandleResult()) 80 | } 81 | 82 | } 83 | 84 | private extension ActionHandlerTests { 85 | enum SimpleError: Error { 86 | case error1 87 | } 88 | } 89 | 90 | extension ActionHandlerTests { 91 | 92 | static var allTests = [ 93 | ("testItCanHandleTheEmptyAction", testItCanHandleTheEmptyAction), 94 | ("testItCanHandleTheAlertAction", testItCanHandleTheAlertAction), 95 | ("testItCanHandleAConfirmableAlertAction", testItCanHandleAConfirmableAlertAction), 96 | ("testItCanHandleARejectableAlertAction", testItCanHandleARejectableAlertAction), 97 | ("testItCanHandleTheCustomAction", testItCanHandleTheCustomAction) 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/ErrorHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorHandlerTests.swift 3 | // ErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 19/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftErrorHandler 11 | 12 | class ErrorHandlerTests: XCTestCase { 13 | 14 | func testItCanHandleSpecificErrors() throws { 15 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 16 | let handler = ErrorHandler(for: view) 17 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 18 | 19 | XCTAssertTrue(handler.handle(error: HandlerError1.error1, onCompleted: view.onCompleted)) 20 | 21 | XCTAssertTrue(view.didHandleResult()) 22 | } 23 | 24 | func testItCantHandleSpecificErrorsWhereThereAreNoSpecificHandlers() throws { 25 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 26 | let handler = ErrorHandler(for: view) 27 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 28 | 29 | XCTAssertFalse(handler.handle(error: HandlerError1.error2, onCompleted: view.onCompleted)) 30 | 31 | XCTAssertFalse(view.didHandleResult()) 32 | } 33 | 34 | func testItCanFallbackToADefaultHandler() throws { 35 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 36 | let handler = ErrorHandler(for: view) 37 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.unexpectedHandlerExecuted)) 38 | .onNoMatch(.perform(action: view.customHandler)) 39 | 40 | XCTAssertTrue(handler.handle(error: HandlerError1.error2, onCompleted: view.onCompleted)) 41 | 42 | XCTAssertTrue(view.didHandleResult()) 43 | } 44 | 45 | func testItPrefersASpecficHandlerAboveTheDefaultHandler() throws { 46 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 47 | let handler = ErrorHandler(for: view) 48 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 49 | .onNoMatch(.perform(action: view.unexpectedHandlerExecuted)) 50 | 51 | XCTAssertTrue(handler.handle(error: HandlerError1.error1, onCompleted: view.onCompleted)) 52 | 53 | XCTAssertTrue(view.didHandleResult()) 54 | } 55 | 56 | func testItCanExecuteMultupleMatches() throws { 57 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 2, numberExpectedonCompleted: 1) 58 | let handler = ErrorHandler(for: view) 59 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 60 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 61 | .onNoMatch(.perform(action: view.unexpectedHandlerExecuted)) 62 | 63 | XCTAssertTrue(handler.handle(error: HandlerError1.error1, onCompleted: view.onCompleted)) 64 | 65 | XCTAssertTrue(view.didHandleResult()) 66 | } 67 | 68 | func testItCanHaveHandlersThatAlwaysGetExecuted() throws { 69 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 2, numberExpectedonCompleted: 1) 70 | let handler = ErrorHandler(for: view) 71 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 72 | .onNoMatch(.perform(action: view.unexpectedHandlerExecuted)) 73 | .always(.perform(action: view.customHandler)) 74 | 75 | XCTAssertTrue(handler.handle(error: HandlerError1.error1, onCompleted: view.onCompleted)) 76 | 77 | XCTAssertTrue(view.didHandleResult()) 78 | } 79 | 80 | func testItCanHaveMultipleAlwaysHandlers() { 81 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 3, numberExpectedonCompleted: 0) 82 | let handler = ErrorHandler(for: view) 83 | .always(.perform(action: view.customHandler)) 84 | .always(.perform(action: view.customHandler)) 85 | .always(.perform(action: view.customHandler)) 86 | 87 | XCTAssertTrue(handler.handle(error: HandlerError1.error1, onCompleted: nil)) 88 | 89 | XCTAssertTrue(view.didHandleResult()) 90 | } 91 | 92 | func testItCanHaveMultipleDefaultHandlers() { 93 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 3, numberExpectedonCompleted: 0) 94 | let handler = ErrorHandler(for: view) 95 | .onNoMatch(.perform(action: view.customHandler)) 96 | .onNoMatch(.perform(action: view.customHandler)) 97 | .onNoMatch(.perform(action: view.customHandler)) 98 | 99 | XCTAssertTrue(handler.handle(error: HandlerError1.error1, onCompleted: nil)) 100 | 101 | XCTAssertTrue(view.didHandleResult()) 102 | } 103 | 104 | } 105 | 106 | private extension ErrorHandlerTests { 107 | enum HandlerError1: Error { 108 | case error1 109 | case error2 110 | case error3 111 | } 112 | 113 | enum HandlerError2: Error { 114 | case error4 115 | } 116 | 117 | } 118 | 119 | extension ErrorHandlerTests { 120 | 121 | static var allTests = [ 122 | ("testItCanHandleSpecificErrors", testItCanHandleSpecificErrors), 123 | ("testItCantHandleSpecificErrorsWhereThereAreNoSpecificHandlers", testItCantHandleSpecificErrorsWhereThereAreNoSpecificHandlers), 124 | ("testItCanFallbackToADefaultHandler", testItCanFallbackToADefaultHandler), 125 | ("testItPrefersASpecficHandlerAboveTheDefaultHandler", testItPrefersASpecficHandlerAboveTheDefaultHandler), 126 | ("testItCanExecuteMultupleMatches", testItCanExecuteMultupleMatches), 127 | ("testItCanHaveHandlersThatAlwaysGetExecuted", testItCanHaveHandlersThatAlwaysGetExecuted), 128 | ("testItCanHaveMultipleAlwaysHandlers", testItCanHaveMultipleAlwaysHandlers), 129 | ("testItCanHaveMultipleDefaultHandlers", testItCanHaveMultipleDefaultHandlers) 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/ErrorMatcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorMatcherTests.swift 3 | // ErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 22/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftErrorHandler 11 | 12 | class ErrorMatcherTests: XCTestCase { 13 | 14 | func testItCanMatchError() throws { 15 | let matcher = ErrorMatcher.type(MatcherError1.error1) 16 | let searchError = MatcherError1.error1 17 | XCTAssertTrue(matcher.catches(searchError)) 18 | } 19 | 20 | func testItCanMatchErrorCodes() throws { 21 | let matcher = ErrorMatcher.code(400) 22 | let searchError = NSError(domain: "damain", code: 400, userInfo: ["data": "value"]) 23 | XCTAssertTrue(matcher.catches(searchError)) 24 | } 25 | 26 | func testItCanMatchErrorDomains() throws { 27 | let matcher = ErrorMatcher.domain("remote") 28 | let searchError = NSError(domain: "remote", code: 401, userInfo: ["data": "value"]) 29 | XCTAssertTrue(matcher.catches(searchError)) 30 | } 31 | 32 | func testItCanMatchWithBlock() throws { 33 | let matcher: ErrorMatcher = .isError3 34 | let searchError = MatcherError1.error3 35 | XCTAssertTrue(matcher.catches(searchError)) 36 | } 37 | 38 | func testItCanMatchCompleteErrorSuites() throws { 39 | let matcher: ErrorMatcher = .isError2 40 | let searchError = MatcherError2.error4 41 | XCTAssertTrue(matcher.catches(searchError)) 42 | } 43 | } 44 | 45 | private extension ErrorMatcher { 46 | static var isError2: ErrorMatcher { 47 | return .init(matcher: { $0 is ErrorMatcherTests.MatcherError2 }) 48 | } 49 | 50 | static var isError3: ErrorMatcher { 51 | return .init(matcher: { error in 52 | guard let testError = error as? ErrorMatcherTests.MatcherError1, testError == .error3 else { return true } 53 | return true 54 | }) 55 | } 56 | } 57 | 58 | fileprivate extension ErrorMatcherTests { 59 | 60 | enum MatcherError1: Error { 61 | case error1 62 | case error2 63 | case error3 64 | } 65 | 66 | enum MatcherError2: Error { 67 | case error4 68 | } 69 | } 70 | 71 | extension ErrorMatcherTests { 72 | 73 | static var allTests = [ 74 | ("testItCanMatchError", testItCanMatchError), 75 | ("testItCanMatchErrorCodes", testItCanMatchErrorCodes), 76 | ("testItCanMatchErrorDomains", testItCanMatchErrorDomains), 77 | ("testItCanMatchWithBlock", testItCanMatchWithBlock), 78 | ("testItCanMatchCompleteErrorSuites", testItCanMatchCompleteErrorSuites) 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/Helpers/DispatchGroup+Enter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchGroup+Enter.swift 3 | // ErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 26/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension DispatchGroup { 12 | class func enter(number: Int) -> DispatchGroup { 13 | let group = DispatchGroup() 14 | if number > 0 { 15 | for _ in 1...number { 16 | group.enter() 17 | } 18 | } 19 | return group 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/Helpers/MockedAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockedAlert.swift 3 | // ErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 26/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import SwiftErrorHandler 11 | 12 | class MockedAlert: ErrorAlert { 13 | 14 | func build(for error: Error, onCompleted: OnErrorHandled) -> AlertController { 15 | let controller = AlertController(title: "title", message: "message", preferredStyle: .alert) 16 | controller.addAction(AlertAction(title: "cancel", style: .cancel, handler: nil)) 17 | return controller 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/Helpers/MockedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockedView.swift 3 | // ErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 26/07/2019. 6 | // Copyright © 2019 stefanrenne. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftErrorHandler 11 | 12 | class MockedView: ErrorHandlerView { 13 | 14 | private let presentedAlertsGroups: DispatchGroup 15 | private let customHandlersGroup: DispatchGroup 16 | private let onCompletedGroups: DispatchGroup 17 | var lastResult: AlertController? 18 | 19 | init(numberExpectedPresentedAlerts: Int = 0, numberExpectedCustomHandlers: Int = 0, numberExpectedonCompleted: Int = 0) { 20 | presentedAlertsGroups = DispatchGroup.enter(number: numberExpectedPresentedAlerts) 21 | customHandlersGroup = DispatchGroup.enter(number: numberExpectedCustomHandlers) 22 | onCompletedGroups = DispatchGroup.enter(number: numberExpectedonCompleted) 23 | } 24 | 25 | func present(alert: AlertController) { 26 | lastResult = alert 27 | presentedAlertsGroups.leave() 28 | } 29 | 30 | func onCompleted() { 31 | onCompletedGroups.leave() 32 | } 33 | 34 | func unexpectedHandlerExecuted(for error: Error, onCompleted: OnErrorHandled) { 35 | XCTFail("Unexpected Handler Executed") 36 | } 37 | 38 | func customHandler(for error: Error, onCompleted: OnErrorHandled) { 39 | customHandlersGroup.leave() 40 | onCompleted?() 41 | } 42 | 43 | func didHandleResult() -> Bool { 44 | let timeout: DispatchTime = .now() + .milliseconds(200) 45 | return presentedAlertsGroups.wait(timeout: timeout) == .success && 46 | customHandlersGroup.wait(timeout: timeout) == .success && 47 | onCompletedGroups.wait(timeout: timeout) == .success 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/ResultErrorHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultErrorHandlerTests.swift 3 | // SwiftErrorHandlerTests 4 | // 5 | // Created by Stefan Renne on 30/12/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftErrorHandler 10 | 11 | class ResultErrorHandlerTests: XCTestCase { 12 | 13 | func testItWontHandleSuccessResults() throws { 14 | let view = MockedView() 15 | let handler = ErrorHandler(for: view) 16 | 17 | let result: Result = .success("win") 18 | XCTAssertNotNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 19 | } 20 | 21 | func testItCanHandleSpecificErrors() throws { 22 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 23 | let handler = ErrorHandler(for: view) 24 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 25 | 26 | let result: Result = .failure(HandlerError1.error1) 27 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 28 | 29 | XCTAssertTrue(view.didHandleResult()) 30 | } 31 | 32 | func testItCantHandleSpecificErrorsWhereThereAreNoSpecificHandlers() throws { 33 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 34 | let handler = ErrorHandler(for: view) 35 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 36 | 37 | let result: Result = .failure(HandlerError1.error2) 38 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 39 | 40 | XCTAssertFalse(view.didHandleResult()) 41 | } 42 | 43 | func testItCanFallbackToADefaultHandler() throws { 44 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 45 | let handler = ErrorHandler(for: view) 46 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.unexpectedHandlerExecuted)) 47 | .onNoMatch(.perform(action: view.customHandler)) 48 | 49 | let result: Result = .failure(HandlerError1.error2) 50 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 51 | 52 | XCTAssertTrue(view.didHandleResult()) 53 | } 54 | 55 | func testItPrefersASpecficHandlerAboveTheDefaultHandler() throws { 56 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 1, numberExpectedonCompleted: 1) 57 | let handler = ErrorHandler(for: view) 58 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 59 | .onNoMatch(.perform(action: view.unexpectedHandlerExecuted)) 60 | 61 | let result: Result = .failure(HandlerError1.error1) 62 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 63 | 64 | XCTAssertTrue(view.didHandleResult()) 65 | } 66 | 67 | func testItCanExecuteMultupleMatches() throws { 68 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 2, numberExpectedonCompleted: 1) 69 | let handler = ErrorHandler(for: view) 70 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 71 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 72 | .onNoMatch(.perform(action: view.unexpectedHandlerExecuted)) 73 | 74 | let result: Result = .failure(HandlerError1.error1) 75 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 76 | 77 | XCTAssertTrue(view.didHandleResult()) 78 | } 79 | 80 | func testItCanHaveHandlersThatAlwaysGetExecuted() throws { 81 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 2, numberExpectedonCompleted: 1) 82 | let handler = ErrorHandler(for: view) 83 | .on(error: .type(HandlerError1.error1), then: .perform(action: view.customHandler)) 84 | .onNoMatch(.perform(action: view.unexpectedHandlerExecuted)) 85 | .always(.perform(action: view.customHandler)) 86 | 87 | let result: Result = .failure(HandlerError1.error1) 88 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: view.onCompleted)) 89 | 90 | XCTAssertTrue(view.didHandleResult()) 91 | } 92 | 93 | func testItCanHaveMultipleAlwaysHandlers() { 94 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 3, numberExpectedonCompleted: 0) 95 | let handler = ErrorHandler(for: view) 96 | .always(.perform(action: view.customHandler)) 97 | .always(.perform(action: view.customHandler)) 98 | .always(.perform(action: view.customHandler)) 99 | 100 | let result: Result = .failure(HandlerError1.error1) 101 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: nil)) 102 | 103 | XCTAssertTrue(view.didHandleResult()) 104 | } 105 | 106 | func testItCanHaveMultipleDefaultHandlers() { 107 | let view = MockedView(numberExpectedPresentedAlerts: 0, numberExpectedCustomHandlers: 3, numberExpectedonCompleted: 0) 108 | let handler = ErrorHandler(for: view) 109 | .onNoMatch(.perform(action: view.customHandler)) 110 | .onNoMatch(.perform(action: view.customHandler)) 111 | .onNoMatch(.perform(action: view.customHandler)) 112 | 113 | let result: Result = .failure(HandlerError1.error1) 114 | XCTAssertNil(result.get(onError: handler, onErrorCompleted: nil)) 115 | 116 | XCTAssertTrue(view.didHandleResult()) 117 | } 118 | } 119 | 120 | private extension ResultErrorHandlerTests { 121 | enum HandlerError1: Error { 122 | case error1 123 | case error2 124 | case error3 125 | } 126 | 127 | enum HandlerError2: Error { 128 | case error4 129 | } 130 | } 131 | 132 | extension ResultErrorHandlerTests { 133 | 134 | static var allTests = [ 135 | ("testItWontHandleSuccessResults", testItWontHandleSuccessResults), 136 | ("testItCanHandleSpecificErrors", testItCanHandleSpecificErrors), 137 | ("testItCantHandleSpecificErrorsWhereThereAreNoSpecificHandlers", testItCantHandleSpecificErrorsWhereThereAreNoSpecificHandlers), 138 | ("testItCanFallbackToADefaultHandler", testItCanFallbackToADefaultHandler), 139 | ("testItPrefersASpecficHandlerAboveTheDefaultHandler", testItPrefersASpecficHandlerAboveTheDefaultHandler), 140 | ("testItCanExecuteMultupleMatches", testItCanExecuteMultupleMatches), 141 | ("testItCanHaveHandlersThatAlwaysGetExecuted", testItCanHaveHandlersThatAlwaysGetExecuted), 142 | ("testItCanHaveMultipleAlwaysHandlers", testItCanHaveMultipleAlwaysHandlers), 143 | ("testItCanHaveMultipleDefaultHandlers", testItCanHaveMultipleDefaultHandlers) 144 | ] 145 | } 146 | -------------------------------------------------------------------------------- /Tests/SwiftErrorHandlerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ActionHandlerTests.allTests), 7 | testCase(ErrorHandlerTests.allTests), 8 | testCase(ResultErrorHandlerTests.allTests), 9 | testCase(ErrorMatcherTests.allTests) 10 | ] 11 | } 12 | #endif 13 | --------------------------------------------------------------------------------