Skip to content

Commit

Permalink
[Runtime] Add support of deepObjects query styling
Browse files Browse the repository at this point in the history
### Motivation

The runtime changes for:
apple/swift-openapi-generator#259

### Modifications

Added `deepObject` style to serializer & parser in order to support nested keys
on query parameters.

### Result

Support nested keys on query parameters.

### Test Plan

These are just the runtime changes, tested together with generated
changes.
  • Loading branch information
kstefanou52 committed Mar 7, 2024
1 parent 76951d7 commit 2529143
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 38 deletions.
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: Should update datatracker
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
77 changes: 77 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ 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, _): return try parseExplodedDeepObjectRoot()
}
}

Expand Down Expand Up @@ -205,6 +207,50 @@ 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 ?? "")
let nestedKey = unescapedFirstValue.parseBetweenCharacters(
startingCharacter: nestedKeyStartingCharacter,
endingCharacter: nestedKeyEndingCharacter
)
return nestedKey
}

while !data.isEmpty {
let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd(
first: keyValueSeparator,
second: pairSeparator
)
let key: Raw
let value: Raw
switch firstResult {
case .foundFirst:
// Hit the key/value separator, so a value will follow.
let secondValue = data.parseUpToCharacterOrEnd(pairSeparator)

key = nestedKey(from: firstValue)
value = secondValue
case .foundSecondOrEnd:
// No key/value separator, treat the string as the key.
key = nestedKey(from: firstValue)
value = .init()
}
appendPair(key, [value])
}
}
}
}

// MARK: - URIParser utilities
Expand Down Expand Up @@ -326,4 +372,35 @@ extension String.SubSequence {
}
return finalize()
}


/// Accumulates characters from the `startingCharacter` character provided,
/// until the `endingCharacter` is reached. Moves the underlying startIndex.
/// - Parameters:
/// - startingCharacter: A character to start with.
/// - endingCharacter: A character to stop at.
/// - Returns: The accumulated substring.
fileprivate mutating func parseBetweenCharacters(startingCharacter: Character, endingCharacter: Character) -> Self {
let startIndex = startIndex
guard startIndex != endIndex else { return .init() }
guard let startingCharacterIndex = firstIndex(of: startingCharacter) else { return self }
var currentIndex = startingCharacterIndex

// var x = self.firstIndex(of: startingCharacter)

func finalize() -> Self {
let parsed = self[index(after: startingCharacterIndex)..<currentIndex]
guard currentIndex == endIndex else {
self = self[index(after: currentIndex)...]
return parsed
}
self = .init()
return parsed
}
while currentIndex != endIndex {
let currentChar = self[currentIndex]
if currentChar == endingCharacter { return finalize() } else { formIndex(after: &currentIndex) }
}
return finalize()
}
}
18 changes: 16 additions & 2 deletions Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ extension URISerializer {
switch configuration.style {
case .form: keyAndValueSeparator = "="
case .simple: keyAndValueSeparator = nil
case .deepObject: keyAndValueSeparator = "="
}
try serializePrimitiveKeyValuePair(primitive, forKey: key, separator: keyAndValueSeparator)
case .array(let array): try serializeArray(array.map(unwrapPrimitiveValue), forKey: key)
Expand Down Expand Up @@ -180,6 +181,9 @@ extension URISerializer {
case (.simple, _):
keyAndValueSeparator = nil
pairSeparator = ","
case (.deepObject, _):
keyAndValueSeparator = "="
pairSeparator = "&"
}
func serializeNext(_ element: URIEncodedNode.Primitive) throws {
if let keyAndValueSeparator {
Expand Down Expand Up @@ -228,8 +232,15 @@ extension URISerializer {
case (.simple, false):
keyAndValueSeparator = ","
pairSeparator = ","
case (.deepObject, _):
keyAndValueSeparator = "="
pairSeparator = "&"
}

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 +249,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+world")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "",
simpleUnexplode: "",
formDataExplode: "empty=",
formDataUnexplode: "empty="
formDataUnexplode: "empty=",
deepObjectExplode: "empty="
)
),
makeCase(
Expand All @@ -43,7 +44,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "fred",
simpleUnexplode: "fred",
formDataExplode: "who=fred",
formDataUnexplode: "who=fred"
formDataUnexplode: "who=fred",
deepObjectExplode: "who=fred"
)
),
makeCase(
Expand All @@ -55,7 +57,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "1234",
simpleUnexplode: "1234",
formDataExplode: "x=1234",
formDataUnexplode: "x=1234"
formDataUnexplode: "x=1234",
deepObjectExplode: "x=1234"
)
),
makeCase(
Expand All @@ -67,7 +70,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "12.34",
simpleUnexplode: "12.34",
formDataExplode: "x=12.34",
formDataUnexplode: "x=12.34"
formDataUnexplode: "x=12.34",
deepObjectExplode: "x=12.34"
)
),
makeCase(
Expand All @@ -79,7 +83,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "true",
simpleUnexplode: "true",
formDataExplode: "enabled=true",
formDataUnexplode: "enabled=true"
formDataUnexplode: "enabled=true",
deepObjectExplode: "enabled=true"
)
),
makeCase(
Expand All @@ -91,7 +96,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "Hello%20World",
simpleUnexplode: "Hello%20World",
formDataExplode: "hello=Hello+World",
formDataUnexplode: "hello=Hello+World"
formDataUnexplode: "hello=Hello+World",
deepObjectExplode: "hello=Hello+World"
)
),
makeCase(
Expand All @@ -103,7 +109,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "red,green,blue",
simpleUnexplode: "red,green,blue",
formDataExplode: "list=red&list=green&list=blue",
formDataUnexplode: "list=red,green,blue"
formDataUnexplode: "list=red,green,blue",
deepObjectExplode: "list=red&list=green&list=blue"
)
),
makeCase(
Expand All @@ -118,7 +125,8 @@ final class Test_URISerializer: Test_Runtime {
simpleExplode: "comma=%2C,dot=.,semi=%3B",
simpleUnexplode: "comma,%2C,dot,.,semi,%3B",
formDataExplode: "comma=%2C&dot=.&semi=%3B",
formDataUnexplode: "keys=comma,%2C,dot,.,semi,%3B"
formDataUnexplode: "keys=comma,%2C,dot,.,semi,%3B",
deepObjectExplode: "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Bsemi%5D=%3B"
)
),
]
Expand All @@ -140,6 +148,7 @@ final class Test_URISerializer: Test_Runtime {
try testVariant(.simpleUnexplode, testCase.variants.simpleUnexplode)
try testVariant(.formDataExplode, testCase.variants.formDataExplode)
try testVariant(.formDataUnexplode, testCase.variants.formDataUnexplode)
try testVariant(.deepObjectExplode, testCase.variants.deepObjectExplode)
}
}
}
Expand All @@ -156,6 +165,7 @@ extension Test_URISerializer {
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 {
var formExplode: String
Expand All @@ -164,6 +174,7 @@ extension Test_URISerializer {
var simpleUnexplode: String
var formDataExplode: String
var formDataUnexplode: String
var deepObjectExplode: String
}
var value: URIEncodedNode
var key: String
Expand Down
Loading

0 comments on commit 2529143

Please sign in to comment.