From fb67817951d7fa64c833073335684abe19d097e0 Mon Sep 17 00:00:00 2001 From: Brendan Lensink Date: Thu, 15 Jun 2023 13:48:24 -0700 Subject: [PATCH] Add support for request interceptors (#119) * add prototype for request interceptors * remove runway prints * Change Interceptor -> InterceptorList and RequestInterceptor -> Interceptor Remove request level interceptors de-duplicate finalize request data calls * remove request level interceptors add a bunch of documentation, examples, etc for interceptors * code clean up --- CHANGELOG.md | 7 ++ .../Example/Services/AuthNetworkService.swift | 14 ++++ ...rvice.swift => SimpleNetworkService.swift} | 0 Netable/Netable.xcodeproj/project.pbxproj | 30 +++++++-- Netable/Netable/Error.swift | 11 +++- .../Netable/Interceptors/AdaptedRequest.swift | 21 ++++++ .../Netable/Interceptors/Interceptor.swift | 23 +++++++ .../Interceptors/InterceptorList.swift | 66 +++++++++++++++++++ Netable/Netable/Netable.swift | 65 ++++++++++++++---- README.md | 34 ++++++++++ 10 files changed, 253 insertions(+), 18 deletions(-) rename Netable/Example/Services/{NetworkService.swift => SimpleNetworkService.swift} (100%) create mode 100644 Netable/Netable/Interceptors/AdaptedRequest.swift create mode 100644 Netable/Netable/Interceptors/Interceptor.swift create mode 100644 Netable/Netable/Interceptors/InterceptorList.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 9424ee0..d8f4a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## UNRELEASED + + +## [2.2.0] 15-6-22 +- [118] Add new `Interceptors` support. + + ## [2.1.0] 14-12-22 - [104] Add support for partially decoding arrays through new `arrayDecodingStrategy` parameter on `Request`. - [106] Fix `RetryConfiguration` not being marked as `Sendable`. diff --git a/Netable/Example/Services/AuthNetworkService.swift b/Netable/Example/Services/AuthNetworkService.swift index 1493f56..ff363cc 100644 --- a/Netable/Example/Services/AuthNetworkService.swift +++ b/Netable/Example/Services/AuthNetworkService.swift @@ -87,3 +87,17 @@ class AuthNetworkService { self.user.send(nil) } } + +final class MockRequestInterceptor: Interceptor { + func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest { + if let requestURL = request.url, + let mockedURL = Bundle.main.url(forResource: "posts", withExtension: "json"), + requestURL.absoluteString.contains("/all") { + return .mocked(mockedURL) + } + + return .notChanged + } +} + + diff --git a/Netable/Example/Services/NetworkService.swift b/Netable/Example/Services/SimpleNetworkService.swift similarity index 100% rename from Netable/Example/Services/NetworkService.swift rename to Netable/Example/Services/SimpleNetworkService.swift diff --git a/Netable/Netable.xcodeproj/project.pbxproj b/Netable/Netable.xcodeproj/project.pbxproj index 20a7f58..f331d9a 100644 --- a/Netable/Netable.xcodeproj/project.pbxproj +++ b/Netable/Netable.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 3B00B3C726D7EA3C00A1DF79 /* DecodingError+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */; }; A63ABCCA24ABB402004DE84E /* RetryConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63ABCC924ABB402004DE84E /* RetryConfiguration.swift */; }; + B31E6D2B2A2FB6480002AE1E /* InterceptorList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */; }; + B367A3352A33A65000032814 /* Interceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B367A3342A33A65000032814 /* Interceptor.swift */; }; + B367A3372A33AA4900032814 /* AdaptedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B367A3362A33AA4900032814 /* AdaptedRequest.swift */; }; B8C9288A23E9F68000DB2B37 /* Netable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B8C9288023E9F68000DB2B37 /* Netable.framework */; }; B8C9288F23E9F68000DB2B37 /* NetableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C9288E23E9F68000DB2B37 /* NetableTests.swift */; }; B8C9289123E9F68000DB2B37 /* Netable.h in Headers */ = {isa = PBXBuildFile; fileRef = B8C9288323E9F68000DB2B37 /* Netable.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -58,7 +61,7 @@ E186202129425EFF009B6E0C /* Netable.framework in Embeded Framworks */ = {isa = PBXBuildFile; fileRef = B8C9288023E9F68000DB2B37 /* Netable.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E188440A297B3C63009EE74B /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1884409297B3C63009EE74B /* DataManager.swift */; }; E18AAA1029312DF700756455 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA0F29312DF700756455 /* Version.swift */; }; - E18AAA19293524AA00756455 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA18293524AA00756455 /* NetworkService.swift */; }; + E18AAA19293524AA00756455 /* SimpleNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA18293524AA00756455 /* SimpleNetworkService.swift */; }; E18AAA1B2935251400756455 /* GetVersionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA1A2935251400756455 /* GetVersionRequest.swift */; }; E18AAA1D293540AD00756455 /* user.json in Resources */ = {isa = PBXBuildFile; fileRef = E18AAA1C293540AD00756455 /* user.json */; }; E18AAA1F2935469B00756455 /* login.json in Resources */ = {isa = PBXBuildFile; fileRef = E18AAA1E2935469B00756455 /* login.json */; }; @@ -114,6 +117,9 @@ /* Begin PBXFileReference section */ 3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodingError+Logging.swift"; sourceTree = ""; }; A63ABCC924ABB402004DE84E /* RetryConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryConfiguration.swift; sourceTree = ""; }; + B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterceptorList.swift; sourceTree = ""; }; + B367A3342A33A65000032814 /* Interceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interceptor.swift; sourceTree = ""; }; + B367A3362A33AA4900032814 /* AdaptedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptedRequest.swift; sourceTree = ""; }; B8C9288023E9F68000DB2B37 /* Netable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Netable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B8C9288323E9F68000DB2B37 /* Netable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Netable.h; sourceTree = ""; }; B8C9288423E9F68000DB2B37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -164,7 +170,7 @@ E17FBD542950E64C00B6533E /* LoginVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVM.swift; sourceTree = ""; }; E1884409297B3C63009EE74B /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; E18AAA0F29312DF700756455 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; - E18AAA18293524AA00756455 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + E18AAA18293524AA00756455 /* SimpleNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNetworkService.swift; sourceTree = ""; }; E18AAA1A2935251400756455 /* GetVersionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetVersionRequest.swift; sourceTree = ""; }; E18AAA1C293540AD00756455 /* user.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = user.json; sourceTree = ""; }; E18AAA1E2935469B00756455 /* login.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = login.json; sourceTree = ""; }; @@ -214,6 +220,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B367A3332A33A5F100032814 /* Interceptors */ = { + isa = PBXGroup; + children = ( + B367A3362A33AA4900032814 /* AdaptedRequest.swift */, + B367A3342A33A65000032814 /* Interceptor.swift */, + B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */, + ); + path = Interceptors; + sourceTree = ""; + }; B8C9287623E9F68000DB2B37 = { isa = PBXGroup; children = ( @@ -238,12 +254,13 @@ B8C9288223E9F68000DB2B37 /* Netable */ = { isa = PBXGroup; children = ( - C639674B28E4F4BF00ADAE3E /* Helper */, + B367A3332A33A5F100032814 /* Interceptors */, C64F8591241FE4870028E0E9 /* CHANGELOG.md */, C65289F426D01829009D486B /* Config.swift */, 3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */, B8C9289C23E9FA0E00DB2B37 /* Error.swift */, C61DC6FB28CFDF3F0089E912 /* GraphQLRequest.swift */, + C639674B28E4F4BF00ADAE3E /* Helper */, B8C928A223E9FBEC00DB2B37 /* HTTPMethod.swift */, B8C9288423E9F68000DB2B37 /* Info.plist */, C6953F41241A95830044D278 /* LogDestination.swift */, @@ -393,7 +410,7 @@ children = ( E18AAA202935486100756455 /* AuthNetworkService.swift */, E19C96E72941135D005A77BD /* GraphQLNetworkService.swift */, - E18AAA18293524AA00756455 /* NetworkService.swift */, + E18AAA18293524AA00756455 /* SimpleNetworkService.swift */, E12D8413294BB215006EF71A /* ErrorService.swift */, ); path = Services; @@ -589,7 +606,9 @@ C61DC6FC28CFDF3F0089E912 /* GraphQLRequest.swift in Sources */, B8C928A123E9FBA100DB2B37 /* Request.swift in Sources */, C6DA3354293822230076F693 /* LossyArray.swift in Sources */, + B367A3352A33A65000032814 /* Interceptor.swift in Sources */, C65289F526D01829009D486B /* Config.swift in Sources */, + B31E6D2B2A2FB6480002AE1E /* InterceptorList.swift in Sources */, C639674D28E4F4CD00ADAE3E /* Netable+Equatable.swift in Sources */, C6953F42241A95830044D278 /* LogDestination.swift in Sources */, C64ADA47293F9ED900695444 /* ArrayDecodeStrategy.swift in Sources */, @@ -598,6 +617,7 @@ B8C928A323E9FBEC00DB2B37 /* HTTPMethod.swift in Sources */, B8C928A923E9FDCC00DB2B37 /* Netable.swift in Sources */, A63ABCCA24ABB402004DE84E /* RetryConfiguration.swift in Sources */, + B367A3372A33AA4900032814 /* AdaptedRequest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -615,7 +635,7 @@ files = ( E19C96E62941130B005A77BD /* GraphQLVM.swift in Sources */, E1BAC499293AA6340042BF60 /* CreatePostView.swift in Sources */, - E18AAA19293524AA00756455 /* NetworkService.swift in Sources */, + E18AAA19293524AA00756455 /* SimpleNetworkService.swift in Sources */, E12D841A294BD893006EF71A /* RootVM.swift in Sources */, E18AAA2529354E0900756455 /* LoginRequest.swift in Sources */, E19C96E82941135D005A77BD /* GraphQLNetworkService.swift in Sources */, diff --git a/Netable/Netable/Error.swift b/Netable/Netable/Error.swift index 19ff107..98983f8 100644 --- a/Netable/Netable/Error.swift +++ b/Netable/Netable/Error.swift @@ -18,12 +18,15 @@ public enum NetableError: Error, Sendable { /// Something went wrong while encoding request parameters. case codingError(String) - /// Something went wrong while decoding the response.√ + /// Something went wrong while decoding the response. case decodingError(Error, Data?) /// The request was successful, but returned a non-200 status code. case httpError(Int, Data?) + /// Something went wrong while trying to apply interceptors to the request. + case interceptorError(String) + /// The URL provided isn't properly formatted. case malformedURL @@ -73,6 +76,8 @@ extension NetableError: LocalizedError { return 8 case .fallbackDecode: return 9 + case .interceptorError: + return 10 } } @@ -92,6 +97,8 @@ extension NetableError: LocalizedError { return "\(message) \(error.loggableDescription())" case .httpError(let statusCode, _): return "HTTP status code: \(statusCode)" + case .interceptorError(let message): + return "Interceptor error: \(message)" case .malformedURL: return "Malformed URL" case .requestFailed(let error): @@ -121,6 +128,8 @@ extension NetableError: Equatable { return lhsError.localizedDescription == rhsError.localizedDescription && lhsData == rhsData case (.httpError(let lhsCode, let lhsData), .httpError(let rhsCode, let rhsData)): return lhsCode == rhsCode && lhsData == rhsData + case (.interceptorError(let lhsMessage), .interceptorError(let rhsMessage)): + return lhsMessage == rhsMessage case (.malformedURL, .malformedURL): return true case (.requestFailed(let lhsError), .requestFailed(let rhsError)): diff --git a/Netable/Netable/Interceptors/AdaptedRequest.swift b/Netable/Netable/Interceptors/AdaptedRequest.swift new file mode 100644 index 0000000..b9d3a1b --- /dev/null +++ b/Netable/Netable/Interceptors/AdaptedRequest.swift @@ -0,0 +1,21 @@ +// +// AdaptedRequest.swift +// Netable +// +// Created by Brendan Lensink on 2023-06-09. +// Copyright © 2023 Steamclock Software. All rights reserved. +// + +import Foundation + +/// Container for the result of `Interceptor.adapt`. +public enum AdaptedRequest: Sendable { + /// The original URLRequest was modified and the new result should be used instead. + case changed(URLRequest) + + /// The original request should be switched out for a local file resource. + case mocked(URL) + + /// The original request was not modified in any way. + case notChanged +} diff --git a/Netable/Netable/Interceptors/Interceptor.swift b/Netable/Netable/Interceptors/Interceptor.swift new file mode 100644 index 0000000..415c3c7 --- /dev/null +++ b/Netable/Netable/Interceptors/Interceptor.swift @@ -0,0 +1,23 @@ +// +// Interceptor.swift +// Netable +// +// Created by Brendan Lensink on 2023-06-09. +// Copyright © 2023 Steamclock Software. All rights reserved. +// + +import Foundation + +/** + * Interceptors are applied to each request in the given `Netable` instance prior to performing the request. + */ +public protocol Interceptor: Sendable { + /** + * Adapts the provided URLRequest, returning a modified copy changed in one of three potentional ways: + * - No changes are made, the request proceeds as normal. + * - The request has been modified in some way before sending. How it has been modified is left to the user to determine. + * - The request has been switched with a mocked resource JSON. + * + */ + func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest +} diff --git a/Netable/Netable/Interceptors/InterceptorList.swift b/Netable/Netable/Interceptors/InterceptorList.swift new file mode 100644 index 0000000..59c119b --- /dev/null +++ b/Netable/Netable/Interceptors/InterceptorList.swift @@ -0,0 +1,66 @@ +// +// Interceptor.swift +// Netable +// +// Created by Brendan Lensink on 2023-06-06. +// Copyright © 2023 Steamclock Software. All rights reserved. +// + + +import Foundation + +/// Container struct for interceptors. +public struct InterceptorList: Sendable { + let interceptors: [Interceptor] + + /** + * Create a new interceptor list with a set of interceptors. + * + * - parameter interceptors: The interceptors that will be applied to each request. + */ + public init(_ interceptors: [Interceptor]) { + self.interceptors = interceptors + } + + /** + * Create a new interceptor list with a single interceptor. + * + * - parameter interceptor: The interceptor that will be applied to each request. + */ + public init(_ interceptor: Interceptor) { + self.interceptors = [interceptor] + } + + /** + * Apply all intereceptors to the given request. + * Interceptors are applied in the order they were passed into the `InterceptorList` constructor, + * except unless a mocked result is found, it will return immedediately. + * + * - parameter request: The request to apply interceptors to. + * - parameter instance: A reference to the Netable instance that is applying these interceptors. + */ + public func applyInterceptors(request: URLRequest, instance: Netable) async throws -> AdaptedRequest { + var adaptedURLRequest: URLRequest? + + for interceptor in interceptors { + let result = try await interceptor.adapt(adaptedURLRequest ?? request, instance: instance) + switch result { + case .changed(let newResult): + adaptedURLRequest = newResult + case .mocked(let mockedUrl): + if !mockedUrl.isFileURL { + throw NetableError.interceptorError("Only file URLs are supported for mocking URLs") + } + + return AdaptedRequest.mocked(mockedUrl) + case .notChanged: continue + } + } + + if let adapted = adaptedURLRequest { + return AdaptedRequest.changed(adapted) + } + + return .notChanged + } +} diff --git a/Netable/Netable/Netable.swift b/Netable/Netable/Netable.swift index f6c305f..aa8cc12 100644 --- a/Netable/Netable/Netable.swift +++ b/Netable/Netable/Netable.swift @@ -29,6 +29,9 @@ public actor Netable { /// The base URL of your api endpoint. public let baseURL: URL + /// Any interceptors to be applied to all outgoing requests. + public let interceptorList: InterceptorList? + /// Destination that logs will be printed to during network requests. public let logDestination: LogDestination @@ -47,17 +50,20 @@ public actor Netable { * * - parameter baseURL: The base URL of your endpoint. * - parameter config: Configuration such as timeouts and caching policies for the underlying url session. + * - parameter interceptorList: Any interceptors to be applied to all outoing requests. * - parameter logDestination: Destination to send request logs to. Default is DefaultLogDestination * - parameter retryConfiguration: Configuration for request retry policies */ public init( baseURL: URL, config: Config = Config(), + interceptorList: InterceptorList? = nil, logDestination: LogDestination = DefaultLogDestination(), retryConfiguration: RetryConfiguration = RetryConfiguration(), requestFailureDelegate: RequestFailureDelegate? = nil) { self.baseURL = baseURL self.config = config + self.interceptorList = interceptorList self.logDestination = logDestination self.retryConfiguration = retryConfiguration self.requestFailureDelegate = requestFailureDelegate @@ -105,7 +111,11 @@ public actor Netable { request.headers.forEach { key, value in urlRequest.setValue(value, forHTTPHeaderField: key) } - let result = try await startRequestTask(request, urlRequest: urlRequest, id: UUID().uuidString) + let result = try await startRequestTask( + request, + urlRequest: urlRequest, + id: UUID().uuidString + ) return try await request.postProcess(result: result) } catch { let netableError = (error as? NetableError) ?? NetableError.unknownError(error) @@ -126,7 +136,10 @@ public actor Netable { */ @available(*, deprecated, message: "Please update to use the new `async`/`await` APIs.") @discardableResult - public nonisolated func request(_ request: T, completion unsafeCompletion: @escaping @Sendable (Result) -> Void) -> Task<(), Never> { + public nonisolated func request( + _ request: T, + completion unsafeCompletion: @escaping @Sendable (Result) -> Void + ) -> Task<(), Never> { // We don't need the whole request to run on the main thread, but DO need to make sure the completion does let completion: @Sendable (Result) -> Void = { result in Task { @MainActor in @@ -155,28 +168,46 @@ public actor Netable { * - returns: A tuple that contains a reference to the `Task`, for cancellation, and a PassthroughSubject to monitor for results. */ - public nonisolated func request(_ request: T) -> (task: Task<(), Never>, subject: Publishers.ReceiveOn, Never>, RunLoop>) { - let resultSubject = PassthroughSubject, Never>() + public nonisolated func request(_ request: T) -> ( + task: Task<(), Never>, + subject: Publishers.ReceiveOn, Never>, RunLoop> + ) { + let resultSubject = PassthroughSubject, Never>() let task = Task { do { let finalResource = try await self.request(request) - resultSubject.send(.success(finalResource)) + resultSubject.send(.success(finalResource)) } catch { - resultSubject.send(.failure(error.netableError)) + resultSubject.send(.failure(error.netableError)) } } return (task: task, subject: resultSubject.receive(on: RunLoop.main)) } - private func startRequestTask(_ request: T, urlRequest: URLRequest, id: String) async throws -> T.FinalResource { + private func startRequestTask( + _ request: T, + urlRequest: URLRequest, + id: String + ) async throws -> T.FinalResource { let startTimestamp = CACurrentMediaTime() + var finalUrlRequest = urlRequest + var mockedUrl: URL? + if let interceptorList = self.interceptorList { + let result = try await interceptorList.applyInterceptors(request: finalUrlRequest, instance: self) + switch result { + case .changed(let newRequest): finalUrlRequest = newRequest + case .mocked(let url): mockedUrl = url + case .notChanged: break + } + } + let requestInfo = LogEvent.RequestInfo( - urlString: urlRequest.url?.absoluteString ?? "UNDEFINED", + urlString: finalUrlRequest.url?.absoluteString ?? "UNDEFINED", method: request.method, - headers: urlRequest.allHTTPHeaderFields ?? [:] + headers: finalUrlRequest.allHTTPHeaderFields ?? [:] ) await log(.requestStarted(request: requestInfo)) @@ -189,17 +220,21 @@ public actor Netable { let retryConfiguration = self.retryConfiguration + if let mocked = mockedUrl { + let data = try Data(contentsOf: mocked) + return try await finalize(request: request, data: data) + } + for retry in 0..(request: T, data: Data) async throws -> T.FinalResource { + let decoded = try await request.decode(data, defaultDecodingStrategy: self.config.jsonDecodingStrategy) + let finalizedResult = try await request.finalize(raw: decoded) + return finalizedResult + } + /** * Helper function for logging, to avoid having to reference the log destination everywhere and so we can possibly change the semantics of, * for example, what thread these are dispatched on later. diff --git a/README.md b/README.md index c6786f6..ee43662 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Modern apps interact with a lot of different APIs. Netable makes that easier by - [Standard Usage](#standard-usage) - [Resource Extraction](#resource-extraction) - [Handling Errors](#handling-errors) + - [Request Interceptors](#request-interceptors) - [GraphQL Support](#graphql-support) - [Example](#example) - [Full Documentation](#full-documentation) @@ -384,6 +385,39 @@ extension GlobalRequestFailureDelegateExample: RequestFailureDelegate { } ``` +### Request Interceptors + +Interceptors are a powerful and flexible way to modify a `Request` before it is executed. When you create your `Netable` instance, you can pass in an optional `InterceptorList`, containing any `Interceptor`s you would like to be applied to requests. + +When you make a request, each `Interceptor` will call its `adapt` function in turn, in the order it was passed in to the `InterceptorList`. `adapt` should return a special `AdaptedRequest` object that indicates the result of the function call. + +You might attached a new header, modifying the request: + +``` +func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest { + var newRequest = request + newRequest.addValue("1a2a3a4a", forHTTPHeaderField: "Authorization") + return .changed(newRequest) +} + +``` + +Or, you might sub out the entire request with a mocked file for specific endpoints, otherwise do nothing: + +``` +func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest { + if request.url.contains("/foo") { + return .mocked("./path/to/foo-mock.json") + } else if request.url.contains("/bar") { + return .mocked("./path/to/bar-mock.json") + } + + return .notChanged +} +``` + +See [MockRequestInterceptor](https://github.com/steamclock/netable/blob/main/Netable/Example/Services/AuthNetworkService.swift) in the Example project for a more detailed example. + #### Using `requestFailurePublisher` If you prefer `Combine`, you can subscribe to this publisher to receive `NetableErrors` from elsewhere in your app.