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
121 changes: 64 additions & 57 deletions Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,66 +167,73 @@ extension OpenAPI.PathItem: ExternallyDereferenceable {

let oldAdditionalOperations = additionalOperations

async let (newParameters, c1, m1) = oldParameters.externallyDereferenced(with: loader)
// async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader)
async let (newGet, c3, m3) = oldGet.externallyDereferenced(with: loader)
async let (newPut, c4, m4) = oldPut.externallyDereferenced(with: loader)
async let (newPost, c5, m5) = oldPost.externallyDereferenced(with: loader)
async let (newDelete, c6, m6) = oldDelete.externallyDereferenced(with: loader)
async let (newOptions, c7, m7) = oldOptions.externallyDereferenced(with: loader)
async let (newHead, c8, m8) = oldHead.externallyDereferenced(with: loader)
async let (newPatch, c9, m9) = oldPatch.externallyDereferenced(with: loader)
async let (newTrace, c10, m10) = oldTrace.externallyDereferenced(with: loader)
async let (newQuery, c11, m11) = oldQuery.externallyDereferenced(with: loader)

async let (newAdditionalOperations, c12, m12) = oldAdditionalOperations.externallyDereferenced(with: loader)

var pathItem = self
var newComponents = try await c1
var newMessages = try await m1

// ideally we would async let all of the props above and then set them here,
// but for now since there seems to be some sort of compiler bug we will do
// newServers in an if let below
pathItem.parameters = try await newParameters
pathItem.get = try await newGet
pathItem.put = try await newPut
pathItem.post = try await newPost
pathItem.delete = try await newDelete
pathItem.options = try await newOptions
pathItem.head = try await newHead
pathItem.patch = try await newPatch
pathItem.trace = try await newTrace
pathItem.query = try await newQuery
pathItem.additionalOperations = try await newAdditionalOperations

try await newComponents.merge(c3)
try await newComponents.merge(c4)
try await newComponents.merge(c5)
try await newComponents.merge(c6)
try await newComponents.merge(c7)
try await newComponents.merge(c8)
try await newComponents.merge(c9)
try await newComponents.merge(c10)
try await newComponents.merge(c11)
try await newComponents.merge(c12)

try await newMessages += m3
try await newMessages += m4
try await newMessages += m5
try await newMessages += m6
try await newMessages += m7
try await newMessages += m8
try await newMessages += m9
try await newMessages += m10
try await newMessages += m11
try await newMessages += m12

// Older Swift 6.0/6.1 Linux runtimes have been observed crashing when this
// dereference path fans out into a large batch of `async let` child tasks.
// The sequential form keeps behavior the same while staying compatible
// across the full test matrix.
let (newParameters, c1, m1) = try await oldParameters.externallyDereferenced(with: loader)
pathItem.parameters = newParameters

var newComponents = c1
var newMessages = m1

let (newGet, c3, m3) = try await oldGet.externallyDereferenced(with: loader)
pathItem.get = newGet
try newComponents.merge(c3)
newMessages += m3

let (newPut, c4, m4) = try await oldPut.externallyDereferenced(with: loader)
pathItem.put = newPut
try newComponents.merge(c4)
newMessages += m4

let (newPost, c5, m5) = try await oldPost.externallyDereferenced(with: loader)
pathItem.post = newPost
try newComponents.merge(c5)
newMessages += m5

let (newDelete, c6, m6) = try await oldDelete.externallyDereferenced(with: loader)
pathItem.delete = newDelete
try newComponents.merge(c6)
newMessages += m6

let (newOptions, c7, m7) = try await oldOptions.externallyDereferenced(with: loader)
pathItem.options = newOptions
try newComponents.merge(c7)
newMessages += m7

let (newHead, c8, m8) = try await oldHead.externallyDereferenced(with: loader)
pathItem.head = newHead
try newComponents.merge(c8)
newMessages += m8

let (newPatch, c9, m9) = try await oldPatch.externallyDereferenced(with: loader)
pathItem.patch = newPatch
try newComponents.merge(c9)
newMessages += m9

let (newTrace, c10, m10) = try await oldTrace.externallyDereferenced(with: loader)
pathItem.trace = newTrace
try newComponents.merge(c10)
newMessages += m10

let (newQuery, c11, m11) = try await oldQuery.externallyDereferenced(with: loader)
pathItem.query = newQuery
try newComponents.merge(c11)
newMessages += m11

let (newAdditionalOperations, c12, m12) = try await oldAdditionalOperations.externallyDereferenced(with: loader)
pathItem.additionalOperations = newAdditionalOperations
try newComponents.merge(c12)
newMessages += m12

if let oldServers {
async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader)
pathItem.servers = try await newServers
try await newComponents.merge(c2)
try await newMessages += m2
let (newServers, c2, m2) = try await oldServers.externallyDereferenced(with: loader)
pathItem.servers = newServers
try newComponents.merge(c2)
newMessages += m2
}

return (pathItem, newComponents, newMessages)
Expand Down
118 changes: 117 additions & 1 deletion Sources/OpenAPIKit/Validator/Validation+Builtins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,97 @@ extension Validation {
)
}

