Skip to content

Commit da7d3a5

Browse files
authored
Merge pull request #404 from ayushshrivastv/feature/link-operationid-validation
Added a Validation that checks all operationIds in Link objects are valid
2 parents 7706507 + 021f451 commit da7d3a5

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed

Sources/OpenAPIKit/Validator/Validation+Builtins.swift

+24
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,30 @@ extension Validation {
479479
public static var serverVarialbeDefaultExistsInEnum : Validation<OpenAPI.Server.Variable> {
480480
return serverVariableDefaultExistsInEnum
481481
}
482+
483+
/// Validate the OpenAPI Document's `Links` with operationIds refer to
484+
/// Operations that exist in the document.
485+
///
486+
/// This validation ensures that Link Objects using operationIds have corresponding
487+
/// Operations in the document that have those IDs.
488+
///
489+
/// - Important: This is not an included validation by default.
490+
public static var linkOperationsExist: Validation<OpenAPI.Link> {
491+
.init(
492+
description: "Links with operationIds have corresponding Operations",
493+
check: { context in
494+
guard case let .b(operationId) = context.subject.operation else {
495+
// don't make assertions about Links that don't have operationIds
496+
return true
497+
}
498+
499+
// Collect all operation IDs from the document
500+
let operationIds = context.document.allOperationIds
501+
502+
return operationIds.contains(operationId)
503+
}
504+
)
505+
}
482506
}
483507

484508
/// Used by both the Path Item parameter check and the

Sources/OpenAPIKit30/Validator/Validation+Builtins.swift

+24
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,30 @@ extension Validation {
396396
}
397397
)
398398
}
399+
400+
/// Validate the OpenAPI Document's `Links` with operationIds refer to
401+
/// Operations that exist in the document.
402+
///
403+
/// This validation ensures that Link Objects using operationIds have corresponding
404+
/// Operations in the document that have those IDs.
405+
///
406+
/// - Important: This is not an included validation by default.
407+
public static var linkOperationsExist: Validation<OpenAPI.Link> {
408+
.init(
409+
description: "Links with operationIds have corresponding Operations",
410+
check: { context in
411+
guard case let .b(operationId) = context.subject.operation else {
412+
// don't make assertions about Links that don't have operationIds
413+
return true
414+
}
415+
416+
// Use the allOperationIds helper to get all operation IDs from the document
417+
let operationIds = context.document.allOperationIds
418+
419+
return operationIds.contains(operationId)
420+
}
421+
)
422+
}
399423
}
400424

401425
/// Used by both the Path Item parameter check and the

Tests/OpenAPIKit30Tests/Validator/BuiltinValidationTests.swift

+59
Original file line numberDiff line numberDiff line change
@@ -749,4 +749,63 @@ final class BuiltinValidationTests: XCTestCase {
749749
// NOTE this is part of default validation
750750
try document.validate()
751751
}
752+
753+
func test_linkOperationsExist_validates() throws {
754+
// Create a link with an operationId that exists in the document
755+
let link = OpenAPI.Link(operationId: "testOperation")
756+
757+
// Create a document with an operation using that ID
758+
let document = OpenAPI.Document(
759+
info: .init(title: "test", version: "1.0"),
760+
servers: [],
761+
paths: [
762+
"/hello": .init(
763+
get: .init(
764+
operationId: "testOperation",
765+
responses: [:]
766+
)
767+
)
768+
],
769+
components: .init(
770+
links: [
771+
"testLink": link
772+
]
773+
)
774+
)
775+
776+
let validator = Validator.blank.validating(.linkOperationsExist)
777+
try document.validate(using: validator)
778+
}
779+
780+
func test_linkOperationsExist_fails() throws {
781+
// Create a link with an operationId that doesn't exist in the document
782+
let link = OpenAPI.Link(operationId: "nonExistentOperation")
783+
784+
// Create a document with an operation using a different ID
785+
let document = OpenAPI.Document(
786+
info: .init(title: "test", version: "1.0"),
787+
servers: [],
788+
paths: [
789+
"/hello": .init(
790+
get: .init(
791+
operationId: "testOperation",
792+
responses: [:]
793+
)
794+
)
795+
],
796+
components: .init(
797+
links: [
798+
"testLink": link
799+
]
800+
)
801+
)
802+
803+
let validator = Validator.blank.validating(.linkOperationsExist)
804+
805+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
806+
let errorCollection = error as? ValidationErrorCollection
807+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: Links with operationIds have corresponding Operations")
808+
XCTAssertTrue((errorCollection?.values.first?.codingPath.map { $0.stringValue }.joined(separator: ".") ?? "").contains("testLink"))
809+
}
810+
}
752811
}

Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift

+80
Original file line numberDiff line numberDiff line change
@@ -860,4 +860,84 @@ final class BuiltinValidationTests: XCTestCase {
860860
// NOTE this is part of default validation
861861
try document.validate()
862862
}
863+
864+
func test_pathItemsTopLevelReferencesReferencingPathItemComponentsSuccess() throws {
865+
let document = OpenAPI.Document(
866+
info: .init(title: "test", version: "1.0"),
867+
servers: [],
868+
paths: [
869+
"/hello": .reference(.component(named: "hello")),
870+
"/world": .reference(.component(named: "world"))
871+
],
872+
components: .init(
873+
pathItems: [
874+
"hello": .init(),
875+
"world": .init()
876+
]
877+
)
878+
)
879+
880+
let validator = Validator.blank.validating(.pathItemReferencesAreValid)
881+
882+
try document.validate(using: validator)
883+
}
884+
885+
func test_linkOperationsExist_validates() throws {
886+
// Create a link with an operationId that exists in the document
887+
let link = OpenAPI.Link(operationId: "testOperation")
888+
889+
// Create a document with an operation using that ID
890+
let document = OpenAPI.Document(
891+
info: .init(title: "test", version: "1.0"),
892+
servers: [],
893+
paths: [
894+
"/hello": .init(
895+
get: .init(
896+
operationId: "testOperation",
897+
responses: [:]
898+
)
899+
)
900+
],
901+
components: .init(
902+
links: [
903+
"testLink": link
904+
]
905+
)
906+
)
907+
908+
let validator = Validator.blank.validating(.linkOperationsExist)
909+
try document.validate(using: validator)
910+
}
911+
912+
func test_linkOperationsExist_fails() throws {
913+
// Create a link with an operationId that doesn't exist in the document
914+
let link = OpenAPI.Link(operationId: "nonExistentOperation")
915+
916+
// Create a document with an operation using a different ID
917+
let document = OpenAPI.Document(
918+
info: .init(title: "test", version: "1.0"),
919+
servers: [],
920+
paths: [
921+
"/hello": .init(
922+
get: .init(
923+
operationId: "testOperation",
924+
responses: [:]
925+
)
926+
)
927+
],
928+
components: .init(
929+
links: [
930+
"testLink": link
931+
]
932+
)
933+
)
934+
935+
let validator = Validator.blank.validating(.linkOperationsExist)
936+
937+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
938+
let errorCollection = error as? ValidationErrorCollection
939+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: Links with operationIds have corresponding Operations")
940+
XCTAssertTrue((errorCollection?.values.first?.codingPath.map { $0.stringValue }.joined(separator: ".") ?? "").contains("testLink"))
941+
}
942+
}
863943
}

0 commit comments

Comments
 (0)