Skip to content

Commit

Permalink
Add support for request interceptors (#119)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
brendanlensink authored Jun 15, 2023
1 parent 920c0e9 commit fb67817
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 18 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
14 changes: 14 additions & 0 deletions Netable/Example/Services/AuthNetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}


30 changes: 25 additions & 5 deletions Netable/Netable.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -114,6 +117,9 @@
/* Begin PBXFileReference section */
3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodingError+Logging.swift"; sourceTree = "<group>"; };
A63ABCC924ABB402004DE84E /* RetryConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryConfiguration.swift; sourceTree = "<group>"; };
B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterceptorList.swift; sourceTree = "<group>"; };
B367A3342A33A65000032814 /* Interceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interceptor.swift; sourceTree = "<group>"; };
B367A3362A33AA4900032814 /* AdaptedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptedRequest.swift; sourceTree = "<group>"; };
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 = "<group>"; };
B8C9288423E9F68000DB2B37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -164,7 +170,7 @@
E17FBD542950E64C00B6533E /* LoginVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVM.swift; sourceTree = "<group>"; };
E1884409297B3C63009EE74B /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = "<group>"; };
E18AAA0F29312DF700756455 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = "<group>"; };
E18AAA18293524AA00756455 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
E18AAA18293524AA00756455 /* SimpleNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNetworkService.swift; sourceTree = "<group>"; };
E18AAA1A2935251400756455 /* GetVersionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetVersionRequest.swift; sourceTree = "<group>"; };
E18AAA1C293540AD00756455 /* user.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = user.json; sourceTree = "<group>"; };
E18AAA1E2935469B00756455 /* login.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = login.json; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 = "<group>";
};
B8C9287623E9F68000DB2B37 = {
isa = PBXGroup;
children = (
Expand All @@ -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 */,
Expand Down Expand Up @@ -393,7 +410,7 @@
children = (
E18AAA202935486100756455 /* AuthNetworkService.swift */,
E19C96E72941135D005A77BD /* GraphQLNetworkService.swift */,
E18AAA18293524AA00756455 /* NetworkService.swift */,
E18AAA18293524AA00756455 /* SimpleNetworkService.swift */,
E12D8413294BB215006EF71A /* ErrorService.swift */,
);
path = Services;
Expand Down Expand Up @@ -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 */,
Expand All @@ -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;
};
Expand All @@ -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 */,
Expand Down
11 changes: 10 additions & 1 deletion Netable/Netable/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -73,6 +76,8 @@ extension NetableError: LocalizedError {
return 8
case .fallbackDecode:
return 9
case .interceptorError:
return 10
}
}

Expand All @@ -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):
Expand Down Expand Up @@ -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)):
Expand Down
21 changes: 21 additions & 0 deletions Netable/Netable/Interceptors/AdaptedRequest.swift
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions Netable/Netable/Interceptors/Interceptor.swift
Original file line number Diff line number Diff line change
@@ -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
}
66 changes: 66 additions & 0 deletions Netable/Netable/Interceptors/InterceptorList.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit fb67817

Please sign in to comment.