diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index f34729339..763757af7 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -400,7 +400,9 @@ extension OpenAPI.Document: Encodable { try encodeSecurity(requirements: security, to: &container, forKey: .security) } - try container.encode(paths, forKey: .paths) + if !paths.isEmpty { + try container.encode(paths, forKey: .paths) + } try encodeExtensions(to: &container) @@ -429,7 +431,7 @@ extension OpenAPI.Document: Decodable { let webhooks = try container.decodeIfPresent(OrderedDictionary, OpenAPI.PathItem>>.self, forKey: .webhooks) ?? [:] self.webhooks = webhooks - let paths = try container.decode(OpenAPI.PathItem.Map.self, forKey: .paths) + let paths = try container.decodeIfPresent(OpenAPI.PathItem.Map.self, forKey: .paths) ?? [:] self.paths = paths try validateSecurityRequirements(in: paths, against: components) diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index cdc36cb09..2316b3890 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -15,14 +15,14 @@ extension Validation { /// /// The OpenAPI Specifcation does not require that the document /// contain any paths for [security reasons](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-filtering) - /// but documentation that is public in nature might only ever have - /// an empty `PathItem.Map` in error. + /// or even because it only contains webhooks, but authors may still + /// want to protect against an empty `PathItem.Map` in some cases. /// /// - Important: This is not an included validation by default. - public static var documentContainsPaths: Validation { + public static var documentContainsPaths: Validation { .init( description: "Document contains at least one path", - check: \.count > 0 + check: \.paths.count > 0 ) } diff --git a/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift index fcc1cab7d..69ccb32da 100644 --- a/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift @@ -11,24 +11,6 @@ import OpenAPIKit import Yams final class PathsErrorTests: XCTestCase { - func test_missingPaths() { - let documentYML = - """ - openapi: "3.1.0" - info: - title: test - version: 1.0 - """ - - XCTAssertThrowsError(try testDecoder.decode(OpenAPI.Document.self, from: documentYML)) { error in - - let openAPIError = OpenAPI.Error(from: error) - - XCTAssertEqual(openAPIError.localizedDescription, "Expected to find `paths` key in the root Document object but it is missing.") - XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, []) - } - } - func test_badPathReference() { let documentYML = """ diff --git a/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift b/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift index 40d3b2fce..298e7ee58 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift @@ -482,7 +482,7 @@ extension DocumentInfoTests { "version" : "1.0" } """.data(using: .utf8)! - XCTAssertThrowsError( try orderUnstableDecode(OpenAPI.Document.Info.self, from: infoData)) { error in + XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.Info.self, from: infoData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `termsOfService`: If specified, must be a valid URL.") } } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 1d3db4546..013ce03dc 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -401,10 +401,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", - "paths" : { - - } + "openapi" : "3.1.0" } """ ) @@ -455,10 +452,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", - "paths" : { - - } + "openapi" : "3.1.0" } """ ) @@ -510,9 +504,6 @@ extension DocumentTests { "version" : "1.0" }, "openapi" : "3.1.0", - "paths" : { - - }, "servers" : [ { "url" : "http:\\/\\/google.com" @@ -642,9 +633,6 @@ extension DocumentTests { "version" : "1.0" }, "openapi" : "3.1.0", - "paths" : { - - }, "security" : [ { "security" : [ @@ -722,9 +710,6 @@ extension DocumentTests { "version" : "1.0" }, "openapi" : "3.1.0", - "paths" : { - - }, "tags" : [ { "name" : "hi" @@ -789,10 +774,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", - "paths" : { - - } + "openapi" : "3.1.0" } """ ) @@ -852,9 +834,6 @@ extension DocumentTests { "version" : "1.0" }, "openapi" : "3.1.0", - "paths" : { - - }, "x-specialFeature" : [ "hello", "world" @@ -932,9 +911,6 @@ extension DocumentTests { "version" : "1.0" }, "openapi" : "3.1.0", - "paths" : { - - }, "webhooks" : { "webhook-test" : { "delete" : { @@ -1044,4 +1020,118 @@ extension DocumentTests { ) ) } + + func test_webhooks_noPaths_encode() throws { + let op = OpenAPI.Operation(responses: [:]) + let pathItem: OpenAPI.PathItem = .init(get: op, put: op, post: op, delete: op, options: op, head: op, patch: op, trace: op) + let pathItemTest: Either, OpenAPI.PathItem> = .pathItem(pathItem) + + let document = OpenAPI.Document( + info: .init(title: "API", version: "1.0"), + servers: [], + paths: [:], + webhooks: [ + "webhook-test": pathItemTest + ], + components: .noComponents, + externalDocs: .init(url: URL(string: "http://google.com")!) + ) + let encodedDocument = try orderUnstableTestStringFromEncoding(of: document) + + let documentJSON: String? = + """ + { + "externalDocs" : { + "url" : "http:\\/\\/google.com" + }, + "info" : { + "title" : "API", + "version" : "1.0" + }, + "openapi" : "3.1.0", + "webhooks" : { + "webhook-test" : { + "delete" : { + + }, + "get" : { + + }, + "head" : { + + }, + "options" : { + + }, + "patch" : { + + }, + "post" : { + + }, + "put" : { + + }, + "trace" : { + + } + } + } + } + """ + + assertJSONEquivalent(encodedDocument, documentJSON) + } + + func test_webhooks_noPaths_decode() throws { + let documentData = + """ + { + "externalDocs": { + "url": "http:\\/\\/google.com" + }, + "info": { + "title": "API", + "version": "1.0" + }, + "openapi": "3.1.0", + "webhooks": { + "webhook-test": { + "delete": { + }, + "get": { + }, + "head": { + }, + "options": { + }, + "patch": { + }, + "post": { + }, + "put": { + }, + "trace": { + } + } + } + } + """.data(using: .utf8)! + let document = try orderUnstableDecode(OpenAPI.Document.self, from: documentData) + + let op = OpenAPI.Operation(responses: [:]) + XCTAssertEqual( + document, + OpenAPI.Document( + info: .init(title: "API", version: "1.0"), + servers: [], + paths: [:], + webhooks: [ + "webhook-test": .pathItem(.init(get: op, put: op, post: op, delete: op, options: op, head: op, patch: op, trace: op)) + ], + components: .noComponents, + externalDocs: .init(url: URL(string: "http://google.com")!) + ) + ) + } } diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index 1a0c2aa0b..6364a7460 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -24,7 +24,7 @@ final class BuiltinValidationTests: XCTestCase { XCTAssertThrowsError(try document.validate(using: validator)) { error in let error = error as? ValidationErrorCollection XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Document contains at least one path") - XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths"]) + XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, []) } } diff --git a/documentation/validation.md b/documentation/validation.md index d6a2dd0da..150262e32 100644 --- a/documentation/validation.md +++ b/documentation/validation.md @@ -579,6 +579,7 @@ Here's a table of Array/Map types for which this quirk is relevant and which mod | `OpenAPI.Components.securitySchemes` | `ComponentDictionary` | x | x | | `OpenAPI.Document.components` | `Components` | x | x | | `OpenAPI.Document.security` | `[SecurityRequirement]` | x | x | + | `OpenAPI.Document.paths` | `PathItem.Map` | | x | | `OpenAPI.Document.servers` | `[Server]` | x | x | | `OpenAPI.Document.webhooks` | `OrderedDictionary` | | x | | `OpenAPI.Link.parameters` | `OrderedDictionary` | x | x |