Skip to content

Fix optional query param parsing #48

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 1 commit into from
Sep 7, 2023
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
2 changes: 1 addition & 1 deletion Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ extension Converter {
dateTranscoder: configuration.dateTranscoder
)
)
let value = try decoder.decode(
let value = try decoder.decodeIfPresent(
T.self,
forKey: name,
from: query
Expand Down
4 changes: 2 additions & 2 deletions Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,9 @@ extension Converter {
explode: Bool?,
name: String,
as type: T.Type,
convert: (String, ParameterStyle, Bool) throws -> T
convert: (String, ParameterStyle, Bool) throws -> T?
) throws -> T? {
guard let query else {
guard let query, !query.isEmpty else {
return nil
}
let (resolvedStyle, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
Expand Down
47 changes: 47 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ extension URIDecoder {
}
}

/// Attempt to decode an object from an URI string, if present.
///
/// Under the hood, `URIDecoder` first parses the string into a
/// `URIParsedNode` using `URIParser`, and then uses
/// `URIValueFromNodeDecoder` to decode the `Decodable` value.
///
/// - Parameters:
/// - type: The type to decode.
/// - key: The key of the decoded value. Only used with certain styles
/// and explode options, ignored otherwise.
/// - data: The URI-encoded string.
/// - Returns: The decoded value.
func decodeIfPresent<T: Decodable>(
_ type: T.Type = T.self,
forKey key: String = "",
from data: String
) throws -> T? {
try withCachedParser(from: data) { decoder in
try decoder.decodeIfPresent(type, forKey: key)
}
}

/// Make multiple decode calls on the parsed URI.
///
/// Use to avoid repeatedly reparsing the raw string.
Expand Down Expand Up @@ -133,4 +155,29 @@ struct URICachedDecoder {
)
return try decoder.decodeRoot()
}

/// Attempt to decode an object from an URI-encoded string, if present.
///
/// Under the hood, `URICachedDecoder` already has a pre-parsed
/// `URIParsedNode` and uses `URIValueFromNodeDecoder` to decode
/// the `Decodable` value.
///
/// - Parameters:
/// - type: The type to decode.
/// - key: The key of the decoded value. Only used with certain styles
/// and explode options, ignored otherwise.
/// - Returns: The decoded value.
func decodeIfPresent<T: Decodable>(
_ type: T.Type = T.self,
forKey key: String = ""
) throws -> T? {
let decoder = URIValueFromNodeDecoder(
node: node,
rootKey: key[...],
style: configuration.style,
explode: configuration.explode,
dateTranscoder: configuration.dateTranscoder
)
return try decoder.decodeRootIfPresent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ final class URIValueFromNodeDecoder {
}
return value
}

/// Decodes the provided type from the root node.
/// - Parameter type: The type to decode from the decoder.
/// - Returns: The decoded value.
/// - Throws: When a decoding error occurs.
func decodeRootIfPresent<T: Decodable>(_ type: T.Type = T.self) throws -> T? {
// The root is only nil if the node is empty.
if try currentElementAsArray().isEmpty {
return nil
}
return try decodeRoot(type)
}
}

extension URIValueFromNodeDecoder {
Expand Down Expand Up @@ -285,7 +297,8 @@ extension URIValueFromNodeDecoder {
array = try rootValue(in: values)
}
guard array.count == 1 else {
try throwMismatch("Cannot parse a value from a node with multiple values.")
let reason = array.isEmpty ? "an empty node" : "a node with multiple values"
try throwMismatch("Cannot parse a value from \(reason).")
}
let value = array[0]
return value
Expand Down
26 changes: 26 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,30 @@ extension URIEncoder {
let encodedString = try serializer.serializeNode(node, forKey: key)
return encodedString
}

/// Attempt to encode an object into an URI string, if not nil.
///
/// Under the hood, `URIEncoder` first encodes the `Encodable` type
/// into a `URIEncodableNode` using `URIValueToNodeEncoder`, and then
/// `URISerializer` encodes the `URIEncodableNode` into a string based
/// on the configured behavior.
///
/// - Parameters:
/// - value: The value to encode.
/// - key: The key for which to encode the value. Can be an empty key,
/// in which case you still get a key-value pair, like `=foo`.
/// - Returns: The URI string.
func encodeIfPresent(
_ value: (some Encodable)?,
forKey key: String
) throws -> String {
guard let value else {
return ""
}
let encoder = URIValueToNodeEncoder()
let node = try encoder.encodeValue(value)
var serializer = serializer
let encodedString = try serializer.serializeNode(node, forKey: key)
return encodedString
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension URISingleValueEncodingContainer: SingleValueEncodingContainer {
}

func encodeNil() throws {
throw URIValueToNodeEncoder.GeneralError.nilNotSupported
// Nil is encoded as no value.
}

func encode(_ value: Bool) throws {
Expand Down
36 changes: 36 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,42 @@ final class Test_ServerConverterExtensions: Test_Runtime {
XCTAssertEqual(value, "foo")
}

// | server | get | request query | URI | optional | getOptionalQueryItemAsURI |
func test_getOptionalQueryItemAsURI_string_nil() throws {
let value: String? = try converter.getOptionalQueryItemAsURI(
in: "",
style: nil,
explode: nil,
name: "search",
as: String.self
)
XCTAssertNil(value)
}

// | server | get | request query | URI | optional | getOptionalQueryItemAsURI |
func test_getOptionalQueryItemAsURI_string_notFound() throws {
let value: String? = try converter.getOptionalQueryItemAsURI(
in: "foo=bar",
style: nil,
explode: nil,
name: "search",
as: String.self
)
XCTAssertNil(value)
}

// | server | get | request query | URI | optional | getOptionalQueryItemAsURI |
func test_getOptionalQueryItemAsURI_string_empty() throws {
let value: String? = try converter.getOptionalQueryItemAsURI(
in: "search=",
style: nil,
explode: nil,
name: "search",
as: String.self
)
XCTAssertEqual(value, "")
}

// | server | get | request query | URI | required | getRequiredQueryItemAsURI |
func test_getRequiredQueryItemAsURI_string() throws {
let value: String = try converter.getRequiredQueryItemAsURI(
Expand Down
52 changes: 52 additions & 0 deletions Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,56 @@ final class Test_URIDecoder: Test_Runtime {
)
XCTAssertEqual(decodedValue, Foo(bar: "hello world"))
}

func testDecoding_structWithOptionalProperty() throws {
struct Foo: Decodable, Equatable {
var bar: String?
var baz: Int
}
let decoder = URIDecoder(configuration: .formDataExplode)
do {
let decodedValue = try decoder.decode(
Foo.self,
forKey: "",
from: "baz=1&bar=hello+world"
)
XCTAssertEqual(decodedValue, Foo(bar: "hello world", baz: 1))
}
do {
let decodedValue = try decoder.decode(
Foo.self,
forKey: "",
from: "baz=1"
)
XCTAssertEqual(decodedValue, Foo(baz: 1))
}
}

func testDecoding_rootValue() throws {
let decoder = URIDecoder(configuration: .formDataExplode)
do {
let decodedValue = try decoder.decode(
Int.self,
forKey: "root",
from: "root=1"
)
XCTAssertEqual(decodedValue, 1)
}
do {
let decodedValue = try decoder.decodeIfPresent(
Int.self,
forKey: "root",
from: "baz=1"
)
XCTAssertEqual(decodedValue, nil)
}
do {
let decodedValue = try decoder.decodeIfPresent(
Int.self,
forKey: "root",
from: ""
)
XCTAssertEqual(decodedValue, nil)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class Test_URICodingRoundtrip: Test_Runtime {
var color: SimpleEnum
var empty: String
var date: Date
var maybeFoo: String?
}

enum SimpleEnum: String, Codable, Equatable {
Expand Down