Skip to content
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

Feature/359/dynamic ref #361

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