Skip to content

Commit

Permalink
Merge pull request #252 from mattpolzin/feature/237/add-links-object
Browse files Browse the repository at this point in the history
Add Links object
  • Loading branch information
mattpolzin authored Feb 7, 2022
2 parents 6316f4f + 9c14e55 commit 328d119
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ extension OpenAPI.SecurityScheme: ComponentDictionaryLocatable {
public static var openAPIComponentsKeyPath: KeyPath<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 }
}

/// A dereferenceable type can be recursively looked up in
/// the `OpenAPI.Components` until there are no `JSONReferences`
/// left in it or any of its properties.
Expand Down
10 changes: 9 additions & 1 deletion Sources/OpenAPIKit/Components Object/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension OpenAPI {
public var headers: ComponentDictionary<Header>
public var securitySchemes: ComponentDictionary<SecurityScheme>
public var callbacks: ComponentDictionary<Callbacks>
// public var links:
public var links: ComponentDictionary<Link>

/// Dictionary of vendor extensions.
///
Expand All @@ -41,6 +41,7 @@ extension OpenAPI {
requestBodies: ComponentDictionary<Request> = [:],
headers: ComponentDictionary<Header> = [:],
securitySchemes: ComponentDictionary<SecurityScheme> = [:],
links: ComponentDictionary<Link> = [:],
callbacks: ComponentDictionary<Callbacks> = [:],
vendorExtensions: [String: AnyCodable] = [:]
) {
Expand All @@ -51,6 +52,7 @@ extension OpenAPI {
self.requestBodies = requestBodies
self.headers = headers
self.securitySchemes = securitySchemes
self.links = links
self.callbacks = callbacks
self.vendorExtensions = vendorExtensions
}
Expand Down Expand Up @@ -160,6 +162,10 @@ extension OpenAPI.Components: Encodable {
try container.encode(securitySchemes, forKey: .securitySchemes)
}

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

if !callbacks.isEmpty {
try container.encode(callbacks, forKey: .callbacks)
}
Expand Down Expand Up @@ -193,6 +199,8 @@ extension OpenAPI.Components: Decodable {

securitySchemes = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.SecurityScheme>.self, forKey: .securitySchemes) ?? [:]

links = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Link>.self, forKey: .links) ?? [:]

callbacks = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Callbacks>.self, forKey: .callbacks) ?? [:]

vendorExtensions = try Self.extensions(from: decoder)
Expand Down
5 changes: 5 additions & 0 deletions Sources/OpenAPIKit/Either/Either+Convenience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ extension Either where B == OpenAPI.Response {
public var responseValue: B? { b }
}

extension Either where B == OpenAPI.Link {
/// Retrieve the link if that is what this property contains.
public var linkValue: B? { b }
}

extension Either where B == OpenAPI.Content.Map {
/// Retrieve the content map if that is what this property contains.
public var contentValue: B? { b }
Expand Down
249 changes: 245 additions & 4 deletions Sources/OpenAPIKit/Link.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,251 @@
// Created by Mathew Polzin on 1/23/20.
//

// TODO: create validation that operationIds in Link objects
// refer to Operation objects in the document that have the
// given ids.

import Foundation

extension OpenAPI {
// TODO: requires runtime expression to be built first.
/// OpenAPI Spec "Link Object"
///
/// See [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object).
public struct Link: Equatable, CodableVendorExtendable {
/// The **OpenAPI**` `operationRef` or `operationId` field, depending on whether
/// a `URL` of a remote or local Operation Object or a `operationId` (String) of an
/// operation defined in the same document is given.
public let operation: Either<URL, String>
/// A map from parameter names to either runtime expressions that evaluate to values or
/// constant values (`AnyCodable`).
///
/// See the docuemntation for the [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object) for more details.
///
/// Empty dictionaries will be omitted from encoding.
public let parameters: OrderedDictionary<String, Either<RuntimeExpression, AnyCodable>>
/// A literal value or expression to use as a request body when calling the target operation.
public let requestBody: Either<RuntimeExpression, AnyCodable>?
public var description: String?
public let server: Server?

// public struct Link: Equatable {
//
// }
/// Dictionary of vendor extensions.
///
/// These should be of the form:
/// `[ "x-extensionKey": <anything>]`
/// where the values are anything codable.
public var vendorExtensions: [String: AnyCodable]

public init(
operation: Either<URL, String>,
parameters: OrderedDictionary<String, Either<RuntimeExpression, AnyCodable>> = [:],
requestBody: Either<RuntimeExpression, AnyCodable>? = nil,
description: String? = nil,
server: Server? = nil,
vendorExtensions: [String: AnyCodable] = [:]
) {
self.operation = operation
self.parameters = parameters
self.requestBody = requestBody
self.description = description
self.server = server
self.vendorExtensions = vendorExtensions
}

/// Create a Link by referring to an `operationId` of some `Operation` elsewhere
/// in the same document.
public init(
operationId: String,
parameters: OrderedDictionary<String, Either<RuntimeExpression, AnyCodable>> = [:],
requestBody: Either<RuntimeExpression, AnyCodable>? = nil,
description: String? = nil,
server: Server? = nil,
vendorExtensions: [String: AnyCodable] = [:]
) {
self.init(
operation: .b(operationId),
parameters: parameters,
requestBody: requestBody,
description: description,
server: server,
vendorExtensions: vendorExtensions
)
}

/// Create a Link by referring to an `operationRef` pointing to an `Operation`
/// either in the same document or elsewhere.
public init(
operationRef: URL,
parameters: OrderedDictionary<String, Either<RuntimeExpression, AnyCodable>> = [:],
requestBody: Either<RuntimeExpression, AnyCodable>? = nil,
description: String? = nil,
server: Server? = nil,
vendorExtensions: [String: AnyCodable] = [:]
) {
self.init(
operation: .a(operationRef),
parameters: parameters,
requestBody: requestBody,
description: description,
server: server,
vendorExtensions: vendorExtensions
)
}
}
}

extension OpenAPI.Link {
public typealias Map = OrderedDictionary<String, Either<JSONReference<OpenAPI.Link>, OpenAPI.Link>>
}

// MARK: `Either` convenience methods
extension Either where A == JSONReference<OpenAPI.Link>, B == OpenAPI.Link {

public static func link(
operationId: String,
parameters: OrderedDictionary<String, Either<OpenAPI.RuntimeExpression, AnyCodable>> = [:],
requestBody: Either<OpenAPI.RuntimeExpression, AnyCodable>? = nil,
description: String? = nil,
server: OpenAPI.Server? = nil
) -> Self {
return .b(
.init(
operationId: operationId,
parameters: parameters,
requestBody: requestBody,
description: description,
server: server
)
)
}

public static func link(
operationRef: URL,
parameters: OrderedDictionary<String, Either<OpenAPI.RuntimeExpression, AnyCodable>> = [:],
requestBody: Either<OpenAPI.RuntimeExpression, AnyCodable>? = nil,
description: String? = nil,
server: OpenAPI.Server? = nil
) -> Self {
return .b(
.init(
operationRef: operationRef,
parameters: parameters,
requestBody: requestBody,
description: description,
server: server
)
)
}
}

// MARK: - Codable

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

switch operation {
case .a(let url):
try container.encode(url.absoluteString, forKey: .operationRef)
case .b(let id):
try container.encode(id, forKey: .operationId)
}

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

try container.encodeIfPresent(requestBody, forKey: .requestBody)
try container.encodeIfPresent(description, forKey: .description)
try container.encodeIfPresent(server, forKey: .server)

try encodeExtensions(to: &container)
}
}

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

