Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 7 additions & 22 deletions Sources/OpenAPIRuntime/Conversion/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,30 +96,12 @@ extension JSONDecoder.DateDecodingStrategy {
}
}

/// A type that allows custom content type encoding and decoding.
public protocol CustomCoder: Sendable {

/// Encodes the given value and returns its custom encoded representation.
///
/// - Parameter value: The value to encode.
/// - Returns: A new `Data` value containing the custom encoded data.
/// - Throws: An error if encoding fails.
func customEncode<T: Encodable>(_ value: T) throws -> Data

/// Decodes a value of the given type from the given custom representation.
///
/// - Parameters:
/// - type: The type of the value to decode.
/// - data: The data to decode from.
/// - Returns: A value of the requested type.
/// - Throws: An error if decoding fails.
func customDecode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T

}

/// A set of configuration values used by the generated client and server types.
public struct Configuration: Sendable {

/// The JSON coder for encoding and decoding JSON bodies.
public var jsonCoder: any CustomCoder

/// The transcoder used when converting between date and string values.
public var dateTranscoder: any DateTranscoder

Expand All @@ -132,15 +114,18 @@ public struct Configuration: Sendable {
/// Creates a new configuration with the specified values.
///
/// - Parameters:
/// - jsonCoder: The JSON coder for encoding and decoding JSON bodies.
/// - dateTranscoder: The transcoder to use when converting between date
/// and string values.
/// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies.
/// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads.
/// - xmlCoder: The XML coder for encoding and decoding XML bodies. Only required when using XML body payloads.
public init(
jsonCoder: any CustomCoder = FoundationJSONCoder(),
dateTranscoder: any DateTranscoder = .iso8601,
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
xmlCoder: (any CustomCoder)? = nil
) {
self.jsonCoder = jsonCoder
self.dateTranscoder = dateTranscoder
self.multipartBoundaryGenerator = multipartBoundaryGenerator
self.xmlCoder = xmlCoder
Expand Down
31 changes: 16 additions & 15 deletions Sources/OpenAPIRuntime/Conversion/Converter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,29 @@ import class Foundation.JSONDecoder
/// Configuration used to set up the converter.
public let configuration: Configuration

/// JSON encoder.
internal var encoder: JSONEncoder

/// JSON decoder.
internal var decoder: JSONDecoder
/// JSON coder for body data.
internal var jsonCoder: any CustomCoder

/// JSON encoder used for header fields.
internal var headerFieldEncoder: JSONEncoder
internal var headerFieldJSONEncoder: JSONEncoder

/// Creates a new converter with the behavior specified by the configuration.
public init(configuration: Configuration) {
self.configuration = configuration

self.encoder = JSONEncoder()
self.encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
self.encoder.dateEncodingStrategy = .from(dateTranscoder: configuration.dateTranscoder)

self.headerFieldEncoder = JSONEncoder()
self.headerFieldEncoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
self.headerFieldEncoder.dateEncodingStrategy = .from(dateTranscoder: configuration.dateTranscoder)
self.jsonCoder = configuration.jsonCoder
self.headerFieldJSONEncoder = Self.newHeaderFieldJSONEncoder(dateTranscoder: configuration.dateTranscoder)
}
}

self.decoder = JSONDecoder()
self.decoder.dateDecodingStrategy = .from(dateTranscoder: configuration.dateTranscoder)
extension Converter {
/// Creates a new JSON encoder specifically for header fields.
/// - Parameter dateTranscoder: The transcoder for dates.
/// - Returns: The configured JSON encoder.
internal static func newHeaderFieldJSONEncoder(dateTranscoder: any DateTranscoder) -> JSONEncoder {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
encoder.dateEncodingStrategy = .from(dateTranscoder: dateTranscoder)
return encoder
}
}
8 changes: 4 additions & 4 deletions Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ extension Converter {
/// - Throws: An error if decoding from the body fails.
func convertJSONToBodyCodable<T: Decodable>(_ body: HTTPBody) async throws -> T {
let data = try await Data(collecting: body, upTo: .max)
return try decoder.decode(T.self, from: data)
return try jsonCoder.customDecode(T.self, from: data)
}

/// Returns a JSON body for the provided encodable value.
/// - Parameter value: The value to encode as JSON.
/// - Returns: The raw JSON body.
/// - Throws: An error if encoding to JSON fails.
func convertBodyCodableToJSON<T: Encodable>(_ value: T) throws -> HTTPBody {
let data = try encoder.encode(value)
let data = try jsonCoder.customEncode(value)
return HTTPBody(data)
}

Expand Down Expand Up @@ -258,7 +258,7 @@ extension Converter {
/// - Returns: A JSON string.
/// - Throws: An error if encoding the value to JSON fails.
func convertHeaderFieldCodableToJSON<T: Encodable>(_ value: T) throws -> String {
let data = try headerFieldEncoder.encode(value)
let data = try headerFieldJSONEncoder.encode(value)
let stringValue = String(decoding: data, as: UTF8.self)
return stringValue
}
Expand All @@ -269,7 +269,7 @@ extension Converter {
/// - Throws: An error if decoding from the JSON string fails.
func convertJSONToHeaderFieldCodable<T: Decodable>(_ stringValue: Substring) throws -> T {
let data = Data(stringValue.utf8)
return try decoder.decode(T.self, from: data)
return try jsonCoder.customDecode(T.self, from: data)
}

// MARK: - Helpers for specific types of parameters
Expand Down
101 changes: 101 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/CustomCoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Foundation

/// A type that allows custom content type encoding and decoding.
public protocol CustomCoder: Sendable {

/// Encodes the given value and returns its custom encoded representation.
///
/// - Parameter value: The value to encode.
/// - Returns: A new `Data` value containing the custom encoded data.
/// - Throws: An error if encoding fails.
func customEncode<T: Encodable>(_ value: T) throws -> Data

/// Decodes a value of the given type from the given custom representation.
///
/// - Parameters:
/// - type: The type of the value to decode.
/// - data: The data to decode from.
/// - Returns: A value of the requested type.
/// - Throws: An error if decoding fails.
func customDecode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T

/// Updates the coder to use the provided date transcoder.
/// - Parameter dateTranscoder: The type to use for transcoding dates.
func updateDateTranscoder(_ dateTranscoder: any DateTranscoder)
}

extension CustomCoder {
/// Updates the coder to use the provided date transcoder.
/// - Parameter dateTranscoder: The type to use for transcoding dates.
public func updateDateTranscoder(_ dateTranscoder: any DateTranscoder) {
// A defaulted implementation, no-op.
}
}

/// A coder that uses the `JSONEncoder` and `JSONDecoder` types from the `Foundation` library.
public struct FoundationJSONCoder: Sendable {

/// The JSON encoder.
internal let encoder: JSONEncoder

/// The JSON decoder.
internal let decoder: JSONDecoder

/// Creates a new coder.
/// - Parameters:
/// - encoder: The JSON encoder.
/// - decoder: The JSON decoder.
public init(encoder: JSONEncoder, decoder: JSONDecoder) {
self.encoder = encoder
self.decoder = decoder
}
/// Creates a new coder.
/// - Parameter outputFormatting: The output formatting provided to the `JSONEncoder`.
public init(outputFormatting: JSONEncoder.OutputFormatting = [.sortedKeys, .prettyPrinted]) {
let encoder = JSONEncoder()
encoder.outputFormatting = outputFormatting
let decoder = JSONDecoder()
self.init(encoder: encoder, decoder: decoder)
}
}

extension FoundationJSONCoder: CustomCoder {

/// Encodes the given value and returns its custom encoded representation.
///
/// - Parameter value: The value to encode.
/// - Returns: A new `Data` value containing the custom encoded data.
/// - Throws: An error if encoding fails.
public func customEncode<T>(_ value: T) throws -> Data where T: Encodable { try encoder.encode(value) }

/// Decodes a value of the given type from the given custom representation.
///
/// - Parameters:
/// - type: The type of the value to decode.
/// - data: The data to decode from.
/// - Returns: A value of the requested type.
/// - Throws: An error if decoding fails.
public func customDecode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
try decoder.decode(type, from: data)
}

/// Updates the coder to use the provided date transcoder.
/// - Parameter dateTranscoder: The type to use for transcoding dates.
public func updateDateTranscoder(_ dateTranscoder: any DateTranscoder) {
self.encoder.dateEncodingStrategy = .from(dateTranscoder: dateTranscoder)
self.decoder.dateDecodingStrategy = .from(dateTranscoder: dateTranscoder)
}
}
21 changes: 21 additions & 0 deletions Sources/OpenAPIRuntime/Deprecated/Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,25 @@ extension Configuration {
) {
self.init(dateTranscoder: dateTranscoder, multipartBoundaryGenerator: multipartBoundaryGenerator, xmlCoder: nil)
}

/// Creates a new configuration with the specified values.
///
/// - Parameters:
/// - dateTranscoder: The transcoder to use when converting between date
/// and string values.
/// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies.
/// - xmlCoder: The XML coder for encoding and decoding XML bodies. Only required when using XML body payloads.
@available(*, deprecated, renamed: "init(jsonCoder:dateTranscoder:multipartBoundaryGenerator:xmlCoder:)")
@_disfavoredOverload public init(
dateTranscoder: any DateTranscoder = .iso8601,
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
xmlCoder: (any CustomCoder)? = nil
) {
self.init(
jsonCoder: FoundationJSONCoder(),
dateTranscoder: dateTranscoder,
multipartBoundaryGenerator: multipartBoundaryGenerator,
xmlCoder: xmlCoder
)
}
}
21 changes: 21 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,25 @@ final class Test_Configuration: Test_Runtime {
XCTAssertEqual(try transcoder.encode(testDateWithFractionalSeconds), testDateWithFractionalSecondsString)
XCTAssertEqual(testDateWithFractionalSeconds, try transcoder.decode(testDateWithFractionalSecondsString))
}
func testJSONCoder_defaultConfiguration() throws {
let configuration = Configuration()
XCTAssertEqualStringifiedData(
try configuration.jsonCoder.customEncode(testPetWithPath),
testPetWithPathPrettifiedWithEscapingSlashes
)
}
func testJSONCoder_defaultInit() throws {
let coder = FoundationJSONCoder()
XCTAssertEqualStringifiedData(
try coder.customEncode(testPetWithPath),
testPetWithPathPrettifiedWithEscapingSlashes
)
}
func testJSONCoder_minifiedWithoutEscapingSlashes() throws {
let coder = FoundationJSONCoder(outputFormatting: [.sortedKeys, .withoutEscapingSlashes])
XCTAssertEqualStringifiedData(
try coder.customEncode(testPetWithPath),
testPetWithPathMinifiedWithoutEscapingSlashes
)
}
}
17 changes: 17 additions & 0 deletions Tests/OpenAPIRuntimeTests/Test_Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ class Test_Runtime: XCTestCase {
}
"""#
}
var testPetWithPath: TestPetWithPath { .init(name: "Fluffz", path: URL(string: "/land/forest")!) }

var testPetWithPathMinifiedWithoutEscapingSlashes: String { #"{"name":"Fluffz","path":"/land/forest"}"# }

var testPetWithPathPrettifiedWithEscapingSlashes: String {
#"""
{
"name" : "Fluffz",
"path" : "\/land\/forest"
}
"""#
}

var testStructURLFormString: String { "age=3&name=Rover%21&type=Golden+Retriever" }

Expand Down Expand Up @@ -247,6 +259,11 @@ public func XCTAssertEqualURLString(_ lhs: URL?, _ rhs: String, file: StaticStri

struct TestPet: Codable, Equatable { var name: String }

struct TestPetWithPath: Codable, Equatable {
var name: String
var path: URL
}

struct TestPetDetailed: Codable, Equatable {
var name: String
var type: String
Expand Down