diff --git a/Sources/OpenAPIKit/Parameter/Parameter.swift b/Sources/OpenAPIKit/Parameter/Parameter.swift index 1d0332a85..40952463e 100644 --- a/Sources/OpenAPIKit/Parameter/Parameter.swift +++ b/Sources/OpenAPIKit/Parameter/Parameter.swift @@ -11,7 +11,7 @@ extension OpenAPI { /// OpenAPI Spec "Parameter Object" /// /// See [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object). - public struct Parameter: Equatable, CodableVendorExtendable, Sendable { + public struct Parameter: HasConditionalWarnings, CodableVendorExtendable, Sendable { public var name: String /// OpenAPI Spec "in" property determines the `Context`. @@ -32,6 +32,8 @@ extension OpenAPI { /// where the values are anything codable. public var vendorExtensions: [String: AnyCodable] + public let conditionalWarnings: [(any Condition, OpenAPI.Warning)] + /// Whether or not this parameter is required. See the context /// which determines whether the parameter is required or not. public var required: Bool { context.required } @@ -71,6 +73,14 @@ extension OpenAPI { } } + /// The parameter's schema `style`, if defined. Note that this is + /// guaranteed to be nil if the parameter has `content` defined. Use + /// the `schemaOrContent` property if you want to switch over the two + /// possibilities. + public var schemaStyle : SchemaContext.Style? { + schemaOrContent.schemaContextValue?.style + } + /// Create a parameter. public init( name: String, @@ -84,10 +94,38 @@ extension OpenAPI { self.description = description self.deprecated = deprecated self.vendorExtensions = vendorExtensions + + self.conditionalWarnings = context.location.conditionalWarnings } } } +extension OpenAPI.Parameter: Equatable { + public static func == (_ lhs: Self, _ rhs: Self) -> Bool { + lhs.name == rhs.name + && lhs.context == rhs.context + && lhs.description == rhs.description + && lhs.deprecated == rhs.deprecated + && lhs.vendorExtensions == rhs.vendorExtensions + } +} + +extension OpenAPI.Parameter.Context.Location { + fileprivate var conditionalWarnings: [(any Condition, OpenAPI.Warning)] { + let querystringWarning: (any Condition, OpenAPI.Warning)? + if self != .querystring { + querystringWarning = nil + } else { + querystringWarning = OpenAPI.Document.ConditionalWarnings.version(lessThan: .v3_2_0, doesNotSupport: "The querystring parameter location") + } + + + return [ + querystringWarning + ].compactMap { $0 } + } +} + extension OpenAPI.Parameter { /// An array of parameters that are `Either` `Parameters` or references to parameters. /// @@ -595,6 +633,8 @@ extension OpenAPI.Parameter: Decodable { deprecated = try container.decodeIfPresent(Bool.self, forKey: .deprecated) ?? false vendorExtensions = try Self.extensions(from: decoder) + + conditionalWarnings = context.location.conditionalWarnings } } diff --git a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift index c3ef75243..c7b3b02fd 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift @@ -12,7 +12,7 @@ extension OpenAPI.Parameter { /// /// See [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object) /// and [OpenAPI Style Values](https://spec.openapis.org/oas/v3.1.1.html#style-values). - public struct SchemaContext: Equatable, Sendable { + public struct SchemaContext: HasConditionalWarnings, Sendable { public var style: Style public var explode: Bool public var allowReserved: Bool //defaults to false @@ -21,6 +21,8 @@ extension OpenAPI.Parameter { public var example: AnyCodable? public var examples: OpenAPI.Example.Map? + public let conditionalWarnings: [(any Condition, OpenAPI.Warning)] + public init(_ schema: JSONSchema, style: Style, explode: Bool, @@ -32,6 +34,8 @@ extension OpenAPI.Parameter { self.schema = .init(schema) self.example = example self.examples = nil + + self.conditionalWarnings = style.conditionalWarnings } public init(_ schema: JSONSchema, @@ -45,6 +49,8 @@ extension OpenAPI.Parameter { self.examples = nil self.explode = style.defaultExplode + + self.conditionalWarnings = style.conditionalWarnings } public init(schemaReference: OpenAPI.Reference, @@ -58,6 +64,8 @@ extension OpenAPI.Parameter { self.schema = .init(schemaReference) self.example = example self.examples = nil + + self.conditionalWarnings = style.conditionalWarnings } public init(schemaReference: OpenAPI.Reference, @@ -71,6 +79,8 @@ extension OpenAPI.Parameter { self.examples = nil self.explode = style.defaultExplode + + self.conditionalWarnings = style.conditionalWarnings } public init(_ schema: JSONSchema, @@ -84,6 +94,8 @@ extension OpenAPI.Parameter { self.schema = .init(schema) self.examples = examples self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + + self.conditionalWarnings = style.conditionalWarnings } public init(_ schema: JSONSchema, @@ -97,6 +109,8 @@ extension OpenAPI.Parameter { self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) self.explode = style.defaultExplode + + self.conditionalWarnings = style.conditionalWarnings } public init(schemaReference: OpenAPI.Reference, @@ -110,6 +124,8 @@ extension OpenAPI.Parameter { self.schema = .init(schemaReference) self.examples = examples self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + + self.conditionalWarnings = style.conditionalWarnings } public init(schemaReference: OpenAPI.Reference, @@ -123,10 +139,39 @@ extension OpenAPI.Parameter { self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) self.explode = style.defaultExplode + + self.conditionalWarnings = style.conditionalWarnings } } } +extension OpenAPI.Parameter.SchemaContext.Style { + fileprivate var conditionalWarnings: [(any Condition, OpenAPI.Warning)] { + let cookieStyleWarning: (any Condition, OpenAPI.Warning)? + if self != .cookie { + cookieStyleWarning = nil + } else { + cookieStyleWarning = OpenAPI.Document.ConditionalWarnings.version(lessThan: .v3_2_0, doesNotSupport: "The cookie style") + } + + + return [ + cookieStyleWarning + ].compactMap { $0 } + } +} + +extension OpenAPI.Parameter.SchemaContext: Equatable { + public static func == (_ lhs: Self, _ rhs: Self) -> Bool { + lhs.style == rhs.style + && lhs.allowReserved == rhs.allowReserved + && lhs.explode == rhs.explode + && lhs.schema == rhs.schema + && lhs.examples == rhs.examples + && lhs.example == rhs.example + } +} + extension OpenAPI.Parameter.SchemaContext { public static func schema(_ schema: JSONSchema, style: Style, @@ -278,6 +323,8 @@ extension OpenAPI.Parameter.SchemaContext { examples = examplesMap example = examplesMap.flatMap(OpenAPI.Content.firstExample(from:)) } + + self.conditionalWarnings = style.conditionalWarnings } } diff --git a/Sources/OpenAPIKit/Parameter/ParameterSchemaContextStyle.swift b/Sources/OpenAPIKit/Parameter/ParameterSchemaContextStyle.swift new file mode 100644 index 000000000..96260473e --- /dev/null +++ b/Sources/OpenAPIKit/Parameter/ParameterSchemaContextStyle.swift @@ -0,0 +1,19 @@ +// +// ParameterSchemaContextStyle.swift +// +// +// Created by Mathew Polzin on 12/18/22. +// + +extension OpenAPI.Parameter.SchemaContext { + public enum Style: String, CaseIterable, Codable, Sendable { + case form + case simple + case matrix + case label + case spaceDelimited + case pipeDelimited + case deepObject + case cookie + } +} diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index 5df0cb34b..def7c7f79 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -505,6 +505,58 @@ extension Validation { } ) } + + /// Validate the OpenAPI Document's `Parameter`s all have styles that are + /// compatible with their locations per the table found at + /// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.2.0.md#style-values + /// + /// - Important: This is included in validation by default. + public static var parameterStyleAndLocationAreCompatible: Validation { + .init( + check: all( + Validation( + description: "the matrix style can only be used for the path location", + check: \.context.location == .path, + when: \.schemaStyle == .matrix + ), + Validation( + description: "the label style can only be used for the path location", + check: \.context.location == .path, + when: \.schemaStyle == .label + ), + Validation( + description: "the simple style can only be used for the path and header locations", + check: \.context.location == .path || \.context.location == .header, + when: \.schemaStyle == .simple + ), + Validation( + description: "the form style can only be used for the query and cookie locations", + check: \.context.location == .query || \.context.location == .cookie, + when: \.schemaStyle == .form + ), + Validation( + description: "the spaceDelimited style can only be used for the query location", + check: \.context.location == .query, + when: \.schemaStyle == .spaceDelimited + ), + Validation( + description: "the pipeDelimited style can only be used for the query location", + check: \.context.location == .query, + when: \.schemaStyle == .pipeDelimited + ), + Validation( + description: "the deepObject style can only be used for the query location", + check: \.context.location == .query, + when: \.schemaStyle == .deepObject + ), + Validation( + description: "the cookie style can only be used for the cookie location", + check: \.context.location == .cookie, + when: \.schemaStyle == .cookie + ) + ) + ) + } } /// Used by both the Path Item parameter check and the diff --git a/Sources/OpenAPIKit/Validator/Validator.swift b/Sources/OpenAPIKit/Validator/Validator.swift index 49e1b8fdc..66fcd8611 100644 --- a/Sources/OpenAPIKit/Validator/Validator.swift +++ b/Sources/OpenAPIKit/Validator/Validator.swift @@ -170,12 +170,12 @@ public final class Validator { /// - Parameters are unique within each Path Item. /// - Parameters are unique within each Operation. /// - Operation Ids are unique across the whole Document. - /// - All OpenAPI.References that refer to components in this - /// document can be found in the components dictionary. - /// - `Enum` must not be empty in the document's - /// Server Variable. - /// - `Default` must exist in the enum values in the document's - /// Server Variable. + /// - All OpenAPI.References that refer to components in this document can + /// be found in the components dictionary. + /// - `Enum` must not be empty in the document's Server Variable. + /// - `Default` must exist in the enum values in the document's Server + /// Variable. + /// - `Parameter` styles and locations are compatible with each other. /// public convenience init() { self.init(validations: [ @@ -193,7 +193,8 @@ public final class Validator { .init(.callbacksReferencesAreValid), .init(.pathItemReferencesAreValid), .init(.serverVariableEnumIsValid), - .init(.serverVariableDefaultExistsInEnum) + .init(.serverVariableDefaultExistsInEnum), + .init(.parameterStyleAndLocationAreCompatible) ]) } diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index 997063193..17e2b37ea 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -31,10 +31,6 @@ public extension OpenAPI.SecurityScheme { typealias Location = OpenAPIKitCore.Shared.SecuritySchemeLocation } -public extension OpenAPI.Parameter.SchemaContext { - typealias Style = OpenAPIKitCore.Shared.ParameterSchemaContextStyle -} - public extension OpenAPI.Response { typealias StatusCode = OpenAPIKitCore.Shared.ResponseStatusCode } diff --git a/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContextStyle.swift similarity index 70% rename from Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift rename to Sources/OpenAPIKit30/Parameter/ParameterSchemaContextStyle.swift index a3166d6dc..41d236e51 100644 --- a/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContextStyle.swift @@ -5,8 +5,8 @@ // Created by Mathew Polzin on 12/18/22. // -extension Shared { - public enum ParameterSchemaContextStyle: String, CaseIterable, Codable, Sendable { +extension OpenAPI.Parameter.SchemaContext { + public enum Style: String, CaseIterable, Codable, Sendable { case form case simple case matrix diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index 997063193..17e2b37ea 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -31,10 +31,6 @@ public extension OpenAPI.SecurityScheme { typealias Location = OpenAPIKitCore.Shared.SecuritySchemeLocation } -public extension OpenAPI.Parameter.SchemaContext { - typealias Style = OpenAPIKitCore.Shared.ParameterSchemaContextStyle -} - public extension OpenAPI.Response { typealias StatusCode = OpenAPIKitCore.Shared.ResponseStatusCode } diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 06b210672..a80d74b3c 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -175,7 +175,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { if let newExamples { return OpenAPIKit.OpenAPI.Parameter.SchemaContext( schemaReference: .init(ref.to31()), - style: style, + style: style.to31(), explode: explode, allowReserved: allowReserved, examples: newExamples @@ -183,7 +183,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { } else { return OpenAPIKit.OpenAPI.Parameter.SchemaContext( schemaReference: .init(ref.to31()), - style: style, + style: style.to31(), explode: explode, allowReserved: allowReserved, example: example @@ -193,7 +193,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { if let newExamples { return OpenAPIKit.OpenAPI.Parameter.SchemaContext( schema.to31(), - style: style, + style: style.to31(), explode: explode, allowReserved: allowReserved, examples: newExamples @@ -201,7 +201,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { } else { return OpenAPIKit.OpenAPI.Parameter.SchemaContext( schema.to31(), - style: style, + style: style.to31(), explode: explode, allowReserved: allowReserved, example: example @@ -211,12 +211,26 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { } } +extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext.Style: To31 { + fileprivate func to31() -> OpenAPIKit.OpenAPI.Parameter.SchemaContext.Style { + switch self { + case .form: .form + case .simple: .simple + case .matrix: .matrix + case .label: .label + case .spaceDelimited: .spaceDelimited + case .pipeDelimited: .pipeDelimited + case .deepObject: .deepObject + } + } +} + extension OpenAPIKit30.OpenAPI.Content.Encoding: To31 { fileprivate func to31() -> OpenAPIKit.OpenAPI.Content.Encoding { OpenAPIKit.OpenAPI.Content.Encoding( contentTypes: [contentType].compactMap { $0 }, headers: headers?.mapValues(eitherRefTo31), - style: style, + style: style.to31(), explode: explode, allowReserved: allowReserved ) diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 4f73d05d2..11a0c75b0 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -1254,11 +1254,29 @@ fileprivate func assertEqualNewToOld(_ newEncoding: OpenAPIKit.OpenAPI.Content.E } else { XCTAssertNil(oldEncoding.headers) } - XCTAssertEqual(newEncoding.style, oldEncoding.style) + try assertEqualNewToOld(newEncoding.style, oldEncoding.style) XCTAssertEqual(newEncoding.explode, oldEncoding.explode) XCTAssertEqual(newEncoding.allowReserved, oldEncoding.allowReserved) } +fileprivate func assertEqualNewToOld(_ newStyle: OpenAPIKit.OpenAPI.Parameter.SchemaContext.Style, _ oldStyle: OpenAPIKit30.OpenAPI.Parameter.SchemaContext.Style) throws { + let equal: Bool + switch (newStyle, oldStyle) { + case (.form, .form): equal = true + case (.simple, .simple): equal = true + case (.matrix, .matrix): equal = true + case (.label, .label): equal = true + case (.spaceDelimited, .spaceDelimited): equal = true + case (.pipeDelimited, .pipeDelimited): equal = true + case (.deepObject, .deepObject): equal = true + default: equal = false + } + + if !equal { + XCTFail("New \(newStyle) is not equivalent to old \(oldStyle)") + } +} + fileprivate func assertEqualNewToOld(_ newHeader: OpenAPIKit.OpenAPI.Header, _ oldHeader: OpenAPIKit30.OpenAPI.Header) throws { XCTAssertEqual(newHeader.description, oldHeader.description) XCTAssertEqual(newHeader.required, oldHeader.required) @@ -1275,7 +1293,7 @@ fileprivate func assertEqualNewToOld(_ newHeader: OpenAPIKit.OpenAPI.Header, _ o } fileprivate func assertEqualNewToOld(_ newSchemaContext: OpenAPIKit.OpenAPI.Parameter.SchemaContext, _ oldSchemaContext: OpenAPIKit30.OpenAPI.Parameter.SchemaContext) throws { - XCTAssertEqual(newSchemaContext.style, oldSchemaContext.style) + try assertEqualNewToOld(newSchemaContext.style, oldSchemaContext.style) XCTAssertEqual(newSchemaContext.explode, oldSchemaContext.explode) XCTAssertEqual(newSchemaContext.allowReserved, oldSchemaContext.allowReserved) switch (newSchemaContext.schema, oldSchemaContext.schema) { diff --git a/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift b/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift index 5300f8f3b..0ac00d6f0 100644 --- a/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift @@ -210,6 +210,11 @@ final class ParameterSchemaTests: XCTestCase { let t7 = Schema(.string, style: .deepObject) XCTAssertFalse(t7.explode) } + + public func test_cookie_style() { + let t1 = Schema(.string, style: .cookie) + XCTAssertEqual(t1.conditionalWarnings.count, 1) + } } // MARK: - Codable Tests diff --git a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift index bb8bd8035..554779cde 100644 --- a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift @@ -95,6 +95,11 @@ final class ParameterTests: XCTestCase { XCTAssertEqual(t1[0].parameterValue, OpenAPI.Parameter.cookie(name: "hello", schema: .string)) XCTAssertEqual(t1[4].reference, .component( named: "hello")) } + + func test_querystringLocation() { + let t1 = OpenAPI.Parameter.querystring(name: "string", content: [:]) + XCTAssertEqual(t1.conditionalWarnings.count, 1) + } } // MARK: - Codable Tests diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index 25a761ae9..57bf0d092 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -951,4 +951,260 @@ final class BuiltinValidationTests: XCTestCase { XCTAssertTrue((errorCollection?.values.first?.codingPath.map { $0.stringValue }.joined(separator: ".") ?? "").contains("testLink")) } } + + func test_badMatrixStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.query( + name: "test", + schemaOrContent: .schema(.init(.string, style: .matrix)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the matrix style can only be used for the path location") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badLabelStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.query( + name: "test", + schemaOrContent: .schema(.init(.string, style: .label)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the label style can only be used for the path location") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badSimpleStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.query( + name: "test", + schemaOrContent: .schema(.init(.string, style: .simple)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the simple style can only be used for the path and header locations") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badFormStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.header( + name: "test", + schemaOrContent: .schema(.init(.string, style: .form)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the form style can only be used for the query and cookie locations") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badSpaceDelimitedStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.header( + name: "test", + schemaOrContent: .schema(.init(.string, style: .spaceDelimited)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the spaceDelimited style can only be used for the query location") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badPipeDelimitedStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.header( + name: "test", + schemaOrContent: .schema(.init(.string, style: .pipeDelimited)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the pipeDelimited style can only be used for the query location") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badDeepObjectStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.header( + name: "test", + schemaOrContent: .schema(.init(.string, style: .deepObject)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the deepObject style can only be used for the query location") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } + + func test_badCookieStyleLocation_fails() throws { + let parameter = OpenAPI.Parameter.header( + name: "test", + schemaOrContent: .schema(.init(.string, style: .cookie)) + ) + + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/hello": .init( + get: .init( + operationId: "testOperation", + parameters: [ + .parameter(parameter) + ], + responses: [:] + ) + ) + ], + components: .noComponents + ) + + let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible) + + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let errorCollection = error as? ValidationErrorCollection + XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the cookie style can only be used for the cookie location") + XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]") + } + } } diff --git a/documentation/migration_guides/v5_migration_guide.md b/documentation/migration_guides/v5_migration_guide.md index cabb0b268..54eac43e9 100644 --- a/documentation/migration_guides/v5_migration_guide.md +++ b/documentation/migration_guides/v5_migration_guide.md @@ -15,6 +15,9 @@ v5.10 and greater). Only relevant when compiling OpenAPIKit on iOS: Now v12+ is required. ### OpenAPI Specification Versions +There are no breaking changes for the `OpenAPIKit30` module (OAS 3.0.x +specification) in this section. + The OpenAPIKit module's `OpenAPI.Document.Version` enum gained `v3_1_2`, `v3_2_0` and `v3_2_x(x: Int)`. @@ -68,7 +71,7 @@ let httpMethod : OpenAPI.HttpMethod = .post ### Parameters There are no breaking changes for the `OpenAPIKit30` module (OAS 3.0.x -specification). +specification) in this section. For the `OpenAPIKit` module (OAS 3.1.x and 3.2.x versions) read on. @@ -149,6 +152,13 @@ Because the `ParameterContext` has taken on the `schemaOrContent` of the similar for the other locations) no longer make sense and have been removed. You must also specify the schema or content, e.g. `ParameterContext.header(schema: .string)`. +### Parameter Styles +There are no breaking changes for the `OpenAPIKit30` module (OAS 3.0.x +specification) in this section. + +A new `cookie` style has been added. Code that exhaustively switches on the +`OpenAPI.Parameter.SchemaContext.Style` enum will need to be updated. + ### Errors Some error messages have been tweaked in small ways. If you match on the string descriptions of any OpenAPIKit errors, you may need to update the