diff --git a/Sources/OpenAPIKit/Validator/Validator.swift b/Sources/OpenAPIKit/Validator/Validator.swift index 07f153a238..4b8c5fadea 100644 --- a/Sources/OpenAPIKit/Validator/Validator.swift +++ b/Sources/OpenAPIKit/Validator/Validator.swift @@ -424,6 +424,10 @@ class _ReferencingValidator: _Validator { userInfo: encoder.userInfo, codingPath: encoder.codingPath + [key] ) + if let typedKey = key as? AnyCodingKey, + let validatableKey = typedKey.originalValue as? Validatable { + applyValidations(to: validatableKey) + } } init(referencing encoder: _Validator, at index: Int) { @@ -573,7 +577,7 @@ extension _Validator: SingleValueEncodingContainer { ) } - fileprivate func applyValidations(to value: Encodable, atKey key: CodingKey? = nil) { + fileprivate func applyValidations(to value: Any, atKey key: CodingKey? = nil) { let pathTail = key.map { [$0] } ?? [] for idx in validations.indices { errors += validations[idx].apply(to: value, at: codingPath + pathTail, in: document) diff --git a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift index ffadaf3932..1b9eedf8bb 100644 --- a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift @@ -282,9 +282,16 @@ extension OrderedDictionary: Sendable where Key: Sendable, Value: Sendable {} public struct AnyCodingKey: CodingKey { public let stringValue: String + public let originalValue: Any public init(stringValue: String) { self.stringValue = stringValue + self.originalValue = stringValue + } + + public init(stringValue: String, originalValue: Any) { + self.stringValue = stringValue + self.originalValue = originalValue } public let intValue: Int? = nil @@ -327,7 +334,8 @@ extension OrderedDictionary: Encodable where Key: Encodable, Value: Encodable { // try for RawRepresentable with String RawValues if let encodableDictionary = self as? StringRawKeyEncodable { - let kvPairs = zip(encodableDictionary.orderedStringKeys, self.values) + let keyPairs = zip(self.orderedKeys, encodableDictionary.orderedStringKeys) + let kvPairs = zip(keyPairs, self.values) try encodeKeyValuePairs(kvPairs, to: encoder) return } @@ -365,15 +373,16 @@ internal func encodeKeyValuePairs( } } -/// Encode a sequence of `String`/`Value` pairs as a hash. -internal func encodeKeyValuePairs( - _ keyValuePairs: Zip2Sequence<[String], [Value]>, +/// Encode a sequence of `String`/`Value` pairs as a hash keeping track of the +/// original RawRepresentable values. +internal func encodeKeyValuePairs( + _ keyValuePairs: Zip2Sequence, [Value]>, to encoder: Encoder ) throws { var container = encoder.container(keyedBy: AnyCodingKey.self) for (key, value) in keyValuePairs { - try container.encode(value, forKey: .init(stringValue: key)) + try container.encode(value, forKey: .init(stringValue: key.1, originalValue: key.0)) } } diff --git a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift index 5a8bfc0163..ca0ca3c227 100644 --- a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift +++ b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift @@ -1086,6 +1086,32 @@ final class ValidatorTests: XCTestCase { try document.validate(using: validator) } + func test_keyedContainerKeysAreValidated() { + let contentMap : OpenAPI.Content.Map = [ + .json: .a(.component(named: "jsonContent")) + ] + let doc = OpenAPI.Document( + info: .init(title: "doc", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + summary: "test", + get: .init(responses: [ + 200: .response(content: contentMap) + ]) + ) + ], + components: .noComponents + ) + let notJson = Validation.init(description: "JSON content not allowed", check: \.rawValue != "application/json") + + XCTAssertThrowsError(try doc.validate(using: Validator.blank.validating(notJson), strict: false)) { error in + let error = error as? ValidationErrorCollection + + XCTAssertEqual(error.map(OpenAPI.Error.init(from:))?.localizedDescription, "Failed to satisfy: JSON content not allowed at path: .paths[\'/hello\'].get.responses.200.content[\'application/json\']") + } + } + /// This test confirms the last example in the validation documentation file (documentation/validation.md) /// functions as-written. func test_requestBodySchemaValidationFails() {