Skip to content

Commit

Permalink
Merge pull request #29 from mattpolzin/json-reference-refactor
Browse files Browse the repository at this point in the history
Json reference refactor
  • Loading branch information
mattpolzin authored Mar 15, 2020
2 parents 058b5cb + 7b1fdb4 commit 673acd0
Show file tree
Hide file tree
Showing 30 changed files with 920 additions and 480 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A library containing Swift types that encode to- and decode from [OpenAPI](https
- [Request/Response Bodies](#requestresponse-bodies)
- [Schemas](#schemas)
- [Generating Schemas from Swift Types](#generating-schemas-from-swift-types)
- [JSON References](#json-references)
- [Notes](#notes)
- [Project Status](#project-status)
- [OpenAPI Object (`OpenAPI.Document`)](#openapi-object-openapidocument)
Expand Down Expand Up @@ -164,6 +165,15 @@ Int32?.openAPINode() == .integer(format: .int32, required: false)

Additional schema generation support can be found in the [`mattpolzin/OpenAPIReflection`](https://github.com/mattpolzin/OpenAPIReflection) library.

#### JSON References
The `JSONReference` type allows you to work with OpenAPIDocuments that store some of their information in the shared Components Object dictionary or even external files. You cannot dereference documents (yet), but you can encode and decode references.

You can create an external reference with `JSONReference.external(URL)`. Internal references usually refer to an object in the Components Object dictionary and are constructed with `JSONReference.component(named:)`. If you need to refer to something in the current file but not in the Components Object, you can use `JSONReference.internal(path:)`.

You can check whether a given `JSONReference` exists in the Components Object with `document.components.contains()`. You can access a referenced object in the Components Object with `document.components[reference]`.

You can create references from the Components Object with `document.components.reference(named:ofType:)`. This method will throw an error if the given component does not exist in the ComponentsObject.

## Notes
This library does *not* currently support file reading at all muchless following `$ref`s to other files and loading them in.

Expand Down Expand Up @@ -362,11 +372,11 @@ See [**A note on dictionary ordering**](#a-note-on-dictionary-ordering) before d

### Reference Object (`JSONReference`)
- [x] $ref
- [x] local (same file) reference (`node` case)
- [x] local (same file) reference (`internal` case)
- [x] encode
- [ ] decode
- [x] decode
- [ ] dereference
- [x] remote (different file) reference (`file` case)
- [x] remote (different file) reference (`external` case)
- [x] encode
- [x] decode
- [ ] dereference
Expand Down
228 changes: 132 additions & 96 deletions Sources/OpenAPIKit/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,32 @@ extension OpenAPI {
/// What the spec calls the "Components Object".
/// This is a place to put reusable components to
/// be referenced from other parts of the spec.
public struct Components: Equatable, ReferenceRoot {
public static var refName: String { return "components" }

public var schemas: SchemasDict
public var responses: ResponsesDict
public var parameters: ParametersDict
public var examples: ExamplesDict
public var requestBodies: RequestBodiesDict
public var headers: HeadersDict
public var securitySchemes: SecuritySchemesDict
public struct Components: Equatable {

public var schemas: OrderedDictionary<String, JSONSchema>
public var responses: OrderedDictionary<String, Response>
public var parameters: OrderedDictionary<String, PathItem.Parameter>
public var examples: OrderedDictionary<String, Example>
public var requestBodies: OrderedDictionary<String, Request>
public var headers: OrderedDictionary<String, Header>
public var securitySchemes: OrderedDictionary<String, SecurityScheme>
// public var links:
// public var callbacks:

public init(schemas: OrderedDictionary<String, SchemasDict.Value> = [:],
responses: OrderedDictionary<String, ResponsesDict.Value> = [:],
parameters: OrderedDictionary<String, ParametersDict.Value> = [:],
examples: OrderedDictionary<String, ExamplesDict.Value> = [:],
requestBodies: OrderedDictionary<String, RequestBodiesDict.Value> = [:],
headers: OrderedDictionary<String, HeadersDict.Value> = [:],
securitySchemes: OrderedDictionary<String, SecuritySchemesDict.Value> = [:]) {
self.schemas = SchemasDict(schemas)
self.responses = ResponsesDict(responses)
self.parameters = ParametersDict(parameters)
self.examples = ExamplesDict(examples)
self.requestBodies = RequestBodiesDict(requestBodies)
self.headers = HeadersDict(headers)
self.securitySchemes = SecuritySchemesDict(securitySchemes)
public init(schemas: OrderedDictionary<String, JSONSchema> = [:],
responses: OrderedDictionary<String, Response> = [:],
parameters: OrderedDictionary<String, PathItem.Parameter> = [:],
examples: OrderedDictionary<String, Example> = [:],
requestBodies: OrderedDictionary<String, Request> = [:],
headers: OrderedDictionary<String, Header> = [:],
securitySchemes: OrderedDictionary<String, SecurityScheme> = [:]) {
self.schemas = schemas
self.responses = responses
self.parameters = parameters
self.examples = examples
self.requestBodies = requestBodies
self.headers = headers
self.securitySchemes = securitySchemes
}

public static let noComponents: Components = .init(
Expand All @@ -54,49 +53,60 @@ extension OpenAPI {
var isEmpty: Bool {
return self == .noComponents
}
}
}

public enum SchemasName: RefName {
public static var refName: String { return "schemas" }
}

public typealias SchemasDict = RefDict<Components, SchemasName, JSONSchema>

public enum ResponsesName: RefName {
public static var refName: String { return "responses" }
}

public typealias ResponsesDict = RefDict<Components, ResponsesName, OpenAPI.Response>

public enum ParametersName: RefName {
public static var refName: String { return "parameters" }
}

public typealias ParametersDict = RefDict<Components, ParametersName, PathItem.Parameter>
/// Anything conforming to ComponentDictionaryLocatable knows
/// where to find resources of its type in the Components Dictionary.
public protocol ComponentDictionaryLocatable {
/// The JSON Reference path of this type.
///
/// This can be used to create a JSON path
/// like `#/name1/name2/name3`
static var openAPIComponentsKey: String { get }
static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { get }
}

public enum ExamplesName: RefName {
public static var refName: String { return "examples" }
}
/// A type conforming to `AnyStringyContainer` can
/// be asked whether it contains a given string.
public protocol AnyStringyContainer {
func contains(_ key: String) -> Bool
}

public typealias ExamplesDict = RefDict<Components, ExamplesName, OpenAPI.Example>
// MARK: - Reference Support
extension JSONSchema: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "schemas" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.schemas }
}

public enum RequestBodiesName: RefName {
public static var refName: String { return "requestBodies" }
}
extension OpenAPI.Response: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "responses" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.responses }
}

public typealias RequestBodiesDict = RefDict<Components, RequestBodiesName, OpenAPI.Request>
extension OpenAPI.PathItem.Parameter: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "parameters" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.parameters }
}

public enum HeadersName: RefName {
public static var refName: String { return "headers" }
}
extension OpenAPI.Example: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "examples" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.examples }
}

public typealias HeadersDict = RefDict<Components, HeadersName, Header>
extension OpenAPI.Request: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "requestBodies" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.requestBodies }
}

public enum SecuritySchemesName: RefName {
public static var refName: String { return "securitySchemes" }
}
extension OpenAPI.Header: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "headers" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.headers }
}

public typealias SecuritySchemesDict = RefDict<Components, SecuritySchemesName, SecurityScheme>
}
extension OpenAPI.SecurityScheme: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "securitySchemes" }
public static var openAPIComponentsKeyPath: KeyPath<OpenAPI.Components, OrderedDictionary<String, Self>> { \.securitySchemes }
}

