From 0d0b316fd1d6cbf6b29b850b9e6faca1b0e9187e Mon Sep 17 00:00:00 2001 From: pjechris Date: Mon, 15 Jun 2026 10:40:07 +0200 Subject: [PATCH 1/2] simplify API --- .../ContentDataCoderConfiguration.swift | 76 ------------------- .../ContentDataCodersConfiguration.swift | 53 +++++++++++++ Sources/SimpleHTTP/Session/Session.swift | 6 +- .../Session/SessionConfiguration.swift | 10 +-- .../Session/SessionTests.swift | 6 +- 5 files changed, 64 insertions(+), 87 deletions(-) delete mode 100644 Sources/SimpleHTTP/ContentData/ContentDataCoderConfiguration.swift create mode 100644 Sources/SimpleHTTP/ContentData/ContentDataCodersConfiguration.swift diff --git a/Sources/SimpleHTTP/ContentData/ContentDataCoderConfiguration.swift b/Sources/SimpleHTTP/ContentData/ContentDataCoderConfiguration.swift deleted file mode 100644 index 62e3623..0000000 --- a/Sources/SimpleHTTP/ContentData/ContentDataCoderConfiguration.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation - -public struct ContentDataCoderConfiguration { - public var encoder: ContentDataEncoderConfiguration - public var decoder: ContentDataDecoderConfiguration - public let defaultType: HTTPContentType - - public init( - default: HTTPContentType, - encoder: ContentDataEncoderConfiguration, - decoder: ContentDataDecoderConfiguration, - ) { - self.encoder = encoder - self.decoder = decoder - self.defaultType = `default` - } - - public init() { - self.init( - default: .json, - encoder: [ - .json: JSONEncoder(), - .formURLEncoded: FormURLEncoder() - ], - decoder: [ - .json: JSONDecoder() - ] - ) - } -} - -@dynamicMemberLookup -public struct ContentDataEncoderConfiguration: ExpressibleByDictionaryLiteral { - private var encoders: [HTTPContentType: ContentDataEncoder] - - public init(encoders: [HTTPContentType: ContentDataEncoder]) { - self.encoders = encoders - } - - public init(dictionaryLiteral elements: (HTTPContentType, ContentDataEncoder)...) { - self.init(encoders: Dictionary(uniqueKeysWithValues: elements)) - } - - public subscript(contentType: HTTPContentType) -> ContentDataEncoder? { - get { encoders[contentType] } - set { encoders[contentType] = newValue } - } - - public subscript(dynamicMember keyPath: KeyPath) -> ContentDataEncoder? { - get { self[HTTPContentType.self[keyPath: keyPath]] } - set { self[HTTPContentType.self[keyPath: keyPath]] = newValue } - } -} - -@dynamicMemberLookup -public struct ContentDataDecoderConfiguration: ExpressibleByDictionaryLiteral { - private var decoders: [HTTPContentType: ContentDataDecoder] - - public init(decoders: [HTTPContentType: ContentDataDecoder]) { - self.decoders = decoders - } - - public init(dictionaryLiteral elements: (HTTPContentType, ContentDataDecoder)...) { - self.init(decoders: Dictionary(uniqueKeysWithValues: elements)) - } - - public subscript(contentType: HTTPContentType) -> ContentDataDecoder? { - get { decoders[contentType] } - set { decoders[contentType] = newValue } - } - - public subscript(dynamicMember keyPath: KeyPath) -> ContentDataDecoder? { - get { self[HTTPContentType.self[keyPath: keyPath]] } - set { self[HTTPContentType.self[keyPath: keyPath]] = newValue } - } -} diff --git a/Sources/SimpleHTTP/ContentData/ContentDataCodersConfiguration.swift b/Sources/SimpleHTTP/ContentData/ContentDataCodersConfiguration.swift new file mode 100644 index 0000000..b077577 --- /dev/null +++ b/Sources/SimpleHTTP/ContentData/ContentDataCodersConfiguration.swift @@ -0,0 +1,53 @@ +import Foundation + +public typealias ContentDataEncodersConfiguration = [HTTPContentType: ContentDataEncoder] +public typealias ContentDataDecodersConfiguration = [HTTPContentType: ContentDataDecoder] + +/// Defines the list of encoders and decoders to use. +public struct ContentDataCodersConfiguration { + public var encoders: ContentDataEncodersConfiguration + public var decoders: ContentDataDecodersConfiguration + public let defaultType: HTTPContentType + + public init( + default: HTTPContentType, + encoders: ContentDataEncodersConfiguration, + decoders: ContentDataDecodersConfiguration, + ) { + self.encoders = encoders + self.decoders = decoders + self.defaultType = `default` + } + + /// Creates a configuration with default coders and decoders. + /// + /// - Note: default encoder/decoder is set to JSON + public init() { + self.init( + default: .json, + encoders: [ + .json: JSONEncoder(), + .formURLEncoded: FormURLEncoder() + ], + decoders: [ + .json: JSONDecoder() + ] + ) + } +} + +extension ContentDataCodersConfiguration { + /// defines a single encoder to use for encoding `contentType` requests. If an encoder was already defined it will be replaced with the new value + public func encoding(_ contentType: HTTPContentType, with encoder: ContentDataEncoder) -> Self { + var copy = self + copy.encoders[contentType] = encoder + return copy + } + + /// defines a single decoder for decoding `contentType` responses. If a decoder was already defined it will be replaced with the new value + public func decoding(_ contentType: HTTPContentType, with decoder: ContentDataDecoder) -> Self { + var copy = self + copy.decoders[contentType] = decoder + return copy + } +} diff --git a/Sources/SimpleHTTP/Session/Session.swift b/Sources/SimpleHTTP/Session/Session.swift index 2f5b635..7710022 100644 --- a/Sources/SimpleHTTP/Session/Session.swift +++ b/Sources/SimpleHTTP/Session/Session.swift @@ -44,7 +44,7 @@ public class Session { public func response(for request: Request) async throws -> Output { let result = try await dataPublisher(for: request) - guard let decoder = config.data.decoder[result.contentType] else { + guard let decoder = config.data.decoders[result.contentType] else { throw SessionConfigurationError.missingDecoder(result.contentType) } @@ -77,10 +77,10 @@ extension Session { // FIXME: we also check body inside toURLRequest switch modifiedRequest.body { case .encodable: - encoder = config.data.encoder[requestContentType] + encoder = config.data.encoders[requestContentType] case .multipart, .none: // this one is supposed to never be nil - encoder = config.data.encoder[config.data.defaultType] + encoder = config.data.encoders[config.data.defaultType] } guard let encoder else { diff --git a/Sources/SimpleHTTP/Session/SessionConfiguration.swift b/Sources/SimpleHTTP/Session/SessionConfiguration.swift index 4396f4d..6b8e7fe 100644 --- a/Sources/SimpleHTTP/Session/SessionConfiguration.swift +++ b/Sources/SimpleHTTP/Session/SessionConfiguration.swift @@ -3,7 +3,7 @@ import Foundation /// a type defining some parameters for a `Session` public struct SessionConfiguration { /// data encoders/decoders configuration per content type - let data: ContentDataCoderConfiguration + let data: ContentDataCodersConfiguration /// queue on which to decode data let decodingQueue: DispatchQueue /// an interceptor to apply custom behavior on the session requests/responses. @@ -16,7 +16,7 @@ public struct SessionConfiguration { /// - Parameter decodeQueue: queue on which to decode data /// - Parameter interceptors: interceptor list to apply on the session requests/responses public init( - data: ContentDataCoderConfiguration = .init(), + data: ContentDataCodersConfiguration = .init(), decodingQueue: DispatchQueue = .main, interceptors: CompositeInterceptor = []) { self.data = data @@ -26,14 +26,14 @@ public struct SessionConfiguration { /// - Parameter dataError: Error type to use when having error with data public init( - data: ContentDataCoderConfiguration, + data: ContentDataCodersConfiguration, decodingQueue: DispatchQueue = .main, interceptors: CompositeInterceptor = [], dataError: DataError.Type ) { self.init(data: data, decodingQueue: decodingQueue, interceptors: interceptors) - self.errorConverter = { [decoder=data.decoder] data, contentType in - guard let decoder = decoder[contentType] else { + self.errorConverter = { [decoders=data.decoders] data, contentType in + guard let decoder = decoders[contentType] else { throw SessionConfigurationError.missingDecoder(contentType) } return try decoder.decode(dataError, from: data) diff --git a/Tests/SimpleHTTPTests/Session/SessionTests.swift b/Tests/SimpleHTTPTests/Session/SessionTests.swift index 63c380d..b32f646 100644 --- a/Tests/SimpleHTTPTests/Session/SessionTests.swift +++ b/Tests/SimpleHTTPTests/Session/SessionTests.swift @@ -3,10 +3,10 @@ import XCTest class SessionAsyncTests: XCTestCase { let baseURL = URL(string: "https://sessionTests.io")! - let data = ContentDataCoderConfiguration( + let data = ContentDataCodersConfiguration( default: .json, - encoder: [.json: JSONEncoder()], - decoder: [.json: JSONDecoder()] + encoders: [.json: JSONEncoder()], + decoders: [.json: JSONDecoder()] ) func test_response_responseIsValid_decodedOutputIsReturned() async throws { From ebcf87d8af09a8a713d8ad4264b56def39a43ce0 Mon Sep 17 00:00:00 2001 From: pjechris Date: Mon, 15 Jun 2026 10:45:36 +0200 Subject: [PATCH 2/2] updated README --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 312ecd3..64fbcb8 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,7 @@ You can also use a `Session` object. [`Session`](https://pjechris.github.io/Simp ```swift -let session = Session( - baseURL: URL(string: "https://github.com")!, - encoder: JSONEncoder(), - decoder: JSONDecoder() -) +let session = Session(baseURL: URL(string: "https://github.com")!) try await session.response(for: .login(UserBody(username: "pjechris", password: "MyPassword"))) @@ -65,9 +61,24 @@ try await session.response(for: .login(UserBody(username: "pjechris", password: A few words about Session: - `baseURL` will be prepended to all call paths -- You can skip encoder and decoder if you use JSON +- By default JSON is used for both encoding and decoding, so you don't have to configure anything for JSON APIs - You can provide a custom `URLSession` instance if ever needed +### Customizing encoders/decoders + +Encoders and decoders are configured per content type through a `SessionConfiguration`. Pass a `ContentDataCodersConfiguration` to register the coders you need: + +```swift +let session = Session( + baseURL: URL(string: "https://github.com")!, + configuration: SessionConfiguration( + data: ContentDataCodersConfiguration() + .encoding(.json, with: JSONEncoder()) + .decoding(.json, with: JSONDecoder()) + ) +) +``` + ## Send a body Request support two body types: