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

Load external references #287

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions Sources/OpenAPIKit/Callbacks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,13 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable {
) throws -> OpenAPI.CallbackURL {
self
}

public func externallyDereferenced<Context>(
with loader: inout ExternalLoader<Context>
) throws -> Self where Context : ExternalLoaderContext {
// TODO: externally dereference security, responses, requestBody, and parameters
#warning("externally dereference security, responses, requestBody, and parameters")
return self
}
}

2 changes: 1 addition & 1 deletion Sources/OpenAPIKit/CodableVendorExtendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public protocol VendorExtendable {
/// These should be of the form:
/// `[ "x-extensionKey": <anything>]`
/// where the values are anything codable.
var vendorExtensions: VendorExtensions { get }
var vendorExtensions: VendorExtensions { get set }
}

public enum VendorExtensionsConfiguration {
Expand Down
27 changes: 16 additions & 11 deletions Sources/OpenAPIKit/Components Object/Components+Locatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import OpenAPIKitCore
import Foundation

/// Anything conforming to ComponentDictionaryLocatable knows
/// where to find resources of its type in the Components Dictionary.
Expand All @@ -15,57 +16,57 @@ public protocol ComponentDictionaryLocatable: SummaryOverridable {
/// This can be used to create a JSON path
/// like `#/name1/name2/name3`
static var openAPIComponentsKey: String { get }
static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { get }
static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { get }
}

extension JSONSchema: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "schemas" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.schemas }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.schemas }
}

extension OpenAPI.Response: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "responses" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.responses }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.responses }
}

extension OpenAPI.Callbacks: ComponentDictionaryLocatable & SummaryOverridable {
public static var openAPIComponentsKey: String { "callbacks" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.callbacks }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.callbacks }
}

extension OpenAPI.Parameter: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "parameters" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.parameters }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.parameters }
}

extension OpenAPI.Example: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "examples" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.examples }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.examples }
}

extension OpenAPI.Request: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "requestBodies" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.requestBodies }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.requestBodies }
}

extension OpenAPI.Header: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "headers" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.headers }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.headers }
}

extension OpenAPI.SecurityScheme: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "securitySchemes" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.securitySchemes }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.securitySchemes }
}

extension OpenAPI.Link: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "links" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.links }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.links }
}

extension OpenAPI.PathItem: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "pathItems" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.pathItems }
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.pathItems }
}

/// A dereferenceable type can be recursively looked up in
Expand Down Expand Up @@ -97,6 +98,10 @@ public protocol LocallyDereferenceable {
following references: Set<AnyHashable>,
dereferencedFromComponentNamed name: String?
) throws -> DereferencedSelf

func externallyDereferenced<Context: ExternalLoaderContext>(
with loader: inout ExternalLoader<Context>
) throws -> Self
}

extension LocallyDereferenceable {
Expand Down
30 changes: 30 additions & 0 deletions Sources/OpenAPIKit/Components Object/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,34 @@ extension OpenAPI.Components {
}
}

extension OpenAPI.Components {
private mutating func externallyDereference<Context, T>(dictionary: OpenAPI.ComponentDictionary<T>, with loader: inout ExternalLoader<Context>) throws -> OpenAPI.ComponentDictionary<T> where Context: ExternalLoaderContext, T: LocallyDereferenceable {
var newValues = OpenAPI.ComponentDictionary<T>()
for (key, value) in dictionary {
newValues[key] = try value.externallyDereferenced(with: &loader)
}
return newValues
}

internal mutating func externallyDereference<Context>(in context: Context) throws -> ExternalLoader<Context> where Context: ExternalLoaderContext {
var loader = ExternalLoader<Context>(components: self, context: context)

schemas = try externallyDereference(dictionary: schemas, with: &loader)
responses = try externallyDereference(dictionary: responses, with: &loader)
parameters = try externallyDereference(dictionary: parameters, with: &loader)
examples = try externallyDereference(dictionary: examples, with: &loader)
requestBodies = try externallyDereference(dictionary: requestBodies, with: &loader)
headers = try externallyDereference(dictionary: headers, with: &loader)
securitySchemes = try externallyDereference(dictionary: securitySchemes, with: &loader)

var newCallbacks = OpenAPI.ComponentDictionary<OpenAPI.Callbacks>()
for (key, value) in callbacks {
newCallbacks[key] = try value.externallyDereferenced(with: &loader)
}
callbacks = newCallbacks

return loader
}
}

