Skip to content

Commit ea62ddb

Browse files
committed
add new builtin validation for parameter schema styles
1 parent ebf4d71 commit ea62ddb

File tree

4 files changed

+324
-7
lines changed

4 files changed

+324
-7
lines changed

Sources/OpenAPIKit/Parameter/Parameter.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ extension OpenAPI {
7373
}
7474
}
7575

76+
/// The parameter's schema `style`, if defined. Note that this is
77+
/// guaranteed to be nil if the parameter has `content` defined. Use
78+
/// the `schemaOrContent` property if you want to switch over the two
79+
/// possibilities.
80+
public var schemaStyle : SchemaContext.Style? {
81+
schemaOrContent.schemaContextValue?.style
82+
}
83+
7684
/// Create a parameter.
7785
public init(
7886
name: String,

Sources/OpenAPIKit/Validator/Validation+Builtins.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,58 @@ extension Validation {
505505
}
506506
)
507507
}
508+
509+
/// Validate the OpenAPI Document's `Parameter`s all have styles that are
510+
/// compatible with their locations per the table found at
511+
/// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.2.0.md#style-values
512+
///
513+
/// - Important: This is included in validation by default.
514+
public static var parameterStyleAndLocationAreCompatible: Validation<OpenAPI.Parameter> {
515+
.init(
516+
check: all(
517+
Validation<OpenAPI.Parameter>(
518+
description: "the matrix style can only be used for the path location",
519+
check: \.context.location == .path,
520+
when: \.schemaStyle == .matrix
521+
),
522+
Validation<OpenAPI.Parameter>(
523+
description: "the label style can only be used for the path location",
524+
check: \.context.location == .path,
525+
when: \.schemaStyle == .label
526+
),
527+
Validation<OpenAPI.Parameter>(
528+
description: "the simple style can only be used for the path and header locations",
529+
check: \.context.location == .path || \.context.location == .header,
530+
when: \.schemaStyle == .simple
531+
),
532+
Validation<OpenAPI.Parameter>(
533+
description: "the form style can only be used for the query and cookie locations",
534+
check: \.context.location == .query || \.context.location == .cookie,
535+
when: \.schemaStyle == .form
536+
),
537+
Validation<OpenAPI.Parameter>(
538+
description: "the spaceDelimited style can only be used for the query location",
539+
check: \.context.location == .query,
540+
when: \.schemaStyle == .spaceDelimited
541+
),
542+
Validation<OpenAPI.Parameter>(
543+
description: "the pipeDelimited style can only be used for the query location",
544+
check: \.context.location == .query,
545+
when: \.schemaStyle == .pipeDelimited
546+
),
547+
Validation<OpenAPI.Parameter>(
548+
description: "the deepObject style can only be used for the query location",
549+
check: \.context.location == .query,
550+
when: \.schemaStyle == .deepObject
551+
),
552+
Validation<OpenAPI.Parameter>(
553+
description: "the cookie style can only be used for the cookie location",
554+
check: \.context.location == .cookie,
555+
when: \.schemaStyle == .cookie
556+
)
557+
)
558+
)
559+
}
508560
}
509561

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

