Skip to content

Feature/359/dynamic ref #361

New issue

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

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

Already on GitHub? # to your account

Draft
wants to merge 8 commits into
base: release/4_0
Choose a base branch
from
125 changes: 125 additions & 0 deletions Sources/OpenAPIKit/JSONDynamicReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// JSONDynamicReference.swift
//
//
// Created by Mathew Polzin.
//

import OpenAPIKitCore
import Foundation

@dynamicMemberLookup
public struct JSONDynamicReference: Equatable, Hashable {
public let jsonReference: JSONReference<JSONSchema>

public init(
_ reference: JSONReference<JSONSchema>
) {
self.jsonReference = reference
}

public subscript<T>(dynamicMember path: KeyPath<JSONReference<JSONSchema>, T>) -> T {
return jsonReference[keyPath: path]
}

/// Reference a component of type `ReferenceType` in the
/// Components Object.
///
/// Example:
///
/// JSONDynamicReference.component(named: "greetings")
/// // encoded string: "#/components/schemas/greetings"
/// // Swift: `document.components.schemas["greetings"]`
public static func component(named name: String) -> Self {
return .init(.internal(.component(name: name)))
}

/// Reference a dynamic anchor local to this file.
///
/// - Important: The anchor does not contain a leading '#'.
public static func anchor(_ anchor: String) -> Self {
return .init(.internal(.anchor(anchor)))
}

/// Reference an external URL.
public static func external(_ url: URL) -> Self {
return .init(.external(url))
}

/// `true` for internal references, `false` for
/// external references (i.e. to another file).
public var isInternal: Bool {
return jsonReference.isInternal
}

/// `true` for external references, `false` for
/// internal references.
public var isExternal: Bool {
return jsonReference.isExternal
}

/// Get the name of the referenced object. This method returns optional
/// because a reference to an external file might not have any path if the
/// file itself is the referenced component.
public var name: String? {
return jsonReference.name
}

/// The absolute value of an external reference's
/// URL or the path fragment string for a local
/// reference as defined in [RFC 3986](https://tools.ietf.org/html/rfc3986).
public var absoluteString: String {
return jsonReference.absoluteString
}
}

extension JSONDynamicReference {
private enum CodingKeys: String, CodingKey {
case dynamicRef = "$dynamicRef"
}
}

extension JSONDynamicReference: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self.jsonReference {
case .internal(let reference):
try container.encode(reference.rawValue, forKey: .dynamicRef)
case .external(uri: let url):
try container.encode(url.absoluteString, forKey: .dynamicRef)
}
}
}

extension JSONDynamicReference: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let referenceString = try container.decode(String.self, forKey: .dynamicRef)

guard referenceString.count > 0 else {
throw DecodingError.dataCorruptedError(forKey: .dynamicRef, in: container, debugDescription: "Expected a reference string, but found an empty string instead.")
}

if referenceString.first == "#" {
guard let internalReference = JSONReference<JSONSchema>.InternalReference(rawValue: referenceString) else {
throw InconsistencyError(
subjectName: "JSON Dynamic Reference",
details: "Failed to parse a JSON Dynamic Reference from '\(referenceString)'",
codingPath: container.codingPath
)
}
self = .init(.internal(internalReference))
} else {
guard let externalReference = URL(string: referenceString) else {
throw InconsistencyError(
subjectName: "JSON Dynamic Reference",
details: "Failed to parse a valid URI for a JSON Dynamic Reference from '\(referenceString)'",
codingPath: container.codingPath
)
}
self = .init(.external(externalReference))
}
}
}
17 changes: 17 additions & 0 deletions Sources/OpenAPIKit/JSONReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
case component(name: String)
/// The reference refers to some path outside the Components Object.
case path(Path)
/// The reference refers to some anchor anywhere in the local Schema.
case anchor(String)

/// Get the name of the referenced object.
///
Expand All @@ -147,6 +149,8 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
return name
case .path(let path):
return path.components.last?.stringValue
case .anchor(let name):
return name
}
}

Expand All @@ -164,6 +168,10 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
}
let fragment = rawValue.dropFirst()
guard fragment.starts(with: "/components") else {
guard fragment.first == "/" else {
self = .anchor(String(fragment))
return
}
self = .path(Path(rawValue: String(fragment)))
return
}
Expand All @@ -190,6 +198,8 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
return "#/components/\(ReferenceType.openAPIComponentsKey)/\(name)"
case .path(let path):
return "#\(path.rawValue)"
case .anchor(let name):
return "#\(name)"
}
}
}
Expand Down Expand Up @@ -395,6 +405,13 @@ public extension JSONReference {
}
}

public extension JSONReference where ReferenceType == JSONSchema {
/// Create a dynamic JSON Reference from the given JSONReference.
var dynamicReference: JSONDynamicReference {
JSONDynamicReference(self)
}
}