if let operationString = try container.decodeIfPresent(String.self, forKey: .operationId) {
operation = .b(operationString)
} else {
operation = .a(try container.decodeURLAsString(forKey: .operationRef))
}

parameters = try container.decodeIfPresent(OrderedDictionary<String, Either<OpenAPI.RuntimeExpression, AnyCodable>>.self, forKey: .parameters) ?? [:]

requestBody = try container.decodeIfPresent(Either<OpenAPI.RuntimeExpression, AnyCodable>.self, forKey: .requestBody)
description = try container.decodeIfPresent(String.self, forKey: .description)
server = try container.decodeIfPresent(OpenAPI.Server.self, forKey: .server)

vendorExtensions = try Self.extensions(from: decoder)
}
}

extension OpenAPI.Link {
internal enum CodingKeys: ExtendableCodingKey {
case operationId
case operationRef
case parameters
case requestBody
case description
case server

case extended(String)

static var allBuiltinKeys: [CodingKeys] {
return [
.operationId,
.operationRef,
.parameters,
.requestBody,
.description,
.server
]
}

static func extendedKey(for value: String) -> CodingKeys {
return .extended(value)
}

init?(stringValue: String) {
switch stringValue {
case "operationId":
self = .operationId
case "operationRef":
self = .operationRef
case "parameters":
self = .parameters
case "requestBody":
self = .requestBody
case "description":
self = .description
case "server":
self = .server
default:
self = .extendedKey(for: stringValue)
}
}

var stringValue: String {
switch self {
case .operationId:
return "operationId"
case .operationRef:
return "operationRef"
case .parameters:
return "parameters"
case .requestBody:
return "requestBody"
case .description:
return "description"
case .server:
return "server"
case .extended(let key):
return key
}
}
}
}

extension OpenAPI.Link: Validatable {}
15 changes: 12 additions & 3 deletions Sources/OpenAPIKit/Response/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension OpenAPI {
public var description: String
public var headers: Header.Map?
public var content: Content.Map
// public var links:
public var links: Link.Map

/// Dictionary of vendor extensions.
///
Expand All @@ -26,11 +26,13 @@ extension OpenAPI {
description: String,
headers: Header.Map? = nil,
content: Content.Map = [:],
links: Link.Map = [:],
vendorExtensions: [String: AnyCodable] = [:]
) {
self.description = description
self.headers = headers
self.content = content
self.links = links
self.vendorExtensions = vendorExtensions
}
}
Expand Down Expand Up @@ -154,13 +156,15 @@ extension Either where A == JSONReference<OpenAPI.Response>, B == OpenAPI.Respon
public static func response(
description: String,
headers: OpenAPI.Header.Map? = nil,
content: OpenAPI.Content.Map = [:]
content: OpenAPI.Content.Map = [:],
links: OpenAPI.Link.Map = [:]
) -> Self {
return .b(
.init(
description: description,
headers: headers,
content: content
content: content,
links: links
)
)
}
Expand Down Expand Up @@ -232,6 +236,10 @@ extension OpenAPI.Response: Encodable {
try container.encode(content, forKey: .content)
}

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

try encodeExtensions(to: &container)
}
}
Expand All @@ -244,6 +252,7 @@ extension OpenAPI.Response: Decodable {
description = try container.decode(String.self, forKey: .description)
headers = try container.decodeIfPresent(OpenAPI.Header.Map.self, forKey: .headers)
content = try container.decodeIfPresent(OpenAPI.Content.Map.self, forKey: .content) ?? [:]
links = try container.decodeIfPresent(OpenAPI.Link.Map.self, forKey: .links) ?? [:]

vendorExtensions = try Self.extensions(from: decoder)

Expand Down
Loading

0 comments on commit 328d119

Please sign in to comment.