Skip to content
Open
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
67 changes: 61 additions & 6 deletions Sources/OpenAPIRuntime/Base/OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,32 @@ import CoreFoundation
/// Define the structure of your types in the OpenAPI document instead.
public struct OpenAPIValueContainer: Codable, Hashable, Sendable {

private var _value: (any Sendable)?

/// The underlying dynamic value.
public var value: (any Sendable)?
public var value: (any Sendable)? {
get { _value }
@available(
*,
deprecated,
message: "Setting `value` directly does not validate the contents. Use `setValue(validating:)` instead."
)
set { _value = newValue }
}

/// Replaces the contained value with the given value after validating that
/// it consists of supported types.
/// - Parameter newValue: A value of a JSON-compatible type, such as
/// `String`, `[Any]`, and `[String: Any]`.
/// - Throws: When the value is not supported.
Comment on lines +61 to +65
public mutating func setValue(validating newValue: (any Sendable)?) throws {
self._value = try Self.tryCast(newValue)
}
Comment on lines +66 to +68

/// Creates a new container with the given validated value.
/// - Parameter value: A value of a JSON-compatible type, such as `String`,
/// `[Any]`, and `[String: Any]`.
init(validatedValue value: (any Sendable)?) { self.value = value }
init(validatedValue value: (any Sendable)?) { self._value = value }

/// Creates a new container with the given unvalidated value.
///
Expand Down Expand Up @@ -349,12 +368,30 @@ extension OpenAPIValueContainer: ExpressibleByFloatLiteral {
/// Define the structure of your types in the OpenAPI document instead.
public struct OpenAPIObjectContainer: Codable, Hashable, Sendable {

private var _value: [String: (any Sendable)?]

/// The underlying dynamic dictionary value.
public var value: [String: (any Sendable)?]
public var value: [String: (any Sendable)?] {
get { _value }
@available(
*,
deprecated,
message: "Setting `value` directly does not validate the contents. Use `setValue(validating:)` instead."
)
set { _value = newValue }
}

/// Replaces the contained dictionary with the given dictionary after
/// validating that all of its values consist of supported types.
/// - Parameter newValue: A dictionary with values of JSON-compatible types.
/// - Throws: When the value is not supported.
public mutating func setValue(validating newValue: [String: (any Sendable)?]) throws {
self._value = try Self.tryCast(newValue)
}

/// Creates a new container with the given validated dictionary.
/// - Parameter value: A dictionary value.
init(validatedValue value: [String: (any Sendable)?]) { self.value = value }
init(validatedValue value: [String: (any Sendable)?]) { self._value = value }

/// Creates a new empty container.
public init() { self.init(validatedValue: [:]) }
Expand Down Expand Up @@ -452,12 +489,30 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable {
/// Define the structure of your types in the OpenAPI document instead.
public struct OpenAPIArrayContainer: Codable, Hashable, Sendable {

private var _value: [(any Sendable)?]

/// The underlying dynamic array value.
public var value: [(any Sendable)?]
public var value: [(any Sendable)?] {
get { _value }
@available(
*,
deprecated,
message: "Setting `value` directly does not validate the contents. Use `setValue(validating:)` instead."
)
set { _value = newValue }
}

/// Replaces the contained array with the given array after validating that
/// all of its elements consist of supported types.
/// - Parameter newValue: An array with values of JSON-compatible types.
/// - Throws: When the value is not supported.
public mutating func setValue(validating newValue: [(any Sendable)?]) throws {
self._value = try Self.tryCast(newValue)
}

/// Creates a new container with the given validated array.
/// - Parameter value: An array value.
init(validatedValue value: [(any Sendable)?]) { self.value = value }
init(validatedValue value: [(any Sendable)?]) { self._value = value }

/// Creates a new empty container.
public init() { self.init(validatedValue: []) }
Expand Down
71 changes: 71 additions & 0 deletions Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,77 @@ final class Test_OpenAPIValue: Test_Runtime {
_ = try OpenAPIArrayContainer(unvalidatedValue: ["hello", ["nestedHello", 2] as [any Sendable]])
}

func testSetValueValidating_valueContainer_acceptsSupportedValues() throws {
var container = try OpenAPIValueContainer()
try container.setValue(validating: "hello")
XCTAssertEqual(container.value as? String, "hello")
try container.setValue(validating: 42)
XCTAssertEqual(container.value as? Int, 42)
try container.setValue(validating: ["nested": 1] as [String: any Sendable])
let dict = try XCTUnwrap(container.value as? [String: Int])
XCTAssertEqual(dict, ["nested": 1])
Comment on lines +52 to +54
}

func testSetValueValidating_valueContainer_rejectsUnsupportedValue() throws {
struct Foobar: Sendable {}
var container = try OpenAPIValueContainer(unvalidatedValue: "seed")
XCTAssertThrowsError(try container.setValue(validating: Foobar())) { error in
guard case .invalidValue = error as? EncodingError else {
XCTFail("Unexpected error: \(error)")
return
}
}
XCTAssertEqual(container.value as? String, "seed", "value must be unchanged after a failed setValue(validating:)")
}

func testSetValueValidating_objectContainer_acceptsSupportedValues() throws {
var container = OpenAPIObjectContainer()
try container.setValue(validating: ["a": 1, "b": "two"])
XCTAssertEqual(container.value["a"] as? Int, 1)
XCTAssertEqual(container.value["b"] as? String, "two")
}

func testSetValueValidating_objectContainer_rejectsUnsupportedValue() throws {
struct Foobar: Sendable {}
var container = try OpenAPIObjectContainer(unvalidatedValue: ["seed": "ok"])
XCTAssertThrowsError(try container.setValue(validating: ["bad": Foobar()])) { error in
guard case .invalidValue = error as? EncodingError else {
XCTFail("Unexpected error: \(error)")
return
}
}
XCTAssertEqual(
container.value["seed"] as? String,
"ok",
"value must be unchanged after a failed setValue(validating:)"
)
}

func testSetValueValidating_arrayContainer_acceptsSupportedValues() throws {
var container = OpenAPIArrayContainer()
try container.setValue(validating: [1, "two", true])
XCTAssertEqual(container.value.count, 3)
XCTAssertEqual(container.value[0] as? Int, 1)
XCTAssertEqual(container.value[1] as? String, "two")
XCTAssertEqual(container.value[2] as? Bool, true)
}

func testSetValueValidating_arrayContainer_rejectsUnsupportedValue() throws {
struct Foobar: Sendable {}
var container = try OpenAPIArrayContainer(unvalidatedValue: ["seed"])
XCTAssertThrowsError(try container.setValue(validating: [Foobar()])) { error in
guard case .invalidValue = error as? EncodingError else {
XCTFail("Unexpected error: \(error)")
return
}
}
XCTAssertEqual(
container.value.first as? String,
"seed",
"value must be unchanged after a failed setValue(validating:)"
)
}

func testEncoding_container_success() throws {
let values: [(any Sendable)?] = [
nil, "Hello", ["key": "value", "anotherKey": [1, "two"] as [any Sendable]] as [String: any Sendable],
Expand Down