├── HTTPRequests.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── UnitTests ├── Info.plist └── UnitTests.swift ├── LICENSE ├── .gitignore ├── README.md └── HTTPRequests ├── main.swift └── HTTPRequests.swift /HTTPRequests.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UnitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nicolas Seriot 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 | -------------------------------------------------------------------------------- /.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 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnitTests.swift 3 | // UnitTests 4 | // 5 | // Created by nst on 25/04/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class UnitTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | 25 | let url = URL(string:"http://seriot.ch/objects.json")! 26 | let request = URLRequest(url: url) 27 | 28 | let expectationGoodType = expectation(description:"expectationGoodType") 29 | let expectationBadType = expectation(description:"expectationBadType") 30 | 31 | 32 | request.dr_fetchTypedJSON([[String:AnyObject]].self) { 33 | do { 34 | let (r, _) = try $0() 35 | XCTAssertEqual(r.status, 200) 36 | expectationGoodType.fulfill() 37 | } catch let e as NSError { 38 | print(e) 39 | } 40 | } 41 | 42 | request.dr_fetchTypedJSON([String:AnyObject].self) { 43 | do { 44 | let (_, _) = try $0() 45 | XCTFail() 46 | } catch let e as NSError { 47 | print(e) 48 | expectationBadType.fulfill() 49 | } 50 | } 51 | 52 | // Loop until the expectation is fulfilled 53 | waitForExpectations(timeout: 5, handler: { error in 54 | XCTAssertNil(error, "Error") 55 | }) 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTPRequests 2 | __NSURLRequest extensions in Swift__ 3 | 4 | __Description__ 5 | 6 | Four different NSURLRequest extensions in Swift: 7 | 8 | 1. sum type 9 | 2. deferred result 10 | 3. deferred result and DRError 11 | 4. success and error closures 12 | 13 | __Features__ 14 | 15 | - cURL description 16 | - fetch data 17 | - fetch JSON 18 | - fetch typed JSON 19 | - HTTP status, headers, data and NSError 20 | 21 | __Motivation__ 22 | 23 | Demonstrates how to handle errors in an asychronous Swift API. 24 | 25 | Initially written as the companion code for my [AppBuilders 2016](https://www.appbuilders.ch/) talk on [error handling](http://seriot.ch/resources/talks_papers/20160426_error_handling.pdf). 26 | 27 | __Example__ 28 | 29 | ```swift 30 | let url = NSURL(string:"http://seriot.ch/objects.json")! 31 | let request = NSURLRequest(URL: url) 32 | ``` 33 | 34 | 1 - sum type 35 | 36 | ```swift 37 | request.st_fetchTypedJSON([[String:AnyObject]].self) { 38 | switch($0) { 39 | case let .Success(r, json): 40 | print(r.status, json) 41 | case let .Failure(r, nsError): 42 | print(r.status, nsError.localizedDescription) 43 | } 44 | } 45 | ``` 46 | 47 | 2 - deferred result 48 | 49 | ```swift 50 | request.dr_fetchTypedJSON([[String:AnyObject]].self) { 51 | do { 52 | let (r, json) = try $0() 53 | print(r.status, json) 54 | } catch let e as NSError { 55 | print(e) 56 | } 57 | } 58 | ``` 59 | 60 | 3 - deferred result and DRError 61 | 62 | ```swift 63 | request.dr2_fetchTypedJSON([[String:AnyObject]].self) { 64 | do { 65 | let (r, json) = try $0() 66 | print(r.status, json) 67 | } catch let DRError.Error(r, nsError) where r.status == 400 { 68 | print(r.status, nsError) 69 | } catch let DRError.Error(r, nsError) { 70 | print(r.status, nsError) 71 | } catch { 72 | assertionFailure() 73 | } 74 | } 75 | ``` 76 | 77 | 4 - success and error closures 78 | 79 | ```swift 80 | request.se_fetchTypedJSON( 81 | successHandler:{ (r, json:[[String:AnyObject]]) in 82 | print(json) 83 | }, errorHandler: { (r, nsError) in 84 | print(nsError) 85 | } 86 | ) 87 | ``` 88 | -------------------------------------------------------------------------------- /HTTPRequests/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // HTTPRequests 4 | // 5 | // Created by nst on 24/04/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func requestWithSumType() { 12 | 13 | let url = URL(string:"http://seriot.ch/objects.json")! 14 | let request = URLRequest(url: url) 15 | 16 | request.st_fetchData { 17 | switch($0) { 18 | case let .success(r, data): 19 | print(r.status, data) 20 | case let .failure(r, nsError): 21 | print(r.status, nsError.localizedDescription) 22 | } 23 | } 24 | 25 | request.st_fetchJSON { 26 | switch($0) { 27 | case let .success(r, json) where 200..<300 ~= r.status: 28 | print(json) 29 | case let .success(r, json) where r.status == 450: 30 | print(json) 31 | case let .success(r, json): 32 | print(r.status, json) 33 | case let .failure(r, nsError): 34 | print(r.status, nsError.localizedDescription) 35 | } 36 | } 37 | 38 | request.st_fetchTypedJSON([[String:AnyObject]].self) { 39 | switch($0) { 40 | case let .success(r, json): 41 | print(r.status, json) 42 | case let .failure(r, nsError): 43 | print(r.status, nsError.localizedDescription) 44 | } 45 | } 46 | } 47 | 48 | func requestWithDeferredResult() { 49 | 50 | let url = URL(string:"http://seriot.ch/objects.json")! 51 | let request = URLRequest(url: url) 52 | 53 | request.dr_fetchData { 54 | do { 55 | let r = try $0() 56 | print(r.status, r.data) 57 | } catch let e as NSError { 58 | print(e) 59 | } 60 | } 61 | 62 | request.dr_fetchJSON() { 63 | do { 64 | let (r, json) = try $0() 65 | print(r.status, json) 66 | } catch let e as NSError { 67 | print(e) 68 | } 69 | } 70 | 71 | request.dr_fetchTypedJSON([[String:AnyObject]].self) { 72 | do { 73 | let (r, json) = try $0() 74 | print(r.status, json) 75 | } catch let e as NSError { 76 | print(e) 77 | } 78 | } 79 | 80 | request.dr_fetchData { 81 | if let r = try? $0() { 82 | print(r.status, r.data) 83 | } 84 | } 85 | } 86 | 87 | func requestWithDeferredResultAndDRError() { 88 | 89 | let url = URL(string:"http://seriot.ch/objects.json")! 90 | let request = URLRequest(url: url) 91 | 92 | request.dr2_fetchJSON() { 93 | do { 94 | let (r, json) = try $0() 95 | print(r.status, json) 96 | } catch let DRError.error(r, nsError) { 97 | print(r.status, nsError) 98 | } catch { 99 | print(error) 100 | } 101 | } 102 | 103 | request.dr2_fetchTypedJSON([[String:AnyObject]].self) { 104 | do { 105 | let (r, json) = try $0() 106 | print(r.status, json) 107 | } catch let DRError.error(r, nsError) { 108 | print(r.status, nsError) 109 | } catch { 110 | print(error) 111 | } 112 | } 113 | 114 | request.dr2_fetchTypedJSON([[String:AnyObject]].self) { 115 | do { 116 | let (r, json) = try $0() 117 | print(r.status, json) 118 | } catch let DRError.error(r, nsError) where r.status == 400 { 119 | print(r.status, nsError) 120 | } catch let DRError.error(r, nsError) { 121 | print(r.status, nsError) 122 | } catch { 123 | assertionFailure() 124 | } 125 | } 126 | } 127 | 128 | func requestWithSuccessError() { 129 | 130 | let url = URL(string:"http://seriot.ch/objects.json")! 131 | let request = URLRequest(url: url) 132 | 133 | request.se_fetchData( 134 | successHandler: { (r) in 135 | print(r.status, r.data) 136 | }, errorHandler: { (nsError) in 137 | print(nsError) 138 | } 139 | ) 140 | 141 | request.se_fetchJSON( 142 | successHandler:{ (r, json) in 143 | print(json) 144 | }, errorHandler: { (r, nsError) in 145 | print(nsError) 146 | } 147 | ) 148 | 149 | request.se_fetchTypedJSON( 150 | successHandler:{ (r, json:[[String:AnyObject]]) in 151 | print(json) 152 | }, errorHandler: { (r, nsError) in 153 | print(nsError) 154 | } 155 | ) 156 | } 157 | 158 | func main() { 159 | 160 | requestWithSumType() 161 | 162 | requestWithDeferredResult() 163 | 164 | requestWithDeferredResultAndDRError() 165 | 166 | requestWithSuccessError() 167 | 168 | RunLoop.main.run() 169 | } 170 | 171 | main() 172 | -------------------------------------------------------------------------------- /HTTPRequests.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 033CBAB81CCCC36A0022469D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033CBAB71CCCC36A0022469D /* main.swift */; }; 11 | 033CBABF1CCCC3760022469D /* HTTPRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033CBABE1CCCC3760022469D /* HTTPRequests.swift */; }; 12 | 03671DFD1CCE8AEC00137253 /* UnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03671DFC1CCE8AEC00137253 /* UnitTests.swift */; }; 13 | 03671E021CCE8D1300137253 /* HTTPRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033CBABE1CCCC3760022469D /* HTTPRequests.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXCopyFilesBuildPhase section */ 17 | 033CBAB21CCCC36A0022469D /* CopyFiles */ = { 18 | isa = PBXCopyFilesBuildPhase; 19 | buildActionMask = 2147483647; 20 | dstPath = /usr/share/man/man1/; 21 | dstSubfolderSpec = 0; 22 | files = ( 23 | ); 24 | runOnlyForDeploymentPostprocessing = 1; 25 | }; 26 | /* End PBXCopyFilesBuildPhase section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 033CBAB41CCCC36A0022469D /* HTTPRequests */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = HTTPRequests; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 033CBAB71CCCC36A0022469D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 31 | 033CBABE1CCCC3760022469D /* HTTPRequests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPRequests.swift; sourceTree = ""; }; 32 | 03671DFA1CCE8AEC00137253 /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 03671DFC1CCE8AEC00137253 /* UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTests.swift; sourceTree = ""; }; 34 | 03671DFE1CCE8AEC00137253 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 033CBAB11CCCC36A0022469D /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | 03671DF71CCE8AEC00137253 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 033CBAAB1CCCC36A0022469D = { 56 | isa = PBXGroup; 57 | children = ( 58 | 033CBAB61CCCC36A0022469D /* HTTPRequests */, 59 | 03671DFB1CCE8AEC00137253 /* UnitTests */, 60 | 033CBAB51CCCC36A0022469D /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | 033CBAB51CCCC36A0022469D /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 033CBAB41CCCC36A0022469D /* HTTPRequests */, 68 | 03671DFA1CCE8AEC00137253 /* UnitTests.xctest */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 033CBAB61CCCC36A0022469D /* HTTPRequests */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 033CBABE1CCCC3760022469D /* HTTPRequests.swift */, 77 | 033CBAB71CCCC36A0022469D /* main.swift */, 78 | ); 79 | path = HTTPRequests; 80 | sourceTree = ""; 81 | }; 82 | 03671DFB1CCE8AEC00137253 /* UnitTests */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 03671DFC1CCE8AEC00137253 /* UnitTests.swift */, 86 | 03671DFE1CCE8AEC00137253 /* Info.plist */, 87 | ); 88 | path = UnitTests; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 033CBAB31CCCC36A0022469D /* HTTPRequests */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 033CBABB1CCCC36A0022469D /* Build configuration list for PBXNativeTarget "HTTPRequests" */; 97 | buildPhases = ( 98 | 033CBAB01CCCC36A0022469D /* Sources */, 99 | 033CBAB11CCCC36A0022469D /* Frameworks */, 100 | 033CBAB21CCCC36A0022469D /* CopyFiles */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = HTTPRequests; 107 | productName = HTTPRequests; 108 | productReference = 033CBAB41CCCC36A0022469D /* HTTPRequests */; 109 | productType = "com.apple.product-type.tool"; 110 | }; 111 | 03671DF91CCE8AEC00137253 /* UnitTests */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = 03671E011CCE8AEC00137253 /* Build configuration list for PBXNativeTarget "UnitTests" */; 114 | buildPhases = ( 115 | 03671DF61CCE8AEC00137253 /* Sources */, 116 | 03671DF71CCE8AEC00137253 /* Frameworks */, 117 | 03671DF81CCE8AEC00137253 /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | name = UnitTests; 124 | productName = UnitTests; 125 | productReference = 03671DFA1CCE8AEC00137253 /* UnitTests.xctest */; 126 | productType = "com.apple.product-type.bundle.unit-test"; 127 | }; 128 | /* End PBXNativeTarget section */ 129 | 130 | /* Begin PBXProject section */ 131 | 033CBAAC1CCCC36A0022469D /* Project object */ = { 132 | isa = PBXProject; 133 | attributes = { 134 | LastSwiftUpdateCheck = 0730; 135 | LastUpgradeCheck = 0730; 136 | ORGANIZATIONNAME = "Nicolas Seriot"; 137 | TargetAttributes = { 138 | 033CBAB31CCCC36A0022469D = { 139 | CreatedOnToolsVersion = 7.3; 140 | LastSwiftMigration = 0800; 141 | }; 142 | 03671DF91CCE8AEC00137253 = { 143 | CreatedOnToolsVersion = 7.3; 144 | }; 145 | }; 146 | }; 147 | buildConfigurationList = 033CBAAF1CCCC36A0022469D /* Build configuration list for PBXProject "HTTPRequests" */; 148 | compatibilityVersion = "Xcode 3.2"; 149 | developmentRegion = English; 150 | hasScannedForEncodings = 0; 151 | knownRegions = ( 152 | en, 153 | ); 154 | mainGroup = 033CBAAB1CCCC36A0022469D; 155 | productRefGroup = 033CBAB51CCCC36A0022469D /* Products */; 156 | projectDirPath = ""; 157 | projectRoot = ""; 158 | targets = ( 159 | 033CBAB31CCCC36A0022469D /* HTTPRequests */, 160 | 03671DF91CCE8AEC00137253 /* UnitTests */, 161 | ); 162 | }; 163 | /* End PBXProject section */ 164 | 165 | /* Begin PBXResourcesBuildPhase section */ 166 | 03671DF81CCE8AEC00137253 /* Resources */ = { 167 | isa = PBXResourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | /* End PBXResourcesBuildPhase section */ 174 | 175 | /* Begin PBXSourcesBuildPhase section */ 176 | 033CBAB01CCCC36A0022469D /* Sources */ = { 177 | isa = PBXSourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 033CBAB81CCCC36A0022469D /* main.swift in Sources */, 181 | 033CBABF1CCCC3760022469D /* HTTPRequests.swift in Sources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | 03671DF61CCE8AEC00137253 /* Sources */ = { 186 | isa = PBXSourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 03671DFD1CCE8AEC00137253 /* UnitTests.swift in Sources */, 190 | 03671E021CCE8D1300137253 /* HTTPRequests.swift in Sources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXSourcesBuildPhase section */ 195 | 196 | /* Begin XCBuildConfiguration section */ 197 | 033CBAB91CCCC36A0022469D /* Debug */ = { 198 | isa = XCBuildConfiguration; 199 | buildSettings = { 200 | ALWAYS_SEARCH_USER_PATHS = NO; 201 | CLANG_ANALYZER_NONNULL = YES; 202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 203 | CLANG_CXX_LIBRARY = "libc++"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 | CLANG_WARN_EMPTY_BODY = YES; 210 | CLANG_WARN_ENUM_CONVERSION = YES; 211 | CLANG_WARN_INT_CONVERSION = YES; 212 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 213 | CLANG_WARN_UNREACHABLE_CODE = YES; 214 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 215 | CODE_SIGN_IDENTITY = "-"; 216 | COPY_PHASE_STRIP = NO; 217 | DEBUG_INFORMATION_FORMAT = dwarf; 218 | ENABLE_STRICT_OBJC_MSGSEND = YES; 219 | ENABLE_TESTABILITY = YES; 220 | GCC_C_LANGUAGE_STANDARD = gnu99; 221 | GCC_DYNAMIC_NO_PIC = NO; 222 | GCC_NO_COMMON_BLOCKS = YES; 223 | GCC_OPTIMIZATION_LEVEL = 0; 224 | GCC_PREPROCESSOR_DEFINITIONS = ( 225 | "DEBUG=1", 226 | "$(inherited)", 227 | ); 228 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 229 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 230 | GCC_WARN_UNDECLARED_SELECTOR = YES; 231 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 232 | GCC_WARN_UNUSED_FUNCTION = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | MACOSX_DEPLOYMENT_TARGET = 10.11; 235 | MTL_ENABLE_DEBUG_INFO = YES; 236 | ONLY_ACTIVE_ARCH = YES; 237 | SDKROOT = macosx; 238 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 239 | }; 240 | name = Debug; 241 | }; 242 | 033CBABA1CCCC36A0022469D /* Release */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_NONNULL = YES; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 248 | CLANG_CXX_LIBRARY = "libc++"; 249 | CLANG_ENABLE_MODULES = YES; 250 | CLANG_ENABLE_OBJC_ARC = YES; 251 | CLANG_WARN_BOOL_CONVERSION = YES; 252 | CLANG_WARN_CONSTANT_CONVERSION = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INT_CONVERSION = YES; 257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 258 | CLANG_WARN_UNREACHABLE_CODE = YES; 259 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 260 | CODE_SIGN_IDENTITY = "-"; 261 | COPY_PHASE_STRIP = NO; 262 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 263 | ENABLE_NS_ASSERTIONS = NO; 264 | ENABLE_STRICT_OBJC_MSGSEND = YES; 265 | GCC_C_LANGUAGE_STANDARD = gnu99; 266 | GCC_NO_COMMON_BLOCKS = YES; 267 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 268 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 269 | GCC_WARN_UNDECLARED_SELECTOR = YES; 270 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 271 | GCC_WARN_UNUSED_FUNCTION = YES; 272 | GCC_WARN_UNUSED_VARIABLE = YES; 273 | MACOSX_DEPLOYMENT_TARGET = 10.11; 274 | MTL_ENABLE_DEBUG_INFO = NO; 275 | SDKROOT = macosx; 276 | }; 277 | name = Release; 278 | }; 279 | 033CBABC1CCCC36A0022469D /* Debug */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | PRODUCT_NAME = "$(TARGET_NAME)"; 283 | SWIFT_VERSION = 3.0; 284 | }; 285 | name = Debug; 286 | }; 287 | 033CBABD1CCCC36A0022469D /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | PRODUCT_NAME = "$(TARGET_NAME)"; 291 | SWIFT_VERSION = 3.0; 292 | }; 293 | name = Release; 294 | }; 295 | 03671DFF1CCE8AEC00137253 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | COMBINE_HIDPI_IMAGES = YES; 299 | INFOPLIST_FILE = UnitTests/Info.plist; 300 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 301 | PRODUCT_BUNDLE_IDENTIFIER = ch.seriot.UnitTests; 302 | PRODUCT_NAME = "$(TARGET_NAME)"; 303 | SWIFT_VERSION = 3.0; 304 | }; 305 | name = Debug; 306 | }; 307 | 03671E001CCE8AEC00137253 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | COMBINE_HIDPI_IMAGES = YES; 311 | INFOPLIST_FILE = UnitTests/Info.plist; 312 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 313 | PRODUCT_BUNDLE_IDENTIFIER = ch.seriot.UnitTests; 314 | PRODUCT_NAME = "$(TARGET_NAME)"; 315 | SWIFT_VERSION = 3.0; 316 | }; 317 | name = Release; 318 | }; 319 | /* End XCBuildConfiguration section */ 320 | 321 | /* Begin XCConfigurationList section */ 322 | 033CBAAF1CCCC36A0022469D /* Build configuration list for PBXProject "HTTPRequests" */ = { 323 | isa = XCConfigurationList; 324 | buildConfigurations = ( 325 | 033CBAB91CCCC36A0022469D /* Debug */, 326 | 033CBABA1CCCC36A0022469D /* Release */, 327 | ); 328 | defaultConfigurationIsVisible = 0; 329 | defaultConfigurationName = Release; 330 | }; 331 | 033CBABB1CCCC36A0022469D /* Build configuration list for PBXNativeTarget "HTTPRequests" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | 033CBABC1CCCC36A0022469D /* Debug */, 335 | 033CBABD1CCCC36A0022469D /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Release; 339 | }; 340 | 03671E011CCE8AEC00137253 /* Build configuration list for PBXNativeTarget "UnitTests" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | 03671DFF1CCE8AEC00137253 /* Debug */, 344 | 03671E001CCE8AEC00137253 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | /* End XCConfigurationList section */ 350 | }; 351 | rootObject = 033CBAAC1CCCC36A0022469D /* Project object */; 352 | } 353 | -------------------------------------------------------------------------------- /HTTPRequests/HTTPRequests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequests.swift 3 | // HTTPRequests 4 | // 5 | // Created by Nicolas Seriot on 30/03/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Error { 12 | func nsError(_ localizedDescription:String?, underlyingError:NSError? = nil) -> NSError { 13 | var userInfo : [AnyHashable: Any] = [:] 14 | if let s = localizedDescription { 15 | userInfo[NSLocalizedDescriptionKey] = s 16 | } 17 | if let u = underlyingError { 18 | userInfo[NSUnderlyingErrorKey] = u 19 | } 20 | return NSError(domain: self._domain, code: self._code, userInfo: userInfo) 21 | } 22 | } 23 | 24 | open class HTTPResponse : NSObject { 25 | 26 | public enum HTTPResponse: Error { 27 | case noData 28 | case unexpectedJSONType 29 | } 30 | 31 | open var status : Int = 0 32 | open var headers : [AnyHashable: Any] = [:] 33 | open var data : Data? = nil 34 | 35 | override public init() { 36 | super.init() 37 | } 38 | 39 | public init(status:Int, headers:[AnyHashable: Any], data:Data) { 40 | super.init() 41 | self.status = status 42 | self.headers = headers 43 | self.data = data 44 | } 45 | 46 | open func json(_ type:T.Type) throws -> T { 47 | guard let existingData = self.data else { 48 | throw HTTPResponse.noData.nsError("No Data") 49 | } 50 | 51 | let json = try JSONSerialization.jsonObject(with: existingData, options: []) 52 | 53 | guard let typedJSON = json as? T else { 54 | throw HTTPResponse.unexpectedJSONType.nsError("Expected JSON type: \(type(of: type)), found: \(type(of: json))") 55 | } 56 | 57 | return typedJSON 58 | } 59 | } 60 | 61 | public enum HTTPResultType { 62 | case success(httpResponse:HTTPResponse, value:T) 63 | case failure(httpResponse:HTTPResponse, nsError:NSError) 64 | } 65 | 66 | public enum DRError : Error { 67 | case error(httpResponse:HTTPResponse, nsError:NSError) 68 | } 69 | 70 | // 1. sum type 71 | extension URLRequest { 72 | 73 | public func st_fetchData(_ completionHandler:@escaping (HTTPResultType)->()) { 74 | 75 | #if DEBUG 76 | print(self.curlDescription()) 77 | #endif 78 | 79 | URLSession.shared.dataTask(with: self, completionHandler: { (optionalData, optionalResponse, optionalError) -> Void in 80 | 81 | DispatchQueue.main.async(execute: { 82 | 83 | guard let data = optionalData else { 84 | guard let e = optionalError else { assertionFailure(); return } 85 | completionHandler(.failure(httpResponse: HTTPResponse(), nsError: e as NSError)) 86 | return 87 | } 88 | 89 | guard let httpResponse = optionalResponse as? HTTPURLResponse else { 90 | guard let e = optionalError else { assertionFailure(); return } 91 | completionHandler(.failure(httpResponse: HTTPResponse(), nsError: e as NSError)) 92 | return 93 | } 94 | 95 | let response = HTTPResponse( 96 | status:httpResponse.statusCode, 97 | headers:httpResponse.allHeaderFields, 98 | data:data) 99 | 100 | completionHandler(.success(httpResponse:response, value:data)) 101 | }) 102 | }) .resume() 103 | } 104 | 105 | public func st_fetchJSON(_ completionHandler:@escaping (HTTPResultType)->()) { 106 | st_fetchTypedJSON(AnyObject.self) { 107 | completionHandler($0) 108 | } 109 | } 110 | 111 | public func st_fetchTypedJSON(_ type:T.Type, completionHandler:@escaping (HTTPResultType) -> ()) { 112 | st_fetchData { (result) -> () in 113 | 114 | switch(result) { 115 | case let .failure(httpResponse, nsError): 116 | completionHandler(.failure(httpResponse: httpResponse, nsError: nsError)) 117 | case let .success(httpResponse, _): 118 | do { 119 | completionHandler(.success(httpResponse: httpResponse, value: try httpResponse.json(T.self))) 120 | } catch let e as NSError { 121 | completionHandler(.failure(httpResponse: httpResponse, nsError: e)) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | // 2. deferred result 129 | extension URLRequest { 130 | 131 | public func dr_fetchData(_ completion:@escaping (_ result: () throws -> HTTPResponse) -> () ) { 132 | 133 | #if DEBUG 134 | print(self.curlDescription()) 135 | #endif 136 | 137 | URLSession.shared.dataTask(with: self, completionHandler: { (optionalData, optionalResponse, optionalError) -> Void in 138 | 139 | DispatchQueue.main.async(execute: { 140 | 141 | guard let data = optionalData else { 142 | guard let e = optionalError else { assertionFailure(); return } 143 | completion({ throw e } ) 144 | return 145 | } 146 | 147 | guard let httpResponse = optionalResponse as? HTTPURLResponse else { 148 | guard let e = optionalError else { assertionFailure(); return } 149 | completion({ throw e } ) 150 | return 151 | } 152 | 153 | let response = HTTPResponse( 154 | status:httpResponse.statusCode, 155 | headers:httpResponse.allHeaderFields, 156 | data:data) 157 | 158 | completion({ return response }) 159 | }) 160 | }) .resume() 161 | } 162 | 163 | public func dr_fetchJSON(_ completion:@escaping (_ result: () throws -> (httpResponse:HTTPResponse, json:AnyObject)) -> () ) { 164 | dr_fetchTypedJSON(AnyObject.self, completion: completion) 165 | } 166 | 167 | public func dr_fetchTypedJSON(_ type:T.Type, completion:@escaping (_ result: () throws -> (httpResponse:HTTPResponse, json:T)) -> () ) { 168 | dr_fetchData { 169 | do { 170 | let httpResponse = try $0() 171 | completion({ return (httpResponse:httpResponse, json:try httpResponse.json(T.self)) } ) 172 | } catch let e as NSError { 173 | completion({ throw e } ) 174 | } 175 | } 176 | } 177 | } 178 | 179 | // 3. deferred result and DRError 180 | extension URLRequest { 181 | 182 | public func dr2_fetchData(_ completion:@escaping (_ result: () throws -> HTTPResponse) -> () ) { 183 | 184 | #if DEBUG 185 | print(self.curlDescription()) 186 | #endif 187 | 188 | URLSession.shared.dataTask(with: self, completionHandler: { (optionalData, optionalResponse, optionalError) -> Void in 189 | 190 | DispatchQueue.main.async(execute: { 191 | 192 | guard let data = optionalData else { 193 | guard let e = optionalError else { assertionFailure(); return } 194 | completion({ throw DRError.error(httpResponse:HTTPResponse(), nsError:e as NSError) } ) 195 | return 196 | } 197 | 198 | guard let httpResponse = optionalResponse as? HTTPURLResponse else { 199 | guard let e = optionalError else { assertionFailure(); return } 200 | completion({ throw DRError.error(httpResponse:HTTPResponse(), nsError:e as NSError) } ) 201 | return 202 | } 203 | 204 | let response = HTTPResponse( 205 | status:httpResponse.statusCode, 206 | headers:httpResponse.allHeaderFields, 207 | data:data) 208 | 209 | completion({ return response }) 210 | }) 211 | }) .resume() 212 | } 213 | 214 | public func dr2_fetchJSON(_ completion:@escaping (_ result: () throws -> (httpResponse:HTTPResponse, json:AnyObject)) -> () ) { 215 | dr2_fetchTypedJSON(AnyObject.self, completion: completion) 216 | } 217 | 218 | public func dr2_fetchTypedJSON(_ type:T.Type, completion:@escaping (_ result: () throws -> (httpResponse:HTTPResponse, json:T)) -> () ) { 219 | dr2_fetchData { 220 | do { 221 | let httpResponse = try $0() 222 | do { 223 | let json = try httpResponse.json(T.self) 224 | completion({ return (httpResponse:httpResponse, json:json) } ) 225 | } catch let nsError as NSError { // JSON error 226 | let dre = DRError.error(httpResponse:httpResponse, nsError:nsError) 227 | completion({ throw dre } ) 228 | } catch { 229 | completion({ throw error } ) 230 | } 231 | } catch let dre as DRError { 232 | completion({ throw dre } ) 233 | } catch let nsError as NSError { 234 | completion({ throw DRError.error(httpResponse:HTTPResponse(), nsError:nsError) } ) 235 | } 236 | } 237 | } 238 | } 239 | 240 | // 4. success and error closures 241 | extension URLRequest { 242 | 243 | 244 | public func se_fetchData(successHandler:@escaping (HTTPResponse)->(), errorHandler:@escaping (NSError)->()) { 245 | 246 | #if DEBUG 247 | print(self.curlDescription()) 248 | #endif 249 | 250 | URLSession.shared.dataTask(with: self, completionHandler: { (optionalData, optionalResponse, optionalError) -> Void in 251 | 252 | DispatchQueue.main.async(execute: { 253 | 254 | guard let data = optionalData else { 255 | guard let e = optionalError else { assertionFailure(); return } 256 | errorHandler(e as NSError) 257 | return 258 | } 259 | 260 | guard let httpResponse = optionalResponse as? HTTPURLResponse else { 261 | guard let e = optionalError else { assertionFailure(); return } 262 | errorHandler(e as NSError) 263 | return 264 | } 265 | 266 | let response = HTTPResponse( 267 | status:httpResponse.statusCode, 268 | headers:httpResponse.allHeaderFields, 269 | data:data) 270 | 271 | successHandler(response) 272 | }) 273 | }) .resume() 274 | } 275 | 276 | 277 | public func se_fetchJSON( 278 | successHandler:@escaping (HTTPResponse, AnyObject)->(), 279 | errorHandler:@escaping (HTTPResponse, NSError)->()) { 280 | 281 | se_fetchTypedJSON(successHandler: { (httpResponse, json:AnyObject) in 282 | successHandler(httpResponse, json) 283 | }, errorHandler: { (httpResponse, error) in 284 | errorHandler(httpResponse, error) 285 | }) 286 | } 287 | 288 | public func se_fetchTypedJSON( 289 | successHandler:@escaping (HTTPResponse, T)->(), 290 | errorHandler:@escaping (HTTPResponse, NSError)->()) { 291 | 292 | se_fetchData( 293 | successHandler:{ (httpResponse) in 294 | do { 295 | let json = try httpResponse.json(T.self) 296 | successHandler(httpResponse, json) 297 | } catch let e as NSError { 298 | errorHandler(httpResponse, e) 299 | } 300 | }, errorHandler:{ (nsError) in 301 | errorHandler(HTTPResponse(), nsError) 302 | }) 303 | } 304 | 305 | } 306 | 307 | // cURL description 308 | extension URLRequest { 309 | 310 | public func curlDescription() -> String { 311 | 312 | var s = "\u{0001F340} curl -i \\\n" 313 | 314 | if let 315 | credential = self.requestCredential(), 316 | let user = credential.user, 317 | let password = credential.password 318 | { 319 | s += "-u \(user):\(password) \\\n" 320 | } 321 | 322 | if let method = self.httpMethod , method != "GET" { 323 | s += "-X \(method) \\\n" 324 | } 325 | 326 | self.allHTTPHeaderFields?.forEach({ (k,v) -> () in 327 | let kEscaped = k.replacingOccurrences(of: "\"", with: "\\\"") 328 | let vEscaped = v.replacingOccurrences(of: "\"", with: "\\\"") 329 | s += "-H \"\(kEscaped): \(vEscaped)\" \\\n" 330 | }) 331 | 332 | if let url = self.url { 333 | if let cookies = HTTPCookieStorage.shared.cookies(for: url) { 334 | for (_,v) in HTTPCookie.requestHeaderFields(with: cookies) { 335 | s += "-H \"Cookie: \(v)\" \\\n" 336 | } 337 | } 338 | } 339 | 340 | if let bodyData = self.httpBody, 341 | let bodyString = NSString(data: bodyData, encoding: String.Encoding.utf8.rawValue) as? String { 342 | let bodyEscaped = bodyString.replacingOccurrences(of: "\"", with: "\\\"") 343 | s += "-d \"\(bodyEscaped)\" \\\n" 344 | } 345 | 346 | if let url = self.url { 347 | s += "\"\(url.absoluteString)\"\n" 348 | } 349 | 350 | return s 351 | } 352 | 353 | fileprivate func requestCredential() -> URLCredential? { 354 | 355 | guard let url = self.url else { return nil } 356 | guard let host = url.host else { return nil } 357 | 358 | let credentialsDictionary = URLCredentialStorage.shared.allCredentials 359 | 360 | for protectionSpace in credentialsDictionary.keys { 361 | 362 | if let c = credentialsDictionary.values.first?.values.first , 363 | // we consider neither realm nor host, NSURL instance doesn't know them in advance 364 | (host as NSString).hasSuffix(protectionSpace.host) && 365 | protectionSpace.`protocol` == url.scheme { 366 | return c 367 | } 368 | } 369 | 370 | return nil 371 | } 372 | } 373 | --------------------------------------------------------------------------------