Skip to content

Commit

Permalink
Merge pull request #172 from mattpolzin/bugfix/171/avoid-ref-cycles
Browse files Browse the repository at this point in the history
Track reference visits to avoid reference cycles…
  • Loading branch information
mattpolzin authored Dec 28, 2020
2 parents 14527b0 + bb0f6f3 commit 4e7f675
Show file tree
Hide file tree
Showing 19 changed files with 185 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,16 @@ extension OpenAPI.Components {
}
}
}

public struct ReferenceCycleError: Swift.Error, Equatable, CustomStringConvertible {
public let ref: String

public var description: String {
return "Encountered a JSON Schema $ref cycle that prevents fully dereferencing document at '\(ref)'. This type of reference cycle is not inherently problematic for JSON Schemas, but it does mean OpenAPIKit cannot fully resolve references because attempting to do so results in an infinite loop over any reference cycles. You should still be able to parse the document, just avoid requesting a `locallyDereferenced()` copy."
}

public var localizedDescription: String {
description
}
}
}
17 changes: 17 additions & 0 deletions Sources/OpenAPIKit/Components Object/Components+Locatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,22 @@ public protocol LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
///
/// Can also throw `ReferenceCycleError` if a reference
/// cycle is encountered while dereferencing this component.
func dereferenced(in components: OpenAPI.Components) throws -> DereferencedSelf

/// An internal-use method that facilitates reference cycle detection by tracking past references followed
/// in the course of dereferencing.
///
/// For all external-use, see `dereferenced(in:)`.
func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedSelf
}

