Skip to content

Custom JSON encoding options #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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
)
}
}
32 changes: 31 additions & 1 deletion Tests/OpenAPIRuntimeTests/Conversion/Test_Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//
import XCTest
@_spi(Generated) import OpenAPIRuntime
@_spi(Generated) @testable import OpenAPIRuntime

final class Test_Configuration: Test_Runtime {

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

func testJSONEncodingOptions_default() throws {
let converter = Converter(configuration: Configuration())
XCTAssertEqualStringifiedData(
try converter.encoder.encode(testPetWithPath),
testPetWithPathPrettifiedWithEscapingSlashes
)
}

func testJSONEncodingOptions_empty() throws {
let converter = Converter(
configuration: Configuration(jsonEncodingOptions: [
.sortedKeys // without sorted keys, this test would be unreliable
])
)
XCTAssertEqualStringifiedData(
try converter.encoder.encode(testPetWithPath),
testPetWithPathMinifiedWithEscapingSlashes
)
}

func testJSONEncodingOptions_prettyWithoutEscapingSlashes() throws {
let converter = Converter(
configuration: Configuration(jsonEncodingOptions: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes])
)
XCTAssertEqualStringifiedData(
try converter.encoder.encode(testPetWithPath),
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