diff --git a/Sources/OpenAPIKit/Callback.swift b/Sources/OpenAPIKit/Callback.swift new file mode 100644 index 000000000..68d1062a7 --- /dev/null +++ b/Sources/OpenAPIKit/Callback.swift @@ -0,0 +1,76 @@ +// +// Callback.swift +// +// +// Created by Mathew Polzin on 11/1/20. +// + +import Foundation + +extension OpenAPI { + + /// A URL template where the placeholders are OpenAPI **Runtime Expressions** instead + /// of named variables. + /// + /// See [OpenAPI Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#callback-object) and [OpenAPI Runtime Expression](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#runtime-expressions) for more. + /// + public struct CallbackURL: Hashable, RawRepresentable { + public let template: URLTemplate + + /// The string value of the URL without variable replacement. + /// + /// Variables cannot be replaced based on other information in the + /// OpenAPI document; they are only available at "runtime" which is + /// where the name of the OpenAPI structure `CallbackURL` + /// represents comes from. + public var rawValue: String { + template.rawValue + } + + /// Get a URL from the runtime expression if it is a valid URL without + /// variable replacement. + /// + /// Callback URLs with variables in them will not be valid URLs + /// and are therefore guaranteed to return `nil`. + public var url: URL? { + template.url + } + + /// Create a CallbackURL from the string if possible. + public init?(rawValue: String) { + guard let template = URLTemplate(rawValue: rawValue) else { + return nil + } + self.template = template + } + + public init(url: URL) { + template = .init(url: url) + } + } + + /// A map from runtime expressions to path items to be used as + /// callbacks for the API. + /// + /// See [OpenAPI Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#callback-object). + /// + public typealias Callback = OrderedDictionary + + public typealias CallbackMap = OrderedDictionary, Callback>> +} + +extension OpenAPI.CallbackURL: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(rawValue) + } +} + +extension OpenAPI.CallbackURL: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + template = try container.decode(URLTemplate.self) + } +} diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index e508c2a3a..7348725e0 100644 --- a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift @@ -26,6 +26,11 @@ extension OpenAPI.Response: ComponentDictionaryLocatable { public static var openAPIComponentsKeyPath: KeyPath> { \.responses } } +extension OpenAPI.Callback: ComponentDictionaryLocatable { + public static var openAPIComponentsKey: String { "callbacks" } + public static var openAPIComponentsKeyPath: KeyPath> { \.callbacks } +} + extension OpenAPI.Parameter: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "parameters" } public static var openAPIComponentsKeyPath: KeyPath> { \.parameters } diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index cd0c841c1..d4a4dfb1c 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -23,8 +23,8 @@ extension OpenAPI { public var requestBodies: ComponentDictionary public var headers: ComponentDictionary
public var securitySchemes: ComponentDictionary + public var callbacks: ComponentDictionary // public var links: - // public var callbacks: /// Dictionary of vendor extensions. /// @@ -41,6 +41,7 @@ extension OpenAPI { requestBodies: ComponentDictionary = [:], headers: ComponentDictionary
= [:], securitySchemes: ComponentDictionary = [:], + callbacks: ComponentDictionary = [:], vendorExtensions: [String: AnyCodable] = [:] ) { self.schemas = schemas @@ -50,6 +51,7 @@ extension OpenAPI { self.requestBodies = requestBodies self.headers = headers self.securitySchemes = securitySchemes + self.callbacks = callbacks self.vendorExtensions = vendorExtensions } @@ -158,6 +160,10 @@ extension OpenAPI.Components: Encodable { try container.encode(securitySchemes, forKey: .securitySchemes) } + if !callbacks.isEmpty { + try container.encode(callbacks, forKey: .callbacks) + } + try encodeExtensions(to: &container) } } @@ -187,6 +193,8 @@ extension OpenAPI.Components: Decodable { securitySchemes = try container.decodeIfPresent(OpenAPI.ComponentDictionary.self, forKey: .securitySchemes) ?? [:] + callbacks = try container.decodeIfPresent(OpenAPI.ComponentDictionary.self, forKey: .callbacks) ?? [:] + vendorExtensions = try Self.extensions(from: decoder) } catch let error as DecodingError { if let underlyingError = error.underlyingError as? KeyDecodingError { diff --git a/Sources/OpenAPIKit/Operation/Operation.swift b/Sources/OpenAPIKit/Operation/Operation.swift index 69bb44ac7..8893c8bdc 100644 --- a/Sources/OpenAPIKit/Operation/Operation.swift +++ b/Sources/OpenAPIKit/Operation/Operation.swift @@ -59,7 +59,13 @@ extension OpenAPI { /// foundResponse = document.components.lookup(successResponse)! /// public var responses: OpenAPI.Response.Map -// public let callbacks: + + /// A map of possible out-of band callbacks related to the parent operation. + /// + /// The key is a unique identifier for the Callback Object. Each value in the + /// map is a Callback Object that describes a request that may be initiated + /// by the API provider and the expected responses. + public let callbacks: OpenAPI.CallbackMap /// Indicates that the operation is deprecated or not. /// @@ -103,6 +109,7 @@ extension OpenAPI { parameters: Parameter.Array = [], requestBody: Either, OpenAPI.Request>, responses: OpenAPI.Response.Map, + callbacks: OpenAPI.CallbackMap = [:], deprecated: Bool = false, security: [OpenAPI.SecurityRequirement]? = nil, servers: [OpenAPI.Server]? = nil, @@ -116,6 +123,7 @@ extension OpenAPI { self.parameters = parameters self.requestBody = requestBody self.responses = responses + self.callbacks = callbacks self.deprecated = deprecated self.security = security self.servers = servers @@ -132,6 +140,7 @@ extension OpenAPI { parameters: Parameter.Array = [], requestBody: OpenAPI.Request? = nil, responses: OpenAPI.Response.Map, + callbacks: OpenAPI.CallbackMap = [:], deprecated: Bool = false, security: [OpenAPI.SecurityRequirement]? = nil, servers: [OpenAPI.Server]? = nil, @@ -145,6 +154,7 @@ extension OpenAPI { self.parameters = parameters self.requestBody = requestBody.map(Either.init) self.responses = responses + self.callbacks = callbacks self.deprecated = deprecated self.security = security self.servers = servers @@ -161,6 +171,7 @@ extension OpenAPI { parameters: Parameter.Array = [], requestBody: OpenAPI.Request? = nil, responses: OpenAPI.Response.Map, + callbacks: OpenAPI.CallbackMap = [:], deprecated: Bool = false, security: [OpenAPI.SecurityRequirement]? = nil, servers: [OpenAPI.Server]? = nil, @@ -175,6 +186,7 @@ extension OpenAPI { parameters: parameters, requestBody: requestBody, responses: responses, + callbacks: callbacks, deprecated: deprecated, security: security, servers: servers, @@ -229,6 +241,10 @@ extension OpenAPI.Operation: Encodable { try container.encode(responses, forKey: .responses) + if !callbacks.isEmpty { + try container.encode(callbacks, forKey: .callbacks) + } + if deprecated { try container.encode(deprecated, forKey: .deprecated) } @@ -264,6 +280,8 @@ extension OpenAPI.Operation: Decodable { responses = try container.decode(OpenAPI.Response.Map.self, forKey: .responses) + callbacks = try container.decodeIfPresent(OpenAPI.CallbackMap.self, forKey: .callbacks) ?? [:] + deprecated = try container.decodeIfPresent(Bool.self, forKey: .deprecated) ?? false security = try decodeSecurityRequirements(from: container, forKey: .security, given: nil) diff --git a/Sources/OpenAPIKit/URLTemplate.swift b/Sources/OpenAPIKit/URLTemplate.swift index c234b3e09..6b8efbe77 100644 --- a/Sources/OpenAPIKit/URLTemplate.swift +++ b/Sources/OpenAPIKit/URLTemplate.swift @@ -51,7 +51,7 @@ public struct URLTemplate: Hashable, RawRepresentable { /// Get a URL from this templated URL if it is a valid URL already. /// - /// Templated URLS with variables in them will not be valid URLs + /// Templated URLs with variables in them will not be valid URLs /// and are therefore guaranteed to return `nil`. public var url: URL? { return URL(string: rawValue) diff --git a/documentation/specification_coverage.md b/documentation/specification_coverage.md index 8a4d40cc6..efb00345f 100644 --- a/documentation/specification_coverage.md +++ b/documentation/specification_coverage.md @@ -88,7 +88,7 @@ For more information on the OpenAPIKit types, see the [full type documentation]( - [x] headers - [x] securitySchemes - [ ] links -- [ ] callbacks +- [x] callbacks - [x] specification extensions (`vendorExtensions`) ### Paths Object (`OpenAPI.PathItem.Map`) @@ -119,7 +119,7 @@ For more information on the OpenAPIKit types, see the [full type documentation]( - [x] parameters - [x] requestBody - [x] responses -- [ ] callbacks +- [x] callbacks - [x] deprecated - [x] security - [x] servers @@ -179,7 +179,7 @@ For more information on the OpenAPIKit types, see the [full type documentation]( - [x] specification extensions (`vendorExtensions`) ### Callback Object -- [ ] *{expression}* +- [x] *{expression}* - [ ] specification extensions ### Example Object (`OpenAPI.Example`) @@ -282,4 +282,4 @@ For more information on the OpenAPIKit types, see the [full type documentation]( - [ ] specification extensions ### Security Requirement Object (`OpenAPI.Document.SecurityRequirement`) -- [x] *{name}* (using `JSONReferences` instead of a stringy API) \ No newline at end of file +- [x] *{name}* (using `JSONReferences` instead of a stringy API)