Skip to content

Commit fb67817

Browse files
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
1 parent 920c0e9 commit fb67817

File tree

10 files changed

+253
-18
lines changed

10 files changed

+253
-18
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## UNRELEASED
8+
9+
10+
## [2.2.0] 15-6-22
11+
- [118] Add new `Interceptors` support.
12+
13+
714
## [2.1.0] 14-12-22
815
- [104] Add support for partially decoding arrays through new `arrayDecodingStrategy` parameter on `Request`.
916
- [106] Fix `RetryConfiguration` not being marked as `Sendable`.

Netable/Example/Services/AuthNetworkService.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,17 @@ class AuthNetworkService {
8787
self.user.send(nil)
8888
}
8989
}
90+
91+
final class MockRequestInterceptor: Interceptor {
92+
func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest {
93+
if let requestURL = request.url,
94+
let mockedURL = Bundle.main.url(forResource: "posts", withExtension: "json"),
95+
requestURL.absoluteString.contains("/all") {
96+
return .mocked(mockedURL)
97+
}
98+
99+
return .notChanged
100+
}
101+
}
102+
103+
File renamed without changes.

Netable/Netable.xcodeproj/project.pbxproj

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
/* Begin PBXBuildFile section */
1010
3B00B3C726D7EA3C00A1DF79 /* DecodingError+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */; };
1111
A63ABCCA24ABB402004DE84E /* RetryConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63ABCC924ABB402004DE84E /* RetryConfiguration.swift */; };
12+
B31E6D2B2A2FB6480002AE1E /* InterceptorList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */; };
13+
B367A3352A33A65000032814 /* Interceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B367A3342A33A65000032814 /* Interceptor.swift */; };
14+
B367A3372A33AA4900032814 /* AdaptedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B367A3362A33AA4900032814 /* AdaptedRequest.swift */; };
1215
B8C9288A23E9F68000DB2B37 /* Netable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B8C9288023E9F68000DB2B37 /* Netable.framework */; };
1316
B8C9288F23E9F68000DB2B37 /* NetableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C9288E23E9F68000DB2B37 /* NetableTests.swift */; };
1417
B8C9289123E9F68000DB2B37 /* Netable.h in Headers */ = {isa = PBXBuildFile; fileRef = B8C9288323E9F68000DB2B37 /* Netable.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -58,7 +61,7 @@
5861
E186202129425EFF009B6E0C /* Netable.framework in Embeded Framworks */ = {isa = PBXBuildFile; fileRef = B8C9288023E9F68000DB2B37 /* Netable.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5962
E188440A297B3C63009EE74B /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1884409297B3C63009EE74B /* DataManager.swift */; };
6063
E18AAA1029312DF700756455 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA0F29312DF700756455 /* Version.swift */; };
61-
E18AAA19293524AA00756455 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA18293524AA00756455 /* NetworkService.swift */; };
64+
E18AAA19293524AA00756455 /* SimpleNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA18293524AA00756455 /* SimpleNetworkService.swift */; };
6265
E18AAA1B2935251400756455 /* GetVersionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18AAA1A2935251400756455 /* GetVersionRequest.swift */; };
6366
E18AAA1D293540AD00756455 /* user.json in Resources */ = {isa = PBXBuildFile; fileRef = E18AAA1C293540AD00756455 /* user.json */; };
6467
E18AAA1F2935469B00756455 /* login.json in Resources */ = {isa = PBXBuildFile; fileRef = E18AAA1E2935469B00756455 /* login.json */; };
@@ -114,6 +117,9 @@
114117
/* Begin PBXFileReference section */
115118
3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodingError+Logging.swift"; sourceTree = "<group>"; };
116119
A63ABCC924ABB402004DE84E /* RetryConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryConfiguration.swift; sourceTree = "<group>"; };
120+
B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterceptorList.swift; sourceTree = "<group>"; };
121+
B367A3342A33A65000032814 /* Interceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interceptor.swift; sourceTree = "<group>"; };
122+
B367A3362A33AA4900032814 /* AdaptedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptedRequest.swift; sourceTree = "<group>"; };
117123
B8C9288023E9F68000DB2B37 /* Netable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Netable.framework; sourceTree = BUILT_PRODUCTS_DIR; };
118124
B8C9288323E9F68000DB2B37 /* Netable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Netable.h; sourceTree = "<group>"; };
119125
B8C9288423E9F68000DB2B37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -164,7 +170,7 @@
164170
E17FBD542950E64C00B6533E /* LoginVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVM.swift; sourceTree = "<group>"; };
165171
E1884409297B3C63009EE74B /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = "<group>"; };
166172
E18AAA0F29312DF700756455 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = "<group>"; };
167-
E18AAA18293524AA00756455 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
173+
E18AAA18293524AA00756455 /* SimpleNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNetworkService.swift; sourceTree = "<group>"; };
168174
E18AAA1A2935251400756455 /* GetVersionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetVersionRequest.swift; sourceTree = "<group>"; };
169175
E18AAA1C293540AD00756455 /* user.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = user.json; sourceTree = "<group>"; };
170176
E18AAA1E2935469B00756455 /* login.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = login.json; sourceTree = "<group>"; };
@@ -214,6 +220,16 @@
214220
/* End PBXFrameworksBuildPhase section */
215221