extension OpenAPI.Components: Validatable {}
8 changes: 8 additions & 0 deletions Sources/OpenAPIKit/Content/DereferencedContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@ extension OpenAPI.Content: LocallyDereferenceable {
) throws -> DereferencedContent {
return try DereferencedContent(self, resolvingIn: components, following: references)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> OpenAPI.Content where Context : ExternalLoaderContext {
var content = self

// TOOD: need to locally dereference the schema, examples, and content encoding here.
#warning("need to locally dereference the schema, examples, and content encoding here.")
return content
}
}
9 changes: 9 additions & 0 deletions Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,13 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable {
) throws -> DereferencedContentEncoding {
return try DereferencedContentEncoding(self, resolvingIn: components, following: references)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> OpenAPI.Content.Encoding where Context : ExternalLoaderContext {
var contentEncoding = self

// TODO: need to externally dereference the headers here.
#warning("need to externally dereference the headers here.")

return self
}
}
9 changes: 9 additions & 0 deletions Sources/OpenAPIKit/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,15 @@ extension OpenAPI.Document {
public func locallyDereferenced() throws -> DereferencedDocument {
return try DereferencedDocument(self)
}

public mutating func externallyDereference<Context>(in context: Context) throws where Context: ExternalLoaderContext {
var loader: ExternalLoader<Context> = try components.externallyDereference(in: context)

paths = try paths.externallyDereferenced(with: &loader)
webhooks = try webhooks.externallyDereferenced(with: &loader)

components = loader.components
}
}

