Skip to content

Commit

Permalink
Merge branch 'develop' into 52-urlmock-validation
Browse files Browse the repository at this point in the history
  • Loading branch information
tdeleon authored Jan 30, 2024
2 parents 817796f + 633dc63 commit 98fb6ae
Show file tree
Hide file tree
Showing 19 changed files with 378 additions and 102 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Relax supports the [Swift Package Manager](https://www.swift.org/package-manager
4. Click on **Add Package**

>Tip: `URLMock` is an additional framework provided to aid in testing by mocking responses to requests, and should be
added to your test targets only. For more information, see <doc:Relax#Testing> below.
added to your test targets only. For more information, see [Testing](#testing) below.

#### Import the framework

Expand Down Expand Up @@ -114,7 +114,8 @@ let request = Request(.post, url: URL(string: "https://example.com/users")!) {
}
```

See <doc:DefiningRequests> for more details.
See [Defining Requests](https://swiftpackageindex.com/tdeleon/relax/documentation/relax/definingrequests) for more
details.

### Define a Complex API Structure

Expand Down Expand Up @@ -147,7 +148,8 @@ enum UserService: Service {
let users = try await UserService.Users.getAll.send()
```

See <doc:DefiningAPIStructure> for more details.
See [Defining an API Structure](https://swiftpackageindex.com/tdeleon/relax/documentation/relax/definingapistructure) for
more details.

## Testing

Expand Down
19 changes: 17 additions & 2 deletions Sources/Relax/API Structure/APIComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,20 @@ public protocol APIComponent {
/// - Important: You should not override this property, doing so will not allow properties to be properly inherited by child components.
static var allProperties: Request.Properties { get }

/// The configuration to use for any Requests provided by this component or its children
/// The configuration to use for any Requests provided by this component or its children.
///
/// The default value is ``Request/Configuration-swift.struct/default``
static var configuration: Request.Configuration { get }

/// The URLSession to use for any Requests defined in this component or its children. The default is `URLSession.shared`.
/// The URLSession to use for any Requests defined in this component or its children.
///
/// The default value is `URLSession.shared`.
static var session: URLSession { get }

/// The decoder to use for any Requests provided by this component or its childern.
///
/// The default value is `JSONDecoder()`.
static var decoder: JSONDecoder { get }
}

//MARK: Default Implementation
Expand All @@ -45,6 +54,8 @@ extension APIComponent {
public static var allProperties: Request.Properties { sharedProperties }

public static var session: URLSession { .shared }

public static var decoder: JSONDecoder { JSONDecoder() }
}

//MARK: - APISubComponent
Expand All @@ -66,5 +77,9 @@ extension APISubComponent {
public static var session: URLSession {
Parent.session
}

public static var decoder: JSONDecoder {
Parent.decoder
}
}

18 changes: 14 additions & 4 deletions Sources/Relax/Relax.docc/Request/DefiningRequests.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ You can create Requests in several different ways, ranging from as simple as a o
definition of an entire REST API service. They can be either pre-defined and static, or accept dynamic parameters at
runtime, depending on your needs.

Once you've created your request, see <doc:SendingRequestsAsync>, <doc:SendingRequestsPublisher>, or <doc:SendingRequestsHandler> on how to send them and receive a response.
Once you've created your request, see <doc:SendingRequestsAsync>, <doc:SendingRequestsPublisher>, or
<doc:SendingRequestsHandler> on how to send them and receive a response.

## Request Basics

Expand All @@ -30,7 +31,8 @@ Usually, you will need to customize at least some other properties on the reques
- ``PathComponents``

These can be set using the `properties` parameter on the init method, which takes a ``Request/Properties/Builder``
closure where you set any number of properties to be used in the request using a [result builder](https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html#ID630).
closure where you set any number of properties to be used in the request using a
[result builder](https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html#ID630).

The following builds on the previous example by adding a `Content-Type: application/json` header and appending a query
parameter to the URL, so the final URL will be `https://example.com/users?name=firstname`.
Expand Down Expand Up @@ -63,8 +65,8 @@ let request = Request(.post, url: URL(string: "https://example.com/users")!) {

When sending requests, a `URLSession` is used, which can be configured through the ``Request/session`` property. If not
specified, this property will inherit from the
[`parent`](<doc:Request/init(_:parent:configuration:session:properties:)>) if defined, otherwise it will be set to
`URLSession.shared` by default. See <doc:DefiningAPIStructure> for more on inheritance.
[`parent`](<doc:Request/init(_:parent:configuration:session:decoder:properties:)>) if defined, otherwise it will be set
to `URLSession.shared` by default. See <doc:DefiningAPIStructure> for more on inheritance.

```swift
enum MyService: Service {
Expand Down Expand Up @@ -114,6 +116,14 @@ If the request is standalone (not linked to a parent), then a

See <doc:DefiningAPIStructure> for more on inheritance.

### Decoding Data

When decoding received data from the request, the ``Request/decoder`` property on the request will be used. This
property defaults to either the ``APIComponent/decoder-74ja3`` of the parent if linked, or `JSONDecoder()` if not. You
can override this for a particular request by specifying a different value for the ``Request/decoder`` property.

See <doc:DefiningAPIStructure> for more on inheritance.

### Modifying a Request

While many properties can be pre-defined on requests, there may be cases where values need to be changed after
Expand Down
1 change: 0 additions & 1 deletion Sources/Relax/Relax.docc/Request/Properties/Body.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ Body {

- ``init(_:)``
- ``init(_:encoder:)``
- ``init(_:options:)``
- ``init(value:)``
9 changes: 5 additions & 4 deletions Sources/Relax/Relax.docc/Request/Request.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
### Creating a Request

- <doc:DefiningRequests>
- ``init(_:url:configuration:session:properties:)``
- ``init(_:parent:configuration:session:properties:)``
- ``init(_:url:configuration:session:decoder:properties:)``
- ``init(_:parent:configuration:session:decoder:properties:)``
- ``HTTPMethod-swift.struct``
- ``Configuration-swift.struct``
- ``RequestBuilder``
Expand All @@ -18,6 +18,7 @@
- ``urlRequest``
- ``configuration-swift.property``
- ``session``
- ``decoder``
- ``headers``
- ``queryItems``
- ``pathComponents``
Expand Down Expand Up @@ -45,15 +46,15 @@

- <doc:SendingRequestsAsync>
- ``send(session:)-74uav``
- ``send(decoder:session:)-2kid8``
- ``send(decoder:session:)-667nw``
- ``AsyncResponse``

### Sending Requests with a Publisher

- <doc:SendingRequestsPublisher>

- ``send(session:)-8vwky``
- ``send(decoder:session:)-9jzsp``
- ``send(decoder:session:)-3j2hs``
- ``PublisherResponse``
- ``PublisherModelResponse``

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@ See <doc:DefiningAPIStructure> for more on inheritance.
### Decoding JSON

You can automatically decode JSON into an expected `Decodable` instance using the
``Request/send(decoder:session:)-2kid8`` method.
``Request/send(decoder:session:)-667nw`` method.

> Tip: By default, `JSONDecoder()` is used, but you
can also pass in your own to the `decoder` parameter.
> Tip: The ``Request/decoder`` defined in the request is used by default, but you can pass in your own to override this.
```swift
Task {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ You can automatically decode received data into a `Decodable` instance with the
``Request/send(decoder:session:completion:)`` method. This method also uses a completion handler, but instead of `Data`,
returns the data decoded to a `Decodable` type.

> Tip: By default, `JSONDecoder()` is used, but you can also pass in your own to the `decoder` parameter.
> Tip: The ``Request/decoder`` defined in the request is used by default, but you can pass in your own to override this.
```swift
let request = Request(.get, url: URL(string: "https://example.com")!)
request.send { (result: Result<User, RequestError> in
request.send { (result: Result<User, RequestError>) in
switch result {
case .success(let user):
print("User: \(user)")
case .failure(let error):
print("Request failed - \(error.localizedDescription)")
print("Request failed - \(error)")
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cancellable = request.send()
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Request failed - \(error.localizedDescription)")
print("Request failed - \(error)")
case .finished:
break
}
Expand Down Expand Up @@ -94,10 +94,10 @@ See <doc:DefiningAPIStructure> for more on inheritance.

### Decoding JSON

You can automatically decode received data into a `Decodable` instance with the``Request/send(decoder:session:)-9jzsp``,
You can automatically decode received data into a `Decodable` instance with the``Request/send(decoder:session:)-3j2hs``,
which will publish a ``Request/PublisherModelResponse`` with the decoded data.

> Tip: By default, `JSONDecoder()` is used, but you can also pass in your own to the `decoder` parameter.
> Tip: The ``Request/decoder`` defined in the request is used by default, but you can pass in your own to override this.
```swift
let request = Request(.get, url: URL(string: "https://example.com")!)
Expand All @@ -109,7 +109,7 @@ cancellable = try request.send()
case .finished:
break
}
}, receiveValue: { response in
}, receiveValue: { (response: User) in
print("User: \(response.responseModel)")
})
```
Expand Down
50 changes: 45 additions & 5 deletions Sources/Relax/Request/Request+Send.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,52 @@ extension Request {

/// Send a request with a completion handler, decoding the received data to a Decodable instance
/// - Parameters:
/// - decoder: The decoder to decode received data with. Default is `JSONDecoder()`.
/// - session: When provided, overrides the ``Request/session`` defined in the Request.
/// - parseHTTPStatusErrors: Whether to parse HTTP status codes returned for errors. The default is `false`.
/// - decoder: When set, overrides the ``Request/decoder`` used to decode received data.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - completion: A completion handler with a `Result` of the model type decoded from received data, or ``RequestError`` on failure.
///
///
/// Use this method when you want to decode data into a given model type, and do not need the full `HTTPURLResponse` from the server.
///
/// ```swift
/// let request = Request(.get, url: URL(string: "https://example.com")!)
/// request.send { (result: Result<User, RequestError>) in
/// switch result {
/// case .success(let user):
/// print("User: \(user)")
/// case .failure(let error):
/// print("Request failed - \(error)")
/// }
/// }
/// ```
///
public func send<ResponseModel: Decodable>(
decoder: JSONDecoder? = nil,
session: URLSession? = nil,
completion: @escaping (_ result: Result<ResponseModel, RequestError>) -> Void
) {
send(
decoder: decoder,
session: session
) { (result: Result<Request.ResponseModel<ResponseModel>, RequestError>) in
switch result {
case .success(let success):
completion(.success(success.responseModel))
case .failure(let failure):
completion(.failure(failure))
}
}
}

/// Send a request with a completion handler, decoding the received data to a Decodable instance
/// - Parameters:
/// - decoder: When set, overrides the ``Request/decoder`` used to decode received data.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - completion: A completion handler with the response from the server, including the decoded data as the Decodable type.
///
/// Use this method when decoding a model `Decodable` type and you also need the full `HTTPURLResponse` from the server.
public func send<ResponseModel: Decodable>(
decoder: JSONDecoder = JSONDecoder(),
decoder: JSONDecoder? = nil,
session: URLSession? = nil,
completion: @escaping Request.ModelCompletion<ResponseModel>
) {
Expand All @@ -101,7 +141,7 @@ extension Request {
) { result in
do {
let success = try result.get()
let decoded = try decoder.decode(ResponseModel.self, from: success.data)
let decoded = try (decoder ?? self.decoder).decode(ResponseModel.self, from: success.data)
completion(.success((self, success.urlResponse, decoded)))
} catch let error as DecodingError {
completion(.failure(.decoding(request: self, error: error)))
Expand Down
16 changes: 7 additions & 9 deletions Sources/Relax/Request/Request+SendAsync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ extension Request {
/// Send a request asynchronously
///
/// - Parameters:
/// - session: When provided, overrides the ``Request/session`` defined in the Request.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - Returns: A response containing the request sent, url response, and data.
/// - Throws: A `RequestError` on error.
@discardableResult
Expand All @@ -38,7 +38,7 @@ extension Request {

return try await withCheckedThrowingContinuation { continuation in
task = send(
session: session,
session: session ?? self.session,
autoResumeTask: true
) { result in
switch result {
Expand All @@ -56,19 +56,17 @@ extension Request {

/// Send a request asynchronously, decoding data received to a Decodable instance.
/// - Parameters:
/// - decoder: The decoder to decode received data with. Default is `JSONDecoder()`.
/// - session: When provided, overrides the ``Request/session`` defined in the Request.
/// - decoder: When set, overrides the ``Request/decoder`` used to decode received data.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - Returns: The model, decoded from received data.
/// - Throws: A `RequestError` on error.
public func send<ResponseModel: Decodable>(
decoder: JSONDecoder = JSONDecoder(),
decoder: JSONDecoder? = nil,
session: URLSession? = nil
) async throws -> ResponseModel {
let response: AsyncResponse = try await send(
session: session
)
let response: AsyncResponse = try await send(session: session)
do {
return try decoder.decode(ResponseModel.self, from: response.data)
return try (decoder ?? self.decoder).decode(ResponseModel.self, from: response.data)
} catch let error as DecodingError {
throw RequestError.decoding(request: self, error: error)
} catch {
Expand Down
25 changes: 12 additions & 13 deletions Sources/Relax/Request/Request+SendPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,19 @@ extension Request {
/// - request: The request made
/// - urlResponse: The response received
/// - responseModel: The model decoded from received data
public typealias PublisherModelResponse<Model: Decodable> = (request: Request, urlResponse: HTTPURLResponse, responseModel: Model)
public typealias PublisherModelResponse<Model: Decodable> = (
request: Request,
urlResponse: HTTPURLResponse,
responseModel: Model
)

/// Send a request, returning a Combine publisher
/// - Parameters:
/// - session: When provided, overrides the ``Request/session`` defined in the Request.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - Returns: A Publisher which returns the received data, or a ``RequestError`` on failure.
public func send(
session: URLSession? = nil
) -> AnyPublisher<PublisherResponse, RequestError> {
public func send(session: URLSession? = nil) -> AnyPublisher<PublisherResponse, RequestError> {
Future<PublisherResponse, RequestError> { promise in
send(
session: session,
autoResumeTask: true
) { result in
send(session: session ?? self.session,autoResumeTask: true) { result in
switch result {
case .success(let successResponse):
promise(.success(successResponse))
Expand All @@ -51,16 +50,16 @@ extension Request {

/// Send a request and decode received data to a Decodable instance, returning a Combine publisher
/// - Parameters:
/// - decoder: The decoder to decode received data with. Default is `JSONDecoder()`.
/// - session: When provided, overrides the ``Request/session`` defined in the Request.
/// - decoder: When set, overrides the ``Request/decoder`` used to decode received data.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - Returns: A Pubisher which returns the received data, or a ``RequestError`` on failure.
public func send<ResponseModel: Decodable>(
decoder: JSONDecoder = JSONDecoder(),
decoder: JSONDecoder? = nil,
session: URLSession? = nil
) -> AnyPublisher<ResponseModel, RequestError> {
send(session: session)
.map(\.data)
.decode(type: ResponseModel.self, decoder: decoder)
.decode(type: ResponseModel.self, decoder: decoder ?? self.decoder)
.mapError {
switch $0 {
case let error as DecodingError:
Expand Down
Loading

0 comments on commit 98fb6ae

Please sign in to comment.