216222
/* Begin PBXGroup section */
223+
B367A3332A33A5F100032814 /* Interceptors */ = {
224+
isa = PBXGroup;
225+
children = (
226+
B367A3362A33AA4900032814 /* AdaptedRequest.swift */,
227+
B367A3342A33A65000032814 /* Interceptor.swift */,
228+
B31E6D2A2A2FB6480002AE1E /* InterceptorList.swift */,
229+
);
230+
path = Interceptors;
231+
sourceTree = "<group>";
232+
};
217233
B8C9287623E9F68000DB2B37 = {
218234
isa = PBXGroup;
219235
children = (
@@ -238,12 +254,13 @@
238254
B8C9288223E9F68000DB2B37 /* Netable */ = {
239255
isa = PBXGroup;
240256
children = (
241-
C639674B28E4F4BF00ADAE3E /* Helper */,
257+
B367A3332A33A5F100032814 /* Interceptors */,
242258
C64F8591241FE4870028E0E9 /* CHANGELOG.md */,
243259
C65289F426D01829009D486B /* Config.swift */,
244260
3B00B3C626D7EA3C00A1DF79 /* DecodingError+Logging.swift */,
245261
B8C9289C23E9FA0E00DB2B37 /* Error.swift */,
246262
C61DC6FB28CFDF3F0089E912 /* GraphQLRequest.swift */,
263+
C639674B28E4F4BF00ADAE3E /* Helper */,
247264
B8C928A223E9FBEC00DB2B37 /* HTTPMethod.swift */,
248265
B8C9288423E9F68000DB2B37 /* Info.plist */,
249266
C6953F41241A95830044D278 /* LogDestination.swift */,
@@ -393,7 +410,7 @@
393410
children = (
394411
E18AAA202935486100756455 /* AuthNetworkService.swift */,
395412
E19C96E72941135D005A77BD /* GraphQLNetworkService.swift */,
396-
E18AAA18293524AA00756455 /* NetworkService.swift */,
413+
E18AAA18293524AA00756455 /* SimpleNetworkService.swift */,
397414
E12D8413294BB215006EF71A /* ErrorService.swift */,
398415
);
399416
path = Services;
@@ -589,7 +606,9 @@
589606
C61DC6FC28CFDF3F0089E912 /* GraphQLRequest.swift in Sources */,
590607
B8C928A123E9FBA100DB2B37 /* Request.swift in Sources */,
591608
C6DA3354293822230076F693 /* LossyArray.swift in Sources */,
609+
B367A3352A33A65000032814 /* Interceptor.swift in Sources */,
592610
C65289F526D01829009D486B /* Config.swift in Sources */,
611+
B31E6D2B2A2FB6480002AE1E /* InterceptorList.swift in Sources */,
593612
C639674D28E4F4CD00ADAE3E /* Netable+Equatable.swift in Sources */,
594613
C6953F42241A95830044D278 /* LogDestination.swift in Sources */,
595614
C64ADA47293F9ED900695444 /* ArrayDecodeStrategy.swift in Sources */,
@@ -598,6 +617,7 @@
598617
B8C928A323E9FBEC00DB2B37 /* HTTPMethod.swift in Sources */,
599618
B8C928A923E9FDCC00DB2B37 /* Netable.swift in Sources */,
600619
A63ABCCA24ABB402004DE84E /* RetryConfiguration.swift in Sources */,
620+
B367A3372A33AA4900032814 /* AdaptedRequest.swift in Sources */,
601621
);
602622
runOnlyForDeploymentPostprocessing = 0;
603623
};
@@ -615,7 +635,7 @@
615635
files = (
616636
E19C96E62941130B005A77BD /* GraphQLVM.swift in Sources */,
617637
E1BAC499293AA6340042BF60 /* CreatePostView.swift in Sources */,
618-
E18AAA19293524AA00756455 /* NetworkService.swift in Sources */,
638+
E18AAA19293524AA00756455 /* SimpleNetworkService.swift in Sources */,
619639
E12D841A294BD893006EF71A /* RootVM.swift in Sources */,
620640
E18AAA2529354E0900756455 /* LoginRequest.swift in Sources */,
621641
E19C96E82941135D005A77BD /* GraphQLNetworkService.swift in Sources */,

Netable/Netable/Error.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ public enum NetableError: Error, Sendable {
1818
/// Something went wrong while encoding request parameters.
1919
case codingError(String)
2020

21-
/// Something went wrong while decoding the response.
21+
/// Something went wrong while decoding the response.
2222
case decodingError(Error, Data?)
2323

2424
/// The request was successful, but returned a non-200 status code.
2525
case httpError(Int, Data?)
2626

27+
/// Something went wrong while trying to apply interceptors to the request.
28+
case interceptorError(String)
29+
2730
/// The URL provided isn't properly formatted.
2831
case malformedURL
2932

@@ -73,6 +76,8 @@ extension NetableError: LocalizedError {
7376
return 8
7477
case .fallbackDecode:
7578
return 9
79+
case .interceptorError:
80+
return 10
7681
}
7782
}
7883

@@ -92,6 +97,8 @@ extension NetableError: LocalizedError {
9297
return "\(message) \(error.loggableDescription())"
9398
case .httpError(let statusCode, _):
9499
return "HTTP status code: \(statusCode)"
100+
case .interceptorError(let message):
101+
return "Interceptor error: \(message)"
95102
case .malformedURL:
96103
return "Malformed URL"
97104
case .requestFailed(let error):
@@ -121,6 +128,8 @@ extension NetableError: Equatable {
121128
return lhsError.localizedDescription == rhsError.localizedDescription && lhsData == rhsData
122129
case (.httpError(let lhsCode, let lhsData), .httpError(let rhsCode, let rhsData)):
123130
return lhsCode == rhsCode && lhsData == rhsData
131+
case (.interceptorError(let lhsMessage), .interceptorError(let rhsMessage)):
132+
return lhsMessage == rhsMessage
124133
case (.malformedURL, .malformedURL):
125134
return true
126135
case (.requestFailed(let lhsError), .requestFailed(let rhsError)):
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// AdaptedRequest.swift
3+
// Netable
4+
//
5+
// Created by Brendan Lensink on 2023-06-09.
6+
// Copyright © 2023 Steamclock Software. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// Container for the result of `Interceptor.adapt`.
12+
public enum AdaptedRequest: Sendable {
13+
/// The original URLRequest was modified and the new result should be used instead.
14+
case changed(URLRequest)
15+
16+
/// The original request should be switched out for a local file resource.
17+
case mocked(URL)
18+
19+
/// The original request was not modified in any way.
20+
case notChanged
21+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Interceptor.swift
3+
// Netable
4+
//
5+
// Created by Brendan Lensink on 2023-06-09.
6+
// Copyright © 2023 Steamclock Software. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/**
12+
* Interceptors are applied to each request in the given `Netable` instance prior to performing the request.
13+
*/
14+
public protocol Interceptor: Sendable {
15+
/**
16+
* Adapts the provided URLRequest, returning a modified copy changed in one of three potentional ways:
17+
* - No changes are made, the request proceeds as normal.
18+
* - The request has been modified in some way before sending. How it has been modified is left to the user to determine.
19+
* - The request has been switched with a mocked resource JSON.
20+
*
21+
*/
22+
func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest
23+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// Interceptor.swift
3+
// Netable
4+
//
5+
// Created by Brendan Lensink on 2023-06-06.
6+
// Copyright © 2023 Steamclock Software. All rights reserved.
7+
//
8+
9+
10+
import Foundation
11+
12+
/// Container struct for interceptors.
13+
public struct InterceptorList: Sendable {
14+
let interceptors: [Interceptor]
15+
16+
/**
17+
* Create a new interceptor list with a set of interceptors.
18+
*
19+
* - parameter interceptors: The interceptors that will be applied to each request.
20+
*/
21+
public init(_ interceptors: [Interceptor]) {
22+
self.interceptors = interceptors
23+
}
24+
25+
/**
26+
* Create a new interceptor list with a single interceptor.
27+
*
28+
* - parameter interceptor: The interceptor that will be applied to each request.
29+
*/
30+
public init(_ interceptor: Interceptor) {
31+
self.interceptors = [interceptor]
32+
}
33+
34+
/**
35+
* Apply all intereceptors to the given request.
36+
* Interceptors are applied in the order they were passed into the `InterceptorList` constructor,
37+
* except unless a mocked result is found, it will return immedediately.
38+
*
39+
* - parameter request: The request to apply interceptors to.
40+
* - parameter instance: A reference to the Netable instance that is applying these interceptors.
41+
*/
42+
public func applyInterceptors(request: URLRequest, instance: Netable) async throws -> AdaptedRequest {
43+
var adaptedURLRequest: URLRequest?
44+
45+
for interceptor in interceptors {
46+
let result = try await interceptor.adapt(adaptedURLRequest ?? request, instance: instance)
47+
switch result {
48+
case .changed(let newResult):
49+
adaptedURLRequest = newResult
50+
case .mocked(let mockedUrl):
51+
if !mockedUrl.isFileURL {
52+
throw NetableError.interceptorError("Only file URLs are supported for mocking URLs")
53+
}
54+
55+
return AdaptedRequest.mocked(mockedUrl)
56+
case .notChanged: continue
57+
}
58+
}
59+
60+
if let adapted = adaptedURLRequest {
61+
return AdaptedRequest.changed(adapted)
62+
}
63+
64+
return .notChanged
65+
}
66+
}

0 commit comments

Comments
 (0)