├── .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 | 
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | [](https://github.com/carson-katri/swift-request/actions)
9 | [](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 | 
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 |
--------------------------------------------------------------------------------