extension OpenAPI {
Expand Down
9 changes: 9 additions & 0 deletions Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,13 @@ extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: Loc
return try value._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil)
}
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> Self where Context : ExternalLoaderContext {
switch self {
case .a(let a):
return .a(try a.externallyDereferenced(with: &loader))
case .b(let b):
return .b(try b.externallyDereferenced(with: &loader))
}
}
}
6 changes: 5 additions & 1 deletion Sources/OpenAPIKit/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension OpenAPI {
/// These should be of the form:
/// `[ "x-extensionKey": <anything>]`
/// where the values are anything codable.
public let vendorExtensions: [String: AnyCodable]
public var vendorExtensions: [String: AnyCodable]

public init(
summary: String? = nil,
Expand Down Expand Up @@ -206,6 +206,10 @@ extension OpenAPI.Example: LocallyDereferenceable {
vendorExtensions: vendorExtensions
)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> OpenAPI.Example where Context : ExternalLoaderContext {
return self
}
}

extension OpenAPI.Example: Validatable {}
58 changes: 58 additions & 0 deletions Sources/OpenAPIKit/ExternalLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// ExternalLoader.swift
//
//
// Created by Mathew Polzin on 7/30/2023.
//

import OpenAPIKitCore
import Foundation

/// An `ExternalLoaderContext` enables `OpenAPIKit` to load external references
/// without knowing the details of what decoder is being used or how new internal
/// references should be named.
public protocol ExternalLoaderContext {
/// Load the given URL and decode it as Type `T`. All Types `T` are `Decodable`, so
/// the only real responsibility of a `load` function is to locate and load the given
/// `URL` and pass its `Data` or `String` (depending on the decoder) to an appropriate
/// `Decoder` for the given file type.
static func load<T>(_: URL) throws -> T where T: Decodable

/// Determine the next Component Key (where to store something in the
/// Components Object) for a new object of the given type that was loaded
/// at the given external URL.
///
/// - Important: Ideally, this function returns distinct keys for all different objects
/// but the same key for all equal objects. In practice, this probably means that any
/// time the same type and URL pair are passed in the same `ComponentKey` should be
/// returned.
mutating func nextComponentKey<T>(type: T.Type, at: URL, given components: OpenAPI.Components) throws -> OpenAPI.ComponentKey
}

public struct ExternalLoader<Context: ExternalLoaderContext> {
public init(components: OpenAPI.Components, context: Context) {
self.components = components
self.context = context
}

/// External references are loaded into this Components Object. This allows for
/// loading external references into a single Document but also retaining the
/// identity of those refernces; that is, if three parts of a Document refer to
/// the same external reference, the external object will be loaded into this
/// Components Object and the three locations will still refer to the same
/// object (these are now internal references).
///
/// In the most common use-cases, the starting place for this `components` property
/// should be the existing `Components` for some OpenAPI `Document`. This allows local
/// references to be followed while external references are loaded.
public internal(set) var components: OpenAPI.Components

internal var context: Context

internal mutating func store<T>(type: T.Type, from url: URL) throws -> OpenAPI.Reference<T> where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable {
let key = try context.nextComponentKey(type: type, at: url, given: components)
let value: T = try Context.load(url)
components[keyPath: T.openAPIComponentsKeyPath][key] = try value.externallyDereferenced(with: &self)
return try components.reference(named: key.rawValue, ofType: T.self)
}
}
6 changes: 6 additions & 0 deletions Sources/OpenAPIKit/Header/DereferencedHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,10 @@ extension OpenAPI.Header: LocallyDereferenceable {
) throws -> DereferencedHeader {
return try DereferencedHeader(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> OpenAPI.Header where Context : ExternalLoaderContext {
// TODO: externally dereference the schemaOrContent
#warning("externally dereference the schemaOrContent")
return self
}
}
22 changes: 20 additions & 2 deletions Sources/OpenAPIKit/JSONReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ extension OpenAPI.Reference: Decodable {
}

// MARK: - LocallyDereferenceable
extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable {
extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable {
/// Look up the component this reference points to and then
/// dereference it.
///
Expand All @@ -535,9 +535,23 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere
.lookup(self)
._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> Self where Context : ExternalLoaderContext {
switch self {
case .internal(let ref):
let value = try loader.components.lookup(self)
.externallyDereferenced(with: &loader)
let key = try OpenAPI.ComponentKey.forceInit(rawValue: ref.name)
loader.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] =
value
return self
case .external(let url):
return try loader.store(type: ReferenceType.self, from: url).jsonReference
}
}
}

extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable {
extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable {
/// Look up the component this reference points to and then
/// dereference it.
///
Expand All @@ -562,6 +576,10 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally
.lookup(self)
._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> Self where Context : ExternalLoaderContext {
return .init(try jsonReference.externallyDereferenced(with: &loader))
}
}

extension OpenAPI.Reference: Validatable where ReferenceType: Validatable {}
8 changes: 8 additions & 0 deletions Sources/OpenAPIKit/Link.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,14 @@ extension OpenAPI.Link: LocallyDereferenceable {
vendorExtensions: vendorExtensions
)
}

public func externallyDereferenced<Context>(
with loader: inout ExternalLoader<Context>
) throws -> Self where Context : ExternalLoaderContext {
// TODO: externally dereference security, responses, requestBody, and parameters
#warning("externally dereference security, responses, requestBody, and parameters")
return self
}
}

extension OpenAPI.Link: Validatable {}
6 changes: 6 additions & 0 deletions Sources/OpenAPIKit/Operation/DereferencedOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,10 @@ extension OpenAPI.Operation: LocallyDereferenceable {
) throws -> DereferencedOperation {
return try DereferencedOperation(self, resolvingIn: components, following: references)
}

public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> OpenAPI.Operation where Context : ExternalLoaderContext {
// TODO: externally dereference security, responses, requestBody, and parameters
#warning("externally dereference security, responses, requestBody, and parameters")
return self
}
}
19 changes: 19 additions & 0 deletions Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// OrderedDictionary+ExternallyDereference.swift
// OpenAPI
//
// Created by Mathew Polzin on 08/05/2023.
//

import OpenAPIKitCore

extension OrderedDictionary where Value: LocallyDereferenceable {
public func externallyDereferenced<Context>(with loader: inout ExternalLoader<Context>) throws -> Self where Context: ExternalLoaderContext {
var newDict = Self()
for (key, value) in self {
let newRef = try value.externallyDereferenced(with: &loader)
newDict[key] = newRef
}
return newDict
}
}
9 changes: 9 additions & 0 deletions Sources/OpenAPIKit/Parameter/DereferencedParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,13 @@ extension OpenAPI.Parameter: LocallyDereferenceable {
) throws -> DereferencedParameter {
return try DereferencedParameter(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name)
}

public func externallyDereferenced<Context: ExternalLoaderContext>(with loader: inout ExternalLoader<Context>) throws -> Self {
var parameter = self

// TODO: externallyDerefence the schemaOrContent
#warning("need to externally dereference the schemaOrContent here")

return parameter
}
}
Loading
Loading