├── .github ├── FUNDING.yml └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ └── xcbaselines │ └── RequestTests.xcbaseline │ ├── 3400B6A4-B231-4550-9C4C-2D5F1474250B.plist │ └── Info.plist ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── README.md ├── Request.xcodeproj ├── Json_Info.plist ├── RequestTests_Info.plist ├── Request_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── Request-Package.xcscheme ├── Resources ├── banner.png ├── planned-features.md ├── swiftui.md └── todolist.png ├── Sources ├── Json │ ├── Json.swift │ ├── JsonBuilder.swift │ ├── JsonSubscript.swift │ └── Literals.swift └── Request │ ├── Helpers │ ├── Auth.swift │ ├── CacheType.swift │ ├── MediaType.swift │ ├── RequestError.swift │ └── UserAgent.swift │ ├── Request │ ├── Extra │ │ ├── RequestChain.swift │ │ ├── RequestGroup+Combine.swift │ │ └── RequestGroup.swift │ ├── FormParam │ │ ├── Form.Data.swift │ │ ├── Form.File.swift │ │ ├── Form.Value.swift │ │ ├── Form.swift │ │ ├── FormBuilder.Combined.swift │ │ ├── FormBuilder.Empty.swift │ │ ├── FormBuilder.swift │ │ └── FormParam.swift │ ├── Request+Combine.swift │ ├── Request.swift │ ├── RequestBuilder.swift │ └── RequestParams │ │ ├── AnyParam.swift │ │ ├── Body.swift │ │ ├── CombinedParams.swift │ │ ├── EmptyParam.swift │ │ ├── Header.swift │ │ ├── Headers.swift │ │ ├── Method.swift │ │ ├── Query.swift │ │ ├── QueryParam.swift │ │ ├── RequestParam.swift │ │ ├── SessionParam.swift │ │ ├── Timeout.Source.swift │ │ ├── Timeout.swift │ │ └── Url │ │ ├── ProtocolType.swift │ │ └── Url.swift │ └── SwiftUI │ ├── PropertyWrappers │ └── Requested.swift │ ├── RequestStatus.swift │ └── Views │ ├── RequestImage.swift │ └── RequestView.swift ├── Tests ├── .DS_Store ├── LinuxMain.swift └── RequestTests │ ├── JsonTests.swift │ ├── RequestTests.swift │ └── XCTestManifests.swift └── codecov.yml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [carson-katri] 2 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | branches: "*" 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | include: 12 | - name: Swift 5.4 13 | xcode: /Applications/Xcode_12.5.1.app 14 | - name: Swift 5.5 15 | xcode: /Applications/Xcode_13.2.1.app 16 | 17 | name: Build and Test (${{ matrix.name }}) 18 | runs-on: macos-11 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Switch Xcode version 23 | run: sudo xcode-select --switch ${{ matrix.xcode }} 24 | - name: Generate xcodeproj 25 | run: swift package generate-xcodeproj 26 | - name: Test 27 | run: xcodebuild clean test -project Request.xcodeproj -scheme "Request-Package" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -quiet -enableCodeCoverage YES -derivedDataPath .build/derivedData 28 | - name: Upload Codecov report 29 | run: bash <(curl -s https://codecov.io/bash) -D .build/derivedData 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | # Xcode 25 | # 26 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 27 | 28 | ## Build generated 29 | build/ 30 | DerivedData/ 31 | 32 | ## Various settings 33 | *.pbxuser 34 | !default.pbxuser 35 | *.mode1v3 36 | !default.mode1v3 37 | *.mode2v3 38 | !default.mode2v3 39 | *.perspectivev3 40 | !default.perspectivev3 41 | xcuserdata/ 42 | 43 | ## Other 44 | *.moved-aside 45 | *.xccheckout 46 | *.xcscmblueprint 47 | 48 | ## Obj-C/Swift specific 49 | *.hmap 50 | *.ipa 51 | *.dSYM.zip 52 | *.dSYM 53 | 54 | ## Playgrounds 55 | timeline.xctimeline 56 | playground.xcworkspace 57 | 58 | # Swift Package Manager 59 | # 60 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 61 | # Packages/ 62 | # Package.pins 63 | # Package.resolved 64 | .build/ 65 | 66 | # CocoaPods 67 | # 68 | # We recommend against adding the Pods directory to your .gitignore. However 69 | # you should judge for yourself, the pros and cons are mentioned at: 70 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 71 | # 72 | # Pods/ 73 | # 74 | # Add this line if you want to avoid checking in source code from the Xcode workspace 75 | # *.xcworkspace 76 | 77 | # Carthage 78 | # 79 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 80 | # Carthage/Checkouts 81 | 82 | Carthage/Build 83 | 84 | # Accio dependency management 85 | Dependencies/ 86 | .accio/ 87 | 88 | # fastlane 89 | # 90 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 91 | # screenshots whenever they are needed. 92 | # For more information about the recommended setup visit: 93 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 94 | 95 | fastlane/report.xml 96 | fastlane/Preview.html 97 | fastlane/screenshots/**/*.png 98 | fastlane/test_output 99 | 100 | # Code Injection 101 | # 102 | # After new code Injection tools there's a generated folder /iOSInjectionProject 103 | # https://github.com/johnno1962/injectionforxcode 104 | 105 | iOSInjectionProject/ 106 | 107 | .DS_Store -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/RequestTests.xcbaseline/3400B6A4-B231-4550-9C4C-2D5F1474250B.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | JsonTests 8 | 9 | testBuilderPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.0003 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | testMeasureBuilder() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 6.633e-05 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | testMeasureParse() 30 | 31 | com.apple.XCTPerformanceMetric_WallClockTime 32 | 33 | baselineAverage 34 | 9.35e-05 35 | baselineIntegrationDisplayName 36 | Local Baseline 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/RequestTests.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 3400B6A4-B231-4550-9C4C-2D5F1474250B 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 400 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 2300 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro15,2 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64h 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before you start writing any code, check to make sure someone else hasn't already started, and that it isn't part of the [Planned Features List](Resources/planned-features.md) 4 | 5 | > **Important:** Any code you contribute must belong to you, and will fall under the [MIT LICENSE](LICENSE) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Carson Katri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Request", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "Request", 13 | targets: ["Request"]), 14 | .library( 15 | name: "Json", 16 | targets: ["Json"]) 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 25 | .target( 26 | name: "Json", 27 | dependencies: []), 28 | .target( 29 | name: "Request", 30 | dependencies: ["Json"]), 31 | .testTarget( 32 | name: "RequestTests", 33 | dependencies: ["Request", "Json"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Request](Resources/banner.png) 2 | 3 | ![swift 5.1](https://img.shields.io/badge/swift-5.1-blue.svg) 4 | ![SwiftUI](https://img.shields.io/badge/-SwiftUI-blue.svg) 5 | ![iOS](https://img.shields.io/badge/os-iOS-green.svg) 6 | ![macOS](https://img.shields.io/badge/os-macOS-green.svg) 7 | ![tvOS](https://img.shields.io/badge/os-tvOS-green.svg) 8 | [![Build](https://github.com/carson-katri/swift-request/workflows/Build/badge.svg)](https://github.com/carson-katri/swift-request/actions) 9 | [![codecov](https://codecov.io/gh/carson-katri/swift-request/branch/master/graph/badge.svg)](https://codecov.io/gh/carson-katri/swift-request) 10 | 11 | [Installation](#installation) - [Getting Started](#getting-started) - [Building a Request](#building-a-request) - [Codable](#codable) - [Combine](#combine) - [How it Works](#how-it-works) - [Request Groups](#request-groups) - [Request Chains](#request-chains) - [Json](#json) - [Contributing](#contributing) - [License](#license) 12 | 13 | [Using with SwiftUI](Resources/swiftui.md) 14 | 15 | 16 | ## Installation 17 | `swift-request` can be installed via the `Swift Package Manager`. 18 | 19 | In Xcode 11, go to `File > Swift Packages > Add Package Dependency...`, then paste in `https://github.com/carson-katri/swift-request` 20 | 21 | Now just `import Request`, and you're ready to [Get Started](#getting-started) 22 | 23 | 24 | ## Getting Started 25 | The old way: 26 | ```swift 27 | var request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos")!) 28 | request.addValue("application/json", forHTTPHeaderField: "Accept") 29 | let task = URLSession.shared.dataTask(with: url!) { (data, res, err) in 30 | if let data = data { 31 | ... 32 | } else if let error = error { 33 | ... 34 | } 35 | } 36 | task.resume() 37 | ``` 38 | The *declarative* way: 39 | ```swift 40 | Request { 41 | Url("https://jsonplaceholder.typicode.com/todo") 42 | Header.Accept(.json) 43 | } 44 | .onData { data in 45 | ... 46 | } 47 | .onError { error in 48 | ... 49 | } 50 | .call() 51 | ``` 52 | The benefit of declaring requests becomes abundantly clear when your data becomes more complex: 53 | ```swift 54 | Request { 55 | Url("https://jsonplaceholder.typicode.com/posts") 56 | Method(.post) 57 | Header.ContentType(.json) 58 | Body(Json([ 59 | "title": "foo", 60 | "body": "bar", 61 | "usedId": 1 62 | ]).stringified) 63 | } 64 | ``` 65 | Once you've built your `Request`, you can specify the response handlers you want to use. 66 | `.onData`, `.onString`, `.onJson`, and `.onError` are available. 67 | You can chain them together to handle multiple response types, as they return a modified version of the `Request`. 68 | 69 | To perform the `Request`, just use `.call()`. This will run the `Request`, and give you the response when complete. 70 | 71 | `Request` also conforms to `Publisher`, so you can manipulate it like any other Combine publisher ([read more](#combine)): 72 | ```swift 73 | let cancellable = Request { 74 | Url("https://jsonplaceholder.typicode.com/todo") 75 | Header.Accept(.json) 76 | } 77 | .sink(receiveCompletion: { ... }, receiveValue: { ... }) 78 | ``` 79 | 80 | ## Building a Request 81 | There are many different tools available to build a `Request`: 82 | - `Url` 83 | 84 | Exactly one must be present in each `Request` 85 | ```swift 86 | Url("https://example.com") 87 | Url(protocol: .secure, url: "example.com") 88 | ``` 89 | - `Method` 90 | 91 | Sets the `MethodType` of the `Request` (`.get` by default) 92 | ```swift 93 | Method(.get) // Available: .get, .head, .post, .put, .delete, .connect, .options, .trace, and .patch 94 | ``` 95 | - `Header` 96 | 97 | Sets an HTTP header field 98 | ```swift 99 | Header.Any(key: "Custom-Header", value: "value123") 100 | Header.Accept(.json) 101 | Header.Authorization(.basic(username: "carsonkatri", password: "password123")) 102 | Header.CacheControl(.noCache) 103 | Header.ContentLength(16) 104 | Header.ContentType(.xml) 105 | Header.Host("en.example.com", port: "8000") 106 | Header.Origin("www.example.com") 107 | Header.Referer("redirectfrom.example.com") 108 | Header.UserAgent(.firefoxMac) 109 | ``` 110 | - `Query` 111 | 112 | Creates the query string 113 | ```swift 114 | Query(["key": "value"]) // ?key=value 115 | ``` 116 | - `Body` 117 | 118 | Sets the request body 119 | ```swift 120 | Body(["key": "value"]) 121 | Body("myBodyContent") 122 | Body(myJson) 123 | ``` 124 | - `Timeout` 125 | 126 | Sets the timeout for a request or resource: 127 | ```swift 128 | Timeout(60) 129 | Timeout(60, for: .request) 130 | Timeout(30, for: .resource) 131 | ``` 132 | - `RequestParam` 133 | 134 | Add a param directly 135 | > **Important:** You must create the logic to handle a custom `RequestParam`. You may also consider adding a case to `RequestParamType`. If you think your custom parameter may be useful for others, see [Contributing](#contributing) 136 | 137 | 138 | ## Codable 139 | Let's look at an example. Here we define our data: 140 | ```swift 141 | struct Todo: Codable { 142 | let title: String 143 | let completed: Bool 144 | let id: Int 145 | let userId: Int 146 | } 147 | ``` 148 | Now we can use `AnyRequest` to pull an array of `Todo`s from the server: 149 | ```swift 150 | AnyRequest<[Todo]> { 151 | Url("https://jsonplaceholder.typicode.com/todos") 152 | } 153 | .onObject { todos in ... } 154 | ``` 155 | In this case, `onObject` gives us `[Todo]?` in response. It's that easy to get data and decode it. 156 | 157 | `Request` is built on `AnyRequest`, so they support all of the same parameters. 158 | 159 | > If you use `onObject` on a standard `Request`, you will receive `Data` in response. 160 | 161 | ## Combine 162 | `Request` and `RequestGroup` both conform to `Publisher`: 163 | ```swift 164 | Request { 165 | Url("https://jsonplaceholder.typicode.com/todos") 166 | } 167 | .sink(receiveCompletion: { ... }, receiveValue: { ... }) 168 | 169 | RequestGroup { 170 | Request { 171 | Url("https://jsonplaceholder.typicode.com/todos") 172 | } 173 | Request { 174 | Url("https://jsonplaceholder.typicode.com/posts") 175 | } 176 | Request { 177 | Url("https://jsonplaceholder.typicode.com/todos/1") 178 | } 179 | } 180 | .sink(receiveCompletion: { ... }, receiveValue: { ... }) 181 | ``` 182 | `Request` publishes the result using `URLSession.DataTaskPublisher`. `RequestGroup` collects the result of each `Request` in its body, and publishes the array of results. 183 | 184 | You can use all of the Combine operators you'd expect on `Request`: 185 | ```swift 186 | Request { 187 | Url("https://jsonplaceholder.typicode.com/todos") 188 | } 189 | .map(\.data) 190 | .decode([Todo].self, decoder: JSONDecoder()) 191 | .sink(receiveCompletion: { ... }, receiveValue: { ... }) 192 | ``` 193 | However, `Request` also comes with several convenience `Publishers` to simplify the process of decoding: 194 | 195 | 1. `objectPublisher` - Decodes the data of an `AnyRequest` using `JSONDecoder` 196 | 2. `stringPublisher` - Decodes the data to a `String` 197 | 3. `jsonPublisher` - Converts the result to a `Json` object 198 | 199 | Here's an example of using `objectPublisher`: 200 | ```swift 201 | AnyRequest<[Todo]> { 202 | Url("https://jsonplaceholder.typicode.com/todos") 203 | } 204 | .objectPublisher 205 | .sink(receiveCompletion: { ... }, receiveValue: { ... }) 206 | ``` 207 | This removes the need to constantly use `.map.decode` to extract the desired `Codable` result. 208 | 209 | To handle errors, you can use the `receiveCompletion` handler in `sink`: 210 | ```swift 211 | Request { 212 | Url("https://jsonplaceholder.typicode.com/todos") 213 | } 214 | .sink(receiveCompletion: { res in 215 | switch res { 216 | case let .failure(err): 217 | // Handle `err` 218 | case .finished: break 219 | } 220 | }, receiveValue: { ... }) 221 | ``` 222 | 223 | ## How it Works 224 | The body of the `Request` is built using the `RequestBuilder` `@resultBuilder`. 225 | 226 | It merges each `RequestParam` in the body into one `CombinedParam` object. This contains all the other params as children. 227 | 228 | When you run `.call()`, the children are filtered to find the `Url`, and any other optional parameters that may have been included. 229 | 230 | For more information, see [RequestBuilder.swift](Sources/Request/Request/RequestBuilder.swift) and [Request.swift](Sources/Request/Request/Request.swift) 231 | 232 | 233 | ## Request Groups 234 | `RequestGroup` can be used to run multiple `Request`s *simulataneously*. You get a response when each `Request` completes (or fails) 235 | ```swift 236 | RequestGroup { 237 | Request { 238 | Url("https://jsonplaceholder.typicode.com/todos") 239 | } 240 | Request { 241 | Url("https://jsonplaceholder.typicode.com/posts") 242 | } 243 | Request { 244 | Url("https://jsonplaceholder.typicode.com/todos/1") 245 | } 246 | } 247 | .onData { (index, data) in 248 | ... 249 | } 250 | .call() 251 | ``` 252 | 253 | 254 | ## Request Chains 255 | `RequestChain` is used to run multiple `Request`s *one at a time*. When one completes, it passes its data on to the next `Request`, so you can use it to build the `Request`. 256 | 257 | `RequestChain.call` can optionally accept a callback that gives you all the data of every `Request` when completed. 258 | 259 | > **Note:** You must use `Request.chained` to build your `Request`. This gives you access to the data and errors of previous `Request`s. 260 | ```swift 261 | RequestChain { 262 | Request.chained { (data, errors) in 263 | Url("https://jsonplaceholder.typicode.com/todos") 264 | } 265 | Request.chained { (data, errors) in 266 | let json = Json(data[0]!) 267 | return Url("https://jsonplaceholder.typicode.com/todos/\(json?[0]["id"].int ?? 0)") 268 | } 269 | } 270 | .call { (data, errors) in 271 | ... 272 | } 273 | ``` 274 | 275 | ## Repeated Calls 276 | `.update` is used to run additional calls after the initial one. You can pass it either a number or a custom `Publisher`. You can also chain together multiple `.update`s. The two `.update`s in the following example are equivalent, so the end result is that the `Request` will be called once immediately and twice every 10 seconds thereafter. 277 | ```swift 278 | Request { 279 | Url("https://jsonplaceholder.typicode.com/todo") 280 | } 281 | .update(every: 10) 282 | .update(publisher: Timer.publish(every: 10, on: .main, in: .common).autoconnect()) 283 | .call() 284 | ``` 285 | 286 | If you want to use `Request` as a `Publisher`, use `updatePublisher`: 287 | ```swift 288 | Request { 289 | Url("https://jsonplaceholder.typicode.com/todo") 290 | } 291 | .updatePublisher(every: 10) 292 | .updatePublisher(publisher: ...) 293 | .sink(receiveCompletion: { ... }, receiveValue: { ... }) 294 | ``` 295 | Unlike `update`, `updatePublisher` does not send a value immediately, but will wait for the first value from the `Publisher`. 296 | 297 | ## Json 298 | `swift-request` includes support for `Json`. 299 | `Json` is used as the response type in the `onJson` callback on a `Request` object. 300 | 301 | You can create `Json` by parsing a `String` or `Data`: 302 | ```swift 303 | Json("{\"firstName\":\"Carson\"}") 304 | Json("{\"firstName\":\"Carson\"}".data(using: .utf8)) 305 | ``` 306 | You can subscript `Json` as you would expect: 307 | ```swift 308 | myJson["firstName"].string // "Carson" 309 | myComplexJson[0]["nestedJson"]["id"].int 310 | ``` 311 | It also supports `dynamicMemberLookup`, so you can subscript it like so: 312 | ```swift 313 | myJson.firstName.string // "Carson" 314 | myComplexJson[0].nestedJson.id.int 315 | ``` 316 | 317 | You can use `.string`, `.int`, `.double`, `.bool`, and `.array` to retrieve values in a desired type. 318 | > **Note:** These return **non-optional** values. If you want to check for `nil`, you can use `.stringOptional`, `.intOptional`, etc. 319 | 320 | ## Contributing 321 | See [CONTRIBUTING](CONTRIBUTING.md) 322 | 323 | 324 | ## License 325 | See [LICENSE](LICENSE) 326 | -------------------------------------------------------------------------------- /Request.xcodeproj/Json_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Request.xcodeproj/RequestTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Request.xcodeproj/Request_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Request.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = "1"; 4 | objectVersion = "46"; 5 | objects = { 6 | "OBJ_1" = { 7 | isa = "PBXProject"; 8 | attributes = { 9 | LastSwiftMigration = "9999"; 10 | LastUpgradeCheck = "9999"; 11 | }; 12 | buildConfigurationList = "OBJ_2"; 13 | compatibilityVersion = "Xcode 3.2"; 14 | developmentRegion = "en"; 15 | hasScannedForEncodings = "0"; 16 | knownRegions = ( 17 | "en" 18 | ); 19 | mainGroup = "OBJ_5"; 20 | productRefGroup = "OBJ_45"; 21 | projectDirPath = "."; 22 | targets = ( 23 | "Request::Json", 24 | "Request::Request", 25 | "Request::SwiftPMPackageDescription", 26 | "Request::RequestPackageTests::ProductTarget", 27 | "Request::RequestTests" 28 | ); 29 | }; 30 | "OBJ_10" = { 31 | isa = "PBXFileReference"; 32 | path = "JsonBuilder.swift"; 33 | sourceTree = ""; 34 | }; 35 | "OBJ_100" = { 36 | isa = "PBXTargetDependency"; 37 | target = "Request::RequestTests"; 38 | }; 39 | "OBJ_102" = { 40 | isa = "XCConfigurationList"; 41 | buildConfigurations = ( 42 | "OBJ_103", 43 | "OBJ_104" 44 | ); 45 | defaultConfigurationIsVisible = "0"; 46 | defaultConfigurationName = "Release"; 47 | }; 48 | "OBJ_103" = { 49 | isa = "XCBuildConfiguration"; 50 | buildSettings = { 51 | CLANG_ENABLE_MODULES = "YES"; 52 | EMBEDDED_CONTENT_CONTAINS_SWIFT = "YES"; 53 | FRAMEWORK_SEARCH_PATHS = ( 54 | "$(inherited)", 55 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 56 | ); 57 | HEADER_SEARCH_PATHS = ( 58 | "$(inherited)" 59 | ); 60 | INFOPLIST_FILE = "Request.xcodeproj/RequestTests_Info.plist"; 61 | IPHONEOS_DEPLOYMENT_TARGET = "13.0"; 62 | LD_RUNPATH_SEARCH_PATHS = ( 63 | "$(inherited)", 64 | "@loader_path/../Frameworks", 65 | "@loader_path/Frameworks" 66 | ); 67 | MACOSX_DEPLOYMENT_TARGET = "10.15"; 68 | OTHER_CFLAGS = ( 69 | "$(inherited)" 70 | ); 71 | OTHER_LDFLAGS = ( 72 | "$(inherited)" 73 | ); 74 | OTHER_SWIFT_FLAGS = ( 75 | "$(inherited)" 76 | ); 77 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 78 | "$(inherited)" 79 | ); 80 | SWIFT_VERSION = "5.0"; 81 | TARGET_NAME = "RequestTests"; 82 | TVOS_DEPLOYMENT_TARGET = "13.0"; 83 | WATCHOS_DEPLOYMENT_TARGET = "6.0"; 84 | }; 85 | name = "Debug"; 86 | }; 87 | "OBJ_104" = { 88 | isa = "XCBuildConfiguration"; 89 | buildSettings = { 90 | CLANG_ENABLE_MODULES = "YES"; 91 | EMBEDDED_CONTENT_CONTAINS_SWIFT = "YES"; 92 | FRAMEWORK_SEARCH_PATHS = ( 93 | "$(inherited)", 94 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 95 | ); 96 | HEADER_SEARCH_PATHS = ( 97 | "$(inherited)" 98 | ); 99 | INFOPLIST_FILE = "Request.xcodeproj/RequestTests_Info.plist"; 100 | IPHONEOS_DEPLOYMENT_TARGET = "13.0"; 101 | LD_RUNPATH_SEARCH_PATHS = ( 102 | "$(inherited)", 103 | "@loader_path/../Frameworks", 104 | "@loader_path/Frameworks" 105 | ); 106 | MACOSX_DEPLOYMENT_TARGET = "10.15"; 107 | OTHER_CFLAGS = ( 108 | "$(inherited)" 109 | ); 110 | OTHER_LDFLAGS = ( 111 | "$(inherited)" 112 | ); 113 | OTHER_SWIFT_FLAGS = ( 114 | "$(inherited)" 115 | ); 116 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 117 | "$(inherited)" 118 | ); 119 | SWIFT_VERSION = "5.0"; 120 | TARGET_NAME = "RequestTests"; 121 | TVOS_DEPLOYMENT_TARGET = "13.0"; 122 | WATCHOS_DEPLOYMENT_TARGET = "6.0"; 123 | }; 124 | name = "Release"; 125 | }; 126 | "OBJ_105" = { 127 | isa = "PBXSourcesBuildPhase"; 128 | files = ( 129 | "OBJ_106", 130 | "OBJ_107", 131 | "OBJ_108" 132 | ); 133 | }; 134 | "OBJ_106" = { 135 | isa = "PBXBuildFile"; 136 | fileRef = "OBJ_42"; 137 | }; 138 | "OBJ_107" = { 139 | isa = "PBXBuildFile"; 140 | fileRef = "OBJ_43"; 141 | }; 142 | "OBJ_108" = { 143 | isa = "PBXBuildFile"; 144 | fileRef = "OBJ_44"; 145 | }; 146 | "OBJ_109" = { 147 | isa = "PBXFrameworksBuildPhase"; 148 | files = ( 149 | "OBJ_110", 150 | "OBJ_111" 151 | ); 152 | }; 153 | "OBJ_11" = { 154 | isa = "PBXGroup"; 155 | children = ( 156 | "OBJ_12", 157 | "OBJ_18", 158 | "OBJ_35" 159 | ); 160 | name = "Request"; 161 | path = "Sources/Request"; 162 | sourceTree = "SOURCE_ROOT"; 163 | }; 164 | "OBJ_110" = { 165 | isa = "PBXBuildFile"; 166 | fileRef = "Request::Request::Product"; 167 | }; 168 | "OBJ_111" = { 169 | isa = "PBXBuildFile"; 170 | fileRef = "Request::Json::Product"; 171 | }; 172 | "OBJ_112" = { 173 | isa = "PBXTargetDependency"; 174 | target = "Request::Request"; 175 | }; 176 | "OBJ_113" = { 177 | isa = "PBXTargetDependency"; 178 | target = "Request::Json"; 179 | }; 180 | "OBJ_12" = { 181 | isa = "PBXGroup"; 182 | children = ( 183 | "OBJ_13", 184 | "OBJ_14", 185 | "OBJ_15", 186 | "OBJ_16", 187 | "OBJ_17" 188 | ); 189 | name = "Helpers"; 190 | path = "Helpers"; 191 | sourceTree = ""; 192 | }; 193 | "OBJ_13" = { 194 | isa = "PBXFileReference"; 195 | path = "Auth.swift"; 196 | sourceTree = ""; 197 | }; 198 | "OBJ_14" = { 199 | isa = "PBXFileReference"; 200 | path = "CacheType.swift"; 201 | sourceTree = ""; 202 | }; 203 | "OBJ_15" = { 204 | isa = "PBXFileReference"; 205 | path = "MediaType.swift"; 206 | sourceTree = ""; 207 | }; 208 | "OBJ_16" = { 209 | isa = "PBXFileReference"; 210 | path = "RequestError.swift"; 211 | sourceTree = ""; 212 | }; 213 | "OBJ_17" = { 214 | isa = "PBXFileReference"; 215 | path = "UserAgent.swift"; 216 | sourceTree = ""; 217 | }; 218 | "OBJ_18" = { 219 | isa = "PBXGroup"; 220 | children = ( 221 | "OBJ_19", 222 | "OBJ_22", 223 | "OBJ_23", 224 | "OBJ_24", 225 | "OBJ_34" 226 | ); 227 | name = "Request"; 228 | path = "Request"; 229 | sourceTree = ""; 230 | }; 231 | "OBJ_19" = { 232 | isa = "PBXGroup"; 233 | children = ( 234 | "OBJ_20", 235 | "OBJ_21" 236 | ); 237 | name = "Extra"; 238 | path = "Extra"; 239 | sourceTree = ""; 240 | }; 241 | "OBJ_2" = { 242 | isa = "XCConfigurationList"; 243 | buildConfigurations = ( 244 | "OBJ_3", 245 | "OBJ_4" 246 | ); 247 | defaultConfigurationIsVisible = "0"; 248 | defaultConfigurationName = "Release"; 249 | }; 250 | "OBJ_20" = { 251 | isa = "PBXFileReference"; 252 | path = "RequestChain.swift"; 253 | sourceTree = ""; 254 | }; 255 | "OBJ_21" = { 256 | isa = "PBXFileReference"; 257 | path = "RequestGroup.swift"; 258 | sourceTree = ""; 259 | }; 260 | "OBJ_22" = { 261 | isa = "PBXFileReference"; 262 | path = "Request.swift"; 263 | sourceTree = ""; 264 | }; 265 | "OBJ_23" = { 266 | isa = "PBXFileReference"; 267 | path = "RequestBuilder.swift"; 268 | sourceTree = ""; 269 | }; 270 | "OBJ_24" = { 271 | isa = "PBXGroup"; 272 | children = ( 273 | "OBJ_25", 274 | "OBJ_26", 275 | "OBJ_27", 276 | "OBJ_28", 277 | "OBJ_29", 278 | "OBJ_30", 279 | "OBJ_31" 280 | ); 281 | name = "RequestParams"; 282 | path = "RequestParams"; 283 | sourceTree = ""; 284 | }; 285 | "OBJ_25" = { 286 | isa = "PBXFileReference"; 287 | path = "Body.swift"; 288 | sourceTree = ""; 289 | }; 290 | "OBJ_26" = { 291 | isa = "PBXFileReference"; 292 | path = "Header.swift"; 293 | sourceTree = ""; 294 | }; 295 | "OBJ_27" = { 296 | isa = "PBXFileReference"; 297 | path = "Headers.swift"; 298 | sourceTree = ""; 299 | }; 300 | "OBJ_28" = { 301 | isa = "PBXFileReference"; 302 | path = "Method.swift"; 303 | sourceTree = ""; 304 | }; 305 | "OBJ_29" = { 306 | isa = "PBXFileReference"; 307 | path = "QueryParam.swift"; 308 | sourceTree = ""; 309 | }; 310 | "OBJ_3" = { 311 | isa = "XCBuildConfiguration"; 312 | buildSettings = { 313 | CLANG_ENABLE_OBJC_ARC = "YES"; 314 | COMBINE_HIDPI_IMAGES = "YES"; 315 | COPY_PHASE_STRIP = "NO"; 316 | DEBUG_INFORMATION_FORMAT = "dwarf"; 317 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 318 | ENABLE_NS_ASSERTIONS = "YES"; 319 | GCC_OPTIMIZATION_LEVEL = "0"; 320 | GCC_PREPROCESSOR_DEFINITIONS = ( 321 | "$(inherited)", 322 | "SWIFT_PACKAGE=1", 323 | "DEBUG=1" 324 | ); 325 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 326 | ONLY_ACTIVE_ARCH = "YES"; 327 | OTHER_SWIFT_FLAGS = ( 328 | "$(inherited)", 329 | "-DXcode" 330 | ); 331 | PRODUCT_NAME = "$(TARGET_NAME)"; 332 | SDKROOT = "macosx"; 333 | SUPPORTED_PLATFORMS = ( 334 | "macosx", 335 | "iphoneos", 336 | "iphonesimulator", 337 | "appletvos", 338 | "appletvsimulator", 339 | "watchos", 340 | "watchsimulator" 341 | ); 342 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 343 | "$(inherited)", 344 | "SWIFT_PACKAGE", 345 | "DEBUG" 346 | ); 347 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 348 | USE_HEADERMAP = "NO"; 349 | }; 350 | name = "Debug"; 351 | }; 352 | "OBJ_30" = { 353 | isa = "PBXFileReference"; 354 | path = "RequestParam.swift"; 355 | sourceTree = ""; 356 | }; 357 | "OBJ_31" = { 358 | isa = "PBXGroup"; 359 | children = ( 360 | "OBJ_32", 361 | "OBJ_33" 362 | ); 363 | name = "Url"; 364 | path = "Url"; 365 | sourceTree = ""; 366 | }; 367 | "OBJ_32" = { 368 | isa = "PBXFileReference"; 369 | path = "ProtocolType.swift"; 370 | sourceTree = ""; 371 | }; 372 | "OBJ_33" = { 373 | isa = "PBXFileReference"; 374 | path = "Url.swift"; 375 | sourceTree = ""; 376 | }; 377 | "OBJ_34" = { 378 | isa = "PBXFileReference"; 379 | path = "Response.swift"; 380 | sourceTree = ""; 381 | }; 382 | "OBJ_35" = { 383 | isa = "PBXGroup"; 384 | children = ( 385 | "OBJ_36", 386 | "OBJ_38" 387 | ); 388 | name = "SwiftUI"; 389 | path = "SwiftUI"; 390 | sourceTree = ""; 391 | }; 392 | "OBJ_36" = { 393 | isa = "PBXGroup"; 394 | children = ( 395 | "OBJ_37" 396 | ); 397 | name = "RequestImage"; 398 | path = "RequestImage"; 399 | sourceTree = ""; 400 | }; 401 | "OBJ_37" = { 402 | isa = "PBXFileReference"; 403 | path = "RequestImage.swift"; 404 | sourceTree = ""; 405 | }; 406 | "OBJ_38" = { 407 | isa = "PBXGroup"; 408 | children = ( 409 | "OBJ_39" 410 | ); 411 | name = "RequestView"; 412 | path = "RequestView"; 413 | sourceTree = ""; 414 | }; 415 | "OBJ_39" = { 416 | isa = "PBXFileReference"; 417 | path = "RequestView.swift"; 418 | sourceTree = ""; 419 | }; 420 | "OBJ_4" = { 421 | isa = "XCBuildConfiguration"; 422 | buildSettings = { 423 | CLANG_ENABLE_OBJC_ARC = "YES"; 424 | COMBINE_HIDPI_IMAGES = "YES"; 425 | COPY_PHASE_STRIP = "YES"; 426 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 427 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 428 | GCC_OPTIMIZATION_LEVEL = "s"; 429 | GCC_PREPROCESSOR_DEFINITIONS = ( 430 | "$(inherited)", 431 | "SWIFT_PACKAGE=1" 432 | ); 433 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 434 | OTHER_SWIFT_FLAGS = ( 435 | "$(inherited)", 436 | "-DXcode" 437 | ); 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SDKROOT = "macosx"; 440 | SUPPORTED_PLATFORMS = ( 441 | "macosx", 442 | "iphoneos", 443 | "iphonesimulator", 444 | "appletvos", 445 | "appletvsimulator", 446 | "watchos", 447 | "watchsimulator" 448 | ); 449 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 450 | "$(inherited)", 451 | "SWIFT_PACKAGE" 452 | ); 453 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 454 | USE_HEADERMAP = "NO"; 455 | }; 456 | name = "Release"; 457 | }; 458 | "OBJ_40" = { 459 | isa = "PBXGroup"; 460 | children = ( 461 | "OBJ_41" 462 | ); 463 | name = "Tests"; 464 | path = ""; 465 | sourceTree = "SOURCE_ROOT"; 466 | }; 467 | "OBJ_41" = { 468 | isa = "PBXGroup"; 469 | children = ( 470 | "OBJ_42", 471 | "OBJ_43", 472 | "OBJ_44" 473 | ); 474 | name = "RequestTests"; 475 | path = "Tests/RequestTests"; 476 | sourceTree = "SOURCE_ROOT"; 477 | }; 478 | "OBJ_42" = { 479 | isa = "PBXFileReference"; 480 | path = "JsonTests.swift"; 481 | sourceTree = ""; 482 | }; 483 | "OBJ_43" = { 484 | isa = "PBXFileReference"; 485 | path = "RequestTests.swift"; 486 | sourceTree = ""; 487 | }; 488 | "OBJ_44" = { 489 | isa = "PBXFileReference"; 490 | path = "XCTestManifests.swift"; 491 | sourceTree = ""; 492 | }; 493 | "OBJ_45" = { 494 | isa = "PBXGroup"; 495 | children = ( 496 | "Request::Request::Product", 497 | "Request::Json::Product", 498 | "Request::RequestTests::Product" 499 | ); 500 | name = "Products"; 501 | path = ""; 502 | sourceTree = "BUILT_PRODUCTS_DIR"; 503 | }; 504 | "OBJ_49" = { 505 | isa = "PBXFileReference"; 506 | path = "Resources"; 507 | sourceTree = "SOURCE_ROOT"; 508 | }; 509 | "OBJ_5" = { 510 | isa = "PBXGroup"; 511 | children = ( 512 | "OBJ_6", 513 | "OBJ_7", 514 | "OBJ_40", 515 | "OBJ_45", 516 | "OBJ_49", 517 | "OBJ_50", 518 | "OBJ_51", 519 | "OBJ_52", 520 | "OBJ_53" 521 | ); 522 | path = ""; 523 | sourceTree = ""; 524 | }; 525 | "OBJ_50" = { 526 | isa = "PBXFileReference"; 527 | path = "build"; 528 | sourceTree = "SOURCE_ROOT"; 529 | }; 530 | "OBJ_51" = { 531 | isa = "PBXFileReference"; 532 | path = "LICENSE"; 533 | sourceTree = ""; 534 | }; 535 | "OBJ_52" = { 536 | isa = "PBXFileReference"; 537 | path = "README.md"; 538 | sourceTree = ""; 539 | }; 540 | "OBJ_53" = { 541 | isa = "PBXFileReference"; 542 | path = "CONTRIBUTING.md"; 543 | sourceTree = ""; 544 | }; 545 | "OBJ_55" = { 546 | isa = "XCConfigurationList"; 547 | buildConfigurations = ( 548 | "OBJ_56", 549 | "OBJ_57" 550 | ); 551 | defaultConfigurationIsVisible = "0"; 552 | defaultConfigurationName = "Release"; 553 | }; 554 | "OBJ_56" = { 555 | isa = "XCBuildConfiguration"; 556 | buildSettings = { 557 | ENABLE_TESTABILITY = "YES"; 558 | FRAMEWORK_SEARCH_PATHS = ( 559 | "$(inherited)", 560 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 561 | ); 562 | HEADER_SEARCH_PATHS = ( 563 | "$(inherited)" 564 | ); 565 | INFOPLIST_FILE = "Request.xcodeproj/Json_Info.plist"; 566 | IPHONEOS_DEPLOYMENT_TARGET = "13.0"; 567 | LD_RUNPATH_SEARCH_PATHS = ( 568 | "$(inherited)", 569 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 570 | ); 571 | MACOSX_DEPLOYMENT_TARGET = "10.15"; 572 | OTHER_CFLAGS = ( 573 | "$(inherited)" 574 | ); 575 | OTHER_LDFLAGS = ( 576 | "$(inherited)" 577 | ); 578 | OTHER_SWIFT_FLAGS = ( 579 | "$(inherited)" 580 | ); 581 | PRODUCT_BUNDLE_IDENTIFIER = "Json"; 582 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 583 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 584 | SKIP_INSTALL = "YES"; 585 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 586 | "$(inherited)" 587 | ); 588 | SWIFT_VERSION = "5.0"; 589 | TARGET_NAME = "Json"; 590 | TVOS_DEPLOYMENT_TARGET = "13.0"; 591 | WATCHOS_DEPLOYMENT_TARGET = "6.0"; 592 | }; 593 | name = "Debug"; 594 | }; 595 | "OBJ_57" = { 596 | isa = "XCBuildConfiguration"; 597 | buildSettings = { 598 | ENABLE_TESTABILITY = "YES"; 599 | FRAMEWORK_SEARCH_PATHS = ( 600 | "$(inherited)", 601 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 602 | ); 603 | HEADER_SEARCH_PATHS = ( 604 | "$(inherited)" 605 | ); 606 | INFOPLIST_FILE = "Request.xcodeproj/Json_Info.plist"; 607 | IPHONEOS_DEPLOYMENT_TARGET = "13.0"; 608 | LD_RUNPATH_SEARCH_PATHS = ( 609 | "$(inherited)", 610 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 611 | ); 612 | MACOSX_DEPLOYMENT_TARGET = "10.15"; 613 | OTHER_CFLAGS = ( 614 | "$(inherited)" 615 | ); 616 | OTHER_LDFLAGS = ( 617 | "$(inherited)" 618 | ); 619 | OTHER_SWIFT_FLAGS = ( 620 | "$(inherited)" 621 | ); 622 | PRODUCT_BUNDLE_IDENTIFIER = "Json"; 623 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 624 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 625 | SKIP_INSTALL = "YES"; 626 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 627 | "$(inherited)" 628 | ); 629 | SWIFT_VERSION = "5.0"; 630 | TARGET_NAME = "Json"; 631 | TVOS_DEPLOYMENT_TARGET = "13.0"; 632 | WATCHOS_DEPLOYMENT_TARGET = "6.0"; 633 | }; 634 | name = "Release"; 635 | }; 636 | "OBJ_58" = { 637 | isa = "PBXSourcesBuildPhase"; 638 | files = ( 639 | "OBJ_59", 640 | "OBJ_60" 641 | ); 642 | }; 643 | "OBJ_59" = { 644 | isa = "PBXBuildFile"; 645 | fileRef = "OBJ_9"; 646 | }; 647 | "OBJ_6" = { 648 | isa = "PBXFileReference"; 649 | explicitFileType = "sourcecode.swift"; 650 | path = "Package.swift"; 651 | sourceTree = ""; 652 | }; 653 | "OBJ_60" = { 654 | isa = "PBXBuildFile"; 655 | fileRef = "OBJ_10"; 656 | }; 657 | "OBJ_61" = { 658 | isa = "PBXFrameworksBuildPhase"; 659 | files = ( 660 | ); 661 | }; 662 | "OBJ_63" = { 663 | isa = "XCConfigurationList"; 664 | buildConfigurations = ( 665 | "OBJ_64", 666 | "OBJ_65" 667 | ); 668 | defaultConfigurationIsVisible = "0"; 669 | defaultConfigurationName = "Release"; 670 | }; 671 | "OBJ_64" = { 672 | isa = "XCBuildConfiguration"; 673 | buildSettings = { 674 | ENABLE_TESTABILITY = "YES"; 675 | FRAMEWORK_SEARCH_PATHS = ( 676 | "$(inherited)", 677 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 678 | ); 679 | HEADER_SEARCH_PATHS = ( 680 | "$(inherited)" 681 | ); 682 | INFOPLIST_FILE = "Request.xcodeproj/Request_Info.plist"; 683 | IPHONEOS_DEPLOYMENT_TARGET = "13.0"; 684 | LD_RUNPATH_SEARCH_PATHS = ( 685 | "$(inherited)", 686 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 687 | ); 688 | MACOSX_DEPLOYMENT_TARGET = "10.15"; 689 | OTHER_CFLAGS = ( 690 | "$(inherited)" 691 | ); 692 | OTHER_LDFLAGS = ( 693 | "$(inherited)" 694 | ); 695 | OTHER_SWIFT_FLAGS = ( 696 | "$(inherited)" 697 | ); 698 | PRODUCT_BUNDLE_IDENTIFIER = "Request"; 699 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 700 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 701 | SKIP_INSTALL = "YES"; 702 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 703 | "$(inherited)" 704 | ); 705 | SWIFT_VERSION = "5.0"; 706 | TARGET_NAME = "Request"; 707 | TVOS_DEPLOYMENT_TARGET = "13.0"; 708 | WATCHOS_DEPLOYMENT_TARGET = "6.0"; 709 | }; 710 | name = "Debug"; 711 | }; 712 | "OBJ_65" = { 713 | isa = "XCBuildConfiguration"; 714 | buildSettings = { 715 | ENABLE_TESTABILITY = "YES"; 716 | FRAMEWORK_SEARCH_PATHS = ( 717 | "$(inherited)", 718 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 719 | ); 720 | HEADER_SEARCH_PATHS = ( 721 | "$(inherited)" 722 | ); 723 | INFOPLIST_FILE = "Request.xcodeproj/Request_Info.plist"; 724 | IPHONEOS_DEPLOYMENT_TARGET = "13.0"; 725 | LD_RUNPATH_SEARCH_PATHS = ( 726 | "$(inherited)", 727 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 728 | ); 729 | MACOSX_DEPLOYMENT_TARGET = "10.15"; 730 | OTHER_CFLAGS = ( 731 | "$(inherited)" 732 | ); 733 | OTHER_LDFLAGS = ( 734 | "$(inherited)" 735 | ); 736 | OTHER_SWIFT_FLAGS = ( 737 | "$(inherited)" 738 | ); 739 | PRODUCT_BUNDLE_IDENTIFIER = "Request"; 740 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 741 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 742 | SKIP_INSTALL = "YES"; 743 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 744 | "$(inherited)" 745 | ); 746 | SWIFT_VERSION = "5.0"; 747 | TARGET_NAME = "Request"; 748 | TVOS_DEPLOYMENT_TARGET = "13.0"; 749 | WATCHOS_DEPLOYMENT_TARGET = "6.0"; 750 | }; 751 | name = "Release"; 752 | }; 753 | "OBJ_66" = { 754 | isa = "PBXSourcesBuildPhase"; 755 | files = ( 756 | "OBJ_67", 757 | "OBJ_68", 758 | "OBJ_69", 759 | "OBJ_70", 760 | "OBJ_71", 761 | "OBJ_72", 762 | "OBJ_73", 763 | "OBJ_74", 764 | "OBJ_75", 765 | "OBJ_76", 766 | "OBJ_77", 767 | "OBJ_78", 768 | "OBJ_79", 769 | "OBJ_80", 770 | "OBJ_81", 771 | "OBJ_82", 772 | "OBJ_83", 773 | "OBJ_84", 774 | "OBJ_85", 775 | "OBJ_86" 776 | ); 777 | }; 778 | "OBJ_67" = { 779 | isa = "PBXBuildFile"; 780 | fileRef = "OBJ_13"; 781 | }; 782 | "OBJ_68" = { 783 | isa = "PBXBuildFile"; 784 | fileRef = "OBJ_14"; 785 | }; 786 | "OBJ_69" = { 787 | isa = "PBXBuildFile"; 788 | fileRef = "OBJ_15"; 789 | }; 790 | "OBJ_7" = { 791 | isa = "PBXGroup"; 792 | children = ( 793 | "OBJ_8", 794 | "OBJ_11" 795 | ); 796 | name = "Sources"; 797 | path = ""; 798 | sourceTree = "SOURCE_ROOT"; 799 | }; 800 | "OBJ_70" = { 801 | isa = "PBXBuildFile"; 802 | fileRef = "OBJ_16"; 803 | }; 804 | "OBJ_71" = { 805 | isa = "PBXBuildFile"; 806 | fileRef = "OBJ_17"; 807 | }; 808 | "OBJ_72" = { 809 | isa = "PBXBuildFile"; 810 | fileRef = "OBJ_20"; 811 | }; 812 | "OBJ_73" = { 813 | isa = "PBXBuildFile"; 814 | fileRef = "OBJ_21"; 815 | }; 816 | "OBJ_74" = { 817 | isa = "PBXBuildFile"; 818 | fileRef = "OBJ_22"; 819 | }; 820 | "OBJ_75" = { 821 | isa = "PBXBuildFile"; 822 | fileRef = "OBJ_23"; 823 | }; 824 | "OBJ_76" = { 825 | isa = "PBXBuildFile"; 826 | fileRef = "OBJ_25"; 827 | }; 828 | "OBJ_77" = { 829 | isa = "PBXBuildFile"; 830 | fileRef = "OBJ_26"; 831 | }; 832 | "OBJ_78" = { 833 | isa = "PBXBuildFile"; 834 | fileRef = "OBJ_27"; 835 | }; 836 | "OBJ_79" = { 837 | isa = "PBXBuildFile"; 838 | fileRef = "OBJ_28"; 839 | }; 840 | "OBJ_8" = { 841 | isa = "PBXGroup"; 842 | children = ( 843 | "OBJ_9", 844 | "OBJ_10" 845 | ); 846 | name = "Json"; 847 | path = "Sources/Json"; 848 | sourceTree = "SOURCE_ROOT"; 849 | }; 850 | "OBJ_80" = { 851 | isa = "PBXBuildFile"; 852 | fileRef = "OBJ_29"; 853 | }; 854 | "OBJ_81" = { 855 | isa = "PBXBuildFile"; 856 | fileRef = "OBJ_30"; 857 | }; 858 | "OBJ_82" = { 859 | isa = "PBXBuildFile"; 860 | fileRef = "OBJ_32"; 861 | }; 862 | "OBJ_83" = { 863 | isa = "PBXBuildFile"; 864 | fileRef = "OBJ_33"; 865 | }; 866 | "OBJ_84" = { 867 | isa = "PBXBuildFile"; 868 | fileRef = "OBJ_34"; 869 | }; 870 | "OBJ_85" = { 871 | isa = "PBXBuildFile"; 872 | fileRef = "OBJ_37"; 873 | }; 874 | "OBJ_86" = { 875 | isa = "PBXBuildFile"; 876 | fileRef = "OBJ_39"; 877 | }; 878 | "OBJ_87" = { 879 | isa = "PBXFrameworksBuildPhase"; 880 | files = ( 881 | "OBJ_88" 882 | ); 883 | }; 884 | "OBJ_88" = { 885 | isa = "PBXBuildFile"; 886 | fileRef = "Request::Json::Product"; 887 | }; 888 | "OBJ_89" = { 889 | isa = "PBXTargetDependency"; 890 | target = "Request::Json"; 891 | }; 892 | "OBJ_9" = { 893 | isa = "PBXFileReference"; 894 | path = "Json.swift"; 895 | sourceTree = ""; 896 | }; 897 | "OBJ_91" = { 898 | isa = "XCConfigurationList"; 899 | buildConfigurations = ( 900 | "OBJ_92", 901 | "OBJ_93" 902 | ); 903 | defaultConfigurationIsVisible = "0"; 904 | defaultConfigurationName = "Release"; 905 | }; 906 | "OBJ_92" = { 907 | isa = "XCBuildConfiguration"; 908 | buildSettings = { 909 | LD = "/usr/bin/true"; 910 | OTHER_SWIFT_FLAGS = ( 911 | "-swift-version", 912 | "5", 913 | "-I", 914 | "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2", 915 | "-target", 916 | "x86_64-apple-macosx10.10", 917 | "-sdk", 918 | "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk", 919 | "-package-description-version", 920 | "5.1" 921 | ); 922 | SWIFT_VERSION = "5.0"; 923 | }; 924 | name = "Debug"; 925 | }; 926 | "OBJ_93" = { 927 | isa = "XCBuildConfiguration"; 928 | buildSettings = { 929 | LD = "/usr/bin/true"; 930 | OTHER_SWIFT_FLAGS = ( 931 | "-swift-version", 932 | "5", 933 | "-I", 934 | "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2", 935 | "-target", 936 | "x86_64-apple-macosx10.10", 937 | "-sdk", 938 | "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk", 939 | "-package-description-version", 940 | "5.1" 941 | ); 942 | SWIFT_VERSION = "5.0"; 943 | }; 944 | name = "Release"; 945 | }; 946 | "OBJ_94" = { 947 | isa = "PBXSourcesBuildPhase"; 948 | files = ( 949 | "OBJ_95" 950 | ); 951 | }; 952 | "OBJ_95" = { 953 | isa = "PBXBuildFile"; 954 | fileRef = "OBJ_6"; 955 | }; 956 | "OBJ_97" = { 957 | isa = "XCConfigurationList"; 958 | buildConfigurations = ( 959 | "OBJ_98", 960 | "OBJ_99" 961 | ); 962 | defaultConfigurationIsVisible = "0"; 963 | defaultConfigurationName = "Release"; 964 | }; 965 | "OBJ_98" = { 966 | isa = "XCBuildConfiguration"; 967 | buildSettings = { 968 | }; 969 | name = "Debug"; 970 | }; 971 | "OBJ_99" = { 972 | isa = "XCBuildConfiguration"; 973 | buildSettings = { 974 | }; 975 | name = "Release"; 976 | }; 977 | "Request::Json" = { 978 | isa = "PBXNativeTarget"; 979 | buildConfigurationList = "OBJ_55"; 980 | buildPhases = ( 981 | "OBJ_58", 982 | "OBJ_61" 983 | ); 984 | dependencies = ( 985 | ); 986 | name = "Json"; 987 | productName = "Json"; 988 | productReference = "Request::Json::Product"; 989 | productType = "com.apple.product-type.framework"; 990 | }; 991 | "Request::Json::Product" = { 992 | isa = "PBXFileReference"; 993 | path = "Json.framework"; 994 | sourceTree = "BUILT_PRODUCTS_DIR"; 995 | }; 996 | "Request::Request" = { 997 | isa = "PBXNativeTarget"; 998 | buildConfigurationList = "OBJ_63"; 999 | buildPhases = ( 1000 | "OBJ_66", 1001 | "OBJ_87" 1002 | ); 1003 | dependencies = ( 1004 | "OBJ_89" 1005 | ); 1006 | name = "Request"; 1007 | productName = "Request"; 1008 | productReference = "Request::Request::Product"; 1009 | productType = "com.apple.product-type.framework"; 1010 | }; 1011 | "Request::Request::Product" = { 1012 | isa = "PBXFileReference"; 1013 | path = "Request.framework"; 1014 | sourceTree = "BUILT_PRODUCTS_DIR"; 1015 | }; 1016 | "Request::RequestPackageTests::ProductTarget" = { 1017 | isa = "PBXAggregateTarget"; 1018 | buildConfigurationList = "OBJ_97"; 1019 | buildPhases = ( 1020 | ); 1021 | dependencies = ( 1022 | "OBJ_100" 1023 | ); 1024 | name = "RequestPackageTests"; 1025 | productName = "RequestPackageTests"; 1026 | }; 1027 | "Request::RequestTests" = { 1028 | isa = "PBXNativeTarget"; 1029 | buildConfigurationList = "OBJ_102"; 1030 | buildPhases = ( 1031 | "OBJ_105", 1032 | "OBJ_109" 1033 | ); 1034 | dependencies = ( 1035 | "OBJ_112", 1036 | "OBJ_113" 1037 | ); 1038 | name = "RequestTests"; 1039 | productName = "RequestTests"; 1040 | productReference = "Request::RequestTests::Product"; 1041 | productType = "com.apple.product-type.bundle.unit-test"; 1042 | }; 1043 | "Request::RequestTests::Product" = { 1044 | isa = "PBXFileReference"; 1045 | path = "RequestTests.xctest"; 1046 | sourceTree = "BUILT_PRODUCTS_DIR"; 1047 | }; 1048 | "Request::SwiftPMPackageDescription" = { 1049 | isa = "PBXNativeTarget"; 1050 | buildConfigurationList = "OBJ_91"; 1051 | buildPhases = ( 1052 | "OBJ_94" 1053 | ); 1054 | dependencies = ( 1055 | ); 1056 | name = "RequestPackageDescription"; 1057 | productName = "RequestPackageDescription"; 1058 | productType = "com.apple.product-type.framework"; 1059 | }; 1060 | }; 1061 | rootObject = "OBJ_1"; 1062 | } 1063 | -------------------------------------------------------------------------------- /Request.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /Request.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Request.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /Request.xcodeproj/xcshareddata/xcschemes/Request-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Resources/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carson-katri/swift-request/4fd7f593d10dfe310721de86bda601d817d11547/Resources/banner.png -------------------------------------------------------------------------------- /Resources/planned-features.md: -------------------------------------------------------------------------------- 1 | 1. Better `Combine` support 2 | 2. `RequestGroup` and `RequestChain` support for `RequestView` 3 | 4. `Socket` and `SocketView` 4 | -------------------------------------------------------------------------------- /Resources/swiftui.md: -------------------------------------------------------------------------------- 1 | # `RequestView` 2 | `RequestView` is a view that asynchronously loads data from the web. 3 | 4 | `RequestView` is powered by a `Request`. It handles loading the data, and you can focus on building your app. 5 | It takes a `Request`, a placeholder and any content you want rendered. 6 | 7 | > **Important:** You must handle when `data` is nil in your content, as the `RequestView` renders the content and the placeholder at the same time, so it will be rendered at least once when data is nil (and possibly again if the request fails). 8 | 9 | ```swift 10 | RequestView(Request { 11 | Url("https://api.example.com") 12 | }) { data in 13 | <> 14 | <> 15 | } 16 | ``` 17 | 18 | ## Example: 19 | Here's an example of loading in a `List` of todos from `jsonplaceholder.typicode.com`: 20 | ```swift 21 | struct Todo: Codable, Identifiable { 22 | let id: Int 23 | let userId: Int 24 | let title: String 25 | let completed: Bool 26 | } 27 | ``` 28 | ```swift 29 | struct ContentView : View { 30 | var body: some View { 31 | NavigationView { 32 | RequestView(Request { 33 | Url("https://jsonplaceholder.typicode.com/todos") 34 | }) { data in 35 | List(data != nil ? try! JSONDecoder().decode([Todo].self, from: data!) : []) { todo in 36 | HStack { 37 | Image(systemName: "checkmark.circle" + (todo.completed ? ".fill" : "")) 38 | Text(todo.title) 39 | } 40 | } 41 | Text("Loading...") 42 | } 43 | .navigationBarTitle(Text("Todos")) 44 | } 45 | } 46 | } 47 | ``` 48 | #### Result: 49 | ![Result](todolist.png) 50 | 51 | # `RequestImage` 52 | `RequestImage` asynchronously loads an image from the web. You can pass in a `Request` or just a `Url`: 53 | ```swift 54 | RequestImage(Request { 55 | Url("image.example.com/myImage.png") 56 | Header.Authorization(.basic(username: "username", password: "password")) 57 | }) 58 | RequestImage(Url("image.example.com/myImage.jpg")) 59 | ``` 60 | -------------------------------------------------------------------------------- /Resources/todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carson-katri/swift-request/4fd7f593d10dfe310721de86bda601d817d11547/Resources/todolist.png -------------------------------------------------------------------------------- /Sources/Json/Json.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/30/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A representation of a JSON object that is more robust than `[String:Any]` 11 | /// 12 | /// `Json` is used as the response type in the `onJson` callback on a `Request` object. 13 | /// 14 | /// You can create `Json` by parsing a `String` or `Data`: 15 | /// 16 | /// Json("{\"firstName\":\"Carson\"}") 17 | /// Json("{\"firstName\":\"Carson\"}".data(using: .utf8)) 18 | /// 19 | /// You can subscript `Json` as you would expect: 20 | /// 21 | /// myJson["firstName"].string // "Carson" 22 | /// myComplexJson[0]["nestedJson"]["id"].int 23 | /// 24 | /// `Json` supports ` dynamicMemberLookup`, so you can subscript with much clearer syntax: 25 | /// 26 | /// myJson.firstName.string // "Carson" 27 | /// myComplexJson[0].nestedJson.id.int 28 | /// 29 | /// You can also subscript with commas: 30 | /// 31 | /// myJson[0, "nestedJson", "id"].int 32 | /// 33 | /// This is the same as: 34 | /// 35 | /// myJson[0]["nestedJson"]["id"].int 36 | @dynamicMemberLookup 37 | public struct Json { 38 | 39 | var jsonData: Any 40 | 41 | public init() { 42 | self.jsonData = [:] 43 | } 44 | 45 | public init(_ rawValue: Any) { 46 | self.jsonData = rawValue 47 | } 48 | 49 | /// Parse a JSON string using `JSONSerialization.jsonObject` to create the `Json` 50 | public init(_ parse: String) throws { 51 | try self.init(parse.data(using: .utf8)!) 52 | } 53 | 54 | /// Create `Json` from data 55 | public init(_ data: Data) throws { 56 | self.jsonData = try JSONSerialization.jsonObject(with: data) 57 | } 58 | 59 | // MARK: Subscripts 60 | public subscript(_ sub: JsonSubscript) -> Self { 61 | get { 62 | var json = Self() 63 | switch sub.jsonKey { 64 | case .key(let s): 65 | json.jsonData = (jsonData as! [String: Any])[s]! 66 | case .index(let i): 67 | json.jsonData = (jsonData as! [Any])[i] 68 | } 69 | return json 70 | } 71 | set { 72 | switch sub.jsonKey { 73 | case .key(let s): 74 | var cast = jsonData as! [String: Any] 75 | cast[s] = newValue.jsonData 76 | jsonData = cast 77 | case .index(let i): 78 | var cast = jsonData as! [Any] 79 | cast[i] = newValue.jsonData 80 | jsonData = cast 81 | } 82 | } 83 | } 84 | public subscript(_ subs: [JsonSubscript]) -> Self { 85 | get { 86 | subs.reduce(self) { $0[$1] } 87 | } 88 | set { 89 | switch subs.count { 90 | case 0: 91 | return 92 | case 1: 93 | self[subs.first!] = newValue 94 | default: 95 | var newSubs = subs 96 | newSubs.remove(at: 0) 97 | var json = self[subs.first!] 98 | json[newSubs] = newValue 99 | self[subs.first!] = json 100 | } 101 | } 102 | } 103 | public subscript(_ subs: JsonSubscript...) -> Self { 104 | get { 105 | self[subs] 106 | } 107 | set { 108 | self[subs] = newValue 109 | } 110 | } 111 | public subscript(dynamicMember member: String) -> Self { 112 | get { 113 | self[member] 114 | } 115 | set { 116 | self[member] = newValue 117 | } 118 | } 119 | 120 | // MARK: Accessors 121 | func accessValue(_ defaultValue: T) -> T { 122 | accessOptional(T.self) ?? defaultValue 123 | } 124 | 125 | func accessOptional(_ type: T.Type) -> T? { 126 | jsonData as? T 127 | } 128 | 129 | /// Stringified version 130 | public var data: Data? { 131 | try? JSONSerialization.data(withJSONObject: jsonData) 132 | } 133 | 134 | public var stringified: String? { 135 | guard let data = data else { 136 | return nil 137 | } 138 | return String(data: data, encoding: .utf8) 139 | } 140 | 141 | /// The stored value of the `Json` 142 | public var value: Any { 143 | jsonData 144 | } 145 | 146 | /// The data as a non-optional `String` 147 | public var string: String { 148 | accessValue("") 149 | } 150 | /// The data as an optional `String` 151 | public var stringOptional: String? { 152 | accessOptional(String.self) 153 | } 154 | 155 | /// The data as a non-optional `Int` 156 | public var int: Int { 157 | accessValue(0) 158 | } 159 | /// The data as an optional `Int` 160 | public var intOptional: Int? { 161 | accessOptional(Int.self) 162 | } 163 | 164 | /// The data as a non-optional `Double` 165 | public var double: Double { 166 | accessValue(0.0) 167 | } 168 | /// The data as an optional `Double` 169 | public var doubleOptional: Double? { 170 | accessOptional(Double.self) 171 | } 172 | 173 | /// The data as a non-optional `Bool` 174 | public var bool: Bool { 175 | accessValue(false) 176 | } 177 | /// The data as an optional `Bool` 178 | public var boolOptional: Bool? { 179 | accessOptional(Bool.self) 180 | } 181 | 182 | /// The data as a non-optional `Array` 183 | public var array: [Any] { 184 | accessValue([]) 185 | } 186 | /// The data as an optional `Array` 187 | public var arrayOptional: [Any]? { 188 | accessOptional([Any].self) 189 | } 190 | /// The number of elements in the data 191 | public var count: Int { 192 | array.count 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Sources/Json/JsonBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/30/19. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct JsonBuilder { 12 | public static func buildBlock(_ props: (String, Any)...) -> [(String, Any)] { 13 | return props 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Json/JsonSubscript.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subscripts.swift 3 | // 4 | // 5 | // Created by Carson Katri on 8/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum JsonSubscriptType { 11 | case key(_ s: String) 12 | case index(_ i: Int) 13 | } 14 | 15 | public protocol JsonSubscript { 16 | var jsonKey: JsonSubscriptType { get } 17 | } 18 | 19 | extension String: JsonSubscript { 20 | public var jsonKey: JsonSubscriptType { 21 | .key(self) 22 | } 23 | } 24 | 25 | extension Int: JsonSubscript { 26 | public var jsonKey: JsonSubscriptType { 27 | .index(self) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Json/Literals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conformance.swift 3 | // 4 | // 5 | // Created by Carson Katri on 8/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Json: ExpressibleByStringLiteral { 11 | public init(stringLiteral value: StringLiteralType) { 12 | jsonData = value 13 | } 14 | public init(extendedGraphemeClusterLiteral value: StringLiteralType) { 15 | jsonData = value 16 | } 17 | public init(unicodeScalarLiteral value: StringLiteralType) { 18 | jsonData = value 19 | } 20 | } 21 | 22 | extension Json: ExpressibleByIntegerLiteral { 23 | public init(integerLiteral value: IntegerLiteralType) { 24 | jsonData = value 25 | } 26 | } 27 | 28 | extension Json: ExpressibleByFloatLiteral { 29 | public init(floatLiteral value: FloatLiteralType) { 30 | jsonData = value 31 | } 32 | } 33 | 34 | extension Json: ExpressibleByBooleanLiteral { 35 | public init(booleanLiteral value: BooleanLiteralType) { 36 | jsonData = value 37 | } 38 | } 39 | 40 | extension Json: ExpressibleByArrayLiteral { 41 | public init(arrayLiteral elements: Any...) { 42 | jsonData = elements 43 | } 44 | } 45 | 46 | extension Json: ExpressibleByDictionaryLiteral { 47 | public init(dictionaryLiteral elements: (String, Any)...) { 48 | jsonData = Dictionary(elements, uniquingKeysWith: { (value1, value2) -> Any in 49 | return value1 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Request/Helpers/Auth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authorization.swift 3 | // PackageTests 4 | // 5 | // Created by Carson Katri on 7/2/19. 6 | // Copyright © 2019 Carson Katri. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The type of `Authentication` to use in the `Request` 12 | /// 13 | /// Used with `Auth` 14 | public enum AuthType: String { 15 | case basic = "Basic" 16 | case bearer = "Bearer" 17 | /*case digest = "Digest" 18 | case hoba = "HOBA" 19 | case mutual = "Mutual" 20 | case aws = "AWS4-HMAC-SHA256"*/ 21 | } 22 | 23 | /// The `Authentication` type, and the key used 24 | /// 25 | /// The `key` and `value` are merged in the `Authentication` header as `key value`. 26 | /// For instance: `Basic username:password`, or `Bearer token` 27 | /// 28 | /// You can use `.basic` and `.bearer` to simplify the process of authenticating your `Request` 29 | public struct Auth { 30 | public let type: AuthType 31 | public let key: String 32 | public var value: String { 33 | get { 34 | return "\(type.rawValue) \(key)" 35 | } 36 | } 37 | 38 | public init(type: AuthType, key: String) { 39 | self.type = type 40 | self.key = key 41 | } 42 | } 43 | 44 | extension Auth { 45 | /// Authenticates using `username` and `password` directly 46 | public static func basic(username: String, password: String) -> Auth { 47 | return Auth(type: .basic, key: Data("\(username):\(password)".utf8).base64EncodedString()) 48 | } 49 | 50 | /// Authenticates using a `token` 51 | public static func bearer(_ token: String) -> Auth { 52 | return Auth(type: .bearer, key: token) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Request/Helpers/CacheType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheType.swift 3 | // PackageTests 4 | // 5 | // Created by Carson Katri on 7/2/19. 6 | // Copyright © 2019 Carson Katri. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The caching method, to be used with the `Cache-Control` header 12 | public struct CacheType { 13 | public let value: String 14 | 15 | public static let noCache = CacheType(value: "no-cache") 16 | public static let noStore = CacheType(value: "no-store") 17 | public static let noTransform = CacheType(value: "no-transform") 18 | public static let onlyIfCached = CacheType(value: "only-if-cached") 19 | public static func maxAge(_ seconds: Int) -> CacheType { 20 | return CacheType(value: "max-age=\(seconds)") 21 | } 22 | public static let maxStale = CacheType(value: "max-stale") 23 | public static func maxStale(_ seconds: Int) -> CacheType { 24 | return CacheType(value: "max-stale=\(seconds)") 25 | } 26 | public static func minFresh(_ seconds: Int) -> CacheType { 27 | return CacheType(value: "min-fresh=\(seconds)") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Request/Helpers/MediaType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentType.swift 3 | // PackageTests 4 | // 5 | // Created by Carson Katri on 7/2/19. 6 | // Copyright © 2019 Carson Katri. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A MIME type to be used with the `Accept` and `Content-Type` headers. 12 | public enum MediaType: ExpressibleByStringLiteral { 13 | public typealias StringLiteralType = String 14 | 15 | case json 16 | case xml 17 | 18 | case text 19 | case html 20 | case css 21 | case javascript 22 | 23 | case gif 24 | case png 25 | case jpeg 26 | case bmp 27 | case webp 28 | 29 | case midi 30 | case mpeg 31 | case wav 32 | 33 | case pdf 34 | 35 | case custom(String) 36 | 37 | public init(stringLiteral value: String) { 38 | self = .custom(value) 39 | } 40 | } 41 | 42 | extension MediaType { 43 | public var rawValue: String { 44 | switch self { 45 | case .json: return "application/json" 46 | case .xml: return "application/xml" 47 | 48 | case .text: return "text/plain" 49 | case .html: return "text/html" 50 | case .css: return "text/css" 51 | case .javascript: return "text/javascript" 52 | 53 | case .gif: return "image/gif" 54 | case .png: return "image/png" 55 | case .jpeg: return "image/jpeg" 56 | case .bmp: return "image/bmp" 57 | case .webp: return "image/webp" 58 | 59 | case .midi: return "audio/midi" 60 | case .mpeg: return "audio/mpeg" 61 | case .wav: return "audio/wav" 62 | 63 | case .pdf: return "application/pdf" 64 | 65 | case .custom(let string): 66 | return string 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Request/Helpers/RequestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // Request 4 | // 5 | // Created by Carson Katri on 7/11/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An error returned by the `Request` 11 | public struct RequestError: Error { 12 | public let statusCode: Int 13 | public let error: Data? 14 | 15 | public var localizedDescription: String { 16 | guard let data = self.error else { 17 | return "Error code: \(self.statusCode)" 18 | } 19 | 20 | return String(data: data, encoding: .utf8) ?? "Error code: \(self.statusCode)" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Request/Helpers/UserAgent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserAgent.swift 3 | // PackageTests 4 | // 5 | // Created by Carson Katri on 7/2/19. 6 | // Copyright © 2019 Carson Katri. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// User agent strings for many different browsers 12 | public enum UserAgent: String { 13 | case chrome = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" 14 | case chromeAndroid = "Mozilla/5.0 (Linux; ; ) AppleWebKit/ (KHTML, like Gecko) Chrome/ Mobile Safari/" 15 | case chromeiOS = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1" 16 | case firefoxWindows = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" 17 | case firefoxMac = "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0" 18 | case opera = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41" 19 | case safari = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1" 20 | case internetExplorer = "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)" 21 | case bot = "Googlebot/2.1 (+http://www.google.com/bot.html)" 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Request/Request/Extra/RequestChain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestChain.swift 3 | // Request 4 | // 5 | // Created by Carson Katri on 7/11/19. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public extension Request { 12 | /// Creates a `Request` to be used in a `RequestChain` 13 | /// 14 | /// This `Request` takes `[Data?]` and `[Error?]` as parameters. 15 | /// These parameters contain the results of the previously called `Request`s 16 | /// 17 | /// Request.chained { (data, err) in 18 | /// Url("https://api.example.com/todos/\(Json(data[0]!)![0]["id"].int)") 19 | /// } 20 | static func chained(@RequestBuilder builder: @escaping ([Data?], [Error?]) -> RequestParam) -> ([Data?], [Error?]) -> RequestParam { 21 | return builder 22 | } 23 | } 24 | 25 | @resultBuilder 26 | public struct RequestChainBuilder { 27 | public static func buildBlock(_ requests: (([Data?], [Error?]) -> RequestParam)...) -> [([Data?], [Error?]) -> RequestParam] { 28 | return requests 29 | } 30 | } 31 | 32 | /// Chains multiple `Request`s together. 33 | /// 34 | /// The `Request`s in the chain are run in order. 35 | /// To run multiple `Request`s in parallel, look at `RequestGroup`. 36 | /// 37 | /// Instead of using `Request`, use `Request.chained` to build each `Request`. 38 | /// This allows you to access the results and errors of every previous `Request` 39 | /// 40 | /// RequestChain { 41 | /// // Make our first request 42 | /// Request.chained { (data, err) in 43 | /// Url("https://api.example.com/todos") 44 | /// } 45 | /// // Now we can use the data from that request to make our 2nd 46 | /// Request.chained { (data, err) in 47 | /// Url("https://api.example.com/todos/\(Json(data[0]!)![0]["id"].int)") 48 | /// } 49 | /// } 50 | /// 51 | /// - Precondition: You must have **at least 2** `Request`s in your chain, or the compiler will have a fit. 52 | public struct RequestChain { 53 | internal let requests: [([Data?], [Error?]) -> RequestParam] 54 | 55 | public init(@RequestChainBuilder requests: () -> [([Data?], [Error?]) -> RequestParam]) { 56 | self.requests = requests() 57 | } 58 | 59 | /// Perform the `Request`s in the chain, and optionally respond with the data from each one when complete. 60 | public func call(_ callback: @escaping ([Data?], [Error?]) -> Void = { (_, _) in }) { 61 | func _call(_ index: Int, data: [Data?], errors: [Error?], callback: @escaping ([Data?], [Error?]) -> Void) { 62 | Request(rootParam: self.requests[index](data, errors)) 63 | .onData { res in 64 | if index + 1 >= self.requests.count { 65 | callback(data + [res], errors + [nil]) 66 | } else { 67 | _call(index + 1, data: data + [res], errors: errors + [nil], callback: callback) 68 | } 69 | } 70 | .onError { err in 71 | if index + 1 >= self.requests.count { 72 | callback(data + [nil], errors + [err]) 73 | } else { 74 | _call(index + 1, data: data + [nil], errors: errors + [err], callback: callback) 75 | } 76 | } 77 | .call() 78 | } 79 | _call(0, data: [], errors: [], callback: callback) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Request/Request/Extra/RequestGroup+Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestGroup+Combine.swift 3 | // 4 | // 5 | // Created by Carson Katri on 7/27/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension RequestGroup: Publisher { 12 | public typealias Output = [URLSession.DataTaskPublisher.Output] 13 | public typealias Failure = Error 14 | 15 | public func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 16 | Publishers.Sequence(sequence: requests) 17 | .flatMap { $0 } 18 | .collect() 19 | .subscribe(subscriber) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Request/Request/Extra/RequestGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestChain.swift 3 | // Request 4 | // 5 | // Created by Carson Katri on 7/10/19. 6 | // 7 | 8 | import Foundation 9 | import Json 10 | 11 | @resultBuilder 12 | public struct RequestGroupBuilder { 13 | public static func buildBlock(_ requests: Request...) -> [Request] { 14 | return requests 15 | } 16 | } 17 | 18 | /// Performs multiple `Request`s simultaneously, and responds with the result of them all. 19 | /// 20 | /// All of the `Request`s are run at the same time, and therefore know nothing of the previous `Request`'s response. 21 | /// To chain requests together to be run in order, see `RequestChain`. 22 | /// 23 | /// **Example:** 24 | /// 25 | /// RequestGroup { 26 | /// Request { 27 | /// Url("https://api.example.com/todos") 28 | /// } 29 | /// Request { 30 | /// Url("https://api.example.com/todos/1/save") 31 | /// Method(.post) 32 | /// Body(["name":"Hello World"]) 33 | /// } 34 | /// } 35 | /// 36 | /// You can use `onData`, `onString`, `onJson`, and `onError` like you would with a normal `Request`. 37 | /// However, it will also return the index of the `Request`, along with the data. 38 | public struct RequestGroup { 39 | internal let requests: [Request] 40 | 41 | private let onData: ((Int, Data?) -> Void)? 42 | private let onString: ((Int, String?) -> Void)? 43 | private let onJson: ((Int, Json?) -> Void)? 44 | private let onError: ((Int, Error) -> Void)? 45 | 46 | public init(@RequestGroupBuilder requests: () -> [Request]) { 47 | self.requests = requests() 48 | self.onData = nil 49 | self.onString = nil 50 | self.onJson = nil 51 | self.onError = nil 52 | } 53 | 54 | internal init(requests: [Request], 55 | onData: ((Int, Data?) -> Void)?, 56 | onString: ((Int, String?) -> Void)?, 57 | onJson: ((Int, Json?) -> Void)?, 58 | onError: ((Int, Error) -> Void)?) { 59 | self.requests = requests 60 | self.onData = onData 61 | self.onString = onString 62 | self.onJson = onJson 63 | self.onError = onError 64 | } 65 | 66 | public func onData(_ callback: @escaping ((Int, Data?) -> Void)) -> RequestGroup { 67 | Self.init(requests: requests, onData: callback, onString: onString, onJson: onJson, onError: onError) 68 | } 69 | 70 | public func onString(_ callback: @escaping ((Int, String?) -> Void)) -> RequestGroup { 71 | Self.init(requests: requests, onData: onData, onString: callback, onJson: onJson, onError: onError) 72 | } 73 | 74 | public func onJson(_ callback: @escaping ((Int, Json?) -> Void)) -> RequestGroup { 75 | Self.init(requests: requests, onData: onData, onString: onString, onJson: callback, onError: onError) 76 | } 77 | 78 | public func onError(_ callback: @escaping ((Int, Error) -> Void)) -> RequestGroup { 79 | Self.init(requests: requests, onData: onData, onString: onString, onJson: onJson, onError: callback) 80 | } 81 | 82 | /// Perform the `Request`s in the group. 83 | public func call() { 84 | self.requests.enumerated().forEach { (index, _req) in 85 | var req = _req 86 | if self.onData != nil { 87 | req = req.onData { data in 88 | self.onData!(index, data) 89 | } 90 | } 91 | if self.onString != nil { 92 | req = req.onString { string in 93 | self.onString!(index, string) 94 | } 95 | } 96 | if self.onJson != nil { 97 | req = req.onJson { json in 98 | self.onJson!(index, json) 99 | } 100 | } 101 | if self.onError != nil { 102 | req = req.onError { error in 103 | self.onError!(index, error) 104 | } 105 | } 106 | req.call() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/Form.Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Form.Data.swift 3 | // 4 | // 5 | // Created by brennobemoura on 16/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Form { 11 | struct Data: FormParam { 12 | private let data: Foundation.Data 13 | private let fileName: String 14 | private let mediaType: MediaType 15 | 16 | /// Creates a multipart form data body from `Data`. 17 | /// 18 | /// - Parameter named: The name of the file being sent. Example: `image.jpg` 19 | /// 20 | public init(_ data: Foundation.Data, named fileName: String, withType mediaType: MediaType) { 21 | self.fileName = fileName 22 | self.data = data 23 | self.mediaType = mediaType 24 | } 25 | 26 | public func buildData(_ data: inout Foundation.Data, with boundary: String) { 27 | data.append(header(boundary)) 28 | data.append(disposition(fileName, withType: mediaType)) 29 | data.append(Foundation.Data("\(breakLine)".utf8)) 30 | data.append(self.data) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/Form.File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Form.File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 16/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Form { 11 | struct File: FormParam { 12 | private let path: Url 13 | private let fileManager: FileManager 14 | private let mediaType: MediaType 15 | 16 | public init(_ url: Url, withType mediaType: MediaType, _ fileManager: FileManager = .default) { 17 | self.path = url 18 | self.fileManager = fileManager 19 | self.mediaType = mediaType 20 | } 21 | 22 | public init(_ url: Url, _ fileManager: FileManager = .default) throws { 23 | fatalError("init(_:, _:) throw not implemented") 24 | } 25 | 26 | public func buildData(_ data: inout Foundation.Data, with boundary: String) { 27 | guard 28 | let fileData = fileManager.contents(atPath: path.absoluteString), 29 | let fileName = path.absoluteString.split(separator: "/").last 30 | else { 31 | fatalError("\(path.absoluteString) is not a file or it doesn't contains a valid file name") 32 | } 33 | 34 | data.append(header(boundary)) 35 | data.append(disposition(fileName, withType: mediaType)) 36 | data.append(Foundation.Data("\(breakLine)".utf8)) 37 | data.append(fileData) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/Form.Value.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 04/02/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Form { 11 | struct Value: FormParam { 12 | private let key: String 13 | private let element: Element 14 | 15 | /// Creates a multipart form data body from `Encodable` value. 16 | /// 17 | /// - Parameter key: The key being sent. Example: `name` 18 | /// - Parameter element: The element that will be inserted in the form data body. Example: `test` 19 | /// 20 | public init(key: String, _ element: Element) { 21 | self.key = key 22 | self.element = element 23 | } 24 | 25 | public func buildData(_ data: inout Foundation.Data, with boundary: String) { 26 | data.append(header(boundary)) 27 | data.append(disposition(key)) 28 | data.append(Foundation.Data("\(breakLine)".utf8)) 29 | data.append(Foundation.Data("\(element)".utf8)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/Form.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Form.swift 3 | // 4 | // 5 | // Created by brennobemoura on 16/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Form: FormParam { 11 | private let rootParam: FormParam 12 | 13 | public init(@FormBuilder params: () -> FormParam) { 14 | self.rootParam = params() 15 | } 16 | 17 | public func buildData(_ data: inout Foundation.Data, with boundary: String) { 18 | if rootParam is EmptyParam { 19 | return 20 | } 21 | 22 | rootParam.buildData(&data, with: boundary) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/FormBuilder.Combined.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormBuilder.Combined.swift 3 | // 4 | // 5 | // Created by brennobemoura on 22/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal extension FormBuilder { 11 | struct Combined: FormParam { 12 | let children: [FormParam] 13 | 14 | init(_ children: [FormParam]) { 15 | self.children = children 16 | } 17 | 18 | func buildData(_ data: inout Data, with boundary: String) { 19 | children.dropLast().forEach { 20 | $0.buildData(&data, with: boundary) 21 | data.append(middle) 22 | } 23 | 24 | children.last?.buildData(&data, with: boundary) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/FormBuilder.Empty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormBuilder.Empty.swift 3 | // 4 | // 5 | // Created by brennobemoura on 22/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal extension FormBuilder { 11 | struct Empty: FormParam { 12 | func buildData(_ data: inout Data, with boundary: String) {} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/FormBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormBuilder.swift 3 | // 4 | // 5 | // Created by brennobemoura on 18/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct FormBuilder { 12 | public static func buildBlock(_ params: FormParam...) -> FormParam { 13 | Combined(params) 14 | } 15 | 16 | public static func buildBlock(_ param: FormParam) -> FormParam { 17 | param 18 | } 19 | 20 | public static func buildBlock() -> EmptyParam { 21 | EmptyParam() 22 | } 23 | 24 | public static func buildIf(_ param: FormParam?) -> FormParam { 25 | param ?? EmptyParam() 26 | } 27 | 28 | public static func buildEither(first: FormParam) -> FormParam { 29 | first 30 | } 31 | 32 | public static func buildEither(second: FormParam) -> FormParam { 33 | second 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Request/Request/FormParam/FormParam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormParam.swift 3 | // 4 | // 5 | // Created by brennobemoura on 16/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol FormParam: RequestParam { 11 | func buildData(_ data: inout Data, with boundary: String) 12 | } 13 | 14 | public extension FormParam { 15 | func buildParam(_ request: inout URLRequest) { 16 | let boundary = self.boundary 17 | request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 18 | 19 | var data = Data() 20 | buildData(&data, with: boundary) 21 | 22 | if !data.isEmpty { 23 | data.append(footer(boundary)) 24 | } 25 | 26 | request.setValue("\(data.count)", forHTTPHeaderField: "Content-Length") 27 | request.httpBody = data 28 | } 29 | } 30 | 31 | internal extension FormParam { 32 | private var random: UInt32 { 33 | .random(in: .min ... .max) 34 | } 35 | 36 | private var boundary: String { 37 | String(format: "request.boundary.%08x%08x", random, random) 38 | } 39 | 40 | var breakLine: String { 41 | "\r\n" 42 | } 43 | 44 | func header(_ boundary: String) -> Data { 45 | .init("--\(boundary)\(breakLine)".utf8) 46 | } 47 | 48 | var middle: Data { 49 | .init("\(breakLine)".utf8) 50 | } 51 | 52 | func footer(_ boundary: String) -> Data { 53 | .init("\(breakLine)--\(boundary)--\(breakLine)".utf8) 54 | } 55 | 56 | func disposition(_ fileName: S, withType mediaType: MediaType) -> Data where S: StringProtocol { 57 | let name: String 58 | if fileName.contains(".") { 59 | name = fileName 60 | .split(separator: ".") 61 | .dropLast() 62 | .joined(separator: ".") 63 | } else { 64 | name = "\(fileName)" 65 | } 66 | 67 | var contents = Data() 68 | 69 | contents.append(Data("Content-Disposition: form-data; name=\"\(name)\";".utf8)) 70 | contents.append(Data("filename=\"\(fileName)\"".utf8)) 71 | contents.append(Data(breakLine.utf8)) 72 | 73 | contents.append(Data("Content-Type: \(mediaType)".utf8)) 74 | contents.append(Data(breakLine.utf8)) 75 | 76 | return contents 77 | } 78 | 79 | func disposition(_ name: S) -> Data where S: StringProtocol { 80 | Data("Content-Disposition: form-data; name=\"\(name)\"\(breakLine)".utf8) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Request/Request/Request+Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Combine.swift 3 | // 4 | // 5 | // Created by Carson Katri on 7/27/20. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Json 11 | 12 | extension AnyRequest: Publisher { 13 | public typealias Output = URLSession.DataTaskPublisher.Output 14 | public typealias Failure = Error 15 | 16 | public func receive(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output { 17 | updatePublisher? 18 | .receive(subscriber: UpdateSubscriber(request: self)) 19 | buildPublisher() 20 | .subscribe(subscriber) 21 | } 22 | 23 | public typealias DataMapPublisher = Publishers.MapKeyPath, JSONDecoder.Input> 24 | public typealias ObjectPublisher = Publishers.Decode 25 | public var objectPublisher: ObjectPublisher { 26 | map(\.data) 27 | .decode(type: ResponseType.self, decoder: JSONDecoder()) 28 | } 29 | 30 | public typealias StringPublisher = Publishers.CompactMap, String> 31 | public var stringPublisher: StringPublisher { 32 | compactMap { 33 | String(data: $0.data, encoding: .utf8) 34 | } 35 | } 36 | 37 | public typealias JsonPublisher = Publishers.TryMap 38 | public var jsonPublisher: JsonPublisher { 39 | stringPublisher 40 | .tryMap { 41 | try Json($0) 42 | } 43 | } 44 | 45 | public typealias UpdatePublisher = Publishers.FlatMap 46 | where Downstream: Publisher, Upstream: Publisher, Downstream.Failure == Error, Upstream.Failure == Error 47 | 48 | func updatePublisher(publisher: T) -> UpdatePublisher { 49 | publisher.flatMap { _ in self } 50 | } 51 | 52 | public typealias UpdateTimerPublisher = UpdatePublisher, Error>> 53 | 54 | func updatePublisher(every seconds: TimeInterval) -> UpdateTimerPublisher { 55 | updatePublisher(publisher: Timer.publish(every: seconds, on: .main, in: .common) 56 | .autoconnect() 57 | .mapError { _ in RequestError(statusCode: -1, error: nil) }) 58 | } 59 | } 60 | 61 | extension AnyRequest: Subscriber { 62 | public typealias Input = (data: Data, response: URLResponse) 63 | 64 | public func receive(subscription: Subscription) { 65 | subscription.request(.max(1)) 66 | } 67 | 68 | public func receive(_ input: Input) -> Subscribers.Demand { 69 | if let res = input.response as? HTTPURLResponse { 70 | let statusCode = res.statusCode 71 | 72 | if let onStatusCode = self.onStatusCode { 73 | onStatusCode(statusCode) 74 | } 75 | 76 | if statusCode < 200 || statusCode >= 300 { 77 | if let onError = self.onError { 78 | onError(RequestError(statusCode: statusCode, error: input.data)) 79 | return .none 80 | } 81 | } 82 | } 83 | if let onData = self.onData { 84 | onData(input.data) 85 | } 86 | if let onString = self.onString { 87 | if let string = String(data: input.data, encoding: .utf8) { 88 | onString(string) 89 | } 90 | } 91 | if let onJson = self.onJson { 92 | if let string = String(data: input.data, encoding: .utf8) { 93 | if let json = try? Json(string) { 94 | onJson(json) 95 | } 96 | } 97 | } 98 | if let onObject = self.onObject { 99 | if let decoded = try? JSONDecoder().decode(ResponseType.self, from: input.data) { 100 | onObject(decoded) 101 | } 102 | } 103 | return .none 104 | } 105 | 106 | public func receive(completion: Subscribers.Completion) { 107 | switch completion { 108 | case .failure(let err): onError?(err) 109 | case .finished: return 110 | } 111 | } 112 | } 113 | 114 | extension AnyRequest { 115 | struct UpdateSubscriber: Subscriber { 116 | let combineIdentifier: CombineIdentifier = CombineIdentifier() 117 | let request: AnyRequest 118 | 119 | typealias Input = Void 120 | typealias Failure = Never 121 | 122 | func receive(subscription: Subscription) { 123 | subscription.request(.unlimited) 124 | } 125 | 126 | func receive(_ input: Input) -> Subscribers.Demand { 127 | request.call() 128 | return .none 129 | } 130 | 131 | func receive(completion: Subscribers.Completion) { 132 | return 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Request/Request/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/30/19. 6 | // 7 | 8 | import Foundation 9 | import Json 10 | import SwiftUI 11 | import Combine 12 | 13 | /// The building block for making an HTTP request 14 | /// 15 | /// Built using a `@resultBuilder`, available in Swift 5.4 16 | /// 17 | /// *Example*: 18 | /// 19 | /// Request { 20 | /// Url("https://api.example.com/todos") 21 | /// } 22 | /// 23 | /// To make the `Request`, use the method `call` 24 | /// 25 | /// To accept data from the `Request`, use `onData`, `onString`, and `onJson`. 26 | /// 27 | /// **See Also:** 28 | /// `Url`, `Method`, `Header`, `Query`, `Body` 29 | /// 30 | /// - Precondition: The `Request` body must contain **exactly one** `Url` 31 | public typealias Request = AnyRequest 32 | 33 | /// Tha base class of `Request` to be used with a `Codable` `ResponseType` when using the `onObject` callback 34 | /// 35 | /// *Example*: 36 | /// 37 | /// AnyRequest<[MyCodableStruct]> { 38 | /// Url("https://api.example.com/myData") 39 | /// } 40 | /// .onObject { myCodableStructs in 41 | /// ... 42 | /// } 43 | public struct AnyRequest where ResponseType: Decodable { 44 | public let combineIdentifier = CombineIdentifier() 45 | 46 | private var rootParam: RequestParam 47 | 48 | internal var onData: ((Data) -> Void)? 49 | internal var onString: ((String) -> Void)? 50 | internal var onJson: ((Json) -> Void)? 51 | internal var onObject: ((ResponseType) -> Void)? 52 | internal var onError: ((Error) -> Void)? 53 | internal var onStatusCode: ((Int) -> Void)? 54 | internal var updatePublisher: AnyPublisher? 55 | 56 | public init(@RequestBuilder builder: () -> RequestParam) { 57 | rootParam = builder() 58 | } 59 | 60 | internal init(rootParam: RequestParam) { 61 | self.rootParam = rootParam 62 | } 63 | 64 | internal func modify(_ modify: (inout Self) -> Void) -> Self { 65 | var mutableSelf = self 66 | modify(&mutableSelf) 67 | return mutableSelf 68 | } 69 | 70 | /// Sets the `onData` callback to be run whenever `Data` is retrieved 71 | public func onData(_ callback: @escaping (Data) -> Void) -> Self { 72 | modify { $0.onData = callback } 73 | } 74 | 75 | /// Sets the `onString` callback to be run whenever a `String` is retrieved 76 | public func onString(_ callback: @escaping (String) -> Void) -> Self { 77 | modify { $0.onString = callback } 78 | } 79 | 80 | /// Sets the `onData` callback to be run whenever `Json` is retrieved 81 | public func onJson(_ callback: @escaping (Json) -> Void) -> Self { 82 | modify { $0.onJson = callback } 83 | } 84 | 85 | /// Sets the `onObject` callback to be run whenever `Data` is retrieved 86 | public func onObject(_ callback: @escaping (ResponseType) -> Void) -> Self { 87 | modify { $0.onObject = callback } 88 | } 89 | 90 | /// Handle any `Error`s thrown by the `Request` 91 | public func onError(_ callback: @escaping (Error) -> Void) -> Self { 92 | modify { $0.onError = callback } 93 | } 94 | 95 | /// Sets the `onStatusCode` callback to be run whenever a `HTTPStatus` is retrieved 96 | public func onStatusCode(_ callback: @escaping (Int) -> Void) -> Self { 97 | modify { $0.onStatusCode = callback } 98 | } 99 | 100 | /// Performs the `Request`, and calls the `onData`, `onString`, `onJson`, and `onError` callbacks when appropriate. 101 | public func call() { 102 | buildPublisher() 103 | .subscribe(self) 104 | if let updatePublisher = self.updatePublisher { 105 | updatePublisher 106 | .subscribe(UpdateSubscriber(request: self)) 107 | } 108 | } 109 | 110 | internal func buildSession() -> (configuration: URLSessionConfiguration, request: URLRequest) { 111 | var request = URLRequest(url: URL(string: "https://")!) 112 | let configuration = URLSessionConfiguration.default 113 | 114 | rootParam.buildParam(&request) 115 | (rootParam as? SessionParam)?.buildConfiguration(configuration) 116 | 117 | return (configuration, request) 118 | } 119 | 120 | internal func buildPublisher() -> AnyPublisher<(data: Data, response: URLResponse), Error> { 121 | // PERFORM REQUEST 122 | let session = buildSession() 123 | return URLSession(configuration: session.configuration).dataTaskPublisher(for: session.request) 124 | .mapError { $0 } 125 | .eraseToAnyPublisher() 126 | } 127 | 128 | /// Sets the `Request` to be performed additional times after the initial `call` 129 | public func update(publisher: T) -> Self { 130 | modify { 131 | $0.updatePublisher = publisher 132 | .map {_ in } 133 | .assertNoFailure() 134 | .merge(with: self.updatePublisher ?? Empty().eraseToAnyPublisher()) 135 | .eraseToAnyPublisher() 136 | } 137 | } 138 | 139 | /// Sets the `Request` to be repeated periodically after the initial `call` 140 | public func update(every seconds: TimeInterval) -> Self { 141 | self.update(publisher: Timer.publish(every: seconds, on: .main, in: .common).autoconnect()) 142 | } 143 | } 144 | 145 | extension AnyRequest: Equatable { 146 | public static func == (lhs: AnyRequest, rhs: AnyRequest) -> Bool { 147 | let lhsSession = lhs.buildSession() 148 | let rhsSession = rhs.buildSession() 149 | return lhsSession.configuration == rhsSession.configuration && lhsSession.request == rhsSession.request 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/30/19. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct RequestBuilder { 12 | public static func buildBlock(_ params: RequestParam...) -> RequestParam { 13 | CombinedParams(children: params) 14 | } 15 | 16 | public static func buildBlock(_ param: RequestParam) -> RequestParam { 17 | param 18 | } 19 | 20 | public static func buildBlock() -> EmptyParam { 21 | EmptyParam() 22 | } 23 | 24 | public static func buildIf(_ param: RequestParam?) -> RequestParam { 25 | param ?? EmptyParam() 26 | } 27 | 28 | public static func buildEither(first: RequestParam) -> RequestParam { 29 | first 30 | } 31 | 32 | public static func buildEither(second: RequestParam) -> RequestParam { 33 | second 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/AnyParam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 15/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A way to create a custom `RequestParam` 11 | /// - Important: You will most likely want to use one of the builtin `RequestParam`s, such as: `Url`, `Method`, `Header`, `Query`, or `Body`. 12 | @available(*, deprecated, message: "`AnyParam` is deprecated. Please conform to the `RequestParam` protocol instead.") 13 | public struct AnyParam: RequestParam { 14 | private let requestParam: RequestParam 15 | 16 | public init(_ requestParam: Param) where Param: RequestParam { 17 | self.requestParam = requestParam 18 | } 19 | 20 | public func buildParam(_ request: inout URLRequest) { 21 | requestParam.buildParam(&request) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Body.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Body.swift 3 | // Request 4 | // 5 | // Created by Carson Katri on 7/10/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Sets the body of the `Request` 11 | /// 12 | /// Expressed as key-value pairs: 13 | /// 14 | /// Request { 15 | /// Url("api.example.com/save") 16 | /// Body([ 17 | /// "doneWorking": true 18 | /// ]) 19 | /// } 20 | /// 21 | /// Or as a `String`: 22 | /// 23 | /// Request { 24 | /// Url("api.example.com/save") 25 | /// Body("myData") 26 | /// } 27 | /// 28 | /// Or as an `Encodable` type: 29 | /// 30 | /// Request { 31 | /// Url("api.example.com/save") 32 | /// Body(codableTodo) 33 | /// } 34 | /// 35 | public struct Body: RequestParam { 36 | private let data: Data? 37 | 38 | /// Creates the `Body` from key-value pairs 39 | public init(_ dict: [String:Any]) { 40 | self.data = try? JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) 41 | } 42 | 43 | /// Creates the `Body` from an `Encodable` type using `JSONEncoder` 44 | public init(_ value: T) { 45 | self.data = try? JSONEncoder().encode(value) 46 | } 47 | 48 | /// Creates the `Body` from a `String` 49 | public init(_ string: String) { 50 | self.data = string.data(using: .utf8) 51 | } 52 | 53 | public func buildParam(_ request: inout URLRequest) { 54 | request.httpBody = data 55 | } 56 | } 57 | 58 | #if canImport(SwiftUI) 59 | public typealias RequestBody = Body 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/CombinedParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 15/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | internal struct CombinedParams: RequestParam, SessionParam { 11 | fileprivate let children: [RequestParam] 12 | 13 | init(children: [RequestParam]) { 14 | self.children = children 15 | } 16 | 17 | func buildParam(_ request: inout URLRequest) { 18 | children 19 | .sorted { a, _ in (a is Url) } 20 | .filter { !($0 is SessionParam) || $0 is CombinedParams } 21 | .forEach { 22 | $0.buildParam(&request) 23 | } 24 | } 25 | 26 | func buildConfiguration(_ configuration: URLSessionConfiguration) { 27 | children 28 | .compactMap { $0 as? SessionParam } 29 | .forEach { 30 | $0.buildConfiguration(configuration) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/EmptyParam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 16/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EmptyParam: RequestParam { 11 | public func buildParam(_ request: inout URLRequest) {} 12 | } 13 | 14 | extension EmptyParam: FormParam { 15 | public func buildData(_ data: inout Data, with boundary: String) {} 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Header.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 15/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Creates a `HeaderParam` for any number of different headers 11 | public struct Header { 12 | /// Sets the value for any header 13 | public struct `Any`: RequestParam { 14 | private let key: String 15 | private let value: String? 16 | 17 | public init(key: String, value: String) { 18 | self.key = key 19 | self.value = value 20 | } 21 | 22 | public func buildParam(_ request: inout URLRequest) { 23 | request.setValue(value, forHTTPHeaderField: key) 24 | } 25 | } 26 | } 27 | 28 | //@resultBuilder 29 | //struct HeadersBuilder { 30 | // static func buildBlock(_ headers: HeaderParam...) -> RequestParam { 31 | // return CombinedParams(children: headers) 32 | // } 33 | //} 34 | // 35 | // 36 | //struct Headers: RequestParam { 37 | // var type: RequestParamType = .header 38 | // var key: String? = nil 39 | // var value: Any? = nil 40 | // var children: [RequestParam]? 41 | // 42 | // init(@HeadersBuilder builder: () -> RequestParam) { 43 | // self.children = builder().children 44 | // } 45 | //} 46 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Headers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Accept.swift 3 | // PackageTests 4 | // 5 | // Created by Carson Katri on 7/2/19. 6 | // Copyright © 2019 Carson Katri. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Header { 12 | /// Specifies the media type the client is expecting and able to handle 13 | static func Accept(_ type: MediaType) -> RequestParam { 14 | Header.Any(key: "Accept", value: type.rawValue) 15 | } 16 | /// Sets the `Accept` header to a custom MIME type 17 | /// - Parameter type: The MIME type, such as: `application/json` 18 | static func Accept(_ type: String) -> RequestParam { 19 | Header.Any(key: "Accept", value: type) 20 | } 21 | /// Authenticates the `Request` for HTTP authentication 22 | static func Authorization(_ auth: Auth) -> RequestParam { 23 | Header.Any(key: "Authorization", value: auth.value) 24 | } 25 | 26 | /// Specifies caching mechanisms for the `Request` 27 | static func CacheControl(_ cache: CacheType) -> RequestParam { 28 | Header.Any(key: "Cache-Control", value: cache.value) 29 | } 30 | /// The length of the `Body` in octets (8-bit bytes) 31 | /// - Parameter octets: Length in 8-bit bytes 32 | static func ContentLength(_ octets: Int) -> RequestParam { 33 | Header.Any(key: "Content-Length", value: "\(octets)") 34 | } 35 | /// The `MediaType` of the `Body` 36 | /// - Note: Used with `Method(.post)` and `Method(.put)` 37 | static func ContentType(_ type: MediaType) -> RequestParam { 38 | Header.Any(key: "Content-Type", value: type.rawValue) 39 | } 40 | 41 | /// The domain name of the server (for virtual hosting), and optionally, the port. 42 | static func Host(_ host: String, port: String = "") -> RequestParam { 43 | Header.Any(key: "Host", value: host + port) 44 | } 45 | 46 | /// Sets the `Request` up for CORS 47 | /// - Parameter origin: The url of the origin of the `Request` 48 | static func Origin(_ origin: String) -> RequestParam { 49 | Header.Any(key: "Origin", value: origin) 50 | } 51 | 52 | /// The address of the previous page which requested the current one 53 | static func Referer(_ url: String) -> RequestParam { 54 | Header.Any(key: "Referer", value: url) 55 | } 56 | 57 | /// Sets the user agent string 58 | static func UserAgent(_ userAgent: UserAgent) -> RequestParam { 59 | Header.Any(key: "User-Agent", value: userAgent.rawValue) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Method.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Method.swift 3 | // Request 4 | // 5 | // Created by Carson Katri on 7/10/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The method of the HTTP request, such as `GET` or `POST` 11 | public enum MethodType: String { 12 | case get = "GET" 13 | case head = "HEAD" 14 | case post = "POST" 15 | case put = "PUT" 16 | case delete = "DELETE" 17 | case connect = "CONNECT" 18 | case options = "OPTIONS" 19 | case trace = "TRACE" 20 | case patch = "PATCH" 21 | } 22 | 23 | /// Sets the method of the `Request` 24 | public struct Method: RequestParam { 25 | public var type: MethodType? 26 | 27 | public init(_ type: MethodType) { 28 | self.type = type 29 | } 30 | 31 | public func buildParam(_ request: inout URLRequest) { 32 | request.httpMethod = type?.rawValue 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Query.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 15/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Sets the query string of the `Request` 11 | /// 12 | /// `[key:value, key2:value2]` becomes `?key=value&key2=value2` 13 | public struct Query: RequestParam { 14 | private let children: [QueryParam] 15 | 16 | /// Creates the `Query` from `[key:value]` pairs 17 | /// - Parameter params: Key-value pairs describing the `Query` 18 | public init(_ params: [String:String]) { 19 | children = params.map { 20 | QueryParam($0.key, value: $0.value) 21 | } 22 | } 23 | 24 | /// Creates the `Query` directly from an array of `QueryParam`s 25 | public init(_ params: [QueryParam]) { 26 | self.children = params 27 | } 28 | 29 | public func buildParam(_ request: inout URLRequest) { 30 | guard 31 | let url = request.url, 32 | var components = URLComponents(string: url.absoluteString) 33 | else { 34 | fatalError("Couldn't create URLComponents, check if parameters are valid") 35 | } 36 | 37 | components.queryItems = children.map { $0.urlQueryItem } 38 | request.url = components.url 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/QueryParam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Query.swift 3 | // Request 4 | // 5 | // Created by Carson Katri on 7/10/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A key-value pair for a part of the query string 11 | public struct QueryParam: RequestParam { 12 | private var key: String 13 | private var value: String 14 | 15 | public init(_ key: String, value: String) { 16 | self.key = key 17 | self.value = value 18 | } 19 | 20 | public func buildParam(_ request: inout URLRequest) { 21 | guard 22 | let url = request.url, 23 | var components = URLComponents(string: url.absoluteString) 24 | else { 25 | fatalError("Couldn't create URLComponents, check if parameters are valid") 26 | } 27 | 28 | components.queryItems = [urlQueryItem] 29 | request.url = components.url 30 | } 31 | } 32 | 33 | extension QueryParam { 34 | var urlQueryItem: URLQueryItem { 35 | URLQueryItem(name: key, value: value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/RequestParam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/30/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A parameter used to build the `Request` 11 | public protocol RequestParam { 12 | func buildParam(_ request: inout URLRequest) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/SessionParam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 15/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol SessionParam: RequestParam { 11 | func buildConfiguration(_ configuration: URLSessionConfiguration) 12 | } 13 | 14 | extension SessionParam { 15 | public func buildParam(_ request: inout URLRequest) { 16 | fatalError("SessionParam shouldn't build URLRequest") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Timeout.Source.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by brennobemoura on 15/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Timeout { 11 | struct Source: OptionSet { 12 | public let rawValue: Int 13 | 14 | public init(rawValue: Int) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | public static let request = Self(rawValue: 1 << 0) 19 | public static let resource = Self(rawValue: 1 << 1) 20 | 21 | public static let all: Self = [.request, .resource] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Timeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/23/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Sets the `timeoutIntervalForRequest` and/or `timeoutIntervalForResource` of the `Request` 11 | public struct Timeout: SessionParam { 12 | private let timeout: TimeInterval 13 | private let source: Source 14 | 15 | public init(_ timeout: TimeInterval, for source: Source = .all) { 16 | self.timeout = timeout 17 | self.source = source 18 | } 19 | 20 | public func buildConfiguration(_ configuration: URLSessionConfiguration) { 21 | if source.contains(.request) { 22 | configuration.timeoutIntervalForRequest = timeout 23 | } 24 | 25 | if source.contains(.resource) { 26 | configuration.timeoutIntervalForResource = timeout 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Url/ProtocolType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 7/13/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The protocol for the `Url` 11 | public enum ProtocolType: String { 12 | case http = "http" 13 | case https = "https" 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Request/Request/RequestParams/Url/Url.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 6/30/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Sets the URL of the `Request`. 11 | /// - Precondition: Only use one URL in your `Request`. To group or chain requests, use a `RequestGroup` or `RequestChain`. 12 | public struct Url: RequestParam, Codable { 13 | private let type: ProtocolType? 14 | fileprivate let path: String 15 | 16 | public init(_ path: String) { 17 | self.type = nil 18 | self.path = path 19 | } 20 | 21 | public init(`protocol` type: ProtocolType, url: String) { 22 | self.type = type 23 | self.path = url 24 | } 25 | 26 | public func buildParam(_ request: inout URLRequest) { 27 | request.url = URL(string: absoluteString) 28 | } 29 | 30 | internal var absoluteString: String { 31 | if let type = type { 32 | return "\(type.rawValue)://\(path)" 33 | } 34 | 35 | return path 36 | } 37 | 38 | public init(from decoder: Decoder) throws { 39 | let container = try decoder.singleValueContainer() 40 | self.init(try container.decode(String.self)) 41 | } 42 | 43 | public func encode(to encoder: Encoder) throws { 44 | var container = encoder.singleValueContainer() 45 | try container.encode(absoluteString) 46 | } 47 | } 48 | 49 | extension Url: Equatable { 50 | public static func == (lhs: Self, rhs: Self) -> Bool { 51 | lhs.absoluteString == rhs.absoluteString 52 | } 53 | } 54 | 55 | public extension Url { 56 | func append(_ string: String) -> Url { 57 | if let type = type { 58 | return .init(protocol: type, url: "\(path)\(string)") 59 | } 60 | 61 | return .init("\(path)\(string)") 62 | } 63 | } 64 | 65 | public func + (_ url: Url, _ complementary: String) -> Url { 66 | url.append(complementary) 67 | } 68 | 69 | public func + (_ lhs: Url, _ rhs: Url) -> Url { 70 | lhs.append(rhs.path) 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Request/SwiftUI/PropertyWrappers/Requested.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Requested.swift 3 | // 4 | // 5 | // Created by Carson Katri on 1/17/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 12 | @propertyWrapper 13 | public struct Requested: DynamicProperty { 14 | @StateObject private var requestStore: RequestStore 15 | 16 | final class RequestStore: ObservableObject { 17 | @Published var status: RequestStatus = .loading 18 | private var cancellable: AnyCancellable? 19 | var request: AnyRequest { 20 | didSet { 21 | call() 22 | } 23 | } 24 | 25 | init(request: AnyRequest) { 26 | self.request = request 27 | call() 28 | } 29 | 30 | func call() { 31 | print("Calling") 32 | self.status = .loading 33 | cancellable = request 34 | .objectPublisher 35 | .receive(on: DispatchQueue.main) 36 | .sink { [weak self] completion in 37 | switch completion { 38 | case let .failure(error): 39 | self?.status = .failure(error) 40 | case .finished: break 41 | } 42 | } receiveValue: { [weak self] result in 43 | self?.status = .success(result) 44 | } 45 | } 46 | } 47 | 48 | public init(wrappedValue: AnyRequest) { 49 | self._requestStore = .init(wrappedValue: .init(request: wrappedValue)) 50 | } 51 | 52 | public var wrappedValue: AnyRequest { 53 | get { 54 | requestStore.request 55 | } 56 | nonmutating set { 57 | requestStore.request = newValue 58 | } 59 | } 60 | 61 | public var projectedValue: RequestStatus { 62 | requestStore.status 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Request/SwiftUI/RequestStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestStatus.swift 3 | // 4 | // 5 | // Created by Carson Katri on 1/17/21. 6 | // 7 | 8 | public enum RequestStatus { 9 | case loading 10 | case success(Value) 11 | case failure(Error) 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Request/SwiftUI/Views/RequestImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestImage.swift 3 | // 4 | // 5 | // Created by Carson Katri on 7/28/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A view that asynchronously loads an image 11 | /// 12 | /// It automatically has animations to transition from a placeholder and the image. 13 | /// 14 | /// It takes a `Url` or `Request`, a placeholder, the `ContentMode` for displaying the image, and an `Animation` for switching 15 | public struct RequestImage: View { 16 | private let request: Request 17 | private let placeholder: Placeholder 18 | private let animation: Animation? 19 | private let contentMode: ContentMode 20 | #if os(OSX) 21 | @State private var image: NSImage? = nil 22 | #else 23 | @State private var image: UIImage? = nil 24 | #endif 25 | 26 | public init(_ url: Url, @ViewBuilder placeholder: () -> Placeholder, contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) { 27 | self.request = Request { url } 28 | self.placeholder = placeholder() 29 | self.animation = animation 30 | self.contentMode = contentMode 31 | } 32 | 33 | public var body: some View { 34 | Group { 35 | if let image = image { 36 | #if os(OSX) 37 | Image(nsImage: image) 38 | .resizable() 39 | #else 40 | Image(uiImage: image) 41 | .resizable() 42 | #endif 43 | } else { 44 | placeholder 45 | .onAppear { 46 | self.request.onData { data in 47 | #if os(OSX) 48 | self.image = NSImage(data: data) 49 | #else 50 | self.image = UIImage(data: data) 51 | #endif 52 | } 53 | .call() 54 | } 55 | } 56 | } 57 | .aspectRatio(contentMode: contentMode) 58 | .animation(animation) 59 | } 60 | } 61 | 62 | extension RequestImage where Placeholder == Image { 63 | #if os(OSX) 64 | public init(_ url: Url, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) { 65 | self.init(Request { url }, placeholder: placeholder, contentMode: contentMode, animation: animation) 66 | } 67 | #else 68 | public init(_ url: Url, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) { 69 | self.init(Request { url }, placeholder: placeholder, contentMode: contentMode, animation: animation) 70 | } 71 | #endif 72 | 73 | #if os(OSX) 74 | public init(_ request: Request, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) { 75 | self.request = request 76 | self.placeholder = placeholder.resizable() 77 | self.animation = animation 78 | self.contentMode = contentMode 79 | } 80 | #else 81 | public init(_ request: Request, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) { 82 | self.request = request 83 | self.placeholder = placeholder.resizable() 84 | self.animation = animation 85 | self.contentMode = contentMode 86 | } 87 | #endif 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Request/SwiftUI/Views/RequestView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestView.swift 3 | // PackageTests 4 | // 5 | // Created by Carson Katri on 7/9/19. 6 | // Copyright © 2019 Carson Katri. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | /// A view that asynchronously loads data from the web 13 | /// 14 | /// `RequestView` is powered by a `Request`. It handles loading the data, and you can focus on building your app. 15 | /// 16 | /// It takes a `Request`, a placeholder and any content you want rendered. 17 | public struct RequestView : View where Value: Decodable, Content: View { 18 | private let request: AnyRequest 19 | private let content: (RequestStatus) -> Content 20 | @State private var result: RequestStatus = .loading 21 | @State private var performedOnAppear = false 22 | @State private var cancellables: [AnyCancellable] = [] 23 | 24 | public init( 25 | _ request: AnyRequest, 26 | @ViewBuilder content: @escaping (RequestStatus) -> Content 27 | ) { 28 | print(request) 29 | self.request = request 30 | self.content = content 31 | } 32 | 33 | func perform() { 34 | self.result = .loading 35 | request 36 | .objectPublisher 37 | .receive(on: DispatchQueue.main) 38 | .sink { 39 | switch $0 { 40 | case let .failure(error): 41 | self.result = .failure(error) 42 | case .finished: break 43 | } 44 | } receiveValue: { 45 | self.result = .success($0) 46 | } 47 | .store(in: &cancellables) 48 | } 49 | 50 | public var body: some View { 51 | if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { 52 | Group { 53 | content(result) 54 | } 55 | .onAppear { 56 | if !performedOnAppear { 57 | perform() 58 | performedOnAppear = true 59 | } 60 | } 61 | .onChange(of: request) { _ in 62 | perform() 63 | } 64 | } else { 65 | Group { 66 | content(result) 67 | } 68 | .onAppear { 69 | if !performedOnAppear { 70 | perform() 71 | performedOnAppear = true 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | extension RequestView where Value == Data, Content == AnyView { 79 | @available(*, deprecated, message: "Optional result bodies are deprecated. Please use `RequestStatus` bodies instead.") 80 | public init( 81 | _ request: Request, 82 | @ViewBuilder content: @escaping (Data?) -> TupleView<(Content, Placeholder)> 83 | ) { 84 | self.request = request 85 | self.content = { 86 | switch $0 { 87 | case .loading, .failure: return AnyView(content(nil).value.1) 88 | case let .success(result): return AnyView(content(result).value.0) 89 | } 90 | } 91 | } 92 | 93 | @available(*, deprecated, message: "Optional result bodies are deprecated. Please use `RequestStatus` bodies instead.") 94 | public init( 95 | _ type: ResponseType.Type, 96 | _ request: Request, 97 | @ViewBuilder content: @escaping (ResponseType?) -> TupleView<(Content, Placeholder)> 98 | ) { 99 | self.request = request 100 | self.content = { 101 | switch $0 { 102 | case .loading, .failure: return AnyView(content(nil).value.1) 103 | case let .success(data): 104 | if let res = try? JSONDecoder().decode(type, from: data) { 105 | return AnyView(content(res).value.0) 106 | } else { 107 | return AnyView(content(nil).value.1) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carson-katri/swift-request/4fd7f593d10dfe310721de86bda601d817d11547/Tests/.DS_Store -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import RequestTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += RequestTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/RequestTests/JsonTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsonTests.swift 3 | // RequestTests 4 | // 5 | // Created by Carson Katri on 7/12/19. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import Json 11 | @testable import Request 12 | 13 | class JsonTests: XCTestCase { 14 | let complexJson = """ 15 | { 16 | "firstName": "Carson", 17 | "lastName": "Katri", 18 | "likes": [ 19 | "programming", 20 | "swiftui", 21 | "webdev" 22 | ], 23 | "isEmployed": true, 24 | "projects": [ 25 | { 26 | "name": "swift-request", 27 | "description": "Make requests in Swift the declarative way.", 28 | "stars": 91, 29 | "passing": true, 30 | "codeCov": 0.98 31 | }, 32 | { 33 | "name": "CardKit", 34 | "description": "iOS 11 Cards in Swift", 35 | "stars": 58, 36 | "passing": null, 37 | "codeCov": null 38 | }, 39 | ], 40 | } 41 | """ 42 | func testParse() { 43 | guard let _ = try? Json(complexJson) else { 44 | XCTAssert(false) 45 | return 46 | } 47 | XCTAssert(true) 48 | } 49 | 50 | func testMeasureParse() { 51 | measure { 52 | let _ = try? Json(complexJson) 53 | } 54 | } 55 | 56 | func testSubscripts() { 57 | guard var json = try? Json(complexJson) else { 58 | XCTAssert(false) 59 | return 60 | } 61 | let subscripts: [JsonSubscript] = ["projects", 0, "codeCov"] 62 | 63 | json[] = 0 64 | 65 | XCTAssertEqual(json["isEmployed"].bool, true) 66 | json["isEmployed"] = false 67 | XCTAssertEqual(json["isEmployed"].bool, false) 68 | 69 | XCTAssertEqual(json["projects", 0, "stars"].int, 91) 70 | json["projects", 0, "stars"] = 10 71 | XCTAssertEqual(json["projects", 0, "stars"].int, 10) 72 | 73 | XCTAssertEqual(json[subscripts].double, 0.98) 74 | json[subscripts] = 0.49 75 | XCTAssertEqual(json[subscripts].double, 0.49) 76 | 77 | } 78 | 79 | func testAccessors() { 80 | guard let json = try? Json(complexJson) else { 81 | XCTAssert(false) 82 | return 83 | } 84 | let _: [Any] = [ 85 | json.firstName.string, 86 | json.firstName.stringOptional as Any, 87 | json.likes.array, 88 | json.likes.arrayOptional as Any, 89 | json.projects.count, 90 | json.projects[0].stars.int, 91 | json.projects[0].stars.intOptional as Any, 92 | json.projects[0].codeCov.double, 93 | json.projects[1].codeCov.double, 94 | json.projects[1].codeCov.doubleOptional as Any, 95 | json.projects[1].passing.boolOptional as Any, 96 | json.projects[1].passing.bool as Any, 97 | json.value, 98 | ] 99 | XCTAssert(true) 100 | } 101 | 102 | func testSet() { 103 | guard var json = try? Json(complexJson) else { 104 | XCTAssert(false) 105 | return 106 | } 107 | json.firstName = "Cameron" 108 | json.projects[0].stars = 100 109 | json.likes = ["Hello", "World"] 110 | json.projects[1] = ["name" : "hello", "description" : "world"] 111 | XCTAssertEqual(json["firstName"].string, "Cameron") 112 | XCTAssertEqual(json["projects"][0].stars.int, 100) 113 | XCTAssertEqual(json["likes"][0].string, "Hello") 114 | XCTAssertEqual(json["likes"][1].string, "World") 115 | XCTAssertEqual(json["projects"][1]["name"].string, "hello") 116 | } 117 | 118 | func testStringify() { 119 | let _ = Json(["title": "hello", "subtitle": "world"]).data 120 | guard let stringified = Json(["title": "hello", "subtitle": 1]).stringified else { 121 | XCTAssert(false) 122 | return 123 | } 124 | XCTAssert(stringified == #"{"title":"hello","subtitle":1}"# || stringified == #"{"subtitle":1,"title":"hello"}"#) 125 | } 126 | 127 | func testMeasureStringify() { 128 | self.measure { 129 | let _ = Json(["title": "hello", "subtitle": 1]).stringified 130 | } 131 | } 132 | 133 | static var allTest = [ 134 | ("parse", testParse), 135 | ("measureParse", testMeasureParse), 136 | ("subscripts", testSubscripts), 137 | ("accessors", testAccessors), 138 | ("set", testSet), 139 | ("stringify", testStringify), 140 | ("measureStringify", testMeasureStringify), 141 | ] 142 | 143 | } 144 | -------------------------------------------------------------------------------- /Tests/RequestTests/RequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Json 3 | import Combine 4 | @testable import Request 5 | 6 | final class RequestTests: XCTestCase { 7 | func performRequest(_ request: Request) { 8 | let expectation = self.expectation(description: #function) 9 | var response: Data? = nil 10 | var error: Error? = nil 11 | request 12 | .onError { err in 13 | error = err 14 | expectation.fulfill() 15 | } 16 | .onData { data in 17 | response = data 18 | expectation.fulfill() 19 | }.call() 20 | waitForExpectations(timeout: 10000) 21 | if error != nil { 22 | XCTAssert(false) 23 | } else if response != nil { 24 | XCTAssert(true) 25 | } 26 | } 27 | 28 | func testSimpleRequest() { 29 | performRequest(Request { 30 | Url("https://jsonplaceholder.typicode.com/todos") 31 | }) 32 | } 33 | 34 | func testRequestWithCondition() { 35 | let condition = true 36 | performRequest(Request { 37 | if condition { 38 | Url(protocol: .https, url: "jsonplaceholder.typicode.com/todos") 39 | } 40 | if !condition { 41 | Url("invalidurl") 42 | } 43 | }) 44 | } 45 | 46 | func testPost() { 47 | struct Todo: Codable { 48 | let title: String 49 | let completed: Bool 50 | let userId: Int 51 | } 52 | performRequest(Request { 53 | Url("https://jsonplaceholder.typicode.com/todos") 54 | Method(.post) 55 | Body([ 56 | "title": "My Post", 57 | "completed": true, 58 | "userId": 3, 59 | ]) 60 | Body("{\"userId\" : 3,\"title\" : \"My Post\",\"completed\" : true}") 61 | RequestBody(Todo( 62 | title: "My Post", 63 | completed: true, 64 | userId: 3 65 | )) 66 | }) 67 | } 68 | 69 | func testQuerySingleParams() { 70 | performRequest(Request { 71 | Url("https://jsonplaceholder.typicode.com/todos") 72 | Method(.get) 73 | QueryParam("userId", value: "1") 74 | QueryParam("password", value: "2") 75 | }) 76 | } 77 | 78 | func testQuery() { 79 | performRequest(Request { 80 | Url("https://jsonplaceholder.typicode.com/todos") 81 | Method(.get) 82 | Query(["userId":"1", "password": "2"]) 83 | Query([QueryParam("key", value: "value"), QueryParam("key2", value: "value2")]) 84 | }) 85 | } 86 | 87 | func testURLConcatenatedStringQuery() { 88 | let baseUrl = Url(protocol: .https, url: "jsonplaceholder.typicode.com") 89 | let todosEndpoint = "/todos" 90 | 91 | XCTAssertEqual(baseUrl + todosEndpoint, Url("https://jsonplaceholder.typicode.com/todos")) 92 | } 93 | 94 | func testURLConcatenatedURLQuery() { 95 | let baseUrl = Url("https://jsonplaceholder.typicode.com") 96 | let todosEndpoint = Url("/todos") 97 | 98 | XCTAssertEqual(baseUrl + todosEndpoint, Url("https://jsonplaceholder.typicode.com/todos")) 99 | } 100 | 101 | func testBuildEitherQuery() { 102 | enum AuthProvider { 103 | case explicity(userId: String, password: String) 104 | case barrer(String) 105 | } 106 | 107 | let provider = AuthProvider.explicity(userId: "1", password: "2") 108 | 109 | performRequest(Request { 110 | Url("https://jsonplaceholder.typicode.com/todos") 111 | Method(.get) 112 | 113 | switch provider { 114 | case .explicity(let userId, let password): 115 | Query(["userId":userId, "password": password]) 116 | case .barrer(let token): 117 | Header.Authorization(.bearer(token)) 118 | } 119 | 120 | Query([QueryParam("key", value: "value"), QueryParam("key2", value: "value2")]) 121 | }) 122 | } 123 | 124 | func testComplexRequest() { 125 | performRequest(Request { 126 | Url("https://jsonplaceholder.typicode.com/todos") 127 | Method(.get) 128 | Query(["userId":"1"]) 129 | Header.CacheControl(.noCache) 130 | }) 131 | } 132 | 133 | func testHeaders() { 134 | performRequest(Request { 135 | Url("https://jsonplaceholder.typicode.com/todos") 136 | Header.Any(key: "Custom-Header", value: "value123") 137 | Header.Accept(.json) 138 | Header.Accept("text/html") 139 | Header.Authorization(.basic(username: "carsonkatri", password: "password123")) 140 | Header.Authorization(.bearer("authorizationToken")) 141 | Header.CacheControl(.maxAge(1000)) 142 | Header.CacheControl(.maxStale(1000)) 143 | Header.CacheControl(.minFresh(1000)) 144 | Header.ContentLength(0) 145 | Header.ContentType(.xml) 146 | Header.Host("jsonplaceholder.typicode.com") 147 | Header.Origin("www.example.com") 148 | Header.Referer("redirectfrom.example.com") 149 | Header.UserAgent(.firefoxMac) 150 | }) 151 | } 152 | 153 | func testStatusCode() { 154 | let expectation = self.expectation(description: #function) 155 | let statusCodeExpectation = self.expectation(description: #function+"status") 156 | var response: String? = nil 157 | var error: Error? = nil 158 | var statusCode: Int? = nil 159 | 160 | Request { 161 | Url("https://jsonplaceholder.typicode.com/todos") 162 | } 163 | .onError { err in 164 | error = err 165 | expectation.fulfill() 166 | } 167 | .onString { result in 168 | response = result 169 | expectation.fulfill() 170 | } 171 | .onStatusCode { code in 172 | statusCode = code 173 | statusCodeExpectation.fulfill() 174 | } 175 | .call() 176 | waitForExpectations(timeout: 10000) 177 | if error != nil { 178 | XCTAssert(false) 179 | } else if statusCode != nil { 180 | XCTAssert(true) 181 | } else if response != nil { 182 | XCTAssert(true) 183 | } 184 | } 185 | 186 | func testObject() { 187 | struct Todo: Decodable { 188 | let id: Int 189 | let userId: Int 190 | let title: String 191 | let completed: Bool 192 | } 193 | 194 | let expectation = self.expectation(description: #function) 195 | var response: [Todo]? = nil 196 | var error: Error? = nil 197 | 198 | AnyRequest<[Todo]> { 199 | Url("https://jsonplaceholder.typicode.com/todos") 200 | } 201 | .onError { err in 202 | error = err 203 | expectation.fulfill() 204 | } 205 | .onObject { (todos: [Todo]?) in 206 | response = todos 207 | expectation.fulfill() 208 | } 209 | .call() 210 | waitForExpectations(timeout: 10000) 211 | if error != nil { 212 | XCTAssert(false) 213 | } else if response != nil { 214 | XCTAssert(true) 215 | } 216 | } 217 | 218 | func testString() { 219 | let expectation = self.expectation(description: #function) 220 | var response: String? = nil 221 | var error: Error? = nil 222 | 223 | Request { 224 | Url("https://jsonplaceholder.typicode.com/todos") 225 | } 226 | .onError { err in 227 | error = err 228 | expectation.fulfill() 229 | } 230 | .onString { result in 231 | response = result 232 | expectation.fulfill() 233 | } 234 | .call() 235 | waitForExpectations(timeout: 10000) 236 | if error != nil { 237 | XCTAssert(false) 238 | } else if response != nil { 239 | XCTAssert(true) 240 | } 241 | } 242 | 243 | func testJson() { 244 | let expectation = self.expectation(description: #function) 245 | var response: Json? = nil 246 | var error: Error? = nil 247 | 248 | Request { 249 | Url("https://jsonplaceholder.typicode.com/todos") 250 | } 251 | .onError { err in 252 | error = err 253 | expectation.fulfill() 254 | } 255 | .onJson { result in 256 | response = result 257 | expectation.fulfill() 258 | } 259 | .call() 260 | waitForExpectations(timeout: 10000) 261 | if error != nil { 262 | XCTAssert(false) 263 | } else if let response = response, response.count > 0 { 264 | XCTAssert(true) 265 | } 266 | } 267 | 268 | func testRequestGroup() { 269 | let expectation = self.expectation(description: #function) 270 | var loaded: Int = 0 271 | var datas: Int = 0 272 | var strings: Int = 0 273 | var jsons: Int = 0 274 | var errors: Int = 0 275 | let numberOfResponses = 10 276 | RequestGroup { 277 | Request { 278 | Url("https://jsonplaceholder.typicode.com/todos") 279 | } 280 | Request { 281 | Url("https://jsonplaceholder.typicode.com/posts") 282 | } 283 | Request { 284 | Url("https://jsonplaceholder.typicode.com/todos/1") 285 | } 286 | Request { 287 | Url("invalidURL") 288 | } 289 | } 290 | .onData { (index, data) in 291 | if data != nil { 292 | loaded += 1 293 | datas += 1 294 | } 295 | if loaded >= numberOfResponses { 296 | expectation.fulfill() 297 | } 298 | } 299 | .onString { (index, string) in 300 | if string != nil { 301 | loaded += 1 302 | strings += 1 303 | } 304 | if loaded >= numberOfResponses { 305 | expectation.fulfill() 306 | } 307 | } 308 | .onJson { (index, json) in 309 | if json != nil { 310 | loaded += 1 311 | jsons += 1 312 | } 313 | if loaded >= numberOfResponses { 314 | expectation.fulfill() 315 | } 316 | } 317 | .onError({ (index, error) in 318 | loaded += 1 319 | errors += 1 320 | if loaded >= numberOfResponses { 321 | expectation.fulfill() 322 | } 323 | }) 324 | .call() 325 | waitForExpectations(timeout: 10000) 326 | XCTAssertEqual(loaded, numberOfResponses) 327 | XCTAssertEqual(datas, 3) 328 | XCTAssertEqual(strings, 3) 329 | XCTAssertEqual(jsons, 3) 330 | XCTAssertEqual(errors, 1) 331 | } 332 | 333 | func testRequestChain() { 334 | let expectation = self.expectation(description: #function) 335 | var success = false 336 | RequestChain { 337 | Request.chained { (data, err) in 338 | Url("https://jsonplaceholder.typicode.com/todos") 339 | Method(.get) 340 | } 341 | Request.chained { (data, err) in 342 | let json = try? Json(data[0]!) 343 | return Url("https://jsonplaceholder.typicode.com/todos/\(json?[0]["id"].int ?? 0)") 344 | } 345 | } 346 | .call { (data, errors) in 347 | if data.count > 1 { 348 | success = true 349 | } 350 | expectation.fulfill() 351 | } 352 | waitForExpectations(timeout: 10000) 353 | XCTAssert(success) 354 | } 355 | 356 | func testRequestChainErrors() { 357 | let expectation = self.expectation(description: #function) 358 | var success = false 359 | RequestChain { 360 | Request.chained { (data, err) in 361 | Url("invalidurl") 362 | } 363 | Request.chained { (data, err) in 364 | Url("https://jsonplaceholder.typicode.com/thispagedoesnotexist") 365 | } 366 | } 367 | .call { (data, errors) in 368 | if errors.count == 2 { 369 | success = true 370 | } 371 | expectation.fulfill() 372 | } 373 | waitForExpectations(timeout: 10000) 374 | XCTAssert(success) 375 | } 376 | 377 | func testAnyRequest() { 378 | let expectation = self.expectation(description: #function) 379 | var success = false 380 | 381 | struct Todo: Codable { 382 | let title: String 383 | let completed: Bool 384 | let id: Int 385 | let userId: Int 386 | } 387 | 388 | AnyRequest<[Todo]> { 389 | Url("https://jsonplaceholder.typicode.com/todos") 390 | } 391 | .onObject { todos in 392 | success = true 393 | expectation.fulfill() 394 | } 395 | .onError { err in 396 | expectation.fulfill() 397 | } 398 | .call() 399 | waitForExpectations(timeout: 10000) 400 | XCTAssert(success) 401 | } 402 | 403 | func testError() { 404 | let expectation = self.expectation(description: #function) 405 | var success = false 406 | 407 | Request { 408 | Url("https://jsonplaceholder.typicode./todos") 409 | } 410 | .onError { err in 411 | print(err) 412 | success = true 413 | expectation.fulfill() 414 | } 415 | .call() 416 | waitForExpectations(timeout: 10000) 417 | XCTAssert(success) 418 | } 419 | 420 | func testUpdate() { 421 | let expectation = self.expectation(description: #function) 422 | expectation.expectedFulfillmentCount = 3 423 | expectation.assertForOverFulfill = false 424 | 425 | Request { 426 | Url("https://jsonplaceholder.typicode.com/todos") 427 | } 428 | .update(every: 1) 429 | .onData { data in 430 | expectation.fulfill() 431 | } 432 | .call() 433 | waitForExpectations(timeout: 10000) 434 | } 435 | 436 | func testTimeout() { 437 | let expectation = self.expectation(description: #function) 438 | 439 | Request { 440 | Url("http://10.255.255.1") 441 | Timeout(1, for: .all) 442 | } 443 | .onError { error in 444 | if error.localizedDescription == "The request timed out." { 445 | expectation.fulfill() 446 | } 447 | } 448 | .call() 449 | 450 | waitForExpectations(timeout: 2000) 451 | } 452 | 453 | func testPublisher() { 454 | let expectation = self.expectation(description: #function) 455 | 456 | let publisher = Request { 457 | Url("https://jsonplaceholder.typicode.com/todos") 458 | } 459 | .sink(receiveCompletion: { res in 460 | switch res { 461 | case let .failure(err): 462 | XCTFail(err.localizedDescription) 463 | case .finished: 464 | expectation.fulfill() 465 | } 466 | }, receiveValue: { _ in }) 467 | XCTAssertNotNil(publisher) 468 | 469 | waitForExpectations(timeout: 10000) 470 | } 471 | 472 | func testPublisherDecode() { 473 | struct Todo: Decodable { 474 | let id: Int 475 | let userId: Int 476 | let title: String 477 | let completed: Bool 478 | } 479 | 480 | let expectation = self.expectation(description: #function) 481 | 482 | let publisher = AnyRequest<[Todo]> { 483 | Url("https://jsonplaceholder.typicode.com/todos") 484 | } 485 | .objectPublisher 486 | .sink(receiveCompletion: { res in 487 | switch res { 488 | case let .failure(err): 489 | XCTFail(err.localizedDescription) 490 | case .finished: 491 | expectation.fulfill() 492 | } 493 | }, receiveValue: { todos in 494 | XCTAssertGreaterThan(todos.count, 1) 495 | }) 496 | XCTAssertNotNil(publisher) 497 | 498 | waitForExpectations(timeout: 10000) 499 | } 500 | 501 | func testPublisherGroup() { 502 | let expectation = self.expectation(description: #function) 503 | 504 | let publisher = RequestGroup { 505 | Request { 506 | Url("https://jsonplaceholder.typicode.com/todos") 507 | } 508 | Request { 509 | Url("https://jsonplaceholder.typicode.com/posts") 510 | } 511 | Request { 512 | Url("https://jsonplaceholder.typicode.com/todos/1") 513 | } 514 | } 515 | .sink(receiveCompletion: { res in 516 | switch res { 517 | case .finished: 518 | expectation.fulfill() 519 | case .failure(let err): 520 | XCTFail(err.localizedDescription) 521 | } 522 | }, receiveValue: { vals in 523 | XCTAssertEqual(vals.count, 3) 524 | }) 525 | 526 | XCTAssertNotNil(publisher) 527 | 528 | waitForExpectations(timeout: 10000) 529 | } 530 | 531 | func testOptionalFormData() { 532 | let data = "This will result in a optional data".data(using: .utf8) 533 | 534 | performRequest( 535 | Request { 536 | Url("http://httpbin.org/post") 537 | Method(.post) 538 | 539 | Form { 540 | if let data = data { 541 | Form.Data(data, named: "data.txt", withType: .text) 542 | } 543 | } 544 | } 545 | ) 546 | } 547 | 548 | func testSwitchFormData() { 549 | enum Payload { 550 | case image(Data) 551 | case cover(Data) 552 | } 553 | 554 | let payload = Payload.image("this is the user image".data(using: .utf8)!) 555 | 556 | performRequest( 557 | Request { 558 | Url("http://httpbin.org/post") 559 | Method(.post) 560 | 561 | Form { 562 | switch payload { 563 | case .image(let data): 564 | Form.Data(data, named: "image.txt", withType: .text) 565 | case .cover(let data): 566 | Form.Data(data, named: "cover.txt", withType: .text) 567 | } 568 | } 569 | } 570 | ) 571 | } 572 | 573 | func testValueFormData() { 574 | performRequest( 575 | Request { 576 | Url("http://httpbin.org/post") 577 | Method(.post) 578 | 579 | Form { 580 | Form.Value(key: "name", "test") 581 | Form.Value(key: "email", "test@gmail.com") 582 | Form.Value(key: "age", 17) 583 | } 584 | } 585 | ) 586 | } 587 | 588 | func testArrayFormData() { 589 | let text1 = "Hello World!".data(using: .utf8)! 590 | let text2 = "This is the second line of the document".data(using: .utf8)! 591 | 592 | performRequest( 593 | Request { 594 | Url("http://httpbin.org/post") 595 | Method(.post) 596 | 597 | Form { 598 | Form.Data(text1, named: "text1.txt", withType: .text) 599 | Form.Data(text2, named: "text2.txt", withType: .text) 600 | } 601 | } 602 | ) 603 | } 604 | 605 | func testNestedArrayFormData() { 606 | let text1 = "Hello World!".data(using: .utf8)! 607 | let text2 = "This is the second line of the document".data(using: .utf8)! 608 | 609 | performRequest( 610 | Request { 611 | Url("http://httpbin.org/post") 612 | Method(.post) 613 | 614 | Form { 615 | Form.Data(text1, named: "text1.txt", withType: .text) 616 | Form.Data(text2, named: "text2.txt", withType: .text) 617 | 618 | if true { 619 | Form.Data(text1, named: "text3.txt", withType: .text) 620 | Form.Data(text2, named: "text4.txt", withType: .text) 621 | } 622 | 623 | Form.Data(text1, named: "text5.txt", withType: .text) 624 | Form.Data(text2, named: "text6.txt", withType: .text) 625 | } 626 | } 627 | ) 628 | } 629 | 630 | func testElseFormData() { 631 | let nilData: Data? = nil 632 | 633 | performRequest( 634 | Request { 635 | Url("http://httpbin.org/post") 636 | Method(.post) 637 | 638 | Form { 639 | if let data = nilData { 640 | Form.Data(data, named: "data.txt", withType: .text) 641 | } else { 642 | Form.Data( 643 | "Empty data sent".data(using: .utf8)!, 644 | named: "data.txt", 645 | withType: .text 646 | ) 647 | } 648 | } 649 | } 650 | ) 651 | } 652 | 653 | func testEmptyFormData() { 654 | performRequest( 655 | Request { 656 | Url("http://httpbin.org/post") 657 | Method(.post) 658 | 659 | Form {} 660 | } 661 | ) 662 | } 663 | 664 | #if os(iOS) && targetEnvironment(simulator) 665 | func testSingleFormFile() { 666 | performRequest( 667 | Request { 668 | Url("http://httpbin.org/post") 669 | Method(.post) 670 | 671 | Form.File( 672 | Url("Media/DCIM/100APPLE/IMG_0001.JPG"), 673 | withType: .custom("image/jpg") 674 | ) 675 | } 676 | ) 677 | } 678 | #endif 679 | 680 | func testPublisherUpdate() { 681 | let expectation = self.expectation(description: #function) 682 | var numResponses = 0 683 | let publisher = Request { 684 | Url("https://jsonplaceholder.typicode.com/todos") 685 | } 686 | .updatePublisher(every: 1) 687 | .sink(receiveCompletion: { res in 688 | switch res { 689 | case let .failure(err): 690 | XCTFail(err.localizedDescription) 691 | case .finished: 692 | expectation.fulfill() 693 | } 694 | }, receiveValue: { _ in 695 | numResponses += 1 696 | if numResponses >= 3 { 697 | expectation.fulfill() 698 | } 699 | }) 700 | XCTAssertNotNil(publisher) 701 | 702 | waitForExpectations(timeout: 10000) 703 | } 704 | 705 | static var allTests = [ 706 | ("simpleRequest", testSimpleRequest), 707 | ("post", testPost), 708 | ("query", testQuery), 709 | ("complexRequest", testComplexRequest), 710 | ("headers", testHeaders), 711 | ("onObject", testObject), 712 | ("onStatusCode", testStatusCode), 713 | ("onString", testString), 714 | ("onJson", testJson), 715 | ("requestGroup", testRequestGroup), 716 | ("requestChain", testRequestChain), 717 | ("requestChainErrors", testRequestChainErrors), 718 | ("anyRequest", testAnyRequest), 719 | ("testError", testError), 720 | ("testUpdate", testUpdate), 721 | 722 | ("testPublisher", testPublisher), 723 | ("testPublisherDecode", testPublisherDecode), 724 | ("testPublisherGroup", testPublisherGroup) 725 | ] 726 | } 727 | -------------------------------------------------------------------------------- /Tests/RequestTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(RequestTests.allTests), 7 | testCase(JsonTests.allTests) 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/**/*" 3 | --------------------------------------------------------------------------------