Skip to content

Commit

Permalink
Custom JSON encoding options (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
czechboy0 authored Jul 29, 2024
1 parent 71fcfa7 commit 26e8ae3
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 1 deletion.
26 changes: 26 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,27 @@ public protocol CustomCoder: Sendable {
/// - 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
}

/// The options that control the encoded JSON data.
public struct JSONEncodingOptions: OptionSet, Sendable {

/// The format's default value.
public let rawValue: UInt

/// Creates a JSONEncodingOptions value with the given raw value.
public init(rawValue: UInt) { self.rawValue = rawValue }

/// Include newlines and indentation to make the output more human-readable.
public static let prettyPrinted: JSONEncodingOptions = .init(rawValue: 1 << 0)

/// Serialize JSON objects with field keys sorted in lexicographic order.
public static let sortedKeys: JSONEncodingOptions = .init(rawValue: 1 << 1)

/// Omit escaping forward slashes with backslashes.
///
/// Important: Only use this option when the output is not embedded in HTML/XML.
public static let withoutEscapingSlashes: JSONEncodingOptions = .init(rawValue: 1 << 2)
}

/// A set of configuration values used by the generated client and server types.
Expand All @@ -123,6 +143,9 @@ public struct Configuration: Sendable {
/// The transcoder used when converting between date and string values.
public var dateTranscoder: any DateTranscoder

/// The options for the underlying JSON encoder.
public var jsonEncodingOptions: JSONEncodingOptions

/// The generator to use when creating mutlipart bodies.
public var multipartBoundaryGenerator: any MultipartBoundaryGenerator

Expand All @@ -134,14 +157,17 @@ public struct Configuration: Sendable {
/// - Parameters:
/// - dateTranscoder: The transcoder to use when converting between date
/// and string values.
/// - jsonEncodingOptions: The options for the underlying JSON encoder.
/// - 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.
public init(
dateTranscoder: any DateTranscoder = .iso8601,
jsonEncodingOptions: JSONEncodingOptions = [.sortedKeys, .prettyPrinted],
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
xmlCoder: (any CustomCoder)? = nil
) {
self.dateTranscoder = dateTranscoder
self.jsonEncodingOptions = jsonEncodingOptions
self.multipartBoundaryGenerator = multipartBoundaryGenerator
self.xmlCoder = xmlCoder
}
Expand Down
13 changes: 12 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/Converter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import class Foundation.JSONDecoder
self.configuration = configuration

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

self.headerFieldEncoder = JSONEncoder()
Expand All @@ -49,3 +49,14 @@ import class Foundation.JSONDecoder
self.decoder.dateDecodingStrategy = .from(dateTranscoder: configuration.dateTranscoder)
}
}

extension JSONEncoder.OutputFormatting {
/// Creates a new value.
/// - Parameter options: The JSON encoding options to represent.
init(_ options: JSONEncodingOptions) {
self.init()
if options.contains(.prettyPrinted) { formUnion(.prettyPrinted) }
if options.contains(.sortedKeys) { formUnion(.sortedKeys) }
if options.contains(.withoutEscapingSlashes) { formUnion(.withoutEscapingSlashes) }
}
}
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: Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads.
@available(*, deprecated, renamed: "init(dateTranscoder:jsonEncodingOptions:multipartBoundaryGenerator:xmlCoder:)")
@_disfavoredOverload public init(
dateTranscoder: any DateTranscoder = .iso8601,
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
xmlCoder: (any CustomCoder)? = nil
) {
self.init(
dateTranscoder: dateTranscoder,
jsonEncodingOptions: [.sortedKeys, .prettyPrinted],
multipartBoundaryGenerator: multipartBoundaryGenerator,
xmlCoder: xmlCoder
)
}
}
34 changes: 34 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//
import XCTest
import HTTPTypes
import Foundation
@_spi(Generated) import OpenAPIRuntime

final class Test_Configuration: Test_Runtime {
Expand All @@ -27,4 +29,36 @@ final class Test_Configuration: Test_Runtime {
XCTAssertEqual(try transcoder.encode(testDateWithFractionalSeconds), testDateWithFractionalSecondsString)
XCTAssertEqual(testDateWithFractionalSeconds, try transcoder.decode(testDateWithFractionalSecondsString))
}

func _testJSON(configuration: Configuration, expected: String) async throws {
let converter = Converter(configuration: configuration)
var headerFields: HTTPFields = [:]
let body = try converter.setResponseBodyAsJSON(
testPetWithPath,
headerFields: &headerFields,
contentType: "application/json"
)
let data = try await Data(collecting: body, upTo: 1024)
XCTAssertEqualStringifiedData(data, expected)
}

func testJSONEncodingOptions_default() async throws {
try await _testJSON(configuration: Configuration(), expected: testPetWithPathPrettifiedWithEscapingSlashes)
}

func testJSONEncodingOptions_empty() async throws {
try await _testJSON(
configuration: Configuration(jsonEncodingOptions: [
.sortedKeys // without sorted keys, this test would be unreliable
]),
expected: testPetWithPathMinifiedWithEscapingSlashes
)
}

func testJSONEncodingOptions_prettyWithoutEscapingSlashes() async throws {
try await _testJSON(
configuration: Configuration(jsonEncodingOptions: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]),
expected: testPetWithPathPrettifiedWithoutEscapingSlashes
)
}
}
27 changes: 27 additions & 0 deletions Tests/OpenAPIRuntimeTests/Test_Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ class Test_Runtime: XCTestCase {

var testStructPrettyData: Data { Data(testStructPrettyString.utf8) }

var testPetWithPath: TestPetWithPath { .init(name: "Fluffz", path: URL(string: "/land/forest")!) }

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

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

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

var testStructURLFormData: Data { Data(testStructURLFormString.utf8) }

var testEvents: [TestPet] { [.init(name: "Rover"), .init(name: "Pancake")] }
Expand Down Expand Up @@ -247,6 +269,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

0 comments on commit 26e8ae3

Please sign in to comment.