diff --git a/Sources/Relax/API Structure/APIComponent.swift b/Sources/Relax/API Structure/APIComponent.swift index 6bf0951..e4afb97 100644 --- a/Sources/Relax/API Structure/APIComponent.swift +++ b/Sources/Relax/API Structure/APIComponent.swift @@ -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 @@ -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 @@ -66,5 +77,9 @@ extension APISubComponent { public static var session: URLSession { Parent.session } + + public static var decoder: JSONDecoder { + Parent.decoder + } } diff --git a/Sources/Relax/Relax.docc/Request/DefiningRequests.md b/Sources/Relax/Relax.docc/Request/DefiningRequests.md index bb38c48..b896bd6 100644 --- a/Sources/Relax/Relax.docc/Request/DefiningRequests.md +++ b/Sources/Relax/Relax.docc/Request/DefiningRequests.md @@ -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 , , or on how to send them and receive a response. +Once you've created your request, see , , or + on how to send them and receive a response. ## Request Basics @@ -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`. @@ -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`]() if defined, otherwise it will be set to -`URLSession.shared` by default. See for more on inheritance. +[`parent`]() if defined, otherwise it will be set +to `URLSession.shared` by default. See for more on inheritance. ```swift enum MyService: Service { @@ -114,6 +116,14 @@ If the request is standalone (not linked to a parent), then a See 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 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 diff --git a/Sources/Relax/Relax.docc/Request/Properties/Body.md b/Sources/Relax/Relax.docc/Request/Properties/Body.md index 944187d..755abc5 100644 --- a/Sources/Relax/Relax.docc/Request/Properties/Body.md +++ b/Sources/Relax/Relax.docc/Request/Properties/Body.md @@ -18,5 +18,4 @@ Body { - ``init(_:)`` - ``init(_:encoder:)`` -- ``init(_:options:)`` - ``init(value:)`` diff --git a/Sources/Relax/Relax.docc/Request/Request.md b/Sources/Relax/Relax.docc/Request/Request.md index e6424bf..6555069 100644 --- a/Sources/Relax/Relax.docc/Request/Request.md +++ b/Sources/Relax/Relax.docc/Request/Request.md @@ -5,8 +5,8 @@ ### Creating a Request - -- ``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`` @@ -18,6 +18,7 @@ - ``urlRequest`` - ``configuration-swift.property`` - ``session`` +- ``decoder`` - ``headers`` - ``queryItems`` - ``pathComponents`` @@ -45,7 +46,7 @@ - - ``send(session:)-74uav`` -- ``send(decoder:session:)-2kid8`` +- ``send(decoder:session:)-667nw`` - ``AsyncResponse`` ### Sending Requests with a Publisher @@ -53,7 +54,7 @@ - - ``send(session:)-8vwky`` -- ``send(decoder:session:)-9jzsp`` +- ``send(decoder:session:)-3j2hs`` - ``PublisherResponse`` - ``PublisherModelResponse`` diff --git a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsAsync.md b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsAsync.md index 1f481e5..c567af1 100644 --- a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsAsync.md +++ b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsAsync.md @@ -59,10 +59,9 @@ See 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 { diff --git a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md index d91250e..84ecf0e 100644 --- a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md +++ b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md @@ -78,7 +78,7 @@ 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")!) diff --git a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md index 0d702b7..8d05f98 100644 --- a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md +++ b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md @@ -94,10 +94,10 @@ See 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")!) diff --git a/Sources/Relax/Request/Request+Send.swift b/Sources/Relax/Request/Request+Send.swift index 73cd2b0..7bba456 100644 --- a/Sources/Relax/Request/Request+Send.swift +++ b/Sources/Relax/Request/Request+Send.swift @@ -86,12 +86,11 @@ 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 the response from the server, including the decoded data as the Decodable type. public func send( - decoder: JSONDecoder = JSONDecoder(), + decoder: JSONDecoder? = nil, session: URLSession? = nil, completion: @escaping Request.ModelCompletion ) { @@ -101,7 +100,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))) diff --git a/Sources/Relax/Request/Request+SendAsync.swift b/Sources/Relax/Request/Request+SendAsync.swift index 73c7f04..210424e 100644 --- a/Sources/Relax/Request/Request+SendAsync.swift +++ b/Sources/Relax/Request/Request+SendAsync.swift @@ -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 @@ -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 { @@ -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( - 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 { diff --git a/Sources/Relax/Request/Request+SendPublisher.swift b/Sources/Relax/Request/Request+SendPublisher.swift index 391df31..637bc61 100644 --- a/Sources/Relax/Request/Request+SendPublisher.swift +++ b/Sources/Relax/Request/Request+SendPublisher.swift @@ -24,20 +24,19 @@ extension Request { /// - request: The request made /// - urlResponse: The response received /// - responseModel: The model decoded from received data - public typealias PublisherModelResponse = (request: Request, urlResponse: HTTPURLResponse, responseModel: Model) + public typealias PublisherModelResponse = ( + 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 { + public func send(session: URLSession? = nil) -> AnyPublisher { Future { 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)) @@ -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( - decoder: JSONDecoder = JSONDecoder(), + decoder: JSONDecoder? = nil, session: URLSession? = nil ) -> AnyPublisher { 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: diff --git a/Sources/Relax/Request/Request.swift b/Sources/Relax/Request/Request.swift index fb104ae..5ad805e 100644 --- a/Sources/Relax/Request/Request.swift +++ b/Sources/Relax/Request/Request.swift @@ -48,7 +48,7 @@ import FoundationNetworking /// > Tip: For more details, see , , and , /// , or . /// -public struct Request: Hashable { +public struct Request { /// The HTTP method of the request public var httpMethod: HTTPMethod @@ -100,18 +100,26 @@ public struct Request: Hashable { /// The configuration of the request /// - /// This value will be inherited from the parent ``APIComponent`` (``Service``/``Endpoint``), if the request is linked to a parent. If there is no parent, - /// the default value is ``Request/Configuration-swift.struct/default``. + /// This value will be inherited from the parent ``APIComponent/configuration-5p4i`` property, if the request is linked to a parent. If there is no + /// parent, the default value is ``Request/Configuration-swift.struct/default``. public var configuration: Configuration /// The URLSession to use for this request /// - /// This value will be inherited from the parent ``APIComponent`` (``Service``/``Endpoint``), if the request is linked to a parent. If there is no parent, - /// the default value is `URLSession.shared`. + /// This value will be inherited from the parent ``APIComponent/session-3qjsw`` property, if the request is linked to a parent. If there is no parent, the + /// default value is `URLSession.shared`. /// /// - Tip: The session can also be overridden when sending requests. public var session: URLSession + /// The decoder to use for this request + /// + /// This value will be inherited from the parent ``APIComponent/decoder-bxgv`` property, if the request is linked to a parent. If there is no parent, the + /// default value is `JSONEncoder()`. + /// + /// - Tip: The decoder can also be overridden when sending requests. + public var decoder: JSONDecoder + /// The request URL public var url: URL { var fullURL = _url @@ -164,6 +172,7 @@ public struct Request: Hashable { /// - url: The base URL of the request (this does not include path components and query items which you provide in `properties`). /// - configuration: The configuration for the request. The default is ``Configuration-swift.struct/default``. /// - session: The session to use for the request. The default is `URLSession.shared` + /// - decoder: The decoder to use for the request when receiving data. The default is `JSONDecoder()`. /// - properties: Any additional properties to use in the request, such as the body, headers, query items, or path components. The default value is /// ``Request/Properties/empty`` (no properties). public init( @@ -171,13 +180,15 @@ public struct Request: Hashable { url: URL, configuration: Configuration = .default, session: URLSession = .shared, - @Request.Properties.Builder properties: () -> Properties = { .empty } + decoder: JSONDecoder = JSONDecoder(), + @Request.Properties.Builder properties: () -> Request.Properties = { .empty } ) { self.init( httpMethod: httpMethod, url: url, configuration: configuration, sesssion: session, + decoder: decoder, properties: properties() ) } @@ -186,24 +197,39 @@ public struct Request: Hashable { /// and its' parents. Properties are provided with a ``Request/Properties/Builder``. /// - Parameters: /// - httpMethod: The HTTP method for the request - /// - parent: A parent which provides the base URL, ``Configuration-swift.struct``, and - /// ``APIComponent/sharedProperties-5764x``. + /// - parent: A parent which provides various attributes that the request inherits from. /// - configuration: An optional configuration to override what is provided by the parent. - /// - session: Overrides the session provided by the parent. - /// - properties: A ``Request/Properties/Builder`` closure which provides properties to use in this request. Any provided here will be - /// appended to any provided by `parent`. The default value is ``Request/Properties/empty`` (no properties). + /// - session: Overrides the ``APIComponent/session-3qjsw`` provided by the parent. + /// - decoder: Overrides the ``APIComponent/decoder-bxgv`` provided by the parent. + /// - properties: A ``Request/Properties/Builder`` closure which provides properties to use in this request. The default value is + /// ``Request/Properties/empty`` (no properties). + /// + /// - Note: Any `properties` provided are appended to the ``APIComponent/allProperties-7xy23`` defined on the parent, not replaced. + /// + /// The request will inherit attributes defined on the `parent`, including: + /// * ``APIComponent/baseURL`` + /// * ``APIComponent/configuration-5p4i`` + /// * ``APIComponent/session-36tuc`` + /// * ``APIComponent/decoder-74ja3`` + /// * ``APIComponent/allProperties-7xy23`` + /// + /// You can override any of the above attributes (except for the `baseURL` and `allProperties`) by passing in the corresponding parameters to this + /// method. + /// public init( _ httpMethod: HTTPMethod, parent: APIComponent.Type, configuration: Configuration? = nil, session: URLSession? = nil, - @Request.Properties.Builder properties: () -> Properties = { .empty } + decoder: JSONDecoder? = nil, + @Request.Properties.Builder properties: () -> Request.Properties = { .empty } ) { self.init( httpMethod: httpMethod, url: parent.baseURL, configuration: configuration ?? parent.configuration, sesssion: session ?? parent.session, + decoder: decoder ?? parent.decoder, properties: parent.allProperties + properties() ) } @@ -213,6 +239,7 @@ public struct Request: Hashable { url: URL, configuration: Configuration, sesssion: URLSession, + decoder: JSONDecoder, properties: Properties ) { self._url = url @@ -220,6 +247,7 @@ public struct Request: Hashable { self.httpMethod = httpMethod self.configuration = configuration self.session = sesssion + self.decoder = decoder self._properties = properties } } @@ -268,3 +296,25 @@ public enum RequestBuilder { } +// JSONEncoder/JSONDecoder are not Hashable, so leave it out of the conformance +extension Request: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(_properties) + hasher.combine(configuration) + hasher.combine(httpMethod) + hasher.combine(url) + hasher.combine(session) + } +} + +// JSONEncoder/JSONDecoder are not Equatable, so leave it out of the conformance +extension Request: Equatable { + public static func == (lhs: Request, rhs: Request) -> Bool { + lhs._properties == rhs._properties && + lhs.configuration == rhs.configuration && + lhs.httpMethod == rhs.httpMethod && + lhs.url == rhs.url && + lhs.session == rhs.session + } +} +