From e1453c1c63b0018f9c40c91fc63c4d8124538bd5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 31 Aug 2026 09:28:46 -0500 Subject: [PATCH] remove Validation system from OpenAPIKit30 --- .../Components Object/Components.swift | 2 - Sources/OpenAPIKit30/Content/Content.swift | 2 - .../Content/ContentEncoding.swift | 2 - Sources/OpenAPIKit30/Discriminator.swift | 2 - Sources/OpenAPIKit30/Document/Document.swift | 3 - .../OpenAPIKit30/Document/DocumentInfo.swift | 4 - .../Either/Either+Validatable.swift | 10 - Sources/OpenAPIKit30/Example.swift | 2 - .../OpenAPIKit30/ExternalDocumentation.swift | 2 - Sources/OpenAPIKit30/Header/Header.swift | 2 - Sources/OpenAPIKit30/JSONReference.swift | 6 - Sources/OpenAPIKit30/Link.swift | 6 - .../OpenAPIKit30/Operation/Operation.swift | 2 - .../OpenAPIKit30/Parameter/Parameter.swift | 2 - .../Parameter/ParameterContextLocation.swift | 2 - .../Parameter/ParameterSchemaContext.swift | 2 - Sources/OpenAPIKit30/Path Item/PathItem.swift | 2 - Sources/OpenAPIKit30/Request/Request.swift | 2 - Sources/OpenAPIKit30/Response/Response.swift | 2 - .../Schema Object/JSONSchema.swift | 2 - .../Schema Object/TypesAndFormats.swift | 4 +- .../OpenAPIKit30/Security/OAuthFlows.swift | 3 - .../Security/SecurityScheme.swift | 2 - Sources/OpenAPIKit30/Server.swift | 3 - Sources/OpenAPIKit30/Tag.swift | 2 - .../URLTemplate+Validatable.swift | 10 - .../Validator/Validation+Builtins.swift | 455 ----- .../OpenAPIKit30/Validator/Validation.swift | 182 -- .../Validator/Validator+Convenience.swift | 357 ---- .../OpenAPIKit30/Validator/Validator.swift | 695 -------- .../PathsErrorTests.swift | 46 - .../GitHubAPITests.swift | 7 - .../GoogleBooksAPITests.swift | 6 - .../PetStoreAPITests.swift | 6 - .../SwaggerDocSamplesTests.swift | 21 - .../TomTomAPITests.swift | 8 - .../Validator/BuiltinValidationTests.swift | 824 --------- .../Validation+ConvenienceTests.swift | 407 ----- .../Validator/ValidationTests.swift | 49 - .../Validator/ValidatorTests.swift | 1496 ----------------- 40 files changed, 1 insertion(+), 4641 deletions(-) delete mode 100644 Sources/OpenAPIKit30/Either/Either+Validatable.swift delete mode 100644 Sources/OpenAPIKit30/URLTemplate+Validatable.swift delete mode 100644 Sources/OpenAPIKit30/Validator/Validation+Builtins.swift delete mode 100644 Sources/OpenAPIKit30/Validator/Validation.swift delete mode 100644 Sources/OpenAPIKit30/Validator/Validator+Convenience.swift delete mode 100644 Sources/OpenAPIKit30/Validator/Validator.swift delete mode 100644 Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift delete mode 100644 Tests/OpenAPIKit30Tests/Validator/Validation+ConvenienceTests.swift delete mode 100644 Tests/OpenAPIKit30Tests/Validator/ValidationTests.swift delete mode 100644 Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift diff --git a/Sources/OpenAPIKit30/Components Object/Components.swift b/Sources/OpenAPIKit30/Components Object/Components.swift index 8a65441e4f..bc15ff6599 100644 --- a/Sources/OpenAPIKit30/Components Object/Components.swift +++ b/Sources/OpenAPIKit30/Components Object/Components.swift @@ -416,5 +416,3 @@ extension OpenAPI.Components { } #endif } - -extension OpenAPI.Components: Validatable {} diff --git a/Sources/OpenAPIKit30/Content/Content.swift b/Sources/OpenAPIKit30/Content/Content.swift index 01fadc36ba..a8814317ce 100644 --- a/Sources/OpenAPIKit30/Content/Content.swift +++ b/Sources/OpenAPIKit30/Content/Content.swift @@ -243,5 +243,3 @@ extension OpenAPI.Content { } } } - -extension OpenAPI.Content: Validatable {} diff --git a/Sources/OpenAPIKit30/Content/ContentEncoding.swift b/Sources/OpenAPIKit30/Content/ContentEncoding.swift index d21c91acd4..8b6565463b 100644 --- a/Sources/OpenAPIKit30/Content/ContentEncoding.swift +++ b/Sources/OpenAPIKit30/Content/ContentEncoding.swift @@ -215,5 +215,3 @@ extension OpenAPI.Content.Encoding { } } } - -extension OpenAPI.Content.Encoding: Validatable {} diff --git a/Sources/OpenAPIKit30/Discriminator.swift b/Sources/OpenAPIKit30/Discriminator.swift index 99d8aa3eee..cd92bb9e94 100644 --- a/Sources/OpenAPIKit30/Discriminator.swift +++ b/Sources/OpenAPIKit30/Discriminator.swift @@ -49,5 +49,3 @@ extension OpenAPI.Discriminator { case mapping } } - -extension OpenAPI.Discriminator: Validatable {} diff --git a/Sources/OpenAPIKit30/Document/Document.swift b/Sources/OpenAPIKit30/Document/Document.swift index f5935b4fae..ef8dcd6a7f 100644 --- a/Sources/OpenAPIKit30/Document/Document.swift +++ b/Sources/OpenAPIKit30/Document/Document.swift @@ -710,6 +710,3 @@ internal func validate(securityRequirements: [OpenAPI.SecurityRequirement], at p } } } - -extension OpenAPI.Document: Validatable {} -extension OpenAPI.Document.Version: Validatable {} diff --git a/Sources/OpenAPIKit30/Document/DocumentInfo.swift b/Sources/OpenAPIKit30/Document/DocumentInfo.swift index 0d123ae817..28fb585bc1 100644 --- a/Sources/OpenAPIKit30/Document/DocumentInfo.swift +++ b/Sources/OpenAPIKit30/Document/DocumentInfo.swift @@ -360,7 +360,3 @@ extension OpenAPI.Document.Info { } } } - -extension OpenAPI.Document.Info: Validatable {} -extension OpenAPI.Document.Info.Contact: Validatable {} -extension OpenAPI.Document.Info.License: Validatable {} diff --git a/Sources/OpenAPIKit30/Either/Either+Validatable.swift b/Sources/OpenAPIKit30/Either/Either+Validatable.swift deleted file mode 100644 index 69f4f24e6e..0000000000 --- a/Sources/OpenAPIKit30/Either/Either+Validatable.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Either+Validatable.swift -// -// -// Created by Mathew Polzin on 2/28/21. -// - -import OpenAPIKitCore - -extension Either: Validatable where A: Validatable, B: Validatable {} diff --git a/Sources/OpenAPIKit30/Example.swift b/Sources/OpenAPIKit30/Example.swift index e145d39ac3..2dd50cd12c 100644 --- a/Sources/OpenAPIKit30/Example.swift +++ b/Sources/OpenAPIKit30/Example.swift @@ -195,5 +195,3 @@ extension OpenAPI.Example: ExternallyDereferenceable { } #endif } - -extension OpenAPI.Example: Validatable {} diff --git a/Sources/OpenAPIKit30/ExternalDocumentation.swift b/Sources/OpenAPIKit30/ExternalDocumentation.swift index c49d6a8af5..035e1a59a4 100644 --- a/Sources/OpenAPIKit30/ExternalDocumentation.swift +++ b/Sources/OpenAPIKit30/ExternalDocumentation.swift @@ -103,5 +103,3 @@ extension OpenAPI.ExternalDocumentation { } } } - -extension OpenAPI.ExternalDocumentation: Validatable {} diff --git a/Sources/OpenAPIKit30/Header/Header.swift b/Sources/OpenAPIKit30/Header/Header.swift index 98b66f7f35..a3a1f28c69 100644 --- a/Sources/OpenAPIKit30/Header/Header.swift +++ b/Sources/OpenAPIKit30/Header/Header.swift @@ -322,5 +322,3 @@ extension OpenAPI.Header: Decodable { vendorExtensions = try Self.extensions(from: decoder) } } - -extension OpenAPI.Header: Validatable {} diff --git a/Sources/OpenAPIKit30/JSONReference.swift b/Sources/OpenAPIKit30/JSONReference.swift index 0574a2b8a1..14fd382e42 100644 --- a/Sources/OpenAPIKit30/JSONReference.swift +++ b/Sources/OpenAPIKit30/JSONReference.swift @@ -37,10 +37,6 @@ import Foundation /// or necessary depending on whether you have the Components Object built out and /// available at the time and location where you need to create a reference. /// -/// Regardless of how you create your reference, internal references to things in the -/// Components Object will be validated when you call `validate()` on an -/// `OpenAPI.Document`. -/// public enum JSONReference: Equatable, Hashable, _OpenAPIReference, Sendable { /// The reference is internal to the file. case `internal`(InternalReference) @@ -404,5 +400,3 @@ extension JSONReference: ExternallyDereferenceable where ReferenceType: External } #endif } - -extension JSONReference: Validatable where ReferenceType: Validatable {} diff --git a/Sources/OpenAPIKit30/Link.swift b/Sources/OpenAPIKit30/Link.swift index 4ed6e2059b..eeb4e0d90e 100644 --- a/Sources/OpenAPIKit30/Link.swift +++ b/Sources/OpenAPIKit30/Link.swift @@ -2,10 +2,6 @@ // Link.swift // -// TODO: create validation that operationIds in Link objects -// refer to Operation objects in the document that have the -// given ids. - import OpenAPIKitCore #if canImport(FoundationEssentials) @@ -294,5 +290,3 @@ extension OpenAPI.Link: ExternallyDereferenceable { } #endif } - -extension OpenAPI.Link: Validatable {} diff --git a/Sources/OpenAPIKit30/Operation/Operation.swift b/Sources/OpenAPIKit30/Operation/Operation.swift index 57d2ec7c12..487f0ac6b1 100644 --- a/Sources/OpenAPIKit30/Operation/Operation.swift +++ b/Sources/OpenAPIKit30/Operation/Operation.swift @@ -427,5 +427,3 @@ extension OpenAPI.Operation { } } } - -extension OpenAPI.Operation: Validatable {} diff --git a/Sources/OpenAPIKit30/Parameter/Parameter.swift b/Sources/OpenAPIKit30/Parameter/Parameter.swift index a162e2e5f1..9f4344993b 100644 --- a/Sources/OpenAPIKit30/Parameter/Parameter.swift +++ b/Sources/OpenAPIKit30/Parameter/Parameter.swift @@ -443,5 +443,3 @@ extension OpenAPI.Parameter { } } } - -extension OpenAPI.Parameter: Validatable {} diff --git a/Sources/OpenAPIKit30/Parameter/ParameterContextLocation.swift b/Sources/OpenAPIKit30/Parameter/ParameterContextLocation.swift index 89d3230215..1abc22b95a 100644 --- a/Sources/OpenAPIKit30/Parameter/ParameterContextLocation.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterContextLocation.swift @@ -15,5 +15,3 @@ extension OpenAPI.Parameter.Context { case cookie } } - -extension OpenAPI.Parameter.Context.Location: Validatable {} diff --git a/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift index 7dddf716db..a5721edc8b 100644 --- a/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift @@ -220,5 +220,3 @@ extension OpenAPI.Parameter.SchemaContext { } } } - -extension OpenAPI.Parameter.SchemaContext.Style: Validatable {} diff --git a/Sources/OpenAPIKit30/Path Item/PathItem.swift b/Sources/OpenAPIKit30/Path Item/PathItem.swift index cceafda355..30156a1d2b 100644 --- a/Sources/OpenAPIKit30/Path Item/PathItem.swift +++ b/Sources/OpenAPIKit30/Path Item/PathItem.swift @@ -389,5 +389,3 @@ extension OpenAPI.PathItem { } } } - -extension OpenAPI.PathItem: Validatable {} diff --git a/Sources/OpenAPIKit30/Request/Request.swift b/Sources/OpenAPIKit30/Request/Request.swift index 6c15e3a09d..1c636bb57e 100644 --- a/Sources/OpenAPIKit30/Request/Request.swift +++ b/Sources/OpenAPIKit30/Request/Request.swift @@ -125,5 +125,3 @@ extension OpenAPI.Request: Decodable { } } } - -extension OpenAPI.Request: Validatable {} diff --git a/Sources/OpenAPIKit30/Response/Response.swift b/Sources/OpenAPIKit30/Response/Response.swift index cab4d79cd5..140c21da6f 100644 --- a/Sources/OpenAPIKit30/Response/Response.swift +++ b/Sources/OpenAPIKit30/Response/Response.swift @@ -188,5 +188,3 @@ extension OpenAPI.Response: Decodable { } } } - -extension OpenAPI.Response: Validatable {} diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift index 8816f58a5e..8cc7e891a1 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift @@ -1920,5 +1920,3 @@ extension JSONSchema: Decodable { .mapValues(AnyCodable.init) } } - -extension JSONSchema: Validatable {} diff --git a/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift b/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift index e01b3e9f35..1b8c81ffde 100644 --- a/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift +++ b/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift @@ -109,7 +109,7 @@ public enum JSONTypeFormat: Equatable, Sendable { /// /// See "formats" under the OpenAPI [data type](https://spec.openapis.org/oas/v3.0.4.html#data-types) /// documentation. -public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, Validatable, Sendable where RawValue == String { +public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, Sendable where RawValue == String { static var unspecified: Self { get } var jsonType: JSONType { get } @@ -243,5 +243,3 @@ extension JSONTypeFormat.StringFormat: OpenAPIFormat { return .string } } - -extension JSONType: Validatable {} diff --git a/Sources/OpenAPIKit30/Security/OAuthFlows.swift b/Sources/OpenAPIKit30/Security/OAuthFlows.swift index 6d803bdedd..6d7f40c4a0 100644 --- a/Sources/OpenAPIKit30/Security/OAuthFlows.swift +++ b/Sources/OpenAPIKit30/Security/OAuthFlows.swift @@ -65,6 +65,3 @@ extension OpenAPI.OAuthFlows: Decodable { authorizationCode = try container.decodeIfPresent(OpenAPI.OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode) } } - - -extension OpenAPI.OAuthFlows: Validatable {} diff --git a/Sources/OpenAPIKit30/Security/SecurityScheme.swift b/Sources/OpenAPIKit30/Security/SecurityScheme.swift index b1621e8673..f89e7d48fb 100644 --- a/Sources/OpenAPIKit30/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit30/Security/SecurityScheme.swift @@ -262,5 +262,3 @@ extension OpenAPI.SecurityScheme: ExternallyDereferenceable { } #endif } - -extension OpenAPI.SecurityScheme.SecurityType.Name: Validatable {} diff --git a/Sources/OpenAPIKit30/Server.swift b/Sources/OpenAPIKit30/Server.swift index dc2174dd45..38c16d999c 100644 --- a/Sources/OpenAPIKit30/Server.swift +++ b/Sources/OpenAPIKit30/Server.swift @@ -259,6 +259,3 @@ extension OpenAPI.Server: ExternallyDereferenceable { } #endif } - -extension OpenAPI.Server: Validatable {} -extension OpenAPI.Server.Variable: Validatable {} diff --git a/Sources/OpenAPIKit30/Tag.swift b/Sources/OpenAPIKit30/Tag.swift index 7e73600513..6ca5c5f7d2 100644 --- a/Sources/OpenAPIKit30/Tag.swift +++ b/Sources/OpenAPIKit30/Tag.swift @@ -121,5 +121,3 @@ extension OpenAPI.Tag { } } } - -extension OpenAPI.Tag: Validatable {} diff --git a/Sources/OpenAPIKit30/URLTemplate+Validatable.swift b/Sources/OpenAPIKit30/URLTemplate+Validatable.swift deleted file mode 100644 index a699d094c4..0000000000 --- a/Sources/OpenAPIKit30/URLTemplate+Validatable.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// URLTemplate+Validatable.swift -// -// -// Created by Mathew Polzin on 2/28/21. -// - -import OpenAPIKitCore - -extension URLTemplate: Validatable {} diff --git a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift deleted file mode 100644 index c13643a564..0000000000 --- a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift +++ /dev/null @@ -1,455 +0,0 @@ -// -// Validation+Builtins.swift -// -// -// Created by Mathew Polzin on 6/3/20. -// - -import OpenAPIKitCore - -extension Validation { - // MARK: - Optionally added with `Validator.validating()` - - /// Validate the OpenAPI Document has at least one path in its - /// `PathItem.Map`. - /// - /// The OpenAPI Specifcation does not require that the document - /// contain any paths for [security reasons](https://spec.openapis.org/oas/v3.0.4.html#security-filtering) - /// but documentation that is public in nature might only ever have - /// an empty `PathItem.Map` in error. - /// - /// - Important: This is not an included validation by default. - public static var documentContainsPaths: Validation { - .init( - description: "Document contains at least one path", - check: \.count > 0 - ) - } - - /// Validate the OpenAPI Document's `PathItems` all have at least - /// one operation. - /// - /// The OpenAPI Specifcation does not require that path items - /// contain any operations for [security reasons](https://spec.openapis.org/oas/v3.0.4.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 { - .init( - description: "Paths contain at least one operation", - check: \.endpoints.count > 0 - ) - } - - /// Validate the OpenAPI Document's `JSONSchemas` all have at least - /// one defining characteristic. - /// - /// The JSON Schema Specifcation 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 { - .init( - description: "JSON Schema components have defining characteristics (i.e. they are not just the empty schema component: `{}`) [Note that one way to end up with empty schema components is by having property names in an object's `required` array that are not defined in that object's `properties` map]", - check: \.subject.isEmpty == false - ) - } - - /// 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 is not an included validation by default. - public static var pathParametersAreDefined: Validation { - .init( - check: { context in - var errors = [ValidationError]() - - for (path, item) in context.subject { - guard let pathItem = context.document.components[item] else { continue } - let variablesInPath = path.components - .lazy - .filter { $0.first == "{" && $0.last == "}" } - .map { String($0.dropFirst().dropLast()) } - - let paramsInPathItem = Array( - pathItem.parameters - .lazy - .compactMap { context.document.components[$0] } - .map { $0.name } - ) - - for endpoint in pathItem.endpoints { - let paramsInOperation = Array( - endpoint.operation.parameters - .lazy - .compactMap { context.document.components[$0]} - .map { $0.name } - ) - - let missingParamDefs = Array( - variablesInPath - .filter { !((paramsInPathItem + paramsInOperation).contains($0)) } - ) - - if !missingParamDefs.isEmpty { - let codingPath = context.codingPath + [ - path.rawValue, - endpoint.method.rawValue - ].map(Validator.CodingKey.init(stringValue:)) - - errors.append( - .init( - reason: "The following path parameters were not defined in the Path Item or Operation `parameters`: \(missingParamDefs)", - at: codingPath - ) - ) - } - } - } - - return errors - } - ) - } - - /// 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 { - .init( - check: { context in - let missingVariables = context.subject.urlTemplate.variables - .filter { !context.subject.variables.contains(key: $0) } - - return missingVariables.map { variableName in - ValidationError( - reason: "Server Object does not define the variable '\(variableName)' that is found in the `urlTemplate` '\(context.subject.urlTemplate.rawValue)'", - at: context.codingPath - ) - } - } - ) - } - - // MARK: - Included with `Validator()` by default - - /// Validate the OpenAPI Document's `Operations` all have at least - /// one response. - /// - /// The OpenAPI Specifcation requires that Responses Objects - /// contain [at least one response](https://spec.openapis.org/oas/v3.0.4.html#responses-object). - /// The specification recommends that if there is only one response then - /// it be a successful response. - /// - /// - Important: This is included in validation by default. - public static var operationsContainResponses: Validation { - .init( - description: "Operations contain at least one response", - check: \.count > 0 - ) - } - - // 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 Specifcation requires that tag names on the Document - /// [are unique](https://spec.openapis.org/oas/v3.0.4.html#openapi-object). - /// - /// - Important: This is included in validation by default. - public static var documentTagNamesAreUnique: Validation { - .init( - description: "The names of Tags in the Document are unique", - check: take(\.tags) { maybeTags in - guard let tags = maybeTags else { return true } - - return Set(tags.map { $0.name }).count == tags.count - } - ) - } - - /// 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.0.4.html#path-item-object). - /// - /// - Important: This is included in validation by default. - /// - public static var pathItemParametersAreUnique: Validation { - .init( - description: "Path Item parameters are unique (identity is defined by the 'name' and 'location')", - check: { parametersAreUnique($0.subject.parameters, components: $0.document.components) }, - when: \.parameters.count > 0 - ) - } - - /// 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.0.4.html#operation-object). - /// - /// - Important: This is included in validation by default. - /// - public static var operationParametersAreUnique: Validation { - .init( - description: "Operation parameters are unique (identity is defined by the 'name' and 'location')", - check: { parametersAreUnique($0.subject.parameters, components: $0.document.components) }, - when: \.parameters.count > 0 - ) - } - - /// 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.0.4.html#operation-object). - /// - /// - Important: This is included in validation by default. - /// - public static var operationIdsAreUnique: Validation { - .init( - description: "All Operation Ids in Document are unique", - check: take(\.allOperationIds) { operationIds in - return Set(operationIds).count == operationIds.count - } - ) - } - - /// Validate that all non-external JSONSchema references are found in the document's - /// components dictionary. - /// - /// - Important: This is included in validation by default. - /// - public static var schemaReferencesAreValid: Validation> { - .init( - description: "JSONSchema reference can be found in components/schemas", - check: { context in - guard case let .internal(internalReference) = context.subject, - 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) - } - ) - } - - /// Validate that all non-external Response 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, - 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) - } - ) - } - - /// Validate that all non-external Parameter 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, - 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) - } - ) - } - - /// Validate that all non-external Example 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, - 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) - } - ) - } - - /// Validate that all non-external Request 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, - 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) - } - ) - } - - /// Validate that all non-external Header 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, - 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) - } - ) - } - - /// Validate that all non-external Link 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, - 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) - } - ) - } - - /// Validate that all non-external Callbacks 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, - 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) - } - ) - } - - /// 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 - } - - // Use the allOperationIds helper to get all operation IDs from the document - let operationIds = context.document.allOperationIds - - return operationIds.contains(operationId) - } - ) - } -} - -/// 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 identities = foundParameters.map { OpenAPI.Parameter.ParameterIdentity(name: $0.name, location: $0.location) } - - return Set(identities).count == foundParameters.count -} diff --git a/Sources/OpenAPIKit30/Validator/Validation.swift b/Sources/OpenAPIKit30/Validator/Validation.swift deleted file mode 100644 index 3e9851d42a..0000000000 --- a/Sources/OpenAPIKit30/Validator/Validation.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// Validation.swift -// -// -// Created by Mathew Polzin on 1/26/20. -// - -import OpenAPIKitCore - -/// The context in which a validation can be applied. -/// -/// It may or may not be important for a particular validation -/// to know what the whole `OpenAPI.Document` looks like -/// or the coding path where the validation is being applied, -/// but it always has access to these two pieces of information -/// in addition to the **subject** (a value of the type on which the -/// validation is specialized). -public struct ValidationContext { - public let document: OpenAPI.Document - public let subject: Subject - public let codingPath: [CodingKey] -} - -/// Holds a function to determine if a validation -/// applies (`predicate`) and a function that applies -/// a validation (`validate`). -public struct Validation { - /// Applies validation on type `Subject`. Throws if validation fails. - /// - /// The context includes - /// - The entire `OpenAPI.Document` - /// - A value of the type in which this validator specializes. - /// - The coding path where the validation is occurring. - public let validate: (ValidationContext) -> [ValidationError] - - /// Returns `true` if this validator should apply to - /// the given value of type `Subject`. - /// - /// The context includes - /// - The entire `OpenAPI.Document` - /// - A value of the type in which this validator specializes. - /// - The coding path where the validation is occurring. - public let predicate: (ValidationContext) -> Bool - - /// Apply the validation to the given value if the predicate - /// returns `true`. - public func apply(to subject: Subject, at codingPath: [CodingKey], in document: OpenAPI.Document) -> [ValidationError] { - - let context = ValidationContext(document: document, subject: subject, codingPath: codingPath) - guard predicate(context) else { - return [] - } - - return validate(context) - } - - /// Create a Validation that appllies to values of type `Subject`. - /// - /// You can return any number of errors from your `validate` - /// function, each with its own description of a problem. Add an - /// optional `predicate` to apply your validation to a subset of - /// all values of the type your `validate` method operates on. - /// - /// - Parameters: - /// - validate: A function taking validation contexts containing - /// subjects of type `Subject` and validating them. This function must - /// return an array of errors. If validation succeeds, return an empty - /// array. - /// - predicate: A function returning `true` if this validator - /// should run against the given value. - /// - public init( - check validate: @escaping (ValidationContext) -> [ValidationError], - when predicate: @escaping (ValidationContext) -> Bool = { _ in true } - ) { - self.validate = validate - self.predicate = predicate - } - - /// Create a Validation with a single error that applies to values of type `Subject`. - /// - /// This version of the initializer assumes only one error can occur for this - /// validation and in exchange you can frontload the description of the validation - /// and simplify the body of the `validate` method to just return `false` - /// if the value is invalid. - /// - /// - Parameters: - /// - description: A description of the correct state described by the - /// `validate` function. Upon failure, the error will read "Failed to satisfy: ". - /// - validate: A function taking validation contexts containing - /// subjects of type `Subject` and validating them. This function returns - /// `true` if validation succeeds and `false` if it fails. - /// - predicate: A function returning `true` if this validator - /// should run against the given value. - /// - public init( - description: String, - check validate: @escaping (ValidationContext) -> Bool, - when predicate: @escaping (ValidationContext) -> Bool = { _ in true } - ) { - let validity: (ValidationContext) -> [ValidationError] = { context in - return validate(context) - ? [] - : [ ValidationError(reason: "Failed to satisfy: \(description)", at: context.codingPath) ] - } - self.init(check: validity, when: predicate) - } -} - -/// Validation errors are just a textual reason for validation failure and -/// a coding path where the validation error occurred. -public struct ValidationError: Swift.Error, CustomStringConvertible, PathContextError { - /// The reason for the validation failure. - public let reason: String - /// The location where the failure occurred. - public let codingPath: [CodingKey] - /// A string representation of the whole coding - /// path. - public var codingPathString: String { codingPath.stringValue } - - /// Create a new `ValidationError` with the given - /// reason and location (coding path). - public init(reason: String, at path: [CodingKey]) { - self.reason = reason - self.codingPath = path - } - - public var localizedDescription: String { description } - - public var description: String { - guard !codingPath.isEmpty else { - return "\(reason) at root of document" - } - return "\(reason) at path: \(codingPath.stringValue)" - } -} - -/// Collects `ValidationErrors`. -/// -/// This type is responsible for making it possible to collect validation -/// errors and throw one value (this collection) at the end of validation. -public struct ValidationErrorCollection: Swift.Error, CustomStringConvertible, ErrorCollection { - public let values: [ValidationError] - - public var localizedDescription: String { - return values.map(String.init(describing:)).joined(separator: "\n") - } - - public var description: String { localizedDescription } - - public var swiftErrors: [Swift.Error] { values } - public var pathContextErrors: [PathContextError] { values } -} - -/// Erases the type on which a `Validator` is specialized and combines -/// the predicate and validation logic into one `apply` function. -internal struct AnyValidation { - // The only reason apply is private is because `apply()` gets us back - // our argument labels but are otherwise just straight-forward calls - // to `_apply()` - private let _apply: (Any, [CodingKey], OpenAPI.Document) -> [ValidationError] - - func apply(to value: Any, at codingPath: [CodingKey], in document: OpenAPI.Document) -> [ValidationError] { - return _apply(value, codingPath, document) - } - - init(_ validation: Validation) { - self._apply = { input, codingPath, document in - - guard let subject = input as? T else { - return [] - } - guard type(of: subject) == type(of: input) else { - // we need to guard against `T?` being - // coerced to `T` above. - return [] - } - - return validation.apply(to: subject, at: codingPath, in: document) - } - } -} diff --git a/Sources/OpenAPIKit30/Validator/Validator+Convenience.swift b/Sources/OpenAPIKit30/Validator/Validator+Convenience.swift deleted file mode 100644 index f872ff78df..0000000000 --- a/Sources/OpenAPIKit30/Validator/Validator+Convenience.swift +++ /dev/null @@ -1,357 +0,0 @@ -// -// Validator+Convenience.swift -// -// -// Created by Mathew Polzin on 6/2/20. -// - -import OpenAPIKitCore - -// MARK: - Operators - -// MARK: Context -> Error Array -public func &&( - lhs: @escaping (ValidationContext) -> [ValidationError], - rhs: @escaping (ValidationContext) -> [ValidationError] -) -> (ValidationContext) -> [ValidationError] { - return { context in - lhs(context) + rhs(context) - } -} - -public func ||( - lhs: @escaping (ValidationContext) -> [ValidationError], - rhs: @escaping (ValidationContext) -> [ValidationError] -) -> (ValidationContext) -> [ValidationError] { - return { context in - let leftHandCheck = lhs(context) - if leftHandCheck.isEmpty { - return [] - } - let rightHandCheck = rhs(context) - if rightHandCheck.isEmpty { - return [] - } - return leftHandCheck + rightHandCheck - } -} - -// MARK: Context -> Bool -public func &&( - lhs: @escaping (ValidationContext) -> Bool, - rhs: @escaping (ValidationContext) -> Bool -) -> (ValidationContext) -> Bool { - return { context in lhs(context) && rhs(context) } -} - -public func ||( - lhs: @escaping (ValidationContext) -> Bool, - rhs: @escaping (ValidationContext) -> Bool -) -> (ValidationContext) -> Bool { - return { context in lhs(context) || rhs(context) } -} - -// MARK: Context KeyPath -> Bool -public func ==(lhs: KeyPath, U>, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context[keyPath: lhs] == rhs - } -} - -public func !=(lhs: KeyPath, U>, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context[keyPath: lhs] != rhs - } -} - -public func >(lhs: KeyPath, U>, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context[keyPath: lhs] > rhs - } -} - -public func >=(lhs: KeyPath, U>, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context[keyPath: lhs] >= rhs - } -} - -public func <(lhs: KeyPath, U>, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context[keyPath: lhs] < rhs - } -} - -public func <=(lhs: KeyPath, U>, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context[keyPath: lhs] <= rhs - } -} - -// MARK: Subject KeyPath -> Bool -public func ==(lhs: KeyPath, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context.subject[keyPath: lhs] == rhs - } -} - -public func !=(lhs: KeyPath, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context.subject[keyPath: lhs] != rhs - } -} - -public func >(lhs: KeyPath, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context.subject[keyPath: lhs] > rhs - } -} - -public func >=(lhs: KeyPath, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context.subject[keyPath: lhs] >= rhs - } -} - -public func <(lhs: KeyPath, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context.subject[keyPath: lhs] < rhs - } -} - -public func <=(lhs: KeyPath, rhs: U) -> (ValidationContext) -> Bool { - return { context in - return context.subject[keyPath: lhs] <= rhs - } -} - -// MARK: - Methods - -/// Create a validation or predicate function from a KeyPath -/// and a function operating on that value. -public func take(_ path: KeyPath, check: @escaping (U) -> Bool) -> (ValidationContext) -> Bool { - return { context in check(context.subject[keyPath: path]) } -} - -/// Create a validation or predicate function from a KeyPath -/// and a function operating on that value. -public func take(_ path: KeyPath, U>, check: @escaping (U) -> Bool) -> (ValidationContext) -> Bool { - return { context in check(context[keyPath: path]) } -} - -/// Lift the value the KeyPath points to into any number of validations -/// on that value producing a new validation function that operates on the type of -/// the root of the KeyPath and results in all the given validations being run. -/// -/// This can be used to perform logic such as: -/// *"When type A has property b of type String with value 'hello',* -/// *run validations alpha and beta (both of which are Validations on type String"* -/// -/// **Example** -/// -/// let alpha = Validator(...) -/// let beta = Validator(...) -/// -/// let validator = Validator( -/// check: lift(\.a, into: alpha, beta), -/// when: \.a == "hello" -/// ) -/// -public func lift( - _ path: KeyPath, U>, - into validations: Validation... -) -> (ValidationContext) -> [ValidationError] { - return { context in - return validations.flatMap { $0.apply(to: context[keyPath: path], at: context.codingPath, in: context.document) } - } -} - -/// Lift the value the KeyPath points to into any number of validations -/// on that value producing a new validation function that operates on the type of -/// the root of the KeyPath and results in all the given validations being run. -/// -/// - Parameters: -/// - path: The path to lift into the given validations. -/// - validations: One or more validations to perform on the value -/// the KeyPath points to. -/// -/// This can be used to perform logic such as: -/// *"When type A has property b of type String with value 'hello',* -/// *run validations alpha and beta (both of which are Validations on type String"* -/// -/// **Example** -/// -/// let alpha = Validator(...) -/// let beta = Validator(...) -/// -/// let validator = Validator( -/// check: lift(\.a, into: alpha, beta), -/// when: \.a == "hello" -/// ) -/// -public func lift( - _ path: KeyPath, - into validations: Validation... -) -> (ValidationContext) -> [ValidationError] { - return { context in - return validations.flatMap { $0.apply(to: context.subject[keyPath: path], at: context.codingPath, in: context.document) } - } -} - -/// Unwrap the value pointed to by the KeyPath. Fail -/// with a `ValidationError` if the value is `nil` or -/// pass onto each validation provided otherwise. -/// -/// - Parameters: -/// - path: The path to unwrap. -/// - validations: One or more validations to perform on the value -/// the KeyPath points to. -/// - description: (Optionally) describe the unwrap operation so that -/// the failure error is more useful to you. -/// -/// See `lift(_:,into:)` for more information -/// on what this function does when the value pointed to -/// is non-nil. -/// -public func unwrap( - _ path: KeyPath, U?>, - into validations: Validation..., - description: String? = nil -) -> (ValidationContext) -> [ValidationError] { - return { context in - guard let subject = context[keyPath: path] else { - let error = description.map { "Tried to unwrap but found nil: \($0)" } - ?? "Tried to unwrap an optional for path \(String(describing: path)) and found `nil`" - return [ ValidationError(reason: error, at: context.codingPath) ] - } - return validations.flatMap { $0.apply(to: subject, at: context.codingPath, in: context.document) } - } -} - -/// Unwrap the value pointed to by the KeyPath. Fail -/// with a `ValidationError` if the value is `nil` or -/// pass onto each validation provided otherwise. -/// -/// - Parameters: -/// - path: The path to unwrap. -/// - validations: One or more validations to perform on the value -/// the KeyPath points to. -/// - description: (Optionally) describe the unwrap operation so that -/// the failure error is more useful to you. -/// -/// See `lift(_:,into:)` for more information -/// on what this function does when the value pointed to -/// is non-nil. -/// -public func unwrap( - _ path: KeyPath, - into validations: Validation..., - description: String? = nil -) -> (ValidationContext) -> [ValidationError] { - return { context in - guard let subject = context.subject[keyPath: path] else { - let error = description.map { "Tried to unwrap but found nil: \($0)" } - ?? "Tried to unwrap an optional for path \(String(describing: path)) and found `nil`" - return [ ValidationError(reason: error, at: context.codingPath) ] - } - return validations.flatMap { $0.apply(to: subject, at: context.codingPath, in: context.document) } - } -} - -/// Look up the value pointed to by the KeyPath. Fail -/// with a `ValidationError` if the value is not -/// found in the `Components` for the current document. -/// -/// - Parameters: -/// - path: The path to lookup. -/// - validations: One or more validations to perform on the value -/// the KeyPath points to. -/// -public func lookup( - _ path: KeyPath, U>>, - thenApply validations: Validation... -) -> (ValidationContext) -> [ValidationError] { - return { context in - return validations.flatMap { validation -> [ValidationError] in - let subject = context.subject[keyPath: path] - do { - return validation.apply( - to: try context.document.components.lookup(subject), - at: context.codingPath, - in: context.document - ) - } catch { - return [ - ValidationError( - reason: "Could not find component being validated: \(String(describing: subject.reference?.absoluteString))", - at: context.codingPath - ) - ] - } - } - } -} - -/// Unwrap and look up the value pointed to by the KeyPath. -/// Fail with a `ValidationError` if the value is `nil` or -/// not found in the `Components` for the current document. -/// -/// - Parameters: -/// - path: The path to lookup. -/// - validations: One or more validations to perform on the value -/// the KeyPath points to. -/// -public func unwrapAndLookup( - _ path: KeyPath, U>?>, - thenApply validations: Validation... -) -> (ValidationContext) -> [ValidationError] { - return { context in - return validations.flatMap { validation -> [ValidationError] in - guard let subject = context.subject[keyPath: path] else { - return [ - ValidationError( - reason: "Tried to unwrap an optional for path \(String(describing: path)) and found `nil`", - at: context.codingPath - ) - ] - } - do { - return validation.apply( - to: try context.document.components.lookup(subject), - at: context.codingPath, - in: context.document - ) - } catch { - return [ - ValidationError( - reason: "Could not find component being validated: \(String(describing: subject.reference?.absoluteString))", - at: context.codingPath - ) - ] - } - } - } -} - -/// Apply all of the given validations to the current context. -/// -/// This is equivalent to calling `lift` with the keypath `\.self` -/// or inlining each of the individual validations with the `&&` -/// operator to apply them all. The benefit to this approach is -/// being able to create reusable separate components that -/// add up to the validation being written. -/// -/// **Example** -/// -/// let isLongerThanThreeChars = Validator(...) -/// let hasSpecialChars = Validator(...) -/// -/// let validator = Validator( -/// check: all(isLongerThanThreeChars, hasSpecialChars) -/// ) -/// -public func all(_ validations: Validation...) -> (ValidationContext) -> [ValidationError] { - return { context in - return validations.flatMap { $0.apply(to: context.subject, at: context.codingPath, in: context.document) } - } -} diff --git a/Sources/OpenAPIKit30/Validator/Validator.swift b/Sources/OpenAPIKit30/Validator/Validator.swift deleted file mode 100644 index b636529fa5..0000000000 --- a/Sources/OpenAPIKit30/Validator/Validator.swift +++ /dev/null @@ -1,695 +0,0 @@ -// -// Validator.swift -// -// -// Created by Mathew Polzin on 6/2/20. -// - -import OpenAPIKitCore - -extension OpenAPI.Document { - /// Validate this `OpenAPI.Document`. - /// - /// - parameters: - /// - 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 - /// return warnings instead of throwing them. True by default. - /// - /// - throws: `ValidationErrors` if any validations failed. - /// `EncodingError` if encoding failed for a structural reason. - /// - /// Call without any arguments to validate some 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 - /// `Validator` to the `validate(using:)` method to use custom validation - /// criteria. - @discardableResult - public func validate(using validator: Validator = .init(), strict: Bool = true) throws -> [OpenAPI.Warning] { - let validator = _Validator(document: self, validations: validator.validations) - var container = validator.singleValueContainer() - - // Validation is accomplished via "encoding" - try container.encode(self) - - let errors: [ValidationError] - if strict { - let warningsAsErrors = validator.warnings.map { warning in - return ValidationError( - reason: warning.localizedDescription, - at: warning.codingPath ?? [] - ) - } - errors = validator.errors + warningsAsErrors - } else { - errors = validator.errors - } - - if !errors.isEmpty { - throw ValidationErrorCollection(values: errors) - } - return (strict ? [] : validator.warnings) - } -} - -/// A validator that works by traversing an `Encodable` object and validating -/// any values that match an included validation's type and pass that validation's -/// predicate. -/// -/// The default Validator will perform a set of default validations -/// that catch OpenAPI Specification violations not possible (or inconvenient) to -/// prevent using the Swift type-system. -/// -/// **Example** -/// -/// let document = OpenAPI.Document(...) -/// let validator = Validator() -/// try document.validate(using: validator) -/// -/// // or, equivalently for the default validator: -/// try document.validate() -/// -/// The default validations are -/// - Operations must contain at least one response. -/// - Document-level tag names are unique. -/// - Parameters are unique within each Path Item. -/// - Parameters are unique within each Operation. -/// - Operation Ids are unique across the whole Document. -/// - All JSONReferences that refer to components in this -/// document can be found in the components dictionary. -/// -/// If you want a Validator that won't perform any -/// validations except the ones you add, use -/// `Validator.blank`. -/// -/// You can add validations to the validator using the -/// `validating()` 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. -/// -/// **Example** -/// -/// let document = OpenAPI.Document(...) -/// let validator = Validator() -/// .validating(.documentContainsPaths) -/// .validating(.pathsContainOperations) -/// try document.validate(using: validator) -/// -/// At their core, all validations are values of the `Validation` -/// type. You can create validations by initializing the `Validation` -/// type directly but it is generally more convenient to add validations -/// to a Validator using one of the convenience `validating()` -/// methods that know how to construct `Validation` on your behalf. -/// -/// For example, you can construct validation methods for simple equalities -/// and inequalities by specifying the KeyPath from any OpenAPI type to -/// the value you want to validate and then writing the equality/inequality -/// that must succeed for the validation to pass. -/// -/// **Example** -/// -/// let validator = Validator() -/// .validating( -/// "API version is 2.0", -/// check: \OpenAPI.Document.Info.version == "2.0" -/// ) -/// -/// You can add a `where` clause to any validation as long as the `check` and -/// `where` clause both examine the same type (i.e. `OpenAPI.Document.Info` -/// from the previous example and `OpenAPI.Document` from the next example). -/// -/// The next example also uses `take()` in its `where` caluse. This allows you to -/// dig into a value based on its KeyPath just like the previous example but you can -/// use it for more complicated criteria than equality/inequality. -/// -/// Finally, the next example also introduces the context access KayPath syntax in its -/// `check` clause. This syntax allows you to access the entire OpenAPI Document -/// that is being validated as `\.document`. It also gives you access to the current value -/// being validated as `\.subject` and the current key path as `\.codingPath`. -/// -/// **Example** -/// -/// let validator = Validator() -/// .validating( -/// "At least two servers are specified if one of them is the test server.", -/// check: \.document.servers.count >= 2, -/// when: take(\OpenAPI.Document.servers) { servers in -/// servers.map { $0.url.absoluteString }.contains("https://test.server.com") -/// } -/// ) -/// -/// Context access, the `take()` method, and the inequality KeyPath syntax are all -/// allowed in both the `check` and `where` clauses. Just keep in mind that if you -/// omit information about the type of thing being validated in one clause (as you do -/// when you access `\.document`) then you need to indicate the type (perhaps with -/// a KeyPath that includes the full type as in `\OpenAPI.Document.servers` above) in the other clause -/// so the type system knows what you are validating. -/// -public final class Validator { - - internal var validations: [AnyValidation] - - /// Creates a `Validator`. - internal init(validations: [AnyValidation]) { - self.validations = 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`. - /// - /// The default validations are - /// - Operations must contain at least one response. - /// - Document-level tag names are unique. - /// - Parameters are unique within each Path Item. - /// - Parameters are unique within each Operation. - /// - Operation Ids are unique across the whole Document. - /// - All JSONReferences that refer to components in this - /// document can be found in the components dictionary. - /// - public convenience init() { - self.init(validations: [ - .init(.operationsContainResponses), - .init(.documentTagNamesAreUnique), - .init(.pathItemParametersAreUnique), - .init(.operationParametersAreUnique), - .init(.operationIdsAreUnique), - .init(.schemaReferencesAreValid), - .init(.responseReferencesAreValid), - .init(.parameterReferencesAreValid), - .init(.exampleReferencesAreValid), - .init(.requestReferencesAreValid), - .init(.headerReferencesAreValid), - .init(.linkReferencesAreValid), - .init(.callbacksReferencesAreValid) - ]) - } - - /// 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)) - return self - } - - /// Add a validation to be performed. - /// - /// - Parameters: - /// - validate: A function taking values of type `T` and validating - /// them. This function should return an array of all validation failures. - /// `ValidationError` is a good general purpose error for this use-case. - /// - public func validating( - _ validate: @escaping (ValidationContext) -> [ValidationError] - ) -> Self { - return validating(Validation(check: validate, when: { _ in true })) - } - - /// Add a validation to be performed. - /// - /// - Parameters: - /// - validate: A function taking values of type `T` and validating - /// them. This function should return an array of all validation failures. - /// `ValidationError` is a good general purpose error for this use-case. - /// - predicate: A function returning `true` if this validator - /// should run against the given value. - /// - public func validating( - _ validate: @escaping (ValidationContext) -> [ValidationError], - when predicate: @escaping (ValidationContext) -> Bool - ) -> Self { - return validating(Validation(check: validate, when: predicate)) - } - - /// Given the description of the correct & valid state being asserted, - /// create a validation function and add it to the `Validator`. - /// - /// - Parameters: - /// - 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. - public func validating( - _ description: String, - check validate: @escaping (ValidationContext) -> Bool - ) -> Self { - return validating({ context in - return validate(context) - ? [] - : [ ValidationError(reason: "Failed to satisfy: \(description)", at: context.codingPath) ] - }) - } - - /// Given the description of the correct & valid state being asserted, - /// create a validation function and add it to the `Validator`. - /// - /// - Parameters: - /// - 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. - /// - predicate: A condition that must be met for this validation to be applied. - public func validating( - _ description: String, - check validate: @escaping (ValidationContext) -> Bool, - when predicate: @escaping (ValidationContext) -> Bool - ) -> Self { - let validity: (ValidationContext) -> [ValidationError] = { context in - return validate(context) - ? [] - : [ ValidationError(reason: "Failed to satisfy: \(description)", at: context.codingPath) ] - } - - return validating(validity, when: predicate) - } -} - -/// Must be used with Encodable dict values and array elements only. -enum ValidityEncoderNode { - case unused - case single - case unkeyed(count: Int) - case keyed -} - -class _Validator: Encoder { - - init( - document: OpenAPI.Document, - validations: [AnyValidation], - userInfo: [CodingUserInfoKey: Any] = [:], - codingPath: [CodingKey] = [] - ) { - self.document = document - self.validations = validations - self.userInfo = userInfo - self.codingPath = codingPath - } - - let codingPath: [CodingKey] - let userInfo: [CodingUserInfoKey: Any] - let document: OpenAPI.Document - private(set) var validations: [AnyValidation] - - var warnings: [OpenAPI.Warning] = [] - var errors: [ValidationError] = [] - var node: ValidityEncoderNode = .unused - - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - return .init(_KeyedEncodingContainer(referencing: self)) - } - - func unkeyedContainer() -> UnkeyedEncodingContainer { - return _UnkeyedEncodingContainer(referencing: self) - } - - func singleValueContainer() -> SingleValueEncodingContainer { - return self - } - - var unkeyedCount: Int { - get { - guard case .unkeyed(let count) = node else { - return 0 - } - return count - } - set { - guard case .unkeyed = node else { - fatalError() - } - node = .unkeyed(count: newValue) - } - } - - /// create a new `_ReferencingEncoder` instance as `key` inheriting `userInfo` - func encoder(for key: CodingKey) -> _ReferencingValidator { - return .init(referencing: self, key: key) - } - - /// create a new `_ReferencingEncoder` instance at `index` inheriting `userInfo` - func encoder(at index: Int) -> _ReferencingValidator { - return .init(referencing: self, at: index) - } - - private var canEncodeNewValue: Bool { - guard case .unused = node else { - return false - } - return true - } -} - -class _ReferencingValidator: _Validator { - private enum Reference { - case dictionary(String) - case array(Int) - } - - private let encoder: _Validator - private let reference: Reference - - init(referencing encoder: _Validator, key: CodingKey) { - self.encoder = encoder - reference = .dictionary(key.stringValue) - super.init( - document: encoder.document, - validations: encoder.validations, - userInfo: encoder.userInfo, - codingPath: encoder.codingPath + [key] - ) - } - - init(referencing encoder: _Validator, at index: Int) { - self.encoder = encoder - reference = .array(index) - super.init( - document: encoder.document, - validations: encoder.validations, - userInfo: encoder.userInfo, - codingPath: encoder.codingPath + [Validator.CodingKey(index: index)] - ) - } - - deinit { - encoder.warnings += warnings - encoder.errors += errors - switch reference { - case .dictionary: - switch encoder.node { - case .keyed, .unused: - break - - case .single, .unkeyed: - fatalError() - } - case .array(let index): - switch encoder.node { - case .unkeyed(var count): - if index == count { - count += 1 - } - encoder.node = .unkeyed(count: count) - case .unused: - encoder.node = .unkeyed(count: 1) - - case .keyed, .single: - fatalError() - } - } - } -} - -extension _Validator: SingleValueEncodingContainer { - func encodeNil() { - assertCanEncodeNewValue() - node = .single - } - - func encode(_ value: Bool) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: String) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Double) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Float) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Int) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Int8) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Int16) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Int32) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: Int64) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: UInt) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: UInt8) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: UInt16) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: UInt32) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: UInt64) { - applyValidations(to: value) - collectWarnings(from: value) - node = .single - } - - func encode(_ value: T) throws where T : Encodable { - assertCanEncodeNewValue() - applyValidations(to: value) - collectWarnings(from: value) - try value.encode(to: self) - } - - /// Asserts that a single value can be encoded at the current coding path - /// (i.e. that one has not already been encoded through this container). - /// `preconditionFailure()`s if one cannot be encoded. - private func assertCanEncodeNewValue() { - precondition( - canEncodeNewValue, - "Attempt to encode value through single value container when previously value already encoded." - ) - } - - fileprivate func applyValidations(to value: Encodable, atKey key: CodingKey? = nil) { - let pathTail = key.map { [$0] } ?? [] - for idx in validations.indices { - errors += validations[idx].apply(to: value, at: codingPath + pathTail, in: document) - } - } - - // take a warning that does not have a coding path associated and give it - // a coding path (when possible). - fileprivate func contextualize(at path: [CodingKey]) -> (OpenAPI.Warning) -> OpenAPI.Warning { - return { warning in - if path.isEmpty { - return warning - } - switch warning { - case .underlyingError: return warning - case .message(let details): return .underlyingError(CodingPathError(details: details, codingPath: path)) - } - } - } - - fileprivate func collectWarnings(from value: Encodable, atKey key: CodingKey? = nil) { - let pathTail = key.map { [$0] } ?? [] - if let warnable = value as? HasWarnings { - warnings += warnable.warnings.map(contextualize(at: codingPath + pathTail)) - } - } -} - -struct _UnkeyedEncodingContainer: UnkeyedEncodingContainer { - let encoder: _Validator - - init(referencing encoder: _Validator) { - self.encoder = encoder - } - - var codingPath: [CodingKey] { encoder.codingPath } - - var count: Int { encoder.unkeyedCount } - - func encodeNil() { - encoder.unkeyedCount += 1 - } - - func encode(_ value: T) throws where T: Encodable { - try currentEncoder.encode(value) - } - - func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - currentEncoder.container(keyedBy: keyType) - } - - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - currentEncoder.unkeyedContainer() - } - - func superEncoder() -> Encoder { currentEncoder } - - private var currentEncoder: _ReferencingValidator { - return encoder.encoder(at: count) - } -} - -struct _KeyedEncodingContainer: KeyedEncodingContainerProtocol { - - let encoder: _Validator - - init(referencing encoder: _Validator) { - self.encoder = encoder - } - - var codingPath: [CodingKey] { encoder.codingPath } - - func encodeNil(forKey key: Key) {} - - func encode(_ value: T, forKey key: Key) throws where T: Encodable { - try encoder(for: key).encode(value) - } - - func encode(_ value: Bool, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: String, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Double, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Float, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Int, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Int8, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Int16, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Int32, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: Int64, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: UInt, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: UInt8, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: UInt16, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: UInt32, forKey key: Key) { - encoder(for: key).encode(value) - } - - func encode(_ value: UInt64, forKey key: Key) { - encoder(for: key).encode(value) - } - - func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { - encoder(for: key).container(keyedBy: keyType) - } - - func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - encoder(for: key).unkeyedContainer() - } - - func superEncoder() -> Encoder { - encoder(for: Validator.CodingKey.super) - } - - func superEncoder(forKey key: Key) -> Encoder { - encoder(for: key) - } - - private func encoder(for key: CodingKey) -> _ReferencingValidator { return encoder.encoder(for: key) } -} - -extension Validator { - public struct CodingKey: Swift.CodingKey { - public var stringValue: String - public var intValue: Int? - - public init(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } - - public init(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } - - init(index: Int) { - self.stringValue = "Index \(index)" - self.intValue = index - } - - static let `super` = CodingKey(stringValue: "super") - } -} diff --git a/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift index 15ddfad474..58c7c9f9d3 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift @@ -191,50 +191,4 @@ final class PathsErrorTests: XCTestCase { ) } } - - func test_paramSchemaHasProblemDeeplyNestedInSchema() throws { - let documentYML = - """ - openapi: "3.0.0" - info: - title: test - version: 1.0 - paths: - /hello/world: - summary: hello - parameters: - - name: world - in: query - schema: - type: object - properties: - hi: - type: object - items: - type: string - """ - - let warnResult = try testDecoder.decode(OpenAPI.Document.self, from: documentYML) - - let openAPIError = try warnResult.validate(using: Validator.blank, strict: false).first - - XCTAssertEqual( - openAPIError?.localizedDescription, - """ - Problem encountered when parsing `OpenAPI Schema`: Found schema attributes not consistent with the type specified: object. Specifically, attributes for these other types: [\"array\"]. - """ - ) - XCTAssertEqual( - openAPIError?.codingPath?.map { $0.stringValue }, - [ - "paths", - "/hello/world", - "parameters", - "Index 0", - "schema", - "properties", - "hi" - ] - ) - } } diff --git a/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift index 57ef0437a8..9e19b7210f 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift @@ -58,13 +58,6 @@ final class GitHubAPICampatibilityTests: XCTestCase { } } - func test_passesValidation() throws { - guard let apiDoc = apiDoc else { return } - - let warnings = try apiDoc.validate(strict: false) - XCTAssertEqual(warnings.count, 4) - } - func test_successfullyParsedBasicMetadata() throws { guard let apiDoc = apiDoc else { return } diff --git a/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift index de2510b237..fa44a31dc2 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift @@ -53,12 +53,6 @@ final class GoogleBooksAPICampatibilityTests: XCTestCase { } } - func test_passesValidation() throws { - guard let apiDoc = apiDoc else { return } - - try apiDoc.validate() - } - func test_successfullyParsedBasicMetadata() throws { guard let apiDoc = apiDoc else { return } diff --git a/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift index 05a41cc85c..d38fe2ba05 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift @@ -53,12 +53,6 @@ final class PetStoreAPICampatibilityTests: XCTestCase { } } - func test_passesValidation() throws { - guard let apiDoc = apiDoc else { return } - - try apiDoc.validate() - } - func test_successfullyParsedBasicMetadata() throws { guard let apiDoc = apiDoc else { return } diff --git a/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift b/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift index 6ec381456f..e5494fa00d 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift @@ -66,9 +66,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - XCTAssertEqual( doc.paths["/pets"]?.pathItemValue?.patch?.requestBody?.requestValue? .content[.json]?.schema?.schemaValue, @@ -156,9 +153,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - // test dereferencing and resolving _ = try doc.locallyDereferenced().resolved() } catch let error { @@ -210,9 +204,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - // test dereferencing and resolving _ = try doc.locallyDereferenced().resolved() } catch let error { @@ -250,9 +241,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - // test dereferencing and resolving _ = try doc.locallyDereferenced().resolved() } catch let error { @@ -282,9 +270,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - // test dereferencing and resolving _ = try doc.locallyDereferenced().resolved() } catch let error { @@ -323,9 +308,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - // test dereferencing and resolving _ = try doc.locallyDereferenced().resolved() } catch let error { @@ -399,9 +381,6 @@ final class SwaggerDocSamplesTests: XCTestCase { do { let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) - // test validating - try doc.validate() - // test dereferencing and resolving _ = try doc.locallyDereferenced().resolved() } catch let error { diff --git a/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift index 8b3af9676b..35534e2843 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift @@ -53,14 +53,6 @@ final class TomTomAPICampatibilityTests: XCTestCase { } } - func test_passesValidation() throws { - guard let apiDoc = apiDoc else { return } - - let warnings = try apiDoc.validate(strict: false) - // a bunch of warnings about Status Code '404/596' which is not valid: - XCTAssertEqual(warnings.count, 18) - } - func test_successfullyParsedBasicMetadata() throws { guard let apiDoc = apiDoc else { return } diff --git a/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift deleted file mode 100644 index eedb41243e..0000000000 --- a/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift +++ /dev/null @@ -1,824 +0,0 @@ -// -// BuiltinValidationTests.swift -// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -import XCTest -import OpenAPIKit30 - -final class BuiltinValidationTests: XCTestCase { - func test_noPathsOnDocumentFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [:], - components: .noComponents - ) - - let validator = Validator.blank.validating(.documentContainsPaths) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let errorCollection = error as? ValidationErrorCollection - XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: Document contains at least one path") - XCTAssertEqual(errorCollection?.values.first?.codingPath.map { $0.stringValue }, ["paths"]) - XCTAssertEqual(errorCollection?.values.count, 1) - - let openAPIError = OpenAPI.Error(from: error) - XCTAssertEqual(openAPIError.localizedDescription, "Failed to satisfy: Document contains at least one path at path: .paths") - XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, ["paths"]) - } - } - - func test_onePathOnDocumentSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init() - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.documentContainsPaths) - try document.validate(using: validator) - } - - func test_noOperationsOnPathItemFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init() - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.pathsContainOperations) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Paths contain at least one operation") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello/world"]) - XCTAssertEqual(error?.values.count, 1) - } - } - - func test_oneOperationOnPathItemSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - get: .init(responses: [:]) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.pathsContainOperations) - try document.validate(using: validator) - } - - func test_emptySchemaFails() throws { - let document = OpenAPI.Document( - info: .init(title: "Test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - get: .init( - responses: [ - 200: .response( - description: "Test", - content: [ - .json: .init( - schema: .fragment(.init()) - ) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.schemaComponentsAreDefined) - XCTAssertThrowsError(try document.validate(using: validator)) - } - - func test_nestedEmptySchemaFails() throws { - let document = OpenAPI.Document( - info: .init(title: "Test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - get: .init( - responses: [ - 200: .response( - description: "Test", - content: [ - .json: .init( - schema: .object( - properties: [ - "nested": .fragment(.init()) - ] - ) - ) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.schemaComponentsAreDefined) - XCTAssertThrowsError(try document.validate(using: validator)) { error in - XCTAssertEqual( - (error as? ValidationErrorCollection)?.values.map(String.init(describing:)), - [#"Failed to satisfy: JSON Schema components have defining characteristics (i.e. they are not just the empty schema component: `{}`) [Note that one way to end up with empty schema components is by having property names in an object's `required` array that are not defined in that object's `properties` map] at path: .paths['/hello/world'].get.responses.200.content['application/json'].schema.properties.nested"#] - ) - } - } - - func test_noEmptySchemasSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "Test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - get: .init( - responses: [ - 200: .response( - description: "Test", - content: [ - .json: .init( - // following is _not_ an empty schema component - // because it is actually a `{ "type": "object" }` - // instead of a `{ }` - schema: .object - ) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.schemaComponentsAreDefined) - try document.validate(using: validator) - } - - func test_missingPathParamFails() throws { - let document = OpenAPI.Document( - info: .init(title: "Test", version: "1.0"), - servers: [], - paths: [ - "/hello/world/{idx}": .init( - get: .init( - responses: [:] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.pathParametersAreDefined) - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual( - error?.values.map(String.init(describing:)), - [#"The following path parameters were not defined in the Path Item or Operation `parameters`: ["idx"] at path: .paths['/hello/world/{idx}'].GET"#] - ) - } - } - - func test_pathParamInPathItemSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "Test", version: "1.0"), - servers: [], - paths: [ - "/hello/world/{idx}": .init( - parameters: [ - .parameter(name: "idx", context: .path, schema: .string) - ], - get: .init( - responses: [:] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.pathParametersAreDefined) - try document.validate(using: validator) - } - - func test_pathParamInOperationSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "Test", version: "1.0"), - servers: [], - paths: [ - "/hello/world/{idx}": .init( - get: .init( - parameters: [ - .parameter(name: "idx", context: .path, schema: .string) - ], - responses: [:] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator.blank.validating(.pathParametersAreDefined) - try document.validate(using: validator) - } - - func test_missingServerVariableFails() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [ - .init(urlTemplate: URLTemplate(rawValue: "https://website.com/{path}")!) - ], - paths: [:], - components: .noComponents - ) - - let validator = Validator.blank.validating(.serverVariablesAreDefined) - XCTAssertThrowsError(try document.validate(using: validator)) { error in - XCTAssertEqual( - (error as? ValidationErrorCollection)?.values.map(String.init(describing:)), - [ - "Server Object does not define the variable 'path' that is found in the `urlTemplate` 'https://website.com/{path}' at path: .servers[0]" - ] - ) - } - } - - func test_partialMissingServerVariablesFails() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [ - .init( - urlTemplate: URLTemplate(rawValue: "{scheme}://website.com/{path}")!, - variables: ["scheme": .init(default: "scheme")] - ) - ], - paths: [:], - components: .noComponents - ) - - let validator = Validator.blank.validating(.serverVariablesAreDefined) - XCTAssertThrowsError(try document.validate(using: validator)) { error in - XCTAssertEqual( - (error as? ValidationErrorCollection)?.values.map(String.init(describing:)), - [ - "Server Object does not define the variable 'path' that is found in the `urlTemplate` '{scheme}://website.com/{path}' at path: .servers[0]" - ] - ) - } - } - - func test_noSevrerVariablesSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [ - .init(urlTemplate: URLTemplate(rawValue: "https://website.com")!) - ], - paths: [:], - components: .noComponents - ) - - let validator = Validator.blank.validating(.serverVariablesAreDefined) - try document.validate(using: validator) - } - - func test_allServerVariablesDefinedSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [ - .init( - urlTemplate: URLTemplate(rawValue: "https://website.com/{path}")!, - variables: ["path": .init(default: "welcome")] - ) - ], - paths: [:], - components: .noComponents - ) - - let validator = Validator.blank.validating(.serverVariablesAreDefined) - try document.validate(using: validator) - } - - func test_duplicateTagOnDocumentFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [:], - components: .noComponents, - tags: ["hello", "hello"] - ) - - // NOTE this is part of default validation - XCTAssertThrowsError(try document.validate()) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: The names of Tags in the Document are unique") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, []) - } - } - - func test_uniqueTagsOnDocumentSocceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [:], - components: .noComponents, - tags: ["hello", "world"] - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_noResponsesOnOperationFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - get: .init(responses: [:]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - XCTAssertThrowsError(try document.validate()) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Operations contain at least one response") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello/world", "get", "responses"]) - } - } - - func test_oneResponseOnOperationSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - get: .init(responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_duplicateOperationParameterFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init( - parameters: [ - .parameter(name: "hiya", context: .path, schema: .string), - .parameter(name: "hiya", context: .path, schema: .string) - ], - responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - XCTAssertThrowsError(try document.validate()) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Operation parameters are unique (identity is defined by the 'name' and 'location')") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello", "get"]) - XCTAssertEqual(error?.values.first?.codingPathString, ".paths['/hello'].get") - } - } - - func test_uniqueOperationParametersSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init( - parameters: [ - .parameter(name: "hiya", context: .query, schema: .string), - .parameter(name: "hiya", context: .path, schema: .string), // changes parameter location but not name - .parameter(name: "cool", context: .path, schema: .string) // changes parameter name but not location - ], - responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_noOperationParametersSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init( - parameters: [], - responses: [ - 200: .response(description: "hi") - ]) - ), - "/hello/world": .init( - put: .init( - responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_duplicateOperationIdFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init(operationId: "test", responses: [ - 200: .response(description: "hi") - ]) - ), - "/hello/world": .init( - put: .init(operationId: "test", responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - XCTAssertThrowsError(try document.validate()) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: All Operation Ids in Document are unique") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, []) - } - } - - func test_uniqueOperationIdsSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init(operationId: "one", responses: [ - 200: .response(description: "hi") - ]) - ), - "/hello/world": .init( - put: .init(operationId: "two", responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_noOperationIdsSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init(operationId: nil, responses: [ - 200: .response(description: "hi") - ]) - ), - "/hello/world": .init( - put: .init(operationId: nil, responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_duplicatePathItemParameterFails() { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - parameters: [ - .parameter(name: "hiya", context: .query, schema: .string), - .parameter(name: "hiya", context: .query, schema: .string) - ], - get: .init( - responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - XCTAssertThrowsError(try document.validate()) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Path Item parameters are unique (identity is defined by the 'name' and 'location')") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello"]) - XCTAssertEqual(error?.values.first?.codingPathString, ".paths['/hello']") - } - } - - func test_uniquePathItemParametersSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - parameters: [ - .parameter(name: "hiya", context: .query, schema: .string), - .parameter(name: "hiya", context: .path, schema: .string), // changes parameter location but not name - .parameter(name: "cool", context: .path, schema: .string) // changes parameter name but not location - ], - get: .init( - responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_noPathItemParametersSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init( - parameters: [], - responses: [ - 200: .response(description: "hi") - ]) - ), - "/hello/world": .init( - put: .init( - responses: [ - 200: .response(description: "hi") - ]) - ) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_oneOfEachReferenceTypeFails() throws { - - let path = OpenAPI.PathItem( - get: .init( - parameters: [ - .reference(.component(named: "parameter1")) - ], - requestBody: .reference(.component(named: "request1")), - responses: [ - 200: .reference(.component(named: "response1")), - 404: .response( - description: "response2", - headers: ["header1": .reference(.component(named: "header1"))], - content: [ - .json: .init( - schema: .string, - examples: [ - "example1": .reference(.component(named: "example1")) - ] - ), - .xml: .init(schemaReference: .component(named: "schema1")) - ], - links: ["linky": .reference(.component(named: "link1"))] - ) - ], - callbacks: [ - "callbacks1": .reference(.component(named: "callbacks1")) - ] - ) - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .pathItem(path) - ], - components: .noComponents - ) - - // NOTE this is part of default validation - XCTAssertThrowsError(try document.validate()) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 8) - XCTAssertEqual(error?.values[0].reason, "Failed to satisfy: Parameter reference can be found in components/parameters") - XCTAssertEqual(error?.values[0].codingPathString, ".paths['/hello'].get.parameters[0]") - XCTAssertEqual(error?.values[1].reason, "Failed to satisfy: Request reference can be found in components/requestBodies") - XCTAssertEqual(error?.values[1].codingPathString, ".paths['/hello'].get.requestBody") - XCTAssertEqual(error?.values[2].reason, "Failed to satisfy: Response reference can be found in components/responses") - XCTAssertEqual(error?.values[2].codingPathString, ".paths['/hello'].get.responses.200") - XCTAssertEqual(error?.values[3].reason, "Failed to satisfy: Header reference can be found in components/headers") - XCTAssertEqual(error?.values[3].codingPathString, ".paths['/hello'].get.responses.404.headers.header1") - XCTAssertEqual(error?.values[4].reason, "Failed to satisfy: Example reference can be found in components/examples") - XCTAssertEqual(error?.values[4].codingPathString, ".paths['/hello'].get.responses.404.content['application/json'].examples.example1") - XCTAssertEqual(error?.values[5].reason, "Failed to satisfy: JSONSchema reference can be found in components/schemas") - XCTAssertEqual(error?.values[5].codingPathString, ".paths['/hello'].get.responses.404.content['application/xml'].schema") - XCTAssertEqual(error?.values[6].reason, "Failed to satisfy: Link reference can be found in components/links") - XCTAssertEqual(error?.values[6].codingPathString, ".paths['/hello'].get.responses.404.links.linky") - XCTAssertEqual(error?.values[7].reason, "Failed to satisfy: Callbacks reference can be found in components/callbacks") - XCTAssertEqual(error?.values[7].codingPathString, ".paths['/hello'].get.callbacks.callbacks1") - } - } - - func test_oneOfEachReferenceTypeSucceeds() throws { - let path = OpenAPI.PathItem( - put: .init( - requestBody: .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)), - responses: [ - 200: .response(description: "empty") - ] - ), - post: .init( - parameters: [ - .reference(.component(named: "parameter1")), - .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)) - ], - requestBody: .reference(.component(named: "request1")), - responses: [ - 200: .reference(.component(named: "response1")), - 301: .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)), - 404: .response( - description: "response2", - headers: [ - "header1": .reference(.component(named: "header1")), - "external": .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)) - ], - content: [ - .json: .init( - schema: .string, - examples: [ - "example1": .reference(.component(named: "example1")), - "external": .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)) - ] - ), - .xml: .init(schemaReference: .component(named: "schema1")), - .txt: .init(schemaReference: .external(URL(string: "https://website.com/file.json#/hello/world")!)) - ], - links: [ - "linky": .reference(.component(named: "link1")), - "linky2": .reference(.external(URL(string: "https://linky.com")!)) - ] - ) - ], - callbacks: [ - "callbacks1": .reference(.component(named: "callbacks1")) - ] - ) - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .pathItem(path) - ], - components: .init( - schemas: [ - "schema1": .object - ], - responses: [ - "response1": .init(description: "test") - ], - parameters: [ - "parameter1": .init(name: "test", context: .header, schema: .string) - ], - examples: [ - "example1": .init(value: .b("hello")) - ], - requestBodies: [ - "request1": .init(content: [.json: .init(schema: .object)]) - ], - headers: [ - "header1": .init(schema: .string) - ], - links: [ - "link1": .init(operationId: "op 1") - ], - callbacks: [ - "callbacks1": .init() - ] - ) - ) - - // NOTE this is part of default validation - try document.validate() - } - - func test_linkOperationsExist_validates() throws { - // Create a link with an operationId that exists in the document - let link = OpenAPI.Link(operationId: "testOperation") - - // Create a document with an operation using that ID - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init( - operationId: "testOperation", - responses: [:] - ) - ) - ], - components: .init( - links: [ - "testLink": link - ] - ) - ) - - let validator = Validator.blank.validating(.linkOperationsExist) - try document.validate(using: validator) - } - - func test_linkOperationsExist_fails() throws { - // Create a link with an operationId that doesn't exist in the document - let link = OpenAPI.Link(operationId: "nonExistentOperation") - - // Create a document with an operation using a different ID - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/hello": .init( - get: .init( - operationId: "testOperation", - responses: [:] - ) - ) - ], - components: .init( - links: [ - "testLink": link - ] - ) - ) - - let validator = Validator.blank.validating(.linkOperationsExist) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let errorCollection = error as? ValidationErrorCollection - XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: Links with operationIds have corresponding Operations") - XCTAssertTrue((errorCollection?.values.first?.codingPath.map { $0.stringValue }.joined(separator: ".") ?? "").contains("testLink")) - } - } -} diff --git a/Tests/OpenAPIKit30Tests/Validator/Validation+ConvenienceTests.swift b/Tests/OpenAPIKit30Tests/Validator/Validation+ConvenienceTests.swift deleted file mode 100644 index c491c7279b..0000000000 --- a/Tests/OpenAPIKit30Tests/Validator/Validation+ConvenienceTests.swift +++ /dev/null @@ -1,407 +0,0 @@ -// -// Validation+ConvenienceTests.swift -// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -import XCTest -@testable import OpenAPIKit30 - -final class ValidationConvenienceTests: XCTestCase { - // MARK: - Operators - // MARK: Context -> Error Array - func test_and_toErrorArray() { - let v1 = { (context: ValidationContext) in - return context.subject.starts(with: "he") - ? [] - : [ ValidationError(reason: "", at: []) ] - } - let v2 = { (context: ValidationContext) in - return context.subject.count == 5 - ? [] - : [ ValidationError(reason: "", at: []) ] - } - - XCTAssertTrue((v1 && v2)(dummyContext(for: "hello")).isEmpty) - XCTAssertEqual((v1 && v2)(dummyContext(for: "heads up")).count, 1) - XCTAssertEqual((v1 && v2)(dummyContext(for: "12345")).count, 1) - XCTAssertEqual((v1 && v2)(dummyContext(for: "look out")).count, 2) - } - - func test_or_toErrorArray() { - let v1 = { (context: ValidationContext) in - return context.subject.starts(with: "he") - ? [] - : [ ValidationError(reason: "", at: []) ] - } - let v2 = { (context: ValidationContext) in - return context.subject.count == 5 - ? [] - : [ ValidationError(reason: "", at: []) ] - } - - XCTAssertTrue((v1 || v2)(dummyContext(for: "hello")).isEmpty) - XCTAssertTrue((v1 || v2)(dummyContext(for: "heads up")).isEmpty) - XCTAssertTrue((v1 || v2)(dummyContext(for: "12345")).isEmpty) - XCTAssertEqual((v1 || v2)(dummyContext(for: "look out")).count, 2) - } - - // MARK: Context -> Bool - func test_and_toBool() { - let v1 = { (context: ValidationContext) in - return context.subject.starts(with: "he") - } - let v2 = { (context: ValidationContext) in - return context.subject.count == 5 - } - - XCTAssertTrue((v1 && v2)(dummyContext(for: "hello"))) - XCTAssertFalse((v1 && v2)(dummyContext(for: "heads up"))) - XCTAssertFalse((v1 && v2)(dummyContext(for: "12345"))) - XCTAssertFalse((v1 && v2)(dummyContext(for: "look out"))) - } - - func test_or_toBool() { - let v1 = { (context: ValidationContext) in - return context.subject.starts(with: "he") - } - let v2 = { (context: ValidationContext) in - return context.subject.count == 5 - } - - XCTAssertTrue((v1 || v2)(dummyContext(for: "hello"))) - XCTAssertTrue((v1 || v2)(dummyContext(for: "heads up"))) - XCTAssertTrue((v1 || v2)(dummyContext(for: "12345"))) - XCTAssertFalse((v1 || v2)(dummyContext(for: "look out"))) - } - - // MARK: Context KeyPath -> Bool - func test_context_equals_toBool() { - let v = \ValidationContext.subject.count == 5 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_context_notEquals_toBool() { - let v = \ValidationContext.subject.count != 8 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_context_lessThan_toBool() { - let v = \ValidationContext.subject.count < 8 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_context_greaterThan_toBool() { - let v = \ValidationContext.subject.count > 4 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "hey"))) - } - - func test_context_lessThanOrEqualTo_toBool() { - let v = \ValidationContext.subject.count <= 5 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "1234"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_context_greaterThanOrEqualTo_toBool() { - let v = \ValidationContext.subject.count >= 5 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "hey"))) - } - - // MARK: Subject KeyPath -> Bool - func test_subject_equals_toBool() { - let v: (ValidationContext) -> Bool = \String.count == 5 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_subject_notEquals_toBool() { - let v: (ValidationContext) -> Bool = \String.count != 8 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_subject_lessThan_toBool() { - let v: (ValidationContext) -> Bool = \String.count < 8 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_subject_greaterThan_toBool() { - let v: (ValidationContext) -> Bool = \String.count > 4 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "hey"))) - } - - func test_subject_lessThanOrEqualTo_toBool() { - let v: (ValidationContext) -> Bool = \String.count <= 5 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "1234"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_subject_greaterThanOrEqualTo_toBool() { - let v: (ValidationContext) -> Bool = \String.count >= 5 - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "hey"))) - } - - // MARK: Methods - func test_context_take() { - let v = take(\ValidationContext.subject) { $0.count == 5 } - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_subject_take() { - let v: (ValidationContext) -> Bool = take(\.count) { $0 == 5 } - - XCTAssertTrue(v(dummyContext(for: "hello"))) - XCTAssertTrue(v(dummyContext(for: "12345"))) - XCTAssertFalse(v(dummyContext(for: "heads up"))) - } - - func test_context_lift() { - let v = Validation( - description: "int is 5", - check: \.self == 5 - ) - - XCTAssertTrue( - lift(\.subject.count, into: v)(dummyContext(for: "hello")).isEmpty - ) - XCTAssertTrue( - lift(\.subject.count, into: v)(dummyContext(for: "12345")).isEmpty - ) - XCTAssertFalse( - lift(\.subject.count, into: v)(dummyContext(for: "heads up")).isEmpty - ) - } - - func test_subject_lift() { - let v = Validation( - description: "int is 5", - check: \.self == 5 - ) - - XCTAssertTrue( - lift(\.count, into: v)(dummyContext(for: "hello")).isEmpty - ) - XCTAssertTrue( - lift(\.count, into: v)(dummyContext(for: "12345")).isEmpty - ) - XCTAssertFalse( - lift(\.count, into: v)(dummyContext(for: "heads up")).isEmpty - ) - } - - func test_context_unwrap() { - let v = Validation( - description: "string is 'H'", - check: \.self == 1 - ) - - XCTAssertTrue( - unwrap(\.subject.first?.wholeNumberValue, into: v)(dummyContext(for: "12345")).isEmpty - ) - XCTAssertFalse( - unwrap(\.subject.first?.wholeNumberValue, into: v)(dummyContext(for: "hello")).isEmpty - ) - XCTAssertTrue( - unwrap(\.subject.first?.wholeNumberValue, into: v)(dummyContext(for: "1eads up")).isEmpty - ) - // default unwrap nil error message - XCTAssertFalse( - unwrap(\.subject.first?.wholeNumberValue, into: v)(dummyContext(for: "")).isEmpty - ) - // given description - XCTAssertFalse( - unwrap(\.subject.first?.wholeNumberValue, into: v, description: "test")(dummyContext(for: "")).isEmpty - ) - } - - func test_subject_unwrap() { - let v = Validation( - description: "character is 'h'", - check: \.self == 1 - ) - - XCTAssertTrue( - unwrap(\.first?.wholeNumberValue, into: v)(dummyContext(for: "12345")).isEmpty - ) - XCTAssertFalse( - unwrap(\.first?.wholeNumberValue, into: v)(dummyContext(for: "hello")).isEmpty - ) - XCTAssertTrue( - unwrap(\.first?.wholeNumberValue, into: v)(dummyContext(for: "1eads up")).isEmpty - ) - // default unwrap nil error message - XCTAssertFalse( - unwrap(\.first?.wholeNumberValue, into: v)(dummyContext(for: "")).isEmpty - ) - // given description - XCTAssertFalse( - unwrap(\.first?.wholeNumberValue, into: v, description: "test")(dummyContext(for: "")).isEmpty - ) - } - - func test_subject_lookup() { - let v = Validation( - description: "parameter is named test", - check: \.name == "test" - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/test": .init( - parameters: [ - .reference(.component(named: "test1")), // passes validation - .reference(.component(named: "test2")), // wrong name - .reference(.component(named: "test3")) // not found - ] - ) - ], - components: .init( - parameters: [ - "test1": .init(name: "test", context: .header, content: [:]), - "test2": .init(name: "test2", context: .query, content: [:]) - ] - ) - ) - - let context = ValidationContext(document: document, subject: document.paths["/test"]!.pathItemValue!, codingPath: []) - - // passses - XCTAssertTrue( - lookup(\OpenAPI.PathItem.parameters[0], thenApply: v)(context).isEmpty - ) - // wrong name - XCTAssertFalse( - lookup(\OpenAPI.PathItem.parameters[1], thenApply: v)(context).isEmpty - ) - // not found reference - XCTAssertFalse( - lookup(\OpenAPI.PathItem.parameters[2], thenApply: v)(context).isEmpty - ) - } - - func test_subject_unwrapAndlookup() throws { - let v = Validation( - description: "parameter is named test", - check: \.name == "test" - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/test": .init( - parameters: [ - .reference(.component(named: "test1")), // passes validation - .reference(.component(named: "test2")), // wrong name - .reference(.component(named: "test3")) // not found - ] - ) - ], - components: .init( - parameters: [ - "test1": .init(name: "test", context: .header, content: [:]), - "test2": .init(name: "test2", context: .query, content: [:]) - ] - ) - ) - - let context = ValidationContext(document: document, subject: document, codingPath: []) - - // passes - XCTAssertTrue( - unwrapAndLookup(\OpenAPI.Document.paths["/test"]?.pathItemValue?.parameters[0], thenApply: v)(context).isEmpty - ) - // wrong name - XCTAssertFalse( - unwrapAndLookup(\OpenAPI.Document.paths["/test"]?.pathItemValue?.parameters[1], thenApply: v)(context).isEmpty - ) - // not found reference - XCTAssertFalse( - unwrapAndLookup(\OpenAPI.Document.paths["/test"]?.pathItemValue?.parameters[2], thenApply: v)(context).isEmpty - ) - #if os(macOS) - if #available(macOS 15.0, *) { - // this is just here because if #unavailable inside this block causes a compilation failure prior to Swift 5.6 on Linux :/ - } else { - throw XCTSkip("Skipping due to Swift/macOS bug resulting in error (line 368): throwing \"std::bad_alloc: std::bad_alloc\"") - } - #endif - // nil keypath - XCTAssertFalse( - unwrapAndLookup(\OpenAPI.Document.paths["/test2"]?.pathItemValue?.parameters.first, thenApply: v)(context).isEmpty - ) - } - - func test_allCombinator() { - let v1 = Validation( - description: "String is more than 5 characters", - check: \.count > 5 - ) - let v2 = Validation( - description: "String starts with 'H'", - check: \.first == "H" - ) - - XCTAssertTrue( - all(v1, v2)(dummyContext(for: "Halloween")).isEmpty - ) - XCTAssertFalse( - all(v1, v2)(dummyContext(for: "Hello")).isEmpty - ) - XCTAssertFalse( - all(v1, v2)(dummyContext(for: "Nanoparticle")).isEmpty - ) - } -} - -fileprivate func dummyContext(for string: String) -> ValidationContext { - .init(document: testDocument, subject: string, codingPath: []) -} - -fileprivate let testDocument = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [:], - components: .noComponents -) diff --git a/Tests/OpenAPIKit30Tests/Validator/ValidationTests.swift b/Tests/OpenAPIKit30Tests/Validator/ValidationTests.swift deleted file mode 100644 index 74d658514a..0000000000 --- a/Tests/OpenAPIKit30Tests/Validator/ValidationTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// ValidationTests.swift -// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -import XCTest -@testable import OpenAPIKit30 - -final class ValidationTests: XCTestCase { - func test_optionalValidation() { - let validation = Validation(check: { _ in [ ValidationError(reason: "because", at: []) ] }) - let check = AnyValidation(validation) - - let errors = check.apply(to: "hello", at: [], in: testDocument) - XCTAssertEqual(errors.map { $0.description }, [ "because at root of document" ]) - - let string: String? = "hello" - let errors2 = check.apply(to: string as Any, at: [], in: testDocument) - XCTAssertTrue(errors2.isEmpty) - } - - func test_wrongTypeValidation() { - let validation = Validation(check: { _ in [ ValidationError(reason: "because", at: []) ] }) - let check = AnyValidation(validation) - - let errors2 = check.apply(to: 10, at: [], in: testDocument) - XCTAssertTrue(errors2.isEmpty) - } - - func test_failsPredicateValidation() { - let validation = Validation(check: { _ in [ ValidationError(reason: "because", at: []) ] }, when: { _ in false }) - let check = AnyValidation(validation) - - let errors2 = check.apply(to: "hi", at: [], in: testDocument) - XCTAssertTrue(errors2.isEmpty) - } -} - -fileprivate let testDocument = OpenAPI.Document( - info: .init(title: "hi", version: "1.0"), - servers: [], - paths: [:], - components: .noComponents -) diff --git a/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift b/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift deleted file mode 100644 index f131d51824..0000000000 --- a/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift +++ /dev/null @@ -1,1496 +0,0 @@ -// -// ValidatorTests.swift -// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -import XCTest -import OpenAPIKit30 - -final class ValidatorTests: XCTestCase { - func test_randomSyntaxConstructions() { - // just here to test out syntax constructions and make sure they compile - let validator = Validator() - - _ = validator.validating( - "", - check: \OpenAPI.Document.openAPIVersion == .v3_0_0 - ) - _ = validator.validating( - "", - check: { (context: ValidationContext) in context.subject.openAPIVersion == .v3_0_0 } - ) - - _ = validator.validating( - "", - check: \[OpenAPI.Server].count > 1 - ) - - _ = validator.validating( - "", - check: take(\OpenAPI.Server.urlTemplate.absoluteString) { $0.contains("prod") } - ) - - _ = validator.validating( - "", - check: \[OpenAPI.Server].count >= 2, - when: { context in - context.subject.map { $0.urlTemplate.absoluteString }.contains("https://test.server.com") - } - ) - - _ = validator.validating( - "At least two servers are specified if one of them is the test server.", - check: \[OpenAPI.Server].count >= 2, - when: take(\.subject) { $0.map { $0.urlTemplate.absoluteString }.contains("https://test.server.com") } - ) - - _ = Validation( - description: "x-string is 'hello'", - check: \.subject == "hello", - when: \.codingPath.last?.stringValue == "x-string" - ) - - let allRoutesOfferJSON = Validation( - description: "All content maps have JSON members.", - check: \OpenAPI.Content.Map[.json] != nil - ) - - _ = Validation( - check: lift(\OpenAPI.Request.content, into: allRoutesOfferJSON) - ) - - _ = Validation( - check: lift(\OpenAPI.Response.content, into: allRoutesOfferJSON) - ) - - let contentValidation = Validation( - check: { _ in [] } - ) - - let _ = Validation( - check: unwrap(\OpenAPI.Content.Map[.json], into: contentValidation), - when: \OpenAPI.Content.Map[.json] != nil - ) - } - - func test_validationSucceedsUnconditionally() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating { (_: ValidationContext) in [] } - - try document.validate(using: validator) - } - - func test_validationNeverRunsAndSucceeds() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: "just because", at: context.codingPath) ] - }, - when: { _ in false } - ) - - try document.validate(using: validator) - } - - func test_validationFailsUnconditionally() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: "just because", at: context.codingPath) ] - }, - when: { _ in true } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "just because") - } - } - - func test_singleConditionallyDoesNotRunAndSucceeds() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: "just because", at: context.codingPath) ] - }, - when: { context in context.subject.description != "hello world" } - ) - - try document.validate(using: validator) - } - - func test_singleConditionallyRunsAndSucceeds() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (_: ValidationContext) in [] }, - when: { context in context.subject.description == "hello world" } - ) - - try document.validate(using: validator) - } - - func test_singleConditionallyRunsAndFails() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: "just because", at: context.codingPath) ] - }, - when: { context in context.subject.description == "hello world" } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "just because") - } - } - - func test_failsTwiceForSameValueOfSameTypeTwice() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server, server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating { (context: ValidationContext) in - [ ValidationError(reason: "just because", at: context.codingPath) ] - } - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 2) - XCTAssertEqual(error?.values.first?.reason, "just because") - XCTAssertEqual(error?.values.last?.reason, "just because") - } - } - - func test_failsOnceForDifferentValuesOfSameType() { - let server1 = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - let server2 = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "coolio", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server1, server2], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: context.subject.description ?? "", at: context.codingPath) ] - }, - when: { context in context.subject.description == "hello world" } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "hello world") - } - } - - func test_failsNestedString() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: String(context.subject), at: context.codingPath) ] - }, - when: { context in context.subject == "world" } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 2) - XCTAssertEqual(error?.values.first?.reason, "world") - } - } - - func test_failsTwoChildStrings() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: context.subject, at: context.codingPath) ] - }, - when: { context in ["hello", "world"].contains(context.subject) } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 5) - XCTAssertEqual(error?.values.map { $0.reason }.filter { $0 == "hello" }.count, 3) - XCTAssertEqual(error?.values.map { $0.reason }.filter { $0 == "world" }.count, 2) - } - } - - func test_equalsPredicate() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - { (context: ValidationContext) in - [ ValidationError(reason: context.subject, at: context.codingPath) ] - }, - when: \.subject == "hello" - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 3) - XCTAssertEqual(error?.values.map { $0.reason }.filter { $0 == "hello" }.count, 3) - XCTAssertEqual(error?.values.map { $0.reason }.filter { $0 == "world" }.count, 0) - } - } - - func test_equalsPredicateAndDescribeValidator() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - "there should be two servers", - check: { context in context.document.servers.count == 2 }, // just something false to check for every "hello" String value - when: \.subject == "hello" - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 3) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: there should be two servers") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["info", "title"]) - } - } - - func test_equalsPredicateAndNotEqualsValidator() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - "API version is not 2.0", - check: \OpenAPI.Document.Info.version != "2.0", - when: \.title == "hello" - ) - - try document.validate(using: validator) - } - - func test_andCheckSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - summary: "get the world", - get: .init( - tags: "World", - responses: [ - 200: .response( - description: "Get the world", - content: [ - .json: .init(schema: .string) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - try document.validate(using: Validator() - .validating( - "Operations must have at least one Response and they all must be status code 200", - check: \OpenAPI.Operation.responseOutcomes.count >= 1 - && { $0.subject.responseOutcomes.allSatisfy { $0.status == 200 } } - ) - ) - } - - func test_andCheckFails() throws { - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - summary: "get the world", - get: .init( - tags: "World", - responses: [ - 200: .response( - description: "Get the world", - content: [ - .json: .init(schema: .string) - ] - ), - 404: .response( - description: "Leave the world", - content: [ - .json: .init(schema: .string) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator() - .validating( - "Operations must have at least one Response and they all must be status code 200", - check: \OpenAPI.Operation.responseOutcomes.count >= 1 - && { $0.subject.responseOutcomes.allSatisfy { $0.status == 200 } } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Operations must have at least one Response and they all must be status code 200") - } - } - - func test_orCheckSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - summary: "get the world", - get: .init( - tags: "World", - responses: [ - 200: .response( - description: "Get the world", - content: [ - .json: .init(schema: .string) - ] - ), - 404: .response( - description: "Leave the world", - content: [ - .json: .init(schema: .string) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - try document.validate(using: Validator() - .validating( - "Operations must contain a status code 500 or there must be two possible response", - check: take(\OpenAPI.Response.Map.keys) { $0.contains(500) } - || \.count == 2 - - ) - ) - } - - func test_orCheckFails() throws { - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - summary: "get the world", - get: .init( - tags: "World", - responses: [ - 200: .response( - description: "Get the world", - content: [ - .json: .init(schema: .string) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - let validator = Validator() - .validating( - "Operations must contain a status code 500 or there must be two possible responses", - check: take(\OpenAPI.Response.Map.keys) { $0.contains(500) } - || \.count == 2 - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Operations must contain a status code 500 or there must be two possible responses") - } - } - - func test_conditionForExtensionWithPassingCheck() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents, - vendorExtensions: [ - "x-string": "hello" - ] - ) - - let validator = Validator() - .validating( - "x-string is 'hello'", - check: \.subject == "hello", - when: take(\.codingPath) { $0.last?.stringValue == "x-string" } // see alternative construction test cases for more concise version of this - ) - - try document.validate(using: validator) - } - - func test_conditionForExtensionWithFailingCheck() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents, - vendorExtensions: [ - "x-string": "world" - ] - ) - - let validator = Validator() - .validating( - "x-string is 'hello'", - check: \.subject == "hello", - when: take(\.codingPath) { $0.last?.stringValue == "x-string" } // see alternative construction test cases for more concise version of this - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 2) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: x-string is 'hello'") - } - } - - // MARK: - Misc Test Cases - func test_pathItemHasAtLeastOneOperationSucceeds() throws { - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - summary: "get the world", - get: .init( - tags: "World", - responses: [ - 200: .response( - description: "Get the world", - content: [ - .json: .init(schema: .string) - ] - ) - ] - ) - ) - ], - components: .noComponents - ) - - try document.validate(using: Validator() - .validating( - "Path Items must have at least one Operation", - check: \OpenAPI.PathItem.endpoints.count > 0 - ) - ) - } - - func test_pathItemHasAtLeastOneOperationFails() throws { - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [], - paths: [ - "/hello/world": .init( - summary: "get the world" - ) - ], - components: .noComponents - ) - - let validator = Validator() - .validating( - "Path Items must have at least one Operation", - check: \OpenAPI.PathItem.endpoints.count > 0 - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Path Items must have at least one Operation") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello/world"]) - } - } - - func test_ifTestServerThenTwoServers() throws { - let server = OpenAPI.Server( - url: URL(string: "https://test.server.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - "At least two servers are specified on root Document if one of them is the test server.", - check: \.document.servers.count >= 2, - when: \OpenAPI.Server.urlTemplate == URLTemplate(rawValue: "https://test.server.com")! - && \.codingPath.first?.stringValue == "servers" - ) - - XCTAssertThrowsError(try document.validate(using: validator)) - - let document2 = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server, server], - paths: [:], - components: .noComponents - ) - - let validator2 = Validator() - .validating( - "At least two servers are specified if one of them is the test server.", - check: \.document.servers.count >= 2, - when: \OpenAPI.Server.urlTemplate == URLTemplate(rawValue: "https://test.server.com")! - ) - - XCTAssertNoThrow(try document2.validate(using: validator2)) - } - - func test_ifTestServerThenTwoServersAlternative() throws { - // in this alternative, the condition is checked against - // the OpenAPI.Document.servers property instead of checking - // against OpenAPI.Server.url. - let server = OpenAPI.Server( - url: URL(string: "https://test.server.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hello", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents - ) - - let validator = Validator() - .validating( - "At least two servers are specified if one of them is the test server.", - check: \.document.servers.count >= 2, - when: take(\OpenAPI.Document.servers) { servers in - servers.map { $0.urlTemplate.absoluteString }.contains("https://test.server.com") - } - ) - - XCTAssertThrowsError(try document.validate(using: validator)) - - let document2 = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server, server], - paths: [:], - components: .noComponents - ) - - let validator2 = Validator() - .validating( - "At least two servers are specified if one of them is the test server.", - check: \.document.servers.count >= 2, - when: take(\OpenAPI.Document.servers) { servers in - servers.map { $0.urlTemplate.absoluteString }.contains("https://test.server.com") - } - ) - - XCTAssertNoThrow(try document2.validate(using: validator2)) - } - - func test_conditionForExtensionWithFailingCheckAlternativeWhereConstruction() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents, - vendorExtensions: [ - "x-string": "world" - ] - ) - - let validator = Validator() - .validating( - "x-string is 'hello'", - check: \.subject == "hello", - when: \.codingPath.last?.stringValue == "x-string" - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 2) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: x-string is 'hello'") - } - } - - func test_conditionForExtensionWithFailingCheckAlternativeConstruction() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server], - paths: [:], - components: .noComponents, - vendorExtensions: [ - "x-string": "world" - ] - ) - - let validation = Validation( - check: { context in - guard context.subject == "hello" else { - return [ ValidationError(reason: "x-string needs to be 'hello'", at: context.codingPath) ] - } - return [] - }, - when: { context in context.codingPath.last?.stringValue == "x-string" } - ) - - let validator = Validator() - .validating(validation) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 2) - XCTAssertEqual(error?.values.first?.reason, "x-string needs to be 'hello'") - } - } - - func test_unconditionalServerCountCheckFails() { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server, server], - paths: [ - "/hello/world": .init( - servers: [server, server], - get: .init(responses: [:], servers: [server]) - ) - ], - components: .noComponents, - vendorExtensions: [ - "x-string": "world" - ] - ) - - let validator = Validator.blank - .validating( - "All server arrays have more than 1 server", - check: \[OpenAPI.Server].count > 1 - ) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 1) - XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: All server arrays have more than 1 server") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello/world", "get", "servers"]) - } - } - - func test_conditionalServerCountCheckSucceeds() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-int": 2244, - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "hello", version: "1.0"), - servers: [server, server], - paths: [ - "/hello/world": .init( - servers: [server, server], - get: .init(responses: [:], servers: [server]) - ) - ], - components: .noComponents, - vendorExtensions: [ - "x-string": "world" - ] - ) - - let validator = Validator.blank - .validating( - "All server arrays not in operations have more than 1 server", - check: \[OpenAPI.Server].count > 1, - when: \.codingPath.count == 1 // server array is under root document (coding path count 1) - || take(\.codingPath) { codingPath in - // server array is not under an HTTP verb (i.e. in an operation) - guard codingPath.count > 1 else { return false } - - let secondToLastPathComponent = codingPath.suffix(2).first!.stringValue - let httpMethods = OpenAPI.BuiltinHttpMethod.allCases.map { $0.rawValue.lowercased() } - - return !httpMethods.contains(secondToLastPathComponent) - } - ) - - try document.validate(using: validator) - } - - func test_requestBodySchemaValidationFails() { - // should fail in three ways: - // 1. No `name` in request schema - // 2. No `name` in response schema - // 3. No `id` in response schema - - let createRequest = OpenAPI.Request( - content: [ - .json: .init( - schema: .object( - properties: [ - "classification": .string(allowedValues: "big", "small") - ] - ) - ) - ] - ) - - let successCreateResponse = OpenAPI.Response( - description: "Created Widget", - content: [ - .json: .init( - schema: .object( - properties: [ - "classification": .string(allowedValues: "big", "small") - ] - ) - ) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/widget/create": .init( - post: .init( - requestBody: createRequest, - responses: [ - 201: .response(successCreateResponse) - ] - ) - ) - ], - components: .noComponents - ) - - let resourceContainsName = Validation( - description: "All JSON resources must have a String name", - check: take(\.subject) { schema in - guard case let .object(_, context) = schema.value, - let nameProperty = context.properties["name"] else { - return false - } - return nameProperty.jsonTypeFormat?.jsonType == .string - } - ) - - let responseResourceContainsId = Validation( - description: "All JSON response resources must have an Id", - check: take(\.subject) { schema in - guard case let .object(_, context) = schema.value, - let idProperty = context.properties["id"] else { - return false - } - return idProperty.jsonTypeFormat?.jsonType == .integer - } - ) - - let requestBodyContainsName = Validation( - check: unwrap( - \.content[.json]?.schema?.schemaValue, - into: resourceContainsName - ), - - when: \OpenAPI.Request.content[.json]?.schema?.schemaValue != nil - ) - - let responseBodyContainsNameAndId = Validation( - check: unwrap( - \.content[.json]?.schema?.schemaValue, - into: resourceContainsName, responseResourceContainsId - ), - - when: \OpenAPI.Response.content[.json]?.schema?.schemaValue != nil - ) - - let successResponseBodyContainsNameAndId = Validation( - check: unwrap( - \OpenAPI.Response.Map[status: 201]?.responseValue, - into: responseBodyContainsNameAndId, - description: "201 status response value" - ) - ) - - let postRequestAndResponsesAreValid = Validation( - check: unwrap( - \OpenAPI.PathItem[.post]?.requestBody?.requestValue, - into: requestBodyContainsName - ) - && unwrap( - \OpenAPI.PathItem[.post]?.responses, - into: successResponseBodyContainsNameAndId - ), - - when: \OpenAPI.PathItem[.post] != nil - ) - - let validator = Validator() - .validating(postRequestAndResponsesAreValid) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let error = error as? ValidationErrorCollection - XCTAssertEqual(error?.values.count, 3) - XCTAssertEqual(error?.values.map { $0.reason }, [ - "Failed to satisfy: All JSON resources must have a String name", - "Failed to satisfy: All JSON resources must have a String name", - "Failed to satisfy: All JSON response resources must have an Id" - ]) - XCTAssertEqual( - error?.values.map { $0.codingPath.map { $0.stringValue } }, - [ - ["paths", "/widget/create"], // request name property - ["paths", "/widget/create"], // response name property - ["paths", "/widget/create"] // response id property - ] - ) - } - } - - func test_requestBodySchemaValidationSucceeds() throws { - let createRequest = OpenAPI.Request( - content: [ - .json: .init( - schema: .object( - properties: [ - "name": .string, - "classification": .string(allowedValues: "big", "small") - ] - ) - ) - ] - ) - - let successCreateResponse = OpenAPI.Response( - description: "Created Widget", - content: [ - .json: .init( - schema: .object( - properties: [ - "id": .integer, - "name": .string, - "classification": .string(allowedValues: "big", "small") - ] - ) - ) - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [], - paths: [ - "/widget/create": .init( - post: .init( - requestBody: createRequest, - responses: [ - 201: .response(successCreateResponse) - ] - ) - ) - ], - components: .noComponents - ) - - let resourceContainsName = Validation( - description: "All JSON resources must have a String name", - check: take(\.subject) { schema in - guard case let .object(_, context) = schema.value, - let nameProperty = context.properties["name"] else { - return false - } - return nameProperty.jsonTypeFormat?.jsonType == .string - } - ) - - let responseResourceContainsId = Validation( - description: "All JSON response resources must have an Id", - check: take(\.subject) { schema in - guard case let .object(_, context) = schema.value, - let idProperty = context.properties["id"] else { - return false - } - return idProperty.jsonTypeFormat?.jsonType == .integer - } - ) - - let requestBodyContainsName = Validation( - check: unwrap( - \.content[.json]?.schema?.schemaValue, - into: resourceContainsName - ), - - when: \OpenAPI.Request.content[.json]?.schema?.schemaValue != nil - ) - - let responseBodyContainsNameAndId = Validation( - check: unwrap( - \.content[.json]?.schema?.schemaValue, - into: resourceContainsName, responseResourceContainsId - ), - - when: \OpenAPI.Response.content[.json]?.schema?.schemaValue != nil - ) - - let successResponseBodyContainsNameAndId = Validation( - check: unwrap( - \OpenAPI.Response.Map[status: 201]?.responseValue, - into: responseBodyContainsNameAndId, - description: "201 status response value" - ) - ) - - let postRequestAndResponsesAreValid = Validation( - check: unwrap( - \OpenAPI.PathItem[.post]?.requestBody?.requestValue, - into: requestBodyContainsName - ) - && unwrap( - \OpenAPI.PathItem[.post]?.responses, - into: successResponseBodyContainsNameAndId - ), - - when: \OpenAPI.PathItem[.post] != nil - ) - - let validator = Validator() - .validating(postRequestAndResponsesAreValid) - - try document.validate(using: validator) - } - - // MARK: - Type validations - func test_typeValidationSucceeds() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]), - "x-float": AnyCodable(22.5 as Float), - "x-bool": true - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [server], - paths: [:], - components: .init( - schemas: [ - "double": .number(multipleOf: 10.5), - "int": .integer(multipleOf: 3), - "bool": .object(deprecated: true) - ] - ) - ) - - let validator = Validator() - .validating("string", check: \String.self == "hiya", when: \.codingPath.last?.stringValue == "x-string") - .validating("int", check: \Int.self == 3) - .validating("double", check: \Double.self == 10.5) - .validating("float", check: \Float.self == 22.5) - .validating("bool", check: \Bool.self == true) - - try document.validate(using: validator) - } - - func test_typeValidationFails() throws { - let server = OpenAPI.Server( - url: URL(string: "https://google.com")!, - description: "hello world", - variables: [:], - vendorExtensions: [ - "x-string": "hiya", - "x-double": 10.5, - "x-dict": .init([ "string": "world"]), - "x-array": AnyCodable(["hello", nil, "world"]), - "x-float": AnyCodable(22.5 as Float), - "x-bool": true - ] - ) - - let document = OpenAPI.Document( - info: .init(title: "test", version: "1.0"), - servers: [server], - paths: [:], - components: .init( - schemas: [ - "double": .number(multipleOf: 10.5), - "int": .integer(multipleOf: 3), - "bool": .object(deprecated: true) - ] - ) - ) - - let validator = Validator() - .validating("string", check: \String.self == "hello there", when: \.codingPath.last?.stringValue == "x-string") - .validating("int", check: \Int.self == 1) - .validating("double", check: \Double.self == 2.2) - .validating("float", check: \Float.self == 10.5) - .validating("bool", check: \Bool.self == false) - - XCTAssertThrowsError(try document.validate(using: validator)) { error in - let errors = error as? ValidationErrorCollection - XCTAssertEqual(errors?.values.count, 7) - } - } - - // MARK: - Warnings - func test_collectsContentTypeWarningNotStrict() throws { - let docData = """ - { - "info": {"title": "test", "version": "1.0"}, - "openapi": "3.0.1", - "paths": { - "test": { - "get": { - "responses": { - "200": { - "description": "test", - "content": { - "gzip": {} - } - } - } - } - } - } - } - """.data(using: .utf8)! - - let doc = try orderUnstableDecode(OpenAPI.Document.self, from: docData) - - XCTAssertEqual( - doc.paths["test"]?.pathItemValue?.get?.responses[200]?.responseValue?.content.keys.first?.warnings.first?.localizedDescription, - "\'gzip\' could not be parsed as a Content Type. Content Types should have the format \'/\'" - ) - - let warnings = try doc.validate(strict: false) - - XCTAssertEqual(warnings.count, 1) - XCTAssertEqual( - warnings.first?.localizedDescription, - "Problem encountered when parsing ``: \'gzip\' could not be parsed as a Content Type. Content Types should have the format \'/\'." - ) - XCTAssertEqual(warnings.first?.codingPathString, ".paths[\'/test\'].get.responses.200.content") - XCTAssertNotNil(warnings.first?.underlyingError) - XCTAssertNotNil(warnings.first?.errorCategory) - XCTAssertEqual(warnings.first?.subjectName, "") - XCTAssertEqual(warnings.first?.contextString, "") - - XCTAssertEqual(warnings.first?.localizedDescription, warnings.first?.description) - } - - func test_collectsContentTypeWarningStrict() throws { - let docData = """ - { - "info": {"title": "test", "version": "1.0"}, - "openapi": "3.0.0", - "paths": { - "test": { - "get": { - "responses": { - "200": { - "description": "test", - "content": { - "gzip": {} - } - } - } - } - } - } - } - """.data(using: .utf8)! - - let doc = try orderUnstableDecode(OpenAPI.Document.self, from: docData) - - XCTAssertEqual( - doc.paths["test"]?.pathItemValue?.get?.responses[200]?.responseValue?.content.keys.first?.warnings.first?.localizedDescription, - "\'gzip\' could not be parsed as a Content Type. Content Types should have the format \'/\'" - ) - - XCTAssertThrowsError(try doc.validate(strict: true)) { error in - let errors = error as? ValidationErrorCollection - XCTAssertEqual(errors?.values.count, 1) - XCTAssertEqual( - errors?.localizedDescription, - "Problem encountered when parsing ``: \'gzip\' could not be parsed as a Content Type. Content Types should have the format \'/\'. at path: .paths[\'/test\'].get.responses.200.content" - ) - XCTAssertEqual(errors?.values.first?.codingPathString, ".paths[\'/test\'].get.responses.200.content") - } - } -}