diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index 327f8991a..1d8638ec8 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -341,6 +341,23 @@ extension Validation { ) } + /// Validate that all named OpenAPI `Server`s have unique names across the + /// whole Document. + /// + /// The OpenAPI Specification requires server names + /// [are unique](https://spec.openapis.org/oas/v3.2.0.html#server-object). + /// + /// - Important: This is included in validation by default. + public static var documentServerNamesAreUnique: Validation { + .init( + description: "The names of Servers in the Document are unique", + check: { context in + let serverNames = context.subject.allServers.compactMap(\.name) + return Set(serverNames).count == serverNames.count + } + ) + } + /// Validate that all OpenAPI Path Items have no duplicate parameters defined /// within them. /// diff --git a/Sources/OpenAPIKit/Validator/Validator.swift b/Sources/OpenAPIKit/Validator/Validator.swift index 4b8c5fade..dcb5066ef 100644 --- a/Sources/OpenAPIKit/Validator/Validator.swift +++ b/Sources/OpenAPIKit/Validator/Validator.swift @@ -75,6 +75,7 @@ extension OpenAPI.Document { /// The default validations are /// - Operations must contain at least one response. /// - Document-level tag names are unique. +/// - Server names are unique across the whole Document. /// - Parameters are unique within each Path Item. /// - Parameters are unique within each Operation. /// - Operation Ids are unique across the whole Document. @@ -156,6 +157,7 @@ public final class Validator { internal var nonReferenceDefaultValidations: [AnyValidation] = [ .init(.documentTagNamesAreUnique), + .init(.documentServerNamesAreUnique), .init(.pathItemParametersAreUnique), .init(.operationParametersAreUnique), .init(.operationIdsAreUnique), @@ -200,6 +202,7 @@ public final class Validator { /// /// The default validations are /// - Document-level tag names are unique. + /// - Server names are unique across the whole Document. /// - Parameters are unique within each Path Item. /// - Parameters are unique within each Operation. /// - Operation Ids are unique across the whole Document. diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index f8ffa15b1..38b95a5f6 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -24,9 +24,10 @@ final class BuiltinValidationTests: XCTestCase { ]) let withoutReferenceValidations = Validator().skippingReferenceValidations() - XCTAssertEqual(withoutReferenceValidations.validationDescriptions.count, 7) + XCTAssertEqual(withoutReferenceValidations.validationDescriptions.count, 8) XCTAssertEqual(withoutReferenceValidations.validationDescriptions, [ "The names of Tags in the Document are unique", + "The names of Servers in the Document are unique", "Path Item parameters are unique (identity is defined by the \'name\' and \'location\')", "Operation parameters are unique (identity is defined by the \'name\' and \'location\')", "All Operation Ids in Document are unique", @@ -36,9 +37,10 @@ final class BuiltinValidationTests: XCTestCase { ]) let defaultValidations = Validator() - XCTAssertEqual(defaultValidations.validationDescriptions.count, 17) + XCTAssertEqual(defaultValidations.validationDescriptions.count, 18) XCTAssertEqual(defaultValidations.validationDescriptions, [ "The names of Tags in the Document are unique", + "The names of Servers in the Document are unique", "Path Item parameters are unique (identity is defined by the \'name\' and \'location\')", "Operation parameters are unique (identity is defined by the \'name\' and \'location\')", "All Operation Ids in Document are unique", @@ -58,9 +60,10 @@ final class BuiltinValidationTests: XCTestCase { ]) let stricterReferenceValidations = Validator().validatingAllReferencesFoundInComponents() - XCTAssertEqual(stricterReferenceValidations.validationDescriptions.count, 17) + XCTAssertEqual(stricterReferenceValidations.validationDescriptions.count, 18) XCTAssertEqual(stricterReferenceValidations.validationDescriptions, [ "The names of Tags in the Document are unique", + "The names of Servers in the Document are unique", "Path Item parameters are unique (identity is defined by the \'name\' and \'location\')", "Operation parameters are unique (identity is defined by the \'name\' and \'location\')", "All Operation Ids in Document are unique", @@ -561,6 +564,75 @@ final class BuiltinValidationTests: XCTestCase { try document.validate() } + func test_duplicateServerNamesOnDocumentFails() { + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [ + .init(url: URL(string: "https://root.example.com")!, name: "shared") + ], + paths: [ + "/hello": .init( + get: .init( + responses: [ + 200: .response(description: "hi") + ], + servers: [ + .init(url: URL(string: "https://operation.example.com")!, name: "shared") + ] + ) + ) + ], + components: .noComponents + ) + + XCTAssertThrowsError(try document.validate()) { error in + let error = error as? ValidationErrorCollection + XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: The names of Servers in the Document are unique") + XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, []) + } + } + + func test_uniqueServerNamesOnDocumentSucceeds() throws { + let document = OpenAPI.Document( + info: .init(title: "test", version: "1.0"), + servers: [ + .init(url: URL(string: "https://root.example.com")!, name: "root") + ], + paths: [ + "/hello": .init( + servers: [ + .init(url: URL(string: "https://path.example.com")!, name: "path"), + .init(url: URL(string: "https://unnamed-path.example.com")!) + ], + get: .init( + responses: [ + 200: .response(description: "hi") + ], + servers: [ + .init(url: URL(string: "https://operation.example.com")!, name: "operation"), + .init(url: URL(string: "https://unnamed-operation.example.com")!) + ] + ) + ) + ], + webhooks: [ + "/event": .init( + post: .init( + responses: [ + 200: .response(description: "ok") + ], + servers: [ + .init(url: URL(string: "https://webhook.example.com")!, name: "webhook") + ] + ) + ) + ], + components: .noComponents + ) + + try document.validate() + } + func test_duplicateOperationParameterFails() { let document = OpenAPI.Document( info: .init(title: "test", version: "1.0"),