From b24b065a2fe13f5ce6096022723a2089ab440e6c Mon Sep 17 00:00:00 2001
From: ayushshrivastv <ayush.srivastav@icloud.com>
Date: Fri, 4 Apr 2025 23:55:30 +0530
Subject: [PATCH 1/2] Add validation for link operationId existence

---
 .../Validator/Validation+Builtins.swift       | 24 ++++++
 .../Validator/Validation+Builtins.swift       | 27 ++++++
 .../Validator/BuiltinValidationTests.swift    | 82 +++++++++++++++++++
 .../Validator/BuiltinValidationTests.swift    | 80 ++++++++++++++++++
 4 files changed, 213 insertions(+)

diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift
index 55af772fb..8e22affe5 100644
--- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift
+++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift
@@ -479,6 +479,30 @@ extension Validation {
     public static var serverVarialbeDefaultExistsInEnum : Validation<OpenAPI.Server.Variable> {
         return serverVariableDefaultExistsInEnum
     }
+
+    /// 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<OpenAPI.Link> {
+        .init(
+            description: "Links with operationIds have corresponding Operations",
+            check: { context in
+                guard case let .b(operationId) = context.subject.operation else {
+                    // don't make assertions about Links that don't have operationIds
+                    return true
+                }
+                
+                // Collect all operation IDs from the document
+                let operationIds = context.document.allOperationIds
+                
+                return operationIds.contains(operationId)
+            }
+        )
+    }
 }
 
 /// Used by both the Path Item parameter check and the
diff --git a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift
index 414d26d24..e46e688ff 100644
--- a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift
+++ b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift
@@ -396,6 +396,33 @@ extension Validation {
             }
         )
     }
+
+    /// Validate the OpenAPI Document's `Links` with operationIds refer to
+    /// Operations that exist in the document.
+    ///
+    /// This validation ensures that Link Objects using operationIds have corresponding
+    /// Operations in the document that have those IDs.
+    ///
+    /// - Important: This is not an included validation by default.
+    public static var linkOperationsExist: Validation<OpenAPI.Link> {
+        .init(
+            description: "Links with operationIds have corresponding Operations",
+            check: { context in
+                guard case let .b(operationId) = context.subject.operation else {
+                    // don't make assertions about Links that don't have operationIds
+                    return true
+                }
+                
+                // Collect all operation IDs from the document
+                let operationIds = context.document.paths.values
+                    .compactMap { context.document.components[$0] }
+                    .flatMap { $0.endpoints }
+                    .compactMap { $0.operation.operationId }
+                
+                return operationIds.contains(operationId)
+            }
+        )
+    }
 }
 
 /// Used by both the Path Item parameter check and the
diff --git a/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift
index 2e1eba612..38d423a87 100644
--- a/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift
+++ b/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift
@@ -749,4 +749,86 @@ final class BuiltinValidationTests: XCTestCase {
         // NOTE this is part of default validation
         try document.validate()
     }
+
+    func test_securityRequirementComponentsExist_fails() throws {
+        let document = OpenAPI.Document(
+            info: .init(title: "test", version: "1.0"),
+            servers: [],
+            paths: [:],
+            components: .noComponents,
+            security: [
+                [.component(named: "oauth"): ["scope2"]]
+            ]
+        )
+
+        XCTAssertThrowsError(try document.validate()) { error in
+            let errorCollection = error as? ValidationErrorCollection
+            XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: Security Requirement security scheme can be found in components/securitySchemes")
+            XCTAssertEqual(errorCollection?.values.first?.codingPath.map { $0.stringValue }, ["security"])
+            XCTAssertEqual(errorCollection?.values.count, 1)
+
+            let openAPIError = OpenAPI.Error(from: error)
+            XCTAssertEqual(openAPIError.localizedDescription, "Failed to satisfy: Security Requirement security scheme can be found in components/securitySchemes at path: .security")
+            XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, ["security"])
+        }
+    }
+    
+    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/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift
index eb683ccee..f74c77a34 100644
--- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift
+++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift
@@ -860,4 +860,84 @@ final class BuiltinValidationTests: XCTestCase {
         // NOTE this is part of default validation
         try document.validate()
     }
+
+    func test_pathItemsTopLevelReferencesReferencingPathItemComponentsSuccess() throws {
+        let document = OpenAPI.Document(
+            info: .init(title: "test", version: "1.0"),
+            servers: [],
+            paths: [
+                "/hello": .reference(.component(named: "hello")),
+                "/world": .reference(.component(named: "world"))
+            ],
+            components: .init(
+                pathItems: [
+                    "hello": .init(),
+                    "world": .init()
+                ]
+            )
+        )
+
+        let validator = Validator.blank.validating(.pathItemReferencesAreValid)
+
+        try document.validate(using: validator)
+    }
+    
+    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"))
+        }
+    }
 }

From 021f45119b37faeab812afe73397903bbdc26d0f Mon Sep 17 00:00:00 2001
From: ayushshrivastv <ayush.srivastav@icloud.com>
Date: Thu, 10 Apr 2025 02:24:37 +0530
Subject: [PATCH 2/2] Use allOperationIds helper in OpenAPIKit30 module and
 remove security requirements validation test

---
 .../Validator/Validation+Builtins.swift       |  7 ++----
 .../Validator/BuiltinValidationTests.swift    | 23 -------------------
 2 files changed, 2 insertions(+), 28 deletions(-)

diff --git a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift
index e46e688ff..d880181df 100644
--- a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift
+++ b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift
@@ -413,11 +413,8 @@ extension Validation {
                     return true
                 }
                 
-                // Collect all operation IDs from the document
-                let operationIds = context.document.paths.values
-                    .compactMap { context.document.components[$0] }
-                    .flatMap { $0.endpoints }
-                    .compactMap { $0.operation.operationId }
+                // Use the allOperationIds helper to get all operation IDs from the document
+                let operationIds = context.document.allOperationIds
                 
                 return operationIds.contains(operationId)
             }
diff --git a/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift
index 38d423a87..4a950c25e 100644
--- a/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift
+++ b/Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift
@@ -750,29 +750,6 @@ final class BuiltinValidationTests: XCTestCase {
         try document.validate()
     }
 
-    func test_securityRequirementComponentsExist_fails() throws {
-        let document = OpenAPI.Document(
-            info: .init(title: "test", version: "1.0"),
-            servers: [],
-            paths: [:],
-            components: .noComponents,
-            security: [
-                [.component(named: "oauth"): ["scope2"]]
-            ]
-        )
-
-        XCTAssertThrowsError(try document.validate()) { error in
-            let errorCollection = error as? ValidationErrorCollection
-            XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: Security Requirement security scheme can be found in components/securitySchemes")
-            XCTAssertEqual(errorCollection?.values.first?.codingPath.map { $0.stringValue }, ["security"])
-            XCTAssertEqual(errorCollection?.values.count, 1)
-
-            let openAPIError = OpenAPI.Error(from: error)
-            XCTAssertEqual(openAPIError.localizedDescription, "Failed to satisfy: Security Requirement security scheme can be found in components/securitySchemes at path: .security")
-            XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, ["security"])
-        }
-    }
-    
     func test_linkOperationsExist_validates() throws {
         // Create a link with an operationId that exists in the document
         let link = OpenAPI.Link(operationId: "testOperation")