extension OpenAPI.Components {
Expand All @@ -108,34 +118,60 @@ extension OpenAPI.Components {
/// instead.
///
/// - throws: If the given reference cannot be checked against `Components`
/// then this method will throw `ReferenceError`. This could be because
/// the given reference is a remote file reference with no local component or
/// an unsafe local reference.
public func contains<Ref: Equatable>(_ ref: JSONReference<Self, Ref>) throws -> Bool {
let localRef: JSONReference<Self, Ref>.Local
if case .internal(let local) = ref {
localRef = local
} else if case .external(_, let local?) = ref {
localRef = local
} else {
/// then this method will throw `ReferenceError`. This will occur when
/// the given reference is a remote file reference.
public func contains<ReferenceType: Equatable & ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>) throws -> Bool {
guard case .internal(let localReference) = reference else {
throw ReferenceError.cannotLookupRemoteReference
}

guard case .node(let internalRef) = localRef else {
throw ReferenceError.cannotLookupUnsafeReference
return contains(localReference)
}

/// Check if the `Components` contains the given internal reference or not.
public func contains<ReferenceType: Equatable & ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>.InternalReference) -> Bool {
return reference.name.map { self[keyPath: ReferenceType.openAPIComponentsKeyPath].contains(key: $0) } ?? false
}

/// Retrieve item referenced from the `Components`.
public subscript<ReferenceType: ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>) -> ReferenceType? {
guard case .internal(let localReference) = reference else {
return nil
}

return contains(internalRef)
return self[localReference]
}

/// Check if the `Components` contains the given internal reference or not.
public func contains<Ref: Equatable>(_ ref: JSONReference<Self, Ref>.InternalReference) -> Bool {
return ref.contained(by: self)
/// Retrieve item referenced from the `Components`.
public subscript<ReferenceType: ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>.InternalReference) -> ReferenceType? {
return reference.name.flatMap { self[keyPath: ReferenceType.openAPIComponentsKeyPath][$0] }
}

public enum ReferenceError: Swift.Error, Equatable {
/// Create a `JSONReference`.
///
/// - throws: If the given name does not refer to an existing component of the given type.
public func reference<ReferenceType: ComponentDictionaryLocatable & Equatable>(named name: String, ofType: ReferenceType.Type) throws -> JSONReference<ReferenceType> {
let internalReference = JSONReference<ReferenceType>.InternalReference.component(name: name)
let reference = JSONReference<ReferenceType>.internal(internalReference)

guard contains(internalReference) else {
throw ReferenceError.missingComponentOnReferenceCreation(name: name, key: ReferenceType.openAPIComponentsKey)
}
return reference
}

public enum ReferenceError: Swift.Error, Equatable, CustomStringConvertible {
case cannotLookupRemoteReference
case cannotLookupUnsafeReference
case missingComponentOnReferenceCreation(name: String, key: String)

public var description: String {
switch self {
case .cannotLookupRemoteReference:
return "You cannot look up remote JSON references in the Components Object local to this file."
case .missingComponentOnReferenceCreation(name: let name, key: let key):
return "You cannot create references to components that do not exist in the Components Object this way. You can construct a `JSONReference` directly if you need to circumvent this protection. '\(name)' was not found in \(key)."
}
}
}
}

Expand All @@ -144,31 +180,31 @@ extension OpenAPI.Components: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

if !schemas.dict.isEmpty {
if !schemas.isEmpty {
try container.encode(schemas, forKey: .schemas)
}

if !responses.dict.isEmpty {
if !responses.isEmpty {
try container.encode(responses, forKey: .responses)
}

if !parameters.dict.isEmpty {
if !parameters.isEmpty {
try container.encode(parameters, forKey: .parameters)
}

if !examples.dict.isEmpty {
if !examples.isEmpty {
try container.encode(examples, forKey: .examples)
}

if !requestBodies.dict.isEmpty {
if !requestBodies.isEmpty {
try container.encode(requestBodies, forKey: .requestBodies)
}

if !headers.dict.isEmpty {
if !headers.isEmpty {
try container.encode(headers, forKey: .headers)
}

if !securitySchemes.dict.isEmpty {
if !securitySchemes.isEmpty {
try container.encode(securitySchemes, forKey: .securitySchemes)
}
}
Expand All @@ -178,25 +214,25 @@ extension OpenAPI.Components: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

schemas = try container.decodeIfPresent(SchemasDict.self, forKey: .schemas)
?? SchemasDict([:])
schemas = try container.decodeIfPresent(OrderedDictionary<String, JSONSchema>.self, forKey: .schemas)
?? [:]

responses = try container.decodeIfPresent(ResponsesDict.self, forKey: .responses)
?? ResponsesDict([:])
responses = try container.decodeIfPresent(OrderedDictionary<String, OpenAPI.Response>.self, forKey: .responses)
?? [:]

parameters = try container.decodeIfPresent(ParametersDict.self, forKey: .parameters)
?? ParametersDict([:])
parameters = try container.decodeIfPresent(OrderedDictionary<String, OpenAPI.PathItem.Parameter>.self, forKey: .parameters)
?? [:]

examples = try container.decodeIfPresent(ExamplesDict.self, forKey: .examples)
?? ExamplesDict([:])
examples = try container.decodeIfPresent(OrderedDictionary<String, OpenAPI.Example>.self, forKey: .examples)
?? [:]

requestBodies = try container.decodeIfPresent(RequestBodiesDict.self, forKey: .requestBodies)
?? RequestBodiesDict([:])
requestBodies = try container.decodeIfPresent(OrderedDictionary<String, OpenAPI.Request>.self, forKey: .requestBodies)
?? [:]

headers = try container.decodeIfPresent(HeadersDict.self, forKey: .headers)
?? HeadersDict([:])
headers = try container.decodeIfPresent(OrderedDictionary<String, OpenAPI.Header>.self, forKey: .headers)
?? [:]

securitySchemes = try container.decodeIfPresent(SecuritySchemesDict.self, forKey: .securitySchemes) ?? SecuritySchemesDict([:])
securitySchemes = try container.decodeIfPresent(OrderedDictionary<String, OpenAPI.SecurityScheme>.self, forKey: .securitySchemes) ?? [:]
}
}

Expand Down
Loading

0 comments on commit 673acd0

Please sign in to comment.