extension LocallyDereferenceable {
// default implementation of public `dereferenced(in:)` based on internal
// method that tracks reference cycles.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedSelf {
try _dereferenced(in: components, following: [])
}
}
14 changes: 9 additions & 5 deletions Sources/OpenAPIKit/Content/DereferencedContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ public struct DereferencedContent: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ content: OpenAPI.Content, resolvingIn components: OpenAPI.Components) throws {
self.schema = try content.schema?.dereferenced(in: components)
internal init(
_ content: OpenAPI.Content,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
self.schema = try content.schema?._dereferenced(in: components, following: references)
let examples = try content.examples?.mapValues { try components.lookup($0) }
self.examples = examples

Expand All @@ -37,7 +41,7 @@ public struct DereferencedContent: Equatable {

self.encoding = try content.encoding.map { encodingMap in
try encodingMap.mapValues { encoding in
try encoding.dereferenced(in: components)
try encoding._dereferenced(in: components, following: references)
}
}

Expand All @@ -55,7 +59,7 @@ extension OpenAPI.Content: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedContent {
return try DereferencedContent(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedContent {
return try DereferencedContent(self, resolvingIn: components, following: references)
}
}
12 changes: 8 additions & 4 deletions Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ public struct DereferencedContentEncoding: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ contentEncoding: OpenAPI.Content.Encoding, resolvingIn components: OpenAPI.Components) throws {
internal init(
_ contentEncoding: OpenAPI.Content.Encoding,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
self.headers = try contentEncoding.headers.map { headersMap in
try headersMap.mapValues { header in
try header.dereferenced(in: components)
try header._dereferenced(in: components, following: references)
}
}

Expand All @@ -43,7 +47,7 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedContentEncoding {
return try DereferencedContentEncoding(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedContentEncoding {
return try DereferencedContentEncoding(self, resolvingIn: components, following: references)
}
}
6 changes: 4 additions & 2 deletions Sources/OpenAPIKit/Document/DereferencedDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ public struct DereferencedDocument: Equatable {
self.paths = try document.paths.mapValues {
try DereferencedPathItem(
$0,
resolvingIn: document.components
resolvingIn: document.components,
following: []
)
}
self.security = try document.security.map {
try DereferencedSecurityRequirement(
$0,
resolvingIn: document.components
resolvingIn: document.components,
following: []
)
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/OpenAPIKit/Either/Either.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ extension Either: Equatable where A: Equatable, B: Equatable {}

// MARK: - LocallyDereferenceable
extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: LocallyDereferenceable, A.DereferencedSelf == B.DereferencedSelf {
public func dereferenced(in components: OpenAPI.Components) throws -> A.DereferencedSelf {
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> A.DereferencedSelf {
switch self {
case .a(let value):
return try value.dereferenced(in: components)
return try value._dereferenced(in: components, following: references)
case .b(let value):
return try value.dereferenced(in: components)
return try value._dereferenced(in: components, following: references)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAPIKit/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ extension OpenAPI.Example: LocallyDereferenceable {

/// Examples do not contain any references but for convenience
/// they can be "dereferenced" to themselves.
public func dereferenced(in components: OpenAPI.Components) throws -> OpenAPI.Example {
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> OpenAPI.Example {
return self
}
}
Expand Down
16 changes: 11 additions & 5 deletions Sources/OpenAPIKit/Header/DereferencedHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,27 @@ public struct DereferencedHeader: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ header: OpenAPI.Header, resolvingIn components: OpenAPI.Components) throws {
internal init(
_ header: OpenAPI.Header,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
switch header.schemaOrContent {
case .a(let schemaContext):
self.schemaOrContent = .a(
try DereferencedSchemaContext(
schemaContext,
resolvingIn: components
resolvingIn: components,
following: references
)
)
case .b(let contentMap):
self.schemaOrContent = .b(
try contentMap.mapValues {
try DereferencedContent(
$0,
resolvingIn: components
resolvingIn: components,
following: references
)
}
)
Expand All @@ -61,7 +67,7 @@ extension OpenAPI.Header: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedHeader {
return try DereferencedHeader(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedHeader {
return try DereferencedHeader(self, resolvingIn: components, following: references)
}
}
13 changes: 11 additions & 2 deletions Sources/OpenAPIKit/JSONReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,17 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere
///
/// If you just want to look the reference up, use the `subscript` or the
/// `lookup()` method on `Components`.
public func dereferenced(in components: OpenAPI.Components) throws -> ReferenceType.DereferencedSelf {
return try components.lookup(self).dereferenced(in: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> ReferenceType.DereferencedSelf {

var newReferences = references
let (inserted, _) = newReferences.insert(self)
guard inserted else {
throw OpenAPI.Components.ReferenceCycleError(ref: self.absoluteString)
}

return try components
.lookup(self)
._dereferenced(in: components, following: newReferences)
}
}

Expand Down
19 changes: 12 additions & 7 deletions Sources/OpenAPIKit/Operation/DereferencedOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,28 @@ public struct DereferencedOperation: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ operation: OpenAPI.Operation, resolvingIn components: OpenAPI.Components) throws {
internal init(
_ operation: OpenAPI.Operation,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
self.parameters = try operation.parameters.map { parameter in
try parameter.dereferenced(in: components)
try parameter._dereferenced(in: components, following: references)
}

self.requestBody = try operation.requestBody.map { request in
try request.dereferenced(in: components)
try request._dereferenced(in: components, following: references)
}

self.responses = try operation.responses.mapValues { response in
try response.dereferenced(in: components)
try response._dereferenced(in: components, following: references)
}

self.security = try operation.security?.map {
try DereferencedSecurityRequirement(
$0,
resolvingIn: components
resolvingIn: components,
following: references
)
}

Expand Down Expand Up @@ -101,7 +106,7 @@ extension OpenAPI.Operation: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedOperation {
return try DereferencedOperation(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedOperation {
return try DereferencedOperation(self, resolvingIn: components, following: references)
}
}
16 changes: 11 additions & 5 deletions Sources/OpenAPIKit/Parameter/DereferencedParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,27 @@ public struct DereferencedParameter: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ parameter: OpenAPI.Parameter, resolvingIn components: OpenAPI.Components) throws {
internal init(
_ parameter: OpenAPI.Parameter,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
switch parameter.schemaOrContent {
case .a(let schemaContext):
self.schemaOrContent = .a(
try DereferencedSchemaContext(
schemaContext,
resolvingIn: components
resolvingIn: components,
following: references
)
)
case .b(let contentMap):
self.schemaOrContent = .b(
try contentMap.mapValues {
try DereferencedContent(
$0,
resolvingIn: components
resolvingIn: components,
following: references
)
}
)
Expand All @@ -61,7 +67,7 @@ extension OpenAPI.Parameter: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedParameter {
return try DereferencedParameter(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedParameter {
return try DereferencedParameter(self, resolvingIn: components, following: references)
}
}
12 changes: 8 additions & 4 deletions Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ public struct DereferencedSchemaContext: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ schemaContext: OpenAPI.Parameter.SchemaContext, resolvingIn components: OpenAPI.Components) throws {
self.schema = try schemaContext.schema.dereferenced(in: components)
internal init(
_ schemaContext: OpenAPI.Parameter.SchemaContext,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
self.schema = try schemaContext.schema._dereferenced(in: components, following: references)
let examples = try schemaContext.examples?.mapValues { try components.lookup($0) }
self.examples = examples

Expand All @@ -55,7 +59,7 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedSchemaContext {
return try DereferencedSchemaContext(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedSchemaContext {
return try DereferencedSchemaContext(self, resolvingIn: components, following: references)
}
}
28 changes: 16 additions & 12 deletions Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,23 @@ public struct DereferencedPathItem: Equatable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
internal init(_ pathItem: OpenAPI.PathItem, resolvingIn components: OpenAPI.Components) throws {
internal init(
_ pathItem: OpenAPI.PathItem,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
) throws {
self.parameters = try pathItem.parameters.map { parameter in
try parameter.dereferenced(in: components)
try parameter._dereferenced(in: components, following: references)
}

self.get = try pathItem.get.map { try DereferencedOperation($0, resolvingIn: components) }
self.put = try pathItem.put.map { try DereferencedOperation($0, resolvingIn: components) }
self.post = try pathItem.post.map { try DereferencedOperation($0, resolvingIn: components) }
self.delete = try pathItem.delete.map { try DereferencedOperation($0, resolvingIn: components) }
self.options = try pathItem.options.map { try DereferencedOperation($0, resolvingIn: components) }
self.head = try pathItem.head.map { try DereferencedOperation($0, resolvingIn: components) }
self.patch = try pathItem.patch.map { try DereferencedOperation($0, resolvingIn: components) }
self.trace = try pathItem.trace.map { try DereferencedOperation($0, resolvingIn: components) }
self.get = try pathItem.get.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.put = try pathItem.put.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.post = try pathItem.post.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.delete = try pathItem.delete.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.options = try pathItem.options.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.head = try pathItem.head.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.patch = try pathItem.patch.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
self.trace = try pathItem.trace.map { try DereferencedOperation($0, resolvingIn: components, following: references) }

self.underlyingPathItem = pathItem
}
Expand Down Expand Up @@ -118,7 +122,7 @@ extension OpenAPI.PathItem: LocallyDereferenceable {
/// `ReferenceError.missingOnLookup(name:key:)` depending
/// on whether an unresolvable reference points to another file or just points to a
/// component in the same file that cannot be found in the Components Object.
public func dereferenced(in components: OpenAPI.Components) throws -> DereferencedPathItem {
return try DereferencedPathItem(self, resolvingIn: components)
public func _dereferenced(in components: OpenAPI.Components, following references: Set<AnyHashable>) throws -> DereferencedPathItem {
return try DereferencedPathItem(self, resolvingIn: components, following: references)
}
}
Loading

0 comments on commit 4e7f675

Please sign in to comment.