Skip to content
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

Custom JSON encoding options #112

Merged
merged 3 commits into from
Jul 29, 2024
Merged
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
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