From 79d3bc214411d90bb012294d4848882c34515962 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 11 Mar 2026 09:40:45 -0500 Subject: [PATCH 1/2] refactor existing valdations --- .../Validator/ReferenceValidations.swift | 93 +++++++ .../Validator/Validation+Builtins.swift | 238 ++++-------------- 2 files changed, 147 insertions(+), 184 deletions(-) create mode 100644 Sources/OpenAPIKit/Validator/ReferenceValidations.swift diff --git a/Sources/OpenAPIKit/Validator/ReferenceValidations.swift b/Sources/OpenAPIKit/Validator/ReferenceValidations.swift new file mode 100644 index 0000000000..e4a4ce0ae6 --- /dev/null +++ b/Sources/OpenAPIKit/Validator/ReferenceValidations.swift @@ -0,0 +1,93 @@ +// +// ReferenceValidations.swift +// +// +// Created by Mathew Polzin on 6/3/20. +// + +import OpenAPIKitCore + +extension Validation { + internal enum References { + /// Create a validation that all non-external OpenAPI references of the + /// given type that point at the Components Object are found in the + /// document's components dictionary. You can choose whether an + /// internal reference to somewhere other than the components + /// dictionary should pass or fail. + internal static func referencesAreValid(ofType type: ReferenceType.Type, mustPointToComponents requireComponents: Bool) -> Validation> { + .init( + description: "OpenAPI \(String(describing: type)) reference can be found in components/\(ReferenceType.openAPIComponentsKey)", + check: { context in + guard case let .internal(internalReference) = context.subject.jsonReference else { + // don't make assertions about external references + return true + } + + guard case .component = internalReference else { + // we can't currently resolve non-components internal + // references, but we can either consider them + // implicitly valid or not depending on the use-case: + return !requireComponents + } + return context.document.components.contains(internalReference) + } + ) + } + + internal static func schemaReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: JSONSchema.self, mustPointToComponents: requireComponents) + } + + internal static func jsonSchemaReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation { + .init( + description: "JSONSchema reference can be found in components/schemas", + check: { context in + guard case let .internal(internalReference) = context.subject.reference else { + // don't make assertions about external references + return true + } + + guard case .component = internalReference else { + // we can't currently resolve non-components internal + // references, but we can either consider them + // implicitly valid or not depending on the use-case: + return !requireComponents + } + return context.document.components.contains(internalReference) + } + ) + } + + internal static func responseReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Response.self, mustPointToComponents: requireComponents) + } + + internal static func parameterReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Parameter.self, mustPointToComponents: requireComponents) + } + + internal static func exampleReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Example.self, mustPointToComponents: requireComponents) + } + + internal static func requestReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Request.self, mustPointToComponents: requireComponents) + } + + internal static func headerReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Header.self, mustPointToComponents: requireComponents) + } + + internal static func linkReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Link.self, mustPointToComponents: requireComponents) + } + + internal static func callbacksReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Callbacks.self, mustPointToComponents: requireComponents) + } + + internal static func pathItemReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.PathItem.self, mustPointToComponents: requireComponents) + } + } +} diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index d1bd722994..90976c4513 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -175,6 +175,30 @@ extension Validation { ) } + /// Validate the OpenAPI Document's `Links` with operationIds refer to + /// Operations that exist in the document. + /// + /// This validation ensures that Link Objects using operationIds have corresponding + /// Operations in the document that have those IDs. + /// + /// - Important: This is not an included validation by default. + public static var linkOperationsExist: Validation { + .init( + description: "Links with operationIds have corresponding Operations", + check: { context in + guard case let .b(operationId) = context.subject.operation else { + // don't make assertions about Links that don't have operationIds + return true + } + + // Collect all operation IDs from the document + let operationIds = context.document.allOperationIds + + return operationIds.contains(operationId) + } + ) + } + // MARK: - Included with `Validator()` by default // You can start with no validations (not even the defaults below) @@ -254,224 +278,94 @@ extension Validation { ) } - /// Validate that all non-external OpenAPI JSONSchema references are found in the document's - /// components dictionary. + /// Validate that all OpenAPI JSONSchema components references are found in + /// the document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var schemaReferencesAreValid: Validation> { - .init( - description: "OpenAPI JSONSchema reference can be found in components/schemas", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.schemaReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external JSONSchema references are found in the document's - /// components dictionary. + /// Validate that all JSONSchema components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var jsonSchemaReferencesAreValid: Validation { - .init( - description: "JSONSchema reference can be found in components/schemas", - check: { context in - guard case let .internal(internalReference) = context.subject.reference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.jsonSchemaReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Response references are found in the document's - /// components dictionary. + /// Validate that all Response components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var responseReferencesAreValid: Validation> { - .init( - description: "Response reference can be found in components/responses", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.responseReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Parameter references are found in the document's - /// components dictionary. + /// Validate that all Parameter components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var parameterReferencesAreValid: Validation> { - .init( - description: "Parameter reference can be found in components/parameters", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.parameterReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Example references are found in the document's - /// components dictionary. + /// Validate that all Example components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var exampleReferencesAreValid: Validation> { - .init( - description: "Example reference can be found in components/examples", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.exampleReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Request references are found in the document's - /// components dictionary. + /// Validate that all Request components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var requestReferencesAreValid: Validation> { - .init( - description: "Request reference can be found in components/requestBodies", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.requestReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Header references are found in the document's - /// components dictionary. + /// Validate that all Header components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var headerReferencesAreValid: Validation> { - .init( - description: "Header reference can be found in components/headers", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.headerReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Link references are found in the document's - /// components dictionary. + /// Validate that all Link components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var linkReferencesAreValid: Validation> { - .init( - description: "Link reference can be found in components/links", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.linkReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external Callbacks references are found in the document's - /// components dictionary. + /// Validate that all Callbacks components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var callbacksReferencesAreValid: Validation> { - .init( - description: "Callbacks reference can be found in components/callbacks", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.callbacksReferencesAreValid(mustPointToComponents: false) } - /// Validate that all non-external PathItem references are found in the document's - /// components dictionary. + /// Validate that all PathItem components references are found in the + /// document's components dictionary. /// /// - Important: This is included in validation by default. /// public static var pathItemReferencesAreValid: Validation> { - .init( - description: "PathItem reference can be found in components/pathItems", - check: { context in - guard case let .internal(internalReference) = context.subject.jsonReference, - case .component = internalReference else { - // don't make assertions about external references - // TODO: could make a stronger assertion including - // internal references outside of components given - // some way to resolve those references. - return true - } - return context.document.components.contains(internalReference) - } - ) + References.pathItemReferencesAreValid(mustPointToComponents: false) } /// Validate that `enum` must not be empty in the document's @@ -504,30 +398,6 @@ extension Validation { ) } - /// Validate the OpenAPI Document's `Links` with operationIds refer to - /// Operations that exist in the document. - /// - /// This validation ensures that Link Objects using operationIds have corresponding - /// Operations in the document that have those IDs. - /// - /// - Important: This is not an included validation by default. - public static var linkOperationsExist: Validation { - .init( - description: "Links with operationIds have corresponding Operations", - check: { context in - guard case let .b(operationId) = context.subject.operation else { - // don't make assertions about Links that don't have operationIds - return true - } - - // Collect all operation IDs from the document - let operationIds = context.document.allOperationIds - - return operationIds.contains(operationId) - } - ) - } - /// 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 From 570ce9b44f9dbaeb6b51017de9b7dcf4370d9415 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 11 Mar 2026 10:01:19 -0500 Subject: [PATCH 2/2] Add new optional stricter reference validations that assert the references are internal and can be found in the components object --- .../Validator/ReferenceValidations.swift | 66 +++++---- .../Validator/Validation+Builtins.swift | 140 ++++++++++++++++-- Sources/OpenAPIKit/Validator/Validator.swift | 101 +++++++++---- .../Validator/BuiltinValidationTests.swift | 114 +++++++++++++- 4 files changed, 346 insertions(+), 75 deletions(-) diff --git a/Sources/OpenAPIKit/Validator/ReferenceValidations.swift b/Sources/OpenAPIKit/Validator/ReferenceValidations.swift index e4a4ce0ae6..d4aad98740 100644 --- a/Sources/OpenAPIKit/Validator/ReferenceValidations.swift +++ b/Sources/OpenAPIKit/Validator/ReferenceValidations.swift @@ -13,14 +13,17 @@ extension Validation { /// given type that point at the Components Object are found in the /// document's components dictionary. You can choose whether an /// internal reference to somewhere other than the components - /// dictionary should pass or fail. - internal static func referencesAreValid(ofType type: ReferenceType.Type, mustPointToComponents requireComponents: Bool) -> Validation> { - .init( - description: "OpenAPI \(String(describing: type)) reference can be found in components/\(ReferenceType.openAPIComponentsKey)", + /// dictionary should pass or fail. You can also choose whether + /// external references should fail or pass. + internal static func referencesAreValid(ofType type: ReferenceType.Type, named name: String, mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + let requireInternalAddendum = if requireInternal { " points to this document and" } else { "" } + + return .init( + description: "\(name) reference\(requireInternalAddendum) can be found in components/\(ReferenceType.openAPIComponentsKey)", check: { context in guard case let .internal(internalReference) = context.subject.jsonReference else { - // don't make assertions about external references - return true + // don't make assertions about external references other than if we are requiring references to be internal + return !requireInternal } guard case .component = internalReference else { @@ -34,17 +37,19 @@ extension Validation { ) } - internal static func schemaReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: JSONSchema.self, mustPointToComponents: requireComponents) + internal static func schemaReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: JSONSchema.self, named: "JSONSchema", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func jsonSchemaReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation { - .init( - description: "JSONSchema reference can be found in components/schemas", + internal static func jsonSchemaReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation { + let requireInternalAddendum = if requireInternal { " points to this document and" } else { "" } + + return .init( + description: "JSONSchema reference\(requireInternalAddendum) can be found in components/schemas", check: { context in - guard case let .internal(internalReference) = context.subject.reference else { + guard case let .internal(internalReference) = context.subject.reference else { // don't make assertions about external references - return true + return !requireInternal } guard case .component = internalReference else { @@ -54,40 +59,41 @@ extension Validation { return !requireComponents } return context.document.components.contains(internalReference) - } + }, + when: \.reference != nil ) } - internal static func responseReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Response.self, mustPointToComponents: requireComponents) + internal static func responseReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Response.self, named: "Response", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func parameterReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Parameter.self, mustPointToComponents: requireComponents) + internal static func parameterReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Parameter.self, named: "Parameter", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func exampleReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Example.self, mustPointToComponents: requireComponents) + internal static func exampleReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Example.self, named: "Example", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func requestReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Request.self, mustPointToComponents: requireComponents) + internal static func requestReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Request.self, named: "Request", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func headerReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Header.self, mustPointToComponents: requireComponents) + internal static func headerReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Header.self, named: "Header", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func linkReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Link.self, mustPointToComponents: requireComponents) + internal static func linkReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Link.self, named: "Link", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func callbacksReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.Callbacks.self, mustPointToComponents: requireComponents) + internal static func callbacksReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.Callbacks.self, named: "Callbacks", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } - internal static func pathItemReferencesAreValid(mustPointToComponents requireComponents: Bool) -> Validation> { - referencesAreValid(ofType: OpenAPI.PathItem.self, mustPointToComponents: requireComponents) + internal static func pathItemReferencesAreValid(mustBeInternal requireInternal: Bool, mustPointToComponents requireComponents: Bool) -> Validation> { + referencesAreValid(ofType: OpenAPI.PathItem.self, named: "PathItem", mustBeInternal: requireInternal, mustPointToComponents: requireComponents) } } } diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index 90976c4513..ee5bcc07f1 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -199,6 +199,126 @@ extension Validation { ) } + /// Validate that all OpenAPI JSONSchema references are internal and found + /// in the document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `schemaReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var schemaReferencesFoundInComponents: Validation> { + References.schemaReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all JSONSchema references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `jsonSchemaReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var jsonSchemaReferencesFoundInComponents: Validation { + References.jsonSchemaReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Response references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `responseReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var responseReferencesFoundInComponents: Validation> { + References.responseReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Parameter references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `parameterReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var parameterReferencesFoundInComponents: Validation> { + References.parameterReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Example references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `exampleReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var exampleReferencesFoundInComponents: Validation> { + References.exampleReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Request references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `requestReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var requestReferencesFoundInComponents: Validation> { + References.requestReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Header references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `headerReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var headerReferencesFoundInComponents: Validation> { + References.headerReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Link references are internal and found in the document's + /// components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `linkReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var linkReferencesFoundInComponents: Validation> { + References.linkReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all Callbacks references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `callbacksReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var callbacksReferencesFoundInComponents: Validation> { + References.callbacksReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + + /// Validate that all PathItem references are internal and found in the + /// document's components dictionary. + /// + /// - See also: The similar but distinct default-on validation + /// `pathItemReferencesAreValid`. + /// + /// - Important: This is not an included in validation by default. + /// + public static var pathItemReferencesFoundInComponents: Validation> { + References.pathItemReferencesAreValid(mustBeInternal: true, mustPointToComponents: true) + } + // MARK: - Included with `Validator()` by default // You can start with no validations (not even the defaults below) @@ -284,7 +404,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var schemaReferencesAreValid: Validation> { - References.schemaReferencesAreValid(mustPointToComponents: false) + References.schemaReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all JSONSchema components references are found in the @@ -293,7 +413,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var jsonSchemaReferencesAreValid: Validation { - References.jsonSchemaReferencesAreValid(mustPointToComponents: false) + References.jsonSchemaReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Response components references are found in the @@ -302,7 +422,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var responseReferencesAreValid: Validation> { - References.responseReferencesAreValid(mustPointToComponents: false) + References.responseReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Parameter components references are found in the @@ -311,7 +431,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var parameterReferencesAreValid: Validation> { - References.parameterReferencesAreValid(mustPointToComponents: false) + References.parameterReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Example components references are found in the @@ -320,7 +440,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var exampleReferencesAreValid: Validation> { - References.exampleReferencesAreValid(mustPointToComponents: false) + References.exampleReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Request components references are found in the @@ -329,7 +449,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var requestReferencesAreValid: Validation> { - References.requestReferencesAreValid(mustPointToComponents: false) + References.requestReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Header components references are found in the @@ -338,7 +458,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var headerReferencesAreValid: Validation> { - References.headerReferencesAreValid(mustPointToComponents: false) + References.headerReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Link components references are found in the @@ -347,7 +467,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var linkReferencesAreValid: Validation> { - References.linkReferencesAreValid(mustPointToComponents: false) + References.linkReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all Callbacks components references are found in the @@ -356,7 +476,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var callbacksReferencesAreValid: Validation> { - References.callbacksReferencesAreValid(mustPointToComponents: false) + References.callbacksReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that all PathItem components references are found in the @@ -365,7 +485,7 @@ extension Validation { /// - Important: This is included in validation by default. /// public static var pathItemReferencesAreValid: Validation> { - References.pathItemReferencesAreValid(mustPointToComponents: false) + References.pathItemReferencesAreValid(mustBeInternal: false, mustPointToComponents: false) } /// Validate that `enum` must not be empty in the document's diff --git a/Sources/OpenAPIKit/Validator/Validator.swift b/Sources/OpenAPIKit/Validator/Validator.swift index bfaa7174ba..1fb923183f 100644 --- a/Sources/OpenAPIKit/Validator/Validator.swift +++ b/Sources/OpenAPIKit/Validator/Validator.swift @@ -152,18 +152,47 @@ extension OpenAPI.Document { /// public final class Validator { - internal var validations: [AnyValidation] - - /// Creates a `Validator`. + internal var customValidations: [AnyValidation] + + internal var nonReferenceDefaultValidations: [AnyValidation] = [ + .init(.documentTagNamesAreUnique), + .init(.pathItemParametersAreUnique), + .init(.operationParametersAreUnique), + .init(.operationIdsAreUnique), + .init(.serverVariableEnumIsValid), + .init(.serverVariableDefaultExistsInEnum), + .init(.parameterStyleAndLocationAreCompatible) + ] + + internal var referenceDefaultValidations: [AnyValidation] = [ + .init(.schemaReferencesAreValid), + .init(.jsonSchemaReferencesAreValid), + .init(.responseReferencesAreValid), + .init(.parameterReferencesAreValid), + .init(.exampleReferencesAreValid), + .init(.requestReferencesAreValid), + .init(.headerReferencesAreValid), + .init(.linkReferencesAreValid), + .init(.callbacksReferencesAreValid), + .init(.pathItemReferencesAreValid) + ] + + internal var validations: [AnyValidation] { + nonReferenceDefaultValidations + referenceDefaultValidations + customValidations + } + + /// Creates a `Validator` with exactly the given validations. + /// + /// - Important: Default builtin validations are not run if the Validator is creaated with this initializer. internal init(validations: [AnyValidation]) { - self.validations = validations + self.referenceDefaultValidations = [] + self.nonReferenceDefaultValidations = [] + self.customValidations = validations } - /// Creates the default `Validator`. Note that - /// this Validator will perform the default validations. - /// If you want a Validator that won't perform any - /// validations except the ones you add, use - /// `Validator.blank`. + /// Creates the default `Validator`. Note that this Validator will perform + /// the default validations. If you want a Validator that won't perform any + /// validations except the ones you add, use `Validator.blank`. /// /// The default validations are /// - Document-level tag names are unique. @@ -177,37 +206,18 @@ public final class Validator { /// Variable. /// - `Parameter` styles and locations are compatible with each other. /// - public convenience init() { - self.init(validations: [ - .init(.documentTagNamesAreUnique), - .init(.pathItemParametersAreUnique), - .init(.operationParametersAreUnique), - .init(.operationIdsAreUnique), - .init(.schemaReferencesAreValid), - .init(.jsonSchemaReferencesAreValid), - .init(.responseReferencesAreValid), - .init(.parameterReferencesAreValid), - .init(.exampleReferencesAreValid), - .init(.requestReferencesAreValid), - .init(.headerReferencesAreValid), - .init(.linkReferencesAreValid), - .init(.callbacksReferencesAreValid), - .init(.pathItemReferencesAreValid), - .init(.serverVariableEnumIsValid), - .init(.serverVariableDefaultExistsInEnum), - .init(.parameterStyleAndLocationAreCompatible) - ]) + public init() { + self.customValidations = [] } - /// A validator with no validation rules at all (not - /// even the defaults). + /// A validator with no validation rules at all (not even the defaults). public static var blank: Validator { return Self.init(validations: []) } /// Add a validation to be performed. public func validating(_ validation: Validation) -> Self { - validations.append(AnyValidation(validation)) + customValidations.append(AnyValidation(validation)) return self } @@ -281,6 +291,33 @@ public final class Validator { } } +extension Validator { + /// Add validations that all references are internal can be looked up in + /// the Components Object. + /// + /// By default, all references must be "valid" but that allows for internal + /// references that do not live in the Components Object. + public func validatingAllReferencesFoundInComponents() -> Self { + // turn off the less strict default reference validations as they are + // encompassed by the stricter validations: + referenceDefaultValidations = [] + + customValidations.append(contentsOf: [ + .init(.schemaReferencesFoundInComponents), + .init(.jsonSchemaReferencesFoundInComponents), + .init(.responseReferencesFoundInComponents), + .init(.parameterReferencesFoundInComponents), + .init(.exampleReferencesFoundInComponents), + .init(.requestReferencesFoundInComponents), + .init(.headerReferencesFoundInComponents), + .init(.linkReferencesFoundInComponents), + .init(.callbacksReferencesFoundInComponents), + .init(.pathItemReferencesFoundInComponents) + ]) + return self + } +} + /// Must be used with Encodable dict values and array elements only. enum ValidityEncoderNode { case unused diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index fc67153a0a..3da56e22b2 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -791,7 +791,7 @@ final class BuiltinValidationTests: XCTestCase { } } - func test_oneOfEachReferenceTypeSucceeds() throws { + fileprivate var oneOfEachReference: OpenAPI.Document { let path = OpenAPI.PathItem( put: .init( requestBody: .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)), @@ -832,7 +832,8 @@ final class BuiltinValidationTests: XCTestCase { ) ], callbacks: [ - "callbacks1": .reference(.component(named: "callbacks1")) + "callbacks1": .reference(.component(named: "callbacks1")), + "callbacks2": .reference(.external(URL(string: "https://callbacks.com")!)) ] ) ) @@ -868,7 +869,7 @@ final class BuiltinValidationTests: XCTestCase { "link1": .init(operationId: "op 1") ], callbacks: [ - "callbacks1": .init() + "callbacks1": .init() ], pathItems: [ "path1": .init() @@ -876,10 +877,117 @@ final class BuiltinValidationTests: XCTestCase { ) ) + return document + } + + func test_oneOfEachReferenceTypeSucceeds() throws { + let document = oneOfEachReference + // NOTE this is part of default validation try document.validate() } + func test_oneOfEachInternalReferenceTypeSucceedsComponentValidation() throws { + let document = oneOfEachReference + + // NOTE this is NOT part of default validation + let validator = Validator().validatingAllReferencesFoundInComponents() + // NOTE this _will_ still fail but only for the external references + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let error = error as? ValidationErrorCollection + XCTAssertEqual(error?.values.count, 9) + XCTAssertEqual(error?.values[0].reason, "Failed to satisfy: Request reference points to this document and can be found in components/requestBodies") + XCTAssertEqual(error?.values[0].codingPathString, ".paths['/hello'].put.requestBody") + XCTAssertEqual(error?.values[1].reason, "Failed to satisfy: Parameter reference points to this document and can be found in components/parameters") + XCTAssertEqual(error?.values[1].codingPathString, ".paths['/hello'].post.parameters[1]") + XCTAssertEqual(error?.values[2].reason, "Failed to satisfy: Response reference points to this document and can be found in components/responses") + XCTAssertEqual(error?.values[2].codingPathString, ".paths['/hello'].post.responses.301") + XCTAssertEqual(error?.values[3].reason, "Failed to satisfy: Header reference points to this document and can be found in components/headers") + XCTAssertEqual(error?.values[3].codingPathString, ".paths['/hello'].post.responses.404.headers.external") + XCTAssertEqual(error?.values[4].reason, "Failed to satisfy: Example reference points to this document and can be found in components/examples") + XCTAssertEqual(error?.values[4].codingPathString, ".paths['/hello'].post.responses.404.content['application/json'].examples.external") + XCTAssertEqual(error?.values[5].reason, "Failed to satisfy: JSONSchema reference points to this document and can be found in components/schemas") + XCTAssertEqual(error?.values[5].codingPathString, ".paths['/hello'].post.responses.404.content['text/plain'].schema") + XCTAssertEqual(error?.values[6].reason, "Failed to satisfy: Link reference points to this document and can be found in components/links") + XCTAssertEqual(error?.values[6].codingPathString, ".paths['/hello'].post.responses.404.links.linky2") + XCTAssertEqual(error?.values[7].reason, "Failed to satisfy: Callbacks reference points to this document and can be found in components/callbacks") + XCTAssertEqual(error?.values[7].codingPathString, ".paths['/hello'].post.callbacks.callbacks2") + XCTAssertEqual(error?.values[8].reason, "Failed to satisfy: PathItem reference points to this document and can be found in components/pathItems") + XCTAssertEqual(error?.values[8].codingPathString, ".paths['/external']") + } + } + + func test_oneOfEachReferenceTypeFailsComponentValidationIfNotInComponents() throws { + var document = oneOfEachReference + document.components = .noComponents + + // NOTE this is NOT part of default validation + let validator = Validator().validatingAllReferencesFoundInComponents() + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let error = error as? ValidationErrorCollection + XCTAssertEqual(error?.values.count, 18) + XCTAssertEqual(error?.values[0].reason, "Failed to satisfy: Request reference points to this document and can be found in components/requestBodies") + XCTAssertEqual(error?.values[0].codingPathString, ".paths['/hello'].put.requestBody") + XCTAssertEqual(error?.values[1].reason, "Failed to satisfy: Parameter reference points to this document and can be found in components/parameters") + XCTAssertEqual(error?.values[1].codingPathString, ".paths['/hello'].post.parameters[0]") + XCTAssertEqual(error?.values[2].reason, "Failed to satisfy: Parameter reference points to this document and can be found in components/parameters") + XCTAssertEqual(error?.values[2].codingPathString, ".paths['/hello'].post.parameters[1]") + XCTAssertEqual(error?.values[3].reason, "Failed to satisfy: Request reference points to this document and can be found in components/requestBodies") + XCTAssertEqual(error?.values[3].codingPathString, ".paths['/hello'].post.requestBody") + XCTAssertEqual(error?.values[4].reason, "Failed to satisfy: Response reference points to this document and can be found in components/responses") + XCTAssertEqual(error?.values[4].codingPathString, ".paths['/hello'].post.responses.200") + XCTAssertEqual(error?.values[5].reason, "Failed to satisfy: Response reference points to this document and can be found in components/responses") + XCTAssertEqual(error?.values[5].codingPathString, ".paths['/hello'].post.responses.301") + XCTAssertEqual(error?.values[6].reason, "Failed to satisfy: Header reference points to this document and can be found in components/headers") + XCTAssertEqual(error?.values[6].codingPathString, ".paths['/hello'].post.responses.404.headers.header1") + XCTAssertEqual(error?.values[7].reason, "Failed to satisfy: Header reference points to this document and can be found in components/headers") + XCTAssertEqual(error?.values[7].codingPathString, ".paths['/hello'].post.responses.404.headers.external") + XCTAssertEqual(error?.values[8].reason, "Failed to satisfy: Example reference points to this document and can be found in components/examples") + XCTAssertEqual(error?.values[8].codingPathString, ".paths['/hello'].post.responses.404.content['application/json'].examples.example1") + XCTAssertEqual(error?.values[9].reason, "Failed to satisfy: Example reference points to this document and can be found in components/examples") + XCTAssertEqual(error?.values[9].codingPathString, ".paths['/hello'].post.responses.404.content['application/json'].examples.external") + XCTAssertEqual(error?.values[10].reason, "Failed to satisfy: JSONSchema reference points to this document and can be found in components/schemas") + XCTAssertEqual(error?.values[10].codingPathString, ".paths['/hello'].post.responses.404.content['application/xml'].schema") + XCTAssertEqual(error?.values[11].reason, "Failed to satisfy: JSONSchema reference points to this document and can be found in components/schemas") + XCTAssertEqual(error?.values[11].codingPathString, ".paths['/hello'].post.responses.404.content['text/plain'].schema") + XCTAssertEqual(error?.values[12].reason, "Failed to satisfy: Link reference points to this document and can be found in components/links") + XCTAssertEqual(error?.values[12].codingPathString, ".paths['/hello'].post.responses.404.links.linky") + XCTAssertEqual(error?.values[13].reason, "Failed to satisfy: Link reference points to this document and can be found in components/links") + XCTAssertEqual(error?.values[13].codingPathString, ".paths['/hello'].post.responses.404.links.linky2") + XCTAssertEqual(error?.values[14].reason, "Failed to satisfy: Callbacks reference points to this document and can be found in components/callbacks") + XCTAssertEqual(error?.values[14].codingPathString, ".paths['/hello'].post.callbacks.callbacks1") + XCTAssertEqual(error?.values[15].reason, "Failed to satisfy: Callbacks reference points to this document and can be found in components/callbacks") + XCTAssertEqual(error?.values[15].codingPathString, ".paths['/hello'].post.callbacks.callbacks2") + XCTAssertEqual(error?.values[16].reason, "Failed to satisfy: PathItem reference points to this document and can be found in components/pathItems") + XCTAssertEqual(error?.values[16].codingPathString, ".paths['/world']") + XCTAssertEqual(error?.values[17].reason, "Failed to satisfy: PathItem reference points to this document and can be found in components/pathItems") + XCTAssertEqual(error?.values[17].codingPathString, ".paths['/external']") + } + } + + func test_internalReferenceToNonComponentFailsFoundInComponents() throws { + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [], + paths: [ + "/internal": .reference(.internal(path: "#/not/important/where")) + ], + components: .noComponents + ) + + // Document will pass less strict default validations + try document.validate() + + // Document will fail stricter reference found in component validations + let validator = Validator().validatingAllReferencesFoundInComponents() + XCTAssertThrowsError(try document.validate(using: validator)) { error in + let error = error as? ValidationErrorCollection + XCTAssertEqual(error?.values.count, 1) + XCTAssertEqual(error?.values[0].reason, "Failed to satisfy: PathItem reference points to this document and can be found in components/pathItems") + XCTAssertEqual(error?.values[0].codingPathString, ".paths['/internal']") + } + } + func test_pathItemsTopLevelReferencesReferencingPathItemComponentsSuccess() throws { let document = OpenAPI.Document( info: .init(title: "test", version: "1.0"),