Skip to content

Commit 6016047

Browse files
authored
feat(configuration): Select encoder/decoder per type (#29)
1 parent a5bb4fc commit 6016047

File tree

9 files changed

+156
-29
lines changed

9 files changed

+156
-29
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import Foundation
2+
3+
public struct ContentDataCoderConfiguration {
4+
public var encoder: ContentDataEncoderConfiguration
5+
public var decoder: ContentDataDecoderConfiguration
6+
public let defaultType: HTTPContentType
7+
8+
public init(
9+
default: HTTPContentType,
10+
encoder: ContentDataEncoderConfiguration,
11+
decoder: ContentDataDecoderConfiguration,
12+
) {
13+
self.encoder = encoder
14+
self.decoder = decoder
15+
self.defaultType = `default`
16+
}
17+
18+
public init() {
19+
self.init(
20+
default: .json,
21+
encoder: [
22+
.json: JSONEncoder(),
23+
.formURLEncoded: FormURLEncoder()
24+
],
25+
decoder: [
26+
.json: JSONDecoder()
27+
]
28+
)
29+
}
30+
}
31+
32+
@dynamicMemberLookup
33+
public struct ContentDataEncoderConfiguration: ExpressibleByDictionaryLiteral {
34+
private var encoders: [HTTPContentType: ContentDataEncoder]
35+
36+
public init(encoders: [HTTPContentType: ContentDataEncoder]) {
37+
self.encoders = encoders
38+
}
39+
40+
public init(dictionaryLiteral elements: (HTTPContentType, ContentDataEncoder)...) {
41+
self.init(encoders: Dictionary(uniqueKeysWithValues: elements))
42+
}
43+
44+
public subscript(contentType: HTTPContentType) -> ContentDataEncoder? {
45+
get { encoders[contentType] }
46+
set { encoders[contentType] = newValue }
47+
}
48+
49+
public subscript(dynamicMember keyPath: KeyPath<HTTPContentType.Type, HTTPContentType>) -> ContentDataEncoder? {
50+
get { self[HTTPContentType.self[keyPath: keyPath]] }
51+
set { self[HTTPContentType.self[keyPath: keyPath]] = newValue }
52+
}
53+
}
54+
55+
@dynamicMemberLookup
56+
public struct ContentDataDecoderConfiguration: ExpressibleByDictionaryLiteral {
57+
private var decoders: [HTTPContentType: ContentDataDecoder]
58+
59+
public init(decoders: [HTTPContentType: ContentDataDecoder]) {
60+
self.decoders = decoders
61+
}
62+
63+
public init(dictionaryLiteral elements: (HTTPContentType, ContentDataDecoder)...) {
64+
self.init(decoders: Dictionary(uniqueKeysWithValues: elements))
65+
}
66+
67+
public subscript(contentType: HTTPContentType) -> ContentDataDecoder? {
68+
get { decoders[contentType] }
69+
set { decoders[contentType] = newValue }
70+
}
71+
72+
public subscript(dynamicMember keyPath: KeyPath<HTTPContentType.Type, HTTPContentType>) -> ContentDataDecoder? {
73+
get { self[HTTPContentType.self[keyPath: keyPath]] }
74+
set { self[HTTPContentType.self[keyPath: keyPath]] = newValue }
75+
}
76+
}

Sources/SimpleHTTP/DataCoder/FormURL/FormURLEncoder.swift renamed to Sources/SimpleHTTP/ContentData/FormURL/FormURLEncoder.swift

File renamed without changes.

Sources/SimpleHTTP/DataCoder/FormURL/HTTPContentType+FormURL.swift renamed to Sources/SimpleHTTP/ContentData/FormURL/HTTPContentType+FormURL.swift

File renamed without changes.

Sources/SimpleHTTP/Session/Session.swift

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ public class Session {
4444
public func response<Output: Decodable>(for request: Request<Output>) async throws -> Output {
4545
let result = try await dataPublisher(for: request)
4646

47+
guard let decoder = config.data.decoder[result.contentType] else {
48+
throw SessionConfigurationError.missingDecoder(result.contentType)
49+
}
50+
4751
do {
48-
let decodedOutput = try config.decoder.decode(Output.self, from: result.data)
52+
let decodedOutput = try decoder.decode(Output.self, from: result.data)
4953
let output = try config.interceptor.adaptOutput(decodedOutput, for: result.request)
5054

5155
log(.success(output), for: result.request)
@@ -67,15 +71,31 @@ public class Session {
6771
extension Session {
6872
private func dataPublisher<Output>(for request: Request<Output>) async throws -> Response<Output> {
6973
let modifiedRequest = try await config.interceptor.adaptRequest(request)
70-
let urlRequest = try modifiedRequest
71-
.toURLRequest(encoder: config.encoder, relativeTo: baseURL, accepting: config.decoder)
74+
let requestContentType = modifiedRequest.headers.contentType ?? config.data.defaultType
75+
let encoder: ContentDataEncoder?
76+
77+
// FIXME: we also check body inside toURLRequest
78+
switch modifiedRequest.body {
79+
case .encodable:
80+
encoder = config.data.encoder[requestContentType]
81+
case .multipart, .none:
82+
// this one is supposed to never be nil
83+
encoder = config.data.encoder[config.data.defaultType]
84+
}
85+
86+
guard let encoder else {
87+
throw SessionConfigurationError.missingEncoder(requestContentType)
88+
}
89+
90+
let urlRequest = try modifiedRequest.toURLRequest(encoder: encoder, relativeTo: baseURL)
7291

7392
do {
7493
let result = try await dataTask(urlRequest)
94+
let responseContentType = result.response.mimeType.map(HTTPContentType.init(value:)) ?? config.data.defaultType
7595

76-
try result.validate(errorDecoder: config.errorConverter)
96+
try result.validate(errorDecoder: errorDecoder(for: responseContentType))
7797

78-
return Response(data: result.data, request: modifiedRequest)
98+
return Response(data: result.data, contentType: responseContentType, request: modifiedRequest)
7999
}
80100
catch {
81101
self.log(.failure(error), for: modifiedRequest)
@@ -91,9 +111,18 @@ extension Session {
91111
private func log<Output>(_ response: Result<Output, Error>, for request: Request<Output>) {
92112
config.interceptor.receivedResponse(response, for: request)
93113
}
114+
115+
private func errorDecoder(for contentType: HTTPContentType) throws -> DataErrorDecoder? {
116+
guard let converter = config.errorConverter else {
117+
return nil
118+
}
119+
120+
return { data in try converter(data, contentType) }
121+
}
94122
}
95123

96124
private struct Response<Output> {
97125
let data: Data
126+
let contentType: HTTPContentType
98127
let request: Request<Output>
99128
}

Sources/SimpleHTTP/Session/SessionConfiguration.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,46 @@ import Foundation
22

33
/// a type defining some parameters for a `Session`
44
public struct SessionConfiguration {
5-
/// encoder to use for request bodies
6-
let encoder: ContentDataEncoder
7-
/// decoder used to decode http responses
8-
let decoder: ContentDataDecoder
5+
/// data encoders/decoders configuration per content type
6+
let data: ContentDataCoderConfiguration
97
/// queue on which to decode data
108
let decodingQueue: DispatchQueue
119
/// an interceptor to apply custom behavior on the session requests/responses.
1210
/// To apply multiple interceptors use `ComposeInterceptor`
1311
let interceptor: Interceptor
14-
/// a function decoding data (using `decoder`) as a custom error
15-
private(set) var errorConverter: DataErrorDecoder?
12+
/// a function decoding data as a custom error given the response content type
13+
private(set) var errorConverter: ContentDataErrorDecoder?
1614

17-
/// - Parameter encoder to use for request bodies
18-
/// - Parameter decoder used to decode http responses
15+
/// - Parameter data: encoders/decoders configuration per content type
1916
/// - Parameter decodeQueue: queue on which to decode data
2017
/// - Parameter interceptors: interceptor list to apply on the session requests/responses
2118
public init(
22-
encoder: ContentDataEncoder = JSONEncoder(),
23-
decoder: ContentDataDecoder = JSONDecoder(),
19+
data: ContentDataCoderConfiguration = .init(),
2420
decodingQueue: DispatchQueue = .main,
2521
interceptors: CompositeInterceptor = []) {
26-
self.encoder = encoder
27-
self.decoder = decoder
22+
self.data = data
2823
self.decodingQueue = decodingQueue
2924
self.interceptor = interceptors
3025
}
3126

3227
/// - Parameter dataError: Error type to use when having error with data
3328
public init<DataError: Error & Decodable>(
34-
encoder: ContentDataEncoder = JSONEncoder(),
35-
decoder: ContentDataDecoder = JSONDecoder(),
29+
data: ContentDataCoderConfiguration,
3630
decodingQueue: DispatchQueue = .main,
3731
interceptors: CompositeInterceptor = [],
3832
dataError: DataError.Type
3933
) {
40-
self.init(encoder: encoder, decoder: decoder, decodingQueue: decodingQueue, interceptors: interceptors)
41-
self.errorConverter = {
42-
try decoder.decode(dataError, from: $0)
34+
self.init(data: data, decodingQueue: decodingQueue, interceptors: interceptors)
35+
self.errorConverter = { [decoder=data.decoder] data, contentType in
36+
guard let decoder = decoder[contentType] else {
37+
throw SessionConfigurationError.missingDecoder(contentType)
38+
}
39+
return try decoder.decode(dataError, from: data)
4340
}
4441
}
4542
}
43+
44+
public enum SessionConfigurationError: Error {
45+
case missingEncoder(HTTPContentType)
46+
case missingDecoder(HTTPContentType)
47+
}

Sources/SimpleHTTPFoundation/Foundation/Coder/DataCoder.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ public protocol ContentDataDecoder: DataDecoder {
2424

2525
/// A function converting data when a http error occur into a custom error
2626
public typealias DataErrorDecoder = (Data) throws -> Error
27+
28+
public typealias ContentDataErrorDecoder = (Data, HTTPContentType) throws -> Error

Sources/SimpleHTTPFoundation/HTTP/HTTPHeader.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,18 @@ extension HTTPHeader {
3131
public static let proxyAuthorization: Self = "Proxy-Authorization"
3232
public static let wwwAuthenticate: Self = "WWW-Authenticate"
3333
}
34+
35+
public extension HTTPHeaderFields {
36+
/// - Returns the content type if HTTPHeader.contentType was set
37+
var contentType: HTTPContentType? {
38+
self[.contentType].map(HTTPContentType.init(value:))
39+
}
40+
41+
func contentType(_ value: HTTPContentType) -> Self {
42+
var copy = self
43+
44+
copy[.contentType] = value.value
45+
46+
return copy
47+
}
48+
}

Tests/SimpleHTTPTests/DataCoder/FormURL/FormURLEncoderTests.swift renamed to Tests/SimpleHTTPTests/ContentData/FormURL/FormURLEncoderTests.swift

File renamed without changes.

Tests/SimpleHTTPTests/Session/SessionTests.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import XCTest
33

44
class SessionAsyncTests: XCTestCase {
55
let baseURL = URL(string: "https://sessionTests.io")!
6-
let encoder = JSONEncoder()
7-
let decoder = JSONDecoder()
6+
let data = ContentDataCoderConfiguration(
7+
default: .json,
8+
encoder: [.json: JSONEncoder()],
9+
decoder: [.json: JSONDecoder()]
10+
)
811

912
func test_response_responseIsValid_decodedOutputIsReturned() async throws {
1013
let expectedResponse = Content(value: "response")
@@ -21,7 +24,7 @@ class SessionAsyncTests: XCTestCase {
2124
let interceptor = InterceptorStub()
2225
let session = sesssionStub(
2326
interceptor: [interceptor],
24-
data: { URLDataResponse(data: try! JSONEncoder().encode(output), response: .success) }
27+
response: { URLDataResponse(data: try! JSONEncoder().encode(output), response: .success) }
2528
)
2629

2730
interceptor.adaptResponseMock = { _, _ in
@@ -73,7 +76,7 @@ class SessionAsyncTests: XCTestCase {
7376
func test_response_httpDataHasCustomError_returnCustomError() async throws {
7477
let session = Session(
7578
baseURL: baseURL,
76-
configuration: SessionConfiguration(encoder: encoder, decoder: decoder, dataError: CustomError.self),
79+
configuration: SessionConfiguration(data: data, dataError: CustomError.self),
7780
dataTask: { _ in
7881
URLDataResponse(data: try! JSONEncoder().encode(CustomError()), response: .unauthorized)
7982
})
@@ -88,11 +91,11 @@ class SessionAsyncTests: XCTestCase {
8891
}
8992

9093
/// helper to create a session for testing
91-
private func sesssionStub(interceptor: CompositeInterceptor = [], data: @escaping () throws -> URLDataResponse)
94+
private func sesssionStub(interceptor: CompositeInterceptor = [], response: @escaping () throws -> URLDataResponse)
9295
-> Session {
93-
let config = SessionConfiguration(encoder: encoder, decoder: decoder, interceptors: interceptor)
96+
let config = SessionConfiguration(data: data, interceptors: interceptor)
9497

95-
return Session(baseURL: baseURL, configuration: config, dataTask: { _ in try data() })
98+
return Session(baseURL: baseURL, configuration: config, dataTask: { _ in try response() })
9699
}
97100
}
98101

0 commit comments

Comments
 (0)