Skip to content

Validate incoming OpenAPI docs using OpenAPIKit's built-in validation #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ enum JSONReferenceParsingError: Swift.Error {
/// An error thrown when parsing a JSON reference that points to
/// other OpenAPI documents.
case externalPathsUnsupported(String)

/// Reference cycle are unsupported.
case referenceCycleUnsupported(String)
}

extension JSONReferenceParsingError: CustomStringConvertible {
Expand All @@ -434,6 +437,8 @@ extension JSONReferenceParsingError: CustomStringConvertible {
return "JSON references outside of #/components are not supported, found: \(string ?? "<nil>")"
case let .externalPathsUnsupported(string):
return "External JSON references are not supported, found: \(string)"
case let .referenceCycleUnsupported(string):
return "Reference cycles are not supported, found: \(string)"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ extension FileTranslator {
/// If a schema is not supported, no references to it should be emitted.
/// - Parameters:
/// - schema: The schema to validate.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if the schema is supported; `false` otherwise.
func isSchemaSupported(
_ schema: JSONSchema
_ schema: JSONSchema,
seenReferences: Set<String> = []
) throws -> Bool {
switch schema.value {
case .string,
Expand All @@ -73,26 +75,30 @@ extension FileTranslator {
.fragment:
return true
case .reference(let ref, _):
let referenceString = ref.absoluteString
guard !seenReferences.contains(referenceString) else {
throw JSONReferenceParsingError.referenceCycleUnsupported(referenceString)
}
// reference is supported iff the existing type is supported
let existingSchema = try components.lookup(ref)
return try isSchemaSupported(existingSchema)
return try isSchemaSupported(existingSchema, seenReferences: seenReferences.union([referenceString]))
case .array(_, let array):
guard let items = array.items else {
// an array of fragments is supported
return true
}
// an array is supported iff its element schema is supported
return try isSchemaSupported(items)
return try isSchemaSupported(items, seenReferences: seenReferences)
case .all(of: let schemas, _):
guard !schemas.isEmpty else {
return false
}
return try areObjectishSchemasAndSupported(schemas)
return try areObjectishSchemasAndSupported(schemas, seenReferences: seenReferences)
case .any(of: let schemas, _):
guard !schemas.isEmpty else {
return false
}
return try areObjectishSchemasAndSupported(schemas)
return try areObjectishSchemasAndSupported(schemas, seenReferences: seenReferences)
case .one(of: let schemas, let context):
guard !schemas.isEmpty else {
return false
Expand All @@ -103,7 +109,7 @@ extension FileTranslator {
guard context.discriminator != nil else {
return try areSchemasSupported(schemas)
}
return try areRefsToObjectishSchemaAndSupported(schemas)
return try areRefsToObjectishSchemaAndSupported(schemas, seenReferences: seenReferences)
case .not:
return false
}
Expand All @@ -114,9 +120,11 @@ extension FileTranslator {
/// If a schema is not supported, no references to it should be emitted.
/// - Parameters:
/// - schema: The schema to validate.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if the schema is supported; `false` otherwise.
func isSchemaSupported(
_ schema: UnresolvedSchema?
_ schema: UnresolvedSchema?,
seenReferences: Set<String> = []
) throws -> Bool {
guard let schema else {
// fragment type is supported
Expand All @@ -127,57 +135,82 @@ extension FileTranslator {
// references are supported
return true
case let .b(schema):
return try isSchemaSupported(schema)
return try isSchemaSupported(schema, seenReferences: seenReferences)
}
}

/// Returns a Boolean value that indicates whether the provided schemas
/// are supported.
/// - Parameter schemas: Schemas to check.
/// - Parameter:
/// - schemas: Schemas to check.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if all schemas are supported; `false` otherwise.
func areSchemasSupported(_ schemas: [JSONSchema]) throws -> Bool {
try schemas.allSatisfy(isSchemaSupported)
func areSchemasSupported(
_ schemas: [JSONSchema],
seenReferences: Set<String> = []
) throws -> Bool {
try schemas.allSatisfy { try isSchemaSupported($0, seenReferences: seenReferences) }
}

/// Returns a Boolean value that indicates whether the provided schemas
/// are reference, object, or allOf schemas and supported.
/// - Parameter schemas: Schemas to check.
/// - Parameter:
/// - schemas: Schemas to check.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if all schemas match; `false` otherwise.
func areObjectishSchemasAndSupported(_ schemas: [JSONSchema]) throws -> Bool {
try schemas.allSatisfy(isObjectishSchemaAndSupported)
func areObjectishSchemasAndSupported(
_ schemas: [JSONSchema],
seenReferences: Set<String> = []
) throws -> Bool {
try schemas.allSatisfy { try isObjectishSchemaAndSupported($0, seenReferences: seenReferences) }
}

/// Returns a Boolean value that indicates whether the provided schema
/// is an reference, object, or allOf (object-ish) schema and is supported.
/// - Parameter schema: A schemas to check.
/// - Parameter:
/// - schemas: Schemas to check.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if the schema matches; `false` otherwise.
func isObjectishSchemaAndSupported(_ schema: JSONSchema) throws -> Bool {
func isObjectishSchemaAndSupported(
_ schema: JSONSchema,
seenReferences: Set<String> = []
) throws -> Bool {
switch schema.value {
case .object, .reference:
return try isSchemaSupported(schema)
return try isSchemaSupported(schema, seenReferences: seenReferences)
case .all(of: let schemas, _):
return try areObjectishSchemasAndSupported(schemas)
return try areObjectishSchemasAndSupported(schemas, seenReferences: seenReferences)
default:
return false
}
}

/// Returns a Boolean value that indicates whether the provided schemas
/// are reference schemas that point to object-ish schemas and supported.
/// - Parameter schemas: Schemas to check.
/// - Parameter:
/// - schemas: Schemas to check.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if all schemas match; `false` otherwise.
func areRefsToObjectishSchemaAndSupported(_ schemas: [JSONSchema]) throws -> Bool {
try schemas.allSatisfy(isRefToObjectishSchemaAndSupported)
func areRefsToObjectishSchemaAndSupported(
_ schemas: [JSONSchema],
seenReferences: Set<String> = []
) throws -> Bool {
try schemas.allSatisfy { try isRefToObjectishSchemaAndSupported($0, seenReferences: seenReferences) }
}

/// Returns a Boolean value that indicates whether the provided schema
/// is a reference schema that points to an object-ish schema and is supported.
/// - Parameter schema: A schema to check.
/// - Parameter:
/// - schemas: Schemas to check.
/// - seenReferences: A set of seen references, used to detect cycles.
/// - Returns: `true` if the schema matches; `false` otherwise.
func isRefToObjectishSchemaAndSupported(_ schema: JSONSchema) throws -> Bool {
func isRefToObjectishSchemaAndSupported(
_ schema: JSONSchema,
seenReferences: Set<String> = []
) throws -> Bool {
switch schema.value {
case .reference:
return try isObjectishSchemaAndSupported(schema)
return try isObjectishSchemaAndSupported(schema, seenReferences: seenReferences)
default:
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ import OpenAPIKit30

class Test_isSchemaSupported: XCTestCase {

var translator: any FileTranslator {
func translator(components: OpenAPI.Components) -> any FileTranslator {
TypesFileTranslator(
config: .init(mode: .types),
diagnostics: PrintingDiagnosticCollector(),
components: components
)
}

var translator: any FileTranslator {
translator(
components: .init(schemas: [
"Foo": .string,
"MyObj": .object,
Expand Down Expand Up @@ -107,4 +113,62 @@ class Test_isSchemaSupported: XCTestCase {
)
}
}

func testRecursion_direct() throws {
let fooA: JSONSchema = .all(of: [
.reference(.component(named: "FooB"))
])
let fooB: JSONSchema = .all(of: [
.reference(.component(named: "FooA"))
])
let translator = translator(
components: .init(schemas: [
"FooA": fooA,
"FooB": fooB,
])
)
for (schema, detectedAtName) in [(fooA, "FooB"), (fooB, "FooA")] {
XCTAssertThrowsError(try translator.isSchemaSupported(schema)) { error in
guard
let jsonError = error as? JSONReferenceParsingError,
case let .referenceCycleUnsupported(string) = jsonError
else {
XCTFail("Unexpected error thrown: \(error)")
return
}
XCTAssertEqual(string, "#/components/schemas/\(detectedAtName)")
}
}
}

func testRecursion_indirect() throws {
let fooA: JSONSchema = .all(of: [
.reference(.component(named: "FooB"))
])
let fooB: JSONSchema = .all(of: [
.reference(.component(named: "FooC"))
])
let fooC: JSONSchema = .all(of: [
.reference(.component(named: "FooA"))
])
let translator = translator(
components: .init(schemas: [
"FooA": fooA,
"FooB": fooB,
"FooC": fooC,
])
)
for (schema, detectedAtName) in [(fooA, "FooB"), (fooB, "FooC"), (fooC, "FooA")] {
XCTAssertThrowsError(try translator.isSchemaSupported(schema)) { error in
guard
let jsonError = error as? JSONReferenceParsingError,
case let .referenceCycleUnsupported(string) = jsonError
else {
XCTFail("Unexpected error thrown: \(error)")
return
}
XCTAssertEqual(string, "#/components/schemas/\(detectedAtName)")
}
}
}
}