Skip to content

address change request #1

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

Closed
wants to merge 11 commits into from
4 changes: 3 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ extension ParameterStyle {
) {
let resolvedStyle = style ?? .defaultForQueryItems
let resolvedExplode = explode ?? ParameterStyle.defaultExplodeFor(forStyle: resolvedStyle)
guard resolvedStyle == .form else {
switch resolvedStyle {
case .form, .deepObject: break
default:
throw RuntimeError.unsupportedParameterStyle(
name: name,
location: .query,
Expand Down
6 changes: 6 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
///
/// Details: https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2
case simple

/// The deepObject style.
///
/// Details: https://spec.openapis.org/oas/v3.1.0.html#style-values
case deepObject
}

extension ParameterStyle {
Expand Down Expand Up @@ -53,6 +58,7 @@ extension URICoderConfiguration.Style {
switch style {
case .form: self = .form
case .simple: self = .simple
case .deepObject: self = .deepObject
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ struct URICoderConfiguration {

/// A style for form-based URI expansion.
case form

/// A style for nested variable expansion
case deepObject
}

/// A character used to escape the space character.
Expand Down
46 changes: 46 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ private enum ParsingError: Swift.Error {

/// A malformed key-value pair was detected.
case malformedKeyValuePair(Raw)

/// An invalid configuration was detected.
case invalidConfiguration(String)
}

// MARK: - Parser implementations
Expand All @@ -61,13 +64,18 @@ extension URIParser {
switch configuration.style {
case .form: return [:]
case .simple: return ["": [""]]
case .deepObject: return [:]
}
}
switch (configuration.style, configuration.explode) {
case (.form, true): return try parseExplodedFormRoot()
case (.form, false): return try parseUnexplodedFormRoot()
case (.simple, true): return try parseExplodedSimpleRoot()
case (.simple, false): return try parseUnexplodedSimpleRoot()
case (.deepObject, true): return try parseExplodedDeepObjectRoot()
case (.deepObject, false):
let reason = "Deep object style is only valid with explode set to true"
throw ParsingError.invalidConfiguration(reason)
}
}

Expand Down Expand Up @@ -205,6 +213,44 @@ extension URIParser {
}
}
}

/// Parses the root node assuming the raw string uses the deepObject style
/// and the explode parameter is enabled.
/// - Returns: The parsed root node.
/// - Throws: An error if parsing fails.
private mutating func parseExplodedDeepObjectRoot() throws -> URIParsedNode {
try parseGenericRoot { data, appendPair in
let keyValueSeparator: Character = "="
let pairSeparator: Character = "&"
let nestedKeyStartingCharacter: Character = "["
let nestedKeyEndingCharacter: Character = "]"

func nestedKey(from deepObjectKey: String.SubSequence) -> Raw {
var unescapedFirstValue = Substring(deepObjectKey.removingPercentEncoding ?? "")
// Move to "["
_ = unescapedFirstValue.parseUpToCharacterOrEnd(nestedKeyStartingCharacter)
let nestedKey = unescapedFirstValue.parseUpToCharacterOrEnd(nestedKeyEndingCharacter)
return nestedKey
}

while !data.isEmpty {
let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd(
first: keyValueSeparator,
second: pairSeparator
)

guard case .foundFirst = firstResult else {
throw ParsingError.malformedKeyValuePair(firstValue)
}
// Hit the key/value separator, so a value will follow.
let secondValue = data.parseUpToCharacterOrEnd(pairSeparator)
let key = nestedKey(from: firstValue)
let value = secondValue

appendPair(key, [value])
}
}
}
}

// MARK: - URIParser utilities
Expand Down
27 changes: 24 additions & 3 deletions Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ extension URISerializer {

/// Nested containers are not supported.
case nestedContainersNotSupported

/// An invalid configuration was detected.
case invalidConfiguration(String)
}

/// Computes an escaped version of the provided string.
Expand Down Expand Up @@ -117,9 +120,12 @@ extension URISerializer {
switch configuration.style {
case .form: keyAndValueSeparator = "="
case .simple: keyAndValueSeparator = nil
case .deepObject: return // deepObject doesn't handle primitives
}
try serializePrimitiveKeyValuePair(primitive, forKey: key, separator: keyAndValueSeparator)
case .array(let array): try serializeArray(array.map(unwrapPrimitiveValue), forKey: key)
case .array(let array):
guard configuration.style != .deepObject else { return } // deepObject doesn't handle arrays
try serializeArray(array.map(unwrapPrimitiveValue), forKey: key)
case .dictionary(let dictionary):
try serializeDictionary(dictionary.mapValues(unwrapPrimitiveValue), forKey: key)
}
Expand Down Expand Up @@ -180,6 +186,8 @@ extension URISerializer {
case (.simple, _):
keyAndValueSeparator = nil
pairSeparator = ","
case (.deepObject, _):
preconditionFailure("serializeArray should not be called for deepObject style")
}
func serializeNext(_ element: URIEncodedNode.Primitive) throws {
if let keyAndValueSeparator {
Expand Down Expand Up @@ -228,8 +236,18 @@ extension URISerializer {
case (.simple, false):
keyAndValueSeparator = ","
pairSeparator = ","
case (.deepObject, true):
keyAndValueSeparator = "="
pairSeparator = "&"
case (.deepObject, false):
let reason = "Deep object style is only valid with explode set to true"
throw SerializationError.invalidConfiguration(reason)
}

func serializeNestedKey(_ elementKey: String, forKey rootKey: String) -> String {
guard case .deepObject = configuration.style else { return elementKey }
return rootKey + "[" + elementKey + "]"
}
func serializeNext(_ element: URIEncodedNode.Primitive, forKey elementKey: String) throws {
try serializePrimitiveKeyValuePair(element, forKey: elementKey, separator: keyAndValueSeparator)
}
Expand All @@ -238,10 +256,13 @@ extension URISerializer {
data.append(containerKeyAndValue)
}
for (elementKey, element) in sortedDictionary.dropLast() {
try serializeNext(element, forKey: elementKey)
try serializeNext(element, forKey: serializeNestedKey(elementKey, forKey: key))
data.append(pairSeparator)
}
if let (elementKey, element) = sortedDictionary.last { try serializeNext(element, forKey: elementKey) }

if let (elementKey, element) = sortedDictionary.last {
try serializeNext(element, forKey: serializeNestedKey(elementKey, forKey: key))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@ final class Test_URIEncoder: Test_Runtime {
let encodedString = try encoder.encode(Foo(bar: "hello world"), forKey: "root")
XCTAssertEqual(encodedString, "bar=hello+world")
}

func testNestedEncoding() throws {
struct Foo: Encodable { var bar: String }
let serializer = URISerializer(configuration: .deepObjectExplode)
let encoder = URIEncoder(serializer: serializer)
let encodedString = try encoder.encode(Foo(bar: "hello world"), forKey: "root")
XCTAssertEqual(encodedString, "root%5Bbar%5D=hello%20world")
}
}
23 changes: 16 additions & 7 deletions Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import XCTest
final class Test_URIParser: Test_Runtime {

let testedVariants: [URICoderConfiguration] = [
.formExplode, .formUnexplode, .simpleExplode, .simpleUnexplode, .formDataExplode, .formDataUnexplode,
.formExplode, .formUnexplode, .simpleExplode, .simpleUnexplode, .formDataExplode, .formDataUnexplode, .deepObjectExplode
]

func testParsing() throws {
Expand All @@ -29,7 +29,8 @@ final class Test_URIParser: Test_Runtime {
simpleExplode: .custom("", value: ["": [""]]),
simpleUnexplode: .custom("", value: ["": [""]]),
formDataExplode: "empty=",
formDataUnexplode: "empty="
formDataUnexplode: "empty=",
deepObjectExplode: .custom("", value: [:])
),
value: ["empty": [""]]
),
Expand All @@ -40,7 +41,8 @@ final class Test_URIParser: Test_Runtime {
simpleExplode: .custom("", value: ["": [""]]),
simpleUnexplode: .custom("", value: ["": [""]]),
formDataExplode: "",
formDataUnexplode: ""
formDataUnexplode: "",
deepObjectExplode: ""
),
value: [:]
),
Expand All @@ -51,7 +53,8 @@ final class Test_URIParser: Test_Runtime {
simpleExplode: .custom("fred", value: ["": ["fred"]]),
simpleUnexplode: .custom("fred", value: ["": ["fred"]]),
formDataExplode: "who=fred",
formDataUnexplode: "who=fred"
formDataUnexplode: "who=fred",
deepObjectExplode: .custom("", value: [:])
),
value: ["who": ["fred"]]
),
Expand All @@ -62,7 +65,8 @@ final class Test_URIParser: Test_Runtime {
simpleExplode: .custom("Hello%20World", value: ["": ["Hello World"]]),
simpleUnexplode: .custom("Hello%20World", value: ["": ["Hello World"]]),
formDataExplode: "hello=Hello+World",
formDataUnexplode: "hello=Hello+World"
formDataUnexplode: "hello=Hello+World",
deepObjectExplode: .custom("", value: [:])
),
value: ["hello": ["Hello World"]]
),
Expand All @@ -73,7 +77,8 @@ final class Test_URIParser: Test_Runtime {
simpleExplode: .custom("red,green,blue", value: ["": ["red", "green", "blue"]]),
simpleUnexplode: .custom("red,green,blue", value: ["": ["red", "green", "blue"]]),
formDataExplode: "list=red&list=green&list=blue",
formDataUnexplode: "list=red,green,blue"
formDataUnexplode: "list=red,green,blue",
deepObjectExplode: .custom("", value: [:])
),
value: ["list": ["red", "green", "blue"]]
),
Expand All @@ -93,7 +98,8 @@ final class Test_URIParser: Test_Runtime {
formDataUnexplode: .custom(
"keys=comma,%2C,dot,.,semi,%3B",
value: ["keys": ["comma", ",", "dot", ".", "semi", ";"]]
)
),
deepObjectExplode: "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Bsemi%5D=%3B"
),
value: ["semi": [";"], "dot": ["."], "comma": [","]]
),
Expand All @@ -117,6 +123,7 @@ final class Test_URIParser: Test_Runtime {
try testVariant(.simpleUnexplode, variants.simpleUnexplode)
try testVariant(.formDataExplode, variants.formDataExplode)
try testVariant(.formDataUnexplode, variants.formDataUnexplode)
try testVariant(.deepObjectExplode, variants.deepObjectExplode)
}
}
}
Expand All @@ -133,6 +140,7 @@ extension Test_URIParser {
static let simpleUnexplode: Self = .init(name: "simpleUnexplode", config: .simpleUnexplode)
static let formDataExplode: Self = .init(name: "formDataExplode", config: .formDataExplode)
static let formDataUnexplode: Self = .init(name: "formDataUnexplode", config: .formDataUnexplode)
static let deepObjectExplode: Self = .init(name: "deepObjectExplode", config: .deepObjectExplode)
}
struct Variants {

Expand Down Expand Up @@ -161,6 +169,7 @@ extension Test_URIParser {
var simpleUnexplode: Input
var formDataExplode: Input
var formDataUnexplode: Input
var deepObjectExplode: Input
}
var variants: Variants
var value: URIParsedNode
Expand Down
Loading