diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 0ed8ff1..ee69249 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -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. + public mutating func setValue(validating newValue: (any Sendable)?) throws { + self._value = try Self.tryCast(newValue) + } /// 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. /// @@ -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: [:]) } @@ -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: []) } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index fb4b51e..1f9ce03 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -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]) + } + + 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],