Sources/OpenAPIKit/Validator/Validator.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ public final class Validator {
170170
/// - Parameters are unique within each Path Item.
171171
/// - Parameters are unique within each Operation.
172172
/// - Operation Ids are unique across the whole Document.
173-
/// - All OpenAPI.References that refer to components in this
174-
/// document can be found in the components dictionary.
175-
/// - `Enum` must not be empty in the document's
176-
/// Server Variable.
177-
/// - `Default` must exist in the enum values in the document's
178-
/// Server Variable.
173+
/// - All OpenAPI.References that refer to components in this document can
174+
/// be found in the components dictionary.
175+
/// - `Enum` must not be empty in the document's Server Variable.
176+
/// - `Default` must exist in the enum values in the document's Server
177+
/// Variable.
178+
/// - `Parameter` styles and locations are compatible with each other.
179179
///
180180
public convenience init() {
181181
self.init(validations: [
@@ -193,7 +193,8 @@ public final class Validator {
193193
.init(.callbacksReferencesAreValid),
194194
.init(.pathItemReferencesAreValid),
195195
.init(.serverVariableEnumIsValid),
196-
.init(.serverVariableDefaultExistsInEnum)
196+
.init(.serverVariableDefaultExistsInEnum),
197+
.init(.parameterStyleAndLocationAreCompatible)
197198
])
198199
}
199200

Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,4 +951,260 @@ final class BuiltinValidationTests: XCTestCase {
951951
XCTAssertTrue((errorCollection?.values.first?.codingPath.map { $0.stringValue }.joined(separator: ".") ?? "").contains("testLink"))
952952
}
953953
}
954+
955+
func test_badMatrixStyleLocation_fails() throws {
956+
let parameter = OpenAPI.Parameter.query(
957+
name: "test",
958+
schemaOrContent: .schema(.init(.string, style: .matrix))
959+
)
960+
961+
let document = OpenAPI.Document(
962+
info: .init(title: "test", version: "1.0"),
963+
servers: [],
964+
paths: [
965+
"/hello": .init(
966+
get: .init(
967+
operationId: "testOperation",
968+
parameters: [
969+
.parameter(parameter)
970+
],
971+
responses: [:]
972+
)
973+
)
974+
],
975+
components: .noComponents
976+
)
977+
978+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
979+
980+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
981+
let errorCollection = error as? ValidationErrorCollection
982+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the matrix style can only be used for the path location")
983+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
984+
}
985+
}
986+
987+
func test_badLabelStyleLocation_fails() throws {
988+
let parameter = OpenAPI.Parameter.query(
989+
name: "test",
990+
schemaOrContent: .schema(.init(.string, style: .label))
991+
)
992+
993+
let document = OpenAPI.Document(
994+
info: .init(title: "test", version: "1.0"),
995+
servers: [],
996+
paths: [
997+
"/hello": .init(
998+
get: .init(
999+
operationId: "testOperation",
1000+
parameters: [
1001+
.parameter(parameter)
1002+
],
1003+
responses: [:]
1004+
)
1005+
)
1006+
],
1007+
components: .noComponents
1008+
)
1009+
1010+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1011+
1012+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1013+
let errorCollection = error as? ValidationErrorCollection
1014+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the label style can only be used for the path location")
1015+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1016+
}
1017+
}
1018+
1019+
func test_badSimpleStyleLocation_fails() throws {
1020+
let parameter = OpenAPI.Parameter.query(
1021+
name: "test",
1022+
schemaOrContent: .schema(.init(.string, style: .simple))
1023+
)
1024+
1025+
let document = OpenAPI.Document(
1026+
info: .init(title: "test", version: "1.0"),
1027+
servers: [],
1028+
paths: [
1029+
"/hello": .init(
1030+
get: .init(
1031+
operationId: "testOperation",
1032+
parameters: [
1033+
.parameter(parameter)
1034+
],
1035+
responses: [:]
1036+
)
1037+
)
1038+
],
1039+
components: .noComponents
1040+
)
1041+
1042+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1043+
1044+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1045+
let errorCollection = error as? ValidationErrorCollection
1046+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the simple style can only be used for the path and header locations")
1047+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1048+
}
1049+
}
1050+
1051+
func test_badFormStyleLocation_fails() throws {
1052+
let parameter = OpenAPI.Parameter.header(
1053+
name: "test",
1054+
schemaOrContent: .schema(.init(.string, style: .form))
1055+
)
1056+
1057+
let document = OpenAPI.Document(
1058+
info: .init(title: "test", version: "1.0"),
1059+
servers: [],
1060+
paths: [
1061+
"/hello": .init(
1062+
get: .init(
1063+
operationId: "testOperation",
1064+
parameters: [
1065+
.parameter(parameter)
1066+
],
1067+
responses: [:]
1068+
)
1069+
)
1070+
],
1071+
components: .noComponents
1072+
)
1073+
1074+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1075+
1076+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1077+
let errorCollection = error as? ValidationErrorCollection
1078+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the form style can only be used for the query and cookie locations")
1079+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1080+
}
1081+
}
1082+
1083+
func test_badSpaceDelimitedStyleLocation_fails() throws {
1084+
let parameter = OpenAPI.Parameter.header(
1085+
name: "test",
1086+
schemaOrContent: .schema(.init(.string, style: .spaceDelimited))
1087+
)
1088+
1089+
let document = OpenAPI.Document(
1090+
info: .init(title: "test", version: "1.0"),
1091+
servers: [],
1092+
paths: [
1093+
"/hello": .init(
1094+
get: .init(
1095+
operationId: "testOperation",
1096+
parameters: [
1097+
.parameter(parameter)
1098+
],
1099+
responses: [:]
1100+
)
1101+
)
1102+
],
1103+
components: .noComponents
1104+
)
1105+
1106+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1107+
1108+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1109+
let errorCollection = error as? ValidationErrorCollection
1110+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the spaceDelimited style can only be used for the query location")
1111+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1112+
}
1113+
}
1114+
1115+
func test_badPipeDelimitedStyleLocation_fails() throws {
1116+
let parameter = OpenAPI.Parameter.header(
1117+
name: "test",
1118+
schemaOrContent: .schema(.init(.string, style: .pipeDelimited))
1119+
)
1120+
1121+
let document = OpenAPI.Document(
1122+
info: .init(title: "test", version: "1.0"),
1123+
servers: [],
1124+
paths: [
1125+
"/hello": .init(
1126+
get: .init(
1127+
operationId: "testOperation",
1128+
parameters: [
1129+
.parameter(parameter)
1130+
],
1131+
responses: [:]
1132+
)
1133+
)
1134+
],
1135+
components: .noComponents
1136+
)
1137+
1138+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1139+
1140+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1141+
let errorCollection = error as? ValidationErrorCollection
1142+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the pipeDelimited style can only be used for the query location")
1143+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1144+
}
1145+
}
1146+
1147+
func test_badDeepObjectStyleLocation_fails() throws {
1148+
let parameter = OpenAPI.Parameter.header(
1149+
name: "test",
1150+
schemaOrContent: .schema(.init(.string, style: .deepObject))
1151+
)
1152+
1153+
let document = OpenAPI.Document(
1154+
info: .init(title: "test", version: "1.0"),
1155+
servers: [],
1156+
paths: [
1157+
"/hello": .init(
1158+
get: .init(
1159+
operationId: "testOperation",
1160+
parameters: [
1161+
.parameter(parameter)
1162+
],
1163+
responses: [:]
1164+
)
1165+
)
1166+
],
1167+
components: .noComponents
1168+
)
1169+
1170+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1171+
1172+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1173+
let errorCollection = error as? ValidationErrorCollection
1174+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the deepObject style can only be used for the query location")
1175+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1176+
}
1177+
}
1178+
1179+
func test_badCookieStyleLocation_fails() throws {
1180+
let parameter = OpenAPI.Parameter.header(
1181+
name: "test",
1182+
schemaOrContent: .schema(.init(.string, style: .cookie))
1183+
)
1184+
1185+
let document = OpenAPI.Document(
1186+
info: .init(title: "test", version: "1.0"),
1187+
servers: [],
1188+
paths: [
1189+
"/hello": .init(
1190+
get: .init(
1191+
operationId: "testOperation",
1192+
parameters: [
1193+
.parameter(parameter)
1194+
],
1195+
responses: [:]
1196+
)
1197+
)
1198+
],
1199+
components: .noComponents
1200+
)
1201+
1202+
let validator = Validator.blank.validating(.parameterStyleAndLocationAreCompatible)
1203+
1204+
XCTAssertThrowsError(try document.validate(using: validator)) { error in
1205+
let errorCollection = error as? ValidationErrorCollection
1206+
XCTAssertEqual(errorCollection?.values.first?.reason, "Failed to satisfy: the cookie style can only be used for the cookie location")
1207+
XCTAssertEqual(errorCollection?.values.first?.codingPath.stringValue, ".paths[\'/hello\'].get.parameters[0]")
1208+
}
1209+
}
9541210
}

0 commit comments

Comments
 (0)