/// `SummaryOverridable` exists to provide a parent protocol to `OpenAPIDescribable`
/// and `OpenAPISummarizable`. The structure is designed to provide default no-op
/// implementations of both the members of this protocol to all types that implement either
Expand Down
13 changes: 13 additions & 0 deletions Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
indirect case one(of: [DereferencedJSONSchema], core: CoreContext<JSONTypeFormat.AnyFormat>)
indirect case any(of: [DereferencedJSONSchema], core: CoreContext<JSONTypeFormat.AnyFormat>)
indirect case not(DereferencedJSONSchema, core: CoreContext<JSONTypeFormat.AnyFormat>)
case dynamicReference(JSONDynamicReference, CoreContext<JSONTypeFormat.AnyFormat>)
/// Schemas without a `type`.
case fragment(CoreContext<JSONTypeFormat.AnyFormat>) // This is the "{}" case where not even a type constraint is given.

Expand Down Expand Up @@ -65,6 +66,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
return .any(of: schemas.map { $0.jsonSchema }, core: coreContext)
case .not(let schema, core: let coreContext):
return .not(schema.jsonSchema, core: coreContext)
case .dynamicReference(let reference, let coreContext):
return .dynamicReference(reference, coreContext)
case .fragment(let context):
return .fragment(context)
}
Expand Down Expand Up @@ -96,6 +99,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
return .any(of: schemas, core: core.optionalContext())
case .not(let schema, core: let core):
return .not(schema, core: core.optionalContext())
case .dynamicReference(let reference, let core):
return .dynamicReference(reference, core.optionalContext())
}
}

Expand Down Expand Up @@ -185,6 +190,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
return coreContext.vendorExtensions
case .not(_, core: let coreContext):
return coreContext.vendorExtensions
case .dynamicReference(_, let coreContext):
return coreContext.vendorExtensions
case .fragment(let context):
return context.vendorExtensions
}
Expand Down Expand Up @@ -215,6 +222,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
return .any(of: schemas, core: coreContext.with(description: description))
case .not(let schema, core: let coreContext):
return .not(schema, core: coreContext.with(description: description))
case .dynamicReference(let reference, let coreContext):
return .dynamicReference(reference, coreContext.with(description: description))
case .fragment(let context):
return .fragment(context.with(description: description))
}
Expand Down Expand Up @@ -245,6 +254,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
return .any(of: schemas, core: coreContext.with(vendorExtensions: vendorExtensions))
case .not(let schema, core: let coreContext):
return .not(schema, core: coreContext.with(vendorExtensions: vendorExtensions))
case .dynamicReference(let reference, let coreContext):
return .dynamicReference(reference, coreContext.with(vendorExtensions: vendorExtensions))
case .fragment(let context):
return .fragment(context.with(vendorExtensions: vendorExtensions))
}
Expand Down Expand Up @@ -526,6 +537,8 @@ extension JSONSchema: LocallyDereferenceable {
return .any(of: schemas, core: addComponentNameExtension(to: coreContext))
case .not(let jsonSchema, core: let coreContext):
return .not(try jsonSchema._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil), core: addComponentNameExtension(to: coreContext))
case .dynamicReference(let reference, let coreContext):
return .dynamicReference(reference, addComponentNameExtension(to: coreContext))
case .fragment(let context):
return .fragment(addComponentNameExtension(to: context))
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ internal struct FragmentCombiner {
self.combinedFragment = .array(try leftCoreContext.combined(with: rightCoreContext), arrayContext)
case (.fragment(let leftCoreContext), .object(let rightCoreContext, let objectContext)):
self.combinedFragment = .object(try leftCoreContext.combined(with: rightCoreContext), objectContext)
case (.fragment(let leftCoreContext), .dynamicReference(let reference, let rightCoreContext)):
self.combinedFragment = .dynamicReference(reference, try leftCoreContext.combined(with: rightCoreContext))

case (.boolean(let leftCoreContext), .boolean(let rightCoreContext)):
self.combinedFragment = .boolean(try leftCoreContext.combined(with: rightCoreContext))
Expand Down Expand Up @@ -208,7 +210,8 @@ internal struct FragmentCombiner {
(.string, _),
(.array, _),
(.object, _),
(.null, _):
(.null, _),
(.dynamicReference, _):
throw (
zip(combinedFragment.jsonType, fragment.jsonType).map {
JSONSchemaResolutionError(.typeConflict(original: $0, new: $1))
Expand Down Expand Up @@ -258,6 +261,8 @@ internal struct FragmentCombiner {
jsonSchema = try .any(of: schemas, core: coreContext.validatedContext())
case .one(of: let schemas, core: let coreContext):
jsonSchema = try .one(of: schemas, core: coreContext.validatedContext())
case .dynamicReference(let reference, let coreContext):
jsonSchema = try .dynamicReference(reference, coreContext.validatedContext())
case .not:
throw JSONSchemaResolutionError(.unsupported(because: "`.not` is not yet supported for schema simplification"))
}
Expand Down
Loading
Loading