diff --git a/Sources/OpenAPIKit/Validator/ReferenceValidations.swift b/Sources/OpenAPIKit/Validator/ReferenceValidations.swift index d4aad9874..4a5f3f8e3 100644 --- a/Sources/OpenAPIKit/Validator/ReferenceValidations.swift +++ b/Sources/OpenAPIKit/Validator/ReferenceValidations.swift @@ -7,7 +7,7 @@ import OpenAPIKitCore -extension Validation { +extension BuiltinValidation { 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 diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index fdeb4d4c8..437b31b8a 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -7,7 +7,7 @@ import OpenAPIKitCore -extension Validation { +public enum BuiltinValidation { // MARK: - Optionally added with `Validator.validating()` /// Validate the OpenAPI Document has at least one path in its @@ -83,6 +83,7 @@ extension Validation { /// - Important: This is not an included validation by default. public static var pathParametersAreDefined: Validation { .init( + description: "Parameters in all paths are documented", check: { context in var errors = [ValidationError]() @@ -143,6 +144,7 @@ extension Validation { /// - Important: This is not an included validation by default. public static var serverVariablesAreDefined: Validation { .init( + description: "All server template variables are defined", check: { context in let missingVariables = context.subject.urlTemplate.variables .filter { !context.subject.variables.contains(key: $0) } @@ -691,6 +693,429 @@ extension Validation { } } +// For backwards compatibilty, expose builtin validations on the Validation +// struct. Going forward, however, exposing builtin validations on the generic +// Validation struct is less convenient. +extension Validation { + // MARK: - Optionally added with `Validator.validating()` + + /// Validate the OpenAPI Document has at least one path in its + /// `PathItem.Map`. + /// + /// The OpenAPI Specification does not require that the document + /// contain any paths for [security reasons](https://spec.openapis.org/oas/v3.2.0.html#security-filtering) + /// or even because it only contains webhooks, but authors may still + /// want to protect against an empty `PathItem.Map` in some cases. + /// + /// - Important: This is not an included validation by default. + public static var documentContainsPaths: Validation { BuiltinValidation.documentContainsPaths } + + /// Validate the OpenAPI Document's `PathItems` all have at least + /// one operation. + /// + /// The OpenAPI Specification does not require that path items + /// contain any operations for [security reasons](https://spec.openapis.org/oas/v3.2.0.html#security-filtering) + /// but documentation that is public in nature might only ever have + /// a `PathItem` with no operations in error. + /// + /// - Important: This is not an included validation by default. + public static var pathsContainOperations: Validation { BuiltinValidation.pathsContainOperations } + + /// Validate the OpenAPI Document's `JSONSchemas` all have at least + /// one defining characteristic. + /// + /// The JSON Schema Specification does not require that components + /// have any defining characteristics. An "empty" schema component can + /// be written as follows: + /// + /// { + /// } + /// + /// It is reasonable, however, to want to validate that all schema components + /// are non-empty and therefore offer some value to the consumer/reader of + /// the OpenAPI documentation beyond just "this property exists." + /// + /// - Note: A sneaky way for the empty object to get into documentation is + /// by putting a property name in a parent object's `required` array + /// without adding that property to the `properties` map. + /// + /// - Important: This is not an included validation by default. + public static var schemaComponentsAreDefined: Validation { + BuiltinValidation.schemaComponentsAreDefined + } + + /// Validate that any `Parameters` in the path of any endpoint are documented. + /// In other words, if a path contains variables (i.e. `"{variable}"`) then there are + /// corresponding `parameters` entries in the `PathItem` or `Operation` for + /// each endpoint. + /// + /// In order to gain easy access to both the path (where the variable placeholders live) + /// and the parameter definitions, this validation runs once per document and performs a + /// loop over each endpoint in the document. + /// + /// - Important: This validation does not assert that all path item references are valid and + /// can be found. Invalid or missing references will be skipped over. + /// + /// - Important: This is not an included validation by default. + public static var pathParametersAreDefined: Validation { + BuiltinValidation.pathParametersAreDefined + } + + /// Validate that all Server Objects define all of the variables found in their URL Templates. + /// + /// For example, a server URL Template of `{scheme}://website.com/{path}` would + /// fail this validation if either "scheme" or "path" were not found in the Server Object's + /// `variables` dictionary. + /// + /// - Important: This is not an included validation by default. + public static var serverVariablesAreDefined: Validation { + BuiltinValidation.serverVariablesAreDefined + } + + /// Validate the OpenAPI Document's `Operations` all have at least + /// one response. + /// + /// The OpenAPI Specification does not require that Responses Objects + /// contain at least one response but you may wish to validate that all + /// operations contain at least one response in your own API. + /// + /// The specification recommends that if there is only one response then + /// it be a successful response but this validation does not require that. + /// + /// - Important: This is not an included validation by default. + public static var operationsContainResponses: Validation { + BuiltinValidation.operationsContainResponses + } + + /// 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 { + BuiltinValidation.linkOperationsExist + } + + /// 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> { + BuiltinValidation.schemaReferencesFoundInComponents + } + + /// 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 { + BuiltinValidation.jsonSchemaReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.responseReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.parameterReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.exampleReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.requestReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.headerReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.linkReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.callbacksReferencesFoundInComponents + } + + /// 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> { + BuiltinValidation.pathItemReferencesFoundInComponents + } + + // MARK: - Included with `Validator()` by default + + // You can start with no validations (not even the defaults below) + // by calling `Validator.blank`. + + /// Validate that the OpenAPI Document's `Tags` all have unique names. + /// + /// The OpenAPI Specification requires that tag names on the Document + /// [are unique](https://spec.openapis.org/oas/v3.2.0.html#openapi-object). + /// + /// - Important: This is included in validation by default. + public static var documentTagNamesAreUnique: Validation { + BuiltinValidation.documentTagNamesAreUnique + } + + /// Validate that all named OpenAPI `Server`s have unique names across the + /// whole Document. + /// + /// The OpenAPI Specification requires server names + /// [are unique](https://spec.openapis.org/oas/v3.2.0.html#server-object). + /// + /// - Important: This is included in validation by default. + public static var documentServerNamesAreUnique: Validation { + BuiltinValidation.documentServerNamesAreUnique + } + + /// Validate that all OpenAPI Path Items have no duplicate parameters defined + /// within them. + /// + /// A Path Item Parameter's identity is defined as the pairing of its `name` and + /// `location`. + /// + /// The OpenAPI Specification requires that these parameters [are unique](https://spec.openapis.org/oas/v3.2.0.html#path-item-object). + /// + /// - Important: This is included in validation by default. + /// + public static var pathItemParametersAreUnique: Validation { + BuiltinValidation.pathItemParametersAreUnique + } + + /// Validate that all OpenAPI Operations have no duplicate parameters defined + /// within them. + /// + /// An Operation's Parameter's identity is defined as the pairing of its `name` and + /// `location`. + /// + /// The OpenAPI Specification requires that these parameters [are unique](https://spec.openapis.org/oas/v3.2.0.html#operation-object). + /// + /// - Important: This is included in validation by default. + /// + public static var operationParametersAreUnique: Validation { + BuiltinValidation.operationParametersAreUnique + } + + /// 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 { + BuiltinValidation.querystringParametersAreCompatible + } + + /// 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). + /// + /// - Important: This validation does not assert that all path references are valid and found in the + /// components for the document. It skips over missing path items. + /// + /// - Important: This is included in validation by default. + /// + public static var operationIdsAreUnique: Validation { + BuiltinValidation.operationIdsAreUnique + } + + /// 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> { + BuiltinValidation.schemaReferencesAreValid + } + + /// 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 { + BuiltinValidation.jsonSchemaReferencesAreValid + } + + /// 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> { + BuiltinValidation.responseReferencesAreValid + } + + /// 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> { + BuiltinValidation.parameterReferencesAreValid + } + + /// 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> { + BuiltinValidation.exampleReferencesAreValid + } + + /// 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> { + BuiltinValidation.requestReferencesAreValid + } + + /// 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> { + BuiltinValidation.headerReferencesAreValid + } + + /// 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> { + BuiltinValidation.linkReferencesAreValid + } + + /// 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> { + BuiltinValidation.callbacksReferencesAreValid + } + + /// 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> { + BuiltinValidation.pathItemReferencesAreValid + } + + /// Validate that `enum` must not be empty in the document's + /// Server Variable. + /// + /// - Important: This is included in validation by default. + /// + public static var serverVariableEnumIsValid: Validation { + BuiltinValidation.serverVariableEnumIsValid + } + + /// Validate that `default` must exist in the enum values in the document's + /// Server Variable, if such values (enum) are defined. + /// + /// - Important: This is included in validation by default. + /// + public static var serverVariableDefaultExistsInEnum : Validation { + BuiltinValidation.serverVariableDefaultExistsInEnum + } + + /// 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 { + BuiltinValidation.parameterStyleAndLocationAreCompatible + } +} + /// 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 { diff --git a/Sources/OpenAPIKit/Validator/Validator.swift b/Sources/OpenAPIKit/Validator/Validator.swift index 698fb4e95..5ecab2ee2 100644 --- a/Sources/OpenAPIKit/Validator/Validator.swift +++ b/Sources/OpenAPIKit/Validator/Validator.swift @@ -14,14 +14,18 @@ extension OpenAPI.Document { /// - validator: Validator to use. By default, /// a validator that just asserts requirements of the OpenAPI /// Specification will be used. - /// - strict: When true, warnings are thrown as errors. Set to false to + /// - strict: When true, parsing warnings become errors. Set to false to /// return warnings instead of throwing them. True by default. + /// **NOTE** that this does not control whether validation errors + /// are treated as errors or warnings, it controls whether warnings + /// from parsing the OpenAPI Document are elevated to errors while + /// validating. /// /// - throws: `ValidationErrors` if any validations failed. /// `EncodingError` if encoding failed for a structural reason. /// - returns: Any warnings that did not cause validation to fail. /// - /// Call without any arguments to validate some aspects of the OpenAPI + /// Call without any arguments to validate aspects of the OpenAPI /// Specification not guaranteed by the Swift types in OpenAPIKit. /// You can create a `Validator` of your own, adding additional steps /// to the validation (or starting from scratch), and then pass that @@ -90,16 +94,19 @@ extension OpenAPI.Document { /// You can add validations to the validator using the /// `validating()` instance methods. /// +/// Builtin validations can be removed selectively using the +/// `withoutValidating()` instance methods. +/// /// There are a few default validations that ship with OpenAPIKit but /// are not used unless explicitly added to a Validator. You can find these -/// validations as static members of the `Validation` type. +/// validations as static members of the `BuiltinValidation` type. /// /// **Example** /// /// let document = OpenAPI.Document(...) /// let validator = Validator() -/// .validating(.documentContainsPaths) -/// .validating(.pathsContainOperations) +/// .validating(\.documentContainsPaths, +/// \.pathsContainOperations) /// try document.validate(using: validator) /// /// At their core, all validations are values of the `Validation` @@ -226,11 +233,32 @@ public final class Validator { } /// Add a validation to be performed. + @discardableResult public func validating(_ validation: Validation) -> Self { customValidations.append(AnyValidation(validation)) return self } + /// Add one or more builtin validations to be performed. + @discardableResult + public func validating(_ validations: repeat KeyPath>) -> Self { + for validationPath in repeat each validations { + customValidations.append(AnyValidation(BuiltinValidation.self[keyPath: validationPath])) + } + return self + } + + /// Remove a builtin validation. + @discardableResult + public func withoutValidating(_ validations: repeat KeyPath>) -> Self { + for validationPath in repeat each validations { + nonReferenceDefaultValidations.removeAll { $0.description == BuiltinValidation.self[keyPath: validationPath].description } + referenceDefaultValidations.removeAll { $0.description == BuiltinValidation.self[keyPath: validationPath].description } + customValidations.removeAll { $0.description == BuiltinValidation.self[keyPath: validationPath].description } + } + return self + } + /// Add a validation to be performed. /// /// - Parameters: @@ -238,6 +266,7 @@ public final class Validator { /// them. This function should return an array of all validation failures. /// `ValidationError` is a good general purpose error for this use-case. /// + @discardableResult public func validating( _ validate: @escaping (ValidationContext) -> [ValidationError] ) -> Self { @@ -267,6 +296,7 @@ public final class Validator { /// - description: The description of the correct state described by the assertion. /// - validate: The function called to assert a condition. The function should return `false` /// if the validity check has failed or `true` if everything is valid. + @discardableResult public func validating( _ description: String, check validate: @escaping (ValidationContext) -> Bool @@ -286,6 +316,7 @@ public final class Validator { /// - validate: The function called to assert a condition. The function should return `false` /// if the validity check has failed or `true` if everything is valid. /// - predicate: A condition that must be met for this validation to be applied. + @discardableResult public func validating( _ description: String, check validate: @escaping (ValidationContext) -> Bool, diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index 73247c666..0c0b9da4d 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -270,6 +270,7 @@ final class BuiltinValidationTests: XCTestCase { ) let validator = Validator.blank.validating(.pathParametersAreDefined) + XCTAssertEqual(validator.validationDescriptions, ["Parameters in all paths are documented"]) XCTAssertThrowsError(try document.validate(using: validator)) { error in let error = error as? ValidationErrorCollection XCTAssertEqual( @@ -332,6 +333,7 @@ final class BuiltinValidationTests: XCTestCase { ) let validator = Validator.blank.validating(.serverVariablesAreDefined) + XCTAssertEqual(validator.validationDescriptions, ["All server template variables are defined"]) XCTAssertThrowsError(try document.validate(using: validator)) { error in XCTAssertEqual( (error as? ValidationErrorCollection)?.values.map(String.init(describing:)), diff --git a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift index ca0ca3c22..656a1995a 100644 --- a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift +++ b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift @@ -78,6 +78,43 @@ final class ValidatorTests: XCTestCase { ) } + func test_initWithOneOrMoreBuiltinsByPath() { + let validator1 = Validator.blank.validating(\.documentContainsPaths) + XCTAssertEqual(validator1.validationDescriptions.count, 1) + + let validator2 = Validator.blank.validating(\.documentContainsPaths, \.pathsContainOperations) + XCTAssertEqual(validator2.validationDescriptions.count, 2) + } + + func test_removingOneOrMoreBuiltinValidationsByPath() { + let validator1 = Validator() + let validator1Count = validator1.validationDescriptions.count + validator1.withoutValidating(\.documentTagNamesAreUnique) + XCTAssertFalse(validator1.validationDescriptions.contains{ $0.description == BuiltinValidation.documentTagNamesAreUnique.description }) + XCTAssertEqual(validator1.validationDescriptions.count, validator1Count - 1) + + let validator2 = Validator() + let validator2Count = validator2.validationDescriptions.count + validator2.withoutValidating(\.documentTagNamesAreUnique, \.documentServerNamesAreUnique) + XCTAssertFalse(validator2.validationDescriptions.contains{ $0.description == BuiltinValidation.documentContainsPaths.description }) + XCTAssertFalse(validator2.validationDescriptions.contains{ $0.description == BuiltinValidation.documentServerNamesAreUnique.description }) + XCTAssertEqual(validator2.validationDescriptions.count, validator2Count - 2) + + // also removes reference builtin validations + let validator3 = Validator() + let validator3Count = validator3.validationDescriptions.count + validator3.withoutValidating(\.headerReferencesAreValid) + XCTAssertFalse(validator3.validationDescriptions.contains{ $0.description == BuiltinValidation.headerReferencesAreValid.description }) + XCTAssertEqual(validator3.validationDescriptions.count, validator3Count - 1) + + // also removes non-default builtin validations + let validator4 = Validator().validating(\.documentContainsPaths) + let validator4Count = validator4.validationDescriptions.count + validator4.withoutValidating(\.documentContainsPaths) + XCTAssertFalse(validator4.validationDescriptions.contains{ $0.description == BuiltinValidation.documentContainsPaths.description }) + XCTAssertEqual(validator4.validationDescriptions.count, validator4Count - 1) + } + func test_validationSucceedsUnconditionally() throws { let server = OpenAPI.Server( url: URL(string: "https://google.com")!,