/// Validate that `querystring` parameters are unique and do not coexist
/// with `query` parameters within a Path Item's effective operation
/// parameters.
///
/// OpenAPI 3.2.0 requires that a `querystring` parameter
/// [must not appear more than once and must not appear in the same operation
/// as any `query` parameters](https://spec.openapis.org/oas/v3.2.0.html#parameter-locations).
///
/// - Important: This is included in validation by default.
public static var querystringParametersAreCompatible: Validation<OpenAPI.PathItem> {
.init(
description: "Querystring parameters are unique and do not coexist with query parameters",
check: { context in
let pathParameters = resolvedParameters(context.subject.parameters, components: context.document.components)
let pathSummary = parameterLocationSummary(pathParameters)
let pathParametersPath = context.codingPath + [Validator.CodingKey.init(stringValue: "parameters")]
var errors = [ValidationError]()

if pathSummary.querystringCount > 1 {
errors.append(
ValidationError(
reason: "Path Item parameters must not contain more than one `querystring` parameter",
at: pathParametersPath
)
)
}

if pathSummary.querystringCount > 0 && pathSummary.queryCount > 0 {
errors.append(
ValidationError(
reason: "Path Item parameters must not mix `querystring` and `query` parameter locations",
at: pathParametersPath
)
)
}

for endpoint in context.subject.endpoints {
let operationParameters = resolvedParameters(endpoint.operation.parameters, components: context.document.components)
let operationSummary = parameterLocationSummary(operationParameters)
let operationParametersPath = context.codingPath + [
Validator.CodingKey.init(stringValue: codingPathKey(for: endpoint.method)),
Validator.CodingKey.init(stringValue: "parameters")
]

if operationSummary.querystringCount > 1 {
errors.append(
ValidationError(
reason: "Operation parameters must not contain more than one `querystring` parameter",
at: operationParametersPath
)
)
}

if operationSummary.querystringCount > 0 && operationSummary.queryCount > 0 {
errors.append(
ValidationError(
reason: "Operation parameters must not mix `querystring` and `query` parameter locations",
at: operationParametersPath
)
)
}

if pathSummary.querystringCount <= 1 &&
operationSummary.querystringCount <= 1 &&
pathSummary.querystringCount + operationSummary.querystringCount > 1 {
errors.append(
ValidationError(
reason: "Operation parameters must not contain more than one `querystring` parameter, including inherited Path Item parameters",
at: operationParametersPath
)
)
}

if !(pathSummary.querystringCount > 0 && pathSummary.queryCount > 0) &&
!(operationSummary.querystringCount > 0 && operationSummary.queryCount > 0) &&
pathSummary.querystringCount + operationSummary.querystringCount > 0 &&
pathSummary.queryCount + operationSummary.queryCount > 0 {
errors.append(
ValidationError(
reason: "Operation parameters must not mix `querystring` and `query` parameter locations, including inherited Path Item parameters",
at: operationParametersPath
)
)
}
}

return errors
}
)
}

/// Validate that all OpenAPI Operation Ids are unique across the whole Document.
///
/// The OpenAPI Specification requires that Operation Ids [are unique](https://spec.openapis.org/oas/v3.2.0.html#operation-object).
Expand Down Expand Up @@ -575,9 +666,34 @@ extension Validation {
/// Used by both the Path Item parameter check and the
/// Operation parameter check in the default validations.
fileprivate func parametersAreUnique(_ parameters: OpenAPI.Parameter.Array, components: OpenAPI.Components) -> Bool {
let foundParameters = parameters.compactMap { try? components.lookup($0) }
let foundParameters = resolvedParameters(parameters, components: components)

let identities = foundParameters.map { OpenAPI.Parameter.ParameterIdentity(name: $0.name, location: $0.location) }

return Set(identities).count == foundParameters.count
}

fileprivate func resolvedParameters(_ parameters: OpenAPI.Parameter.Array, components: OpenAPI.Components) -> [OpenAPI.Parameter] {
parameters.compactMap { try? components.lookup($0) }
}

fileprivate struct ParameterLocationSummary {
let queryCount: Int
let querystringCount: Int
}

fileprivate func parameterLocationSummary(_ parameters: [OpenAPI.Parameter]) -> ParameterLocationSummary {
.init(
queryCount: parameters.filter { $0.location == .query }.count,
querystringCount: parameters.filter { $0.location == .querystring }.count
)
}

fileprivate func codingPathKey(for method: OpenAPI.HttpMethod) -> String {
switch method {
case .builtin(let builtin):
return builtin.rawValue.lowercased()
case .other(let other):
return other
}
}
3 changes: 3 additions & 0 deletions Sources/OpenAPIKit/Validator/Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ extension OpenAPI.Document {
/// - Document-level tag names are unique.
/// - Parameters are unique within each Path Item.
/// - Parameters are unique within each Operation.
/// - Querystring parameters are unique and do not coexist with query parameters.
/// - 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.
Expand Down Expand Up @@ -158,6 +159,7 @@ public final class Validator {
.init(.documentTagNamesAreUnique),
.init(.pathItemParametersAreUnique),
.init(.operationParametersAreUnique),
.init(.querystringParametersAreCompatible),
.init(.operationIdsAreUnique),
.init(.serverVariableEnumIsValid),
.init(.serverVariableDefaultExistsInEnum),
Expand Down Expand Up @@ -202,6 +204,7 @@ public final class Validator {
/// - Document-level tag names are unique.
/// - Parameters are unique within each Path Item.
/// - Parameters are unique within each Operation.
/// - Querystring parameters are unique and do not coexist with query parameters.
/// - 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.
Expand Down
Loading