From 8817f8472f1f748e0e28303d587bf4a08f849996 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:16:32 -0600 Subject: [PATCH 01/30] start to create compatibility layer between v3.0 and v3.1 documents --- Package.swift | 9 +- Sources/OpenAPIKitCompat/Compat30To31.swift | 144 ++++++++++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 Sources/OpenAPIKitCompat/Compat30To31.swift diff --git a/Package.swift b/Package.swift index 9becff554..982d0a01f 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,9 @@ let package = Package( .library( name: "OpenAPIKit", targets: ["OpenAPIKit"]), + .library( + name: "OpenAPIKitCompat", + targets: ["OpenAPIKitCompat"]), ], dependencies: [ .package(url: "https://github.com/jpsim/Yams.git", "4.0.0"..<"6.0.0") // just for tests @@ -60,7 +63,11 @@ let package = Package( dependencies: ["OpenAPIKit", "Yams"]), .testTarget( name: "OpenAPIKitErrorReportingTests", - dependencies: ["OpenAPIKit", "Yams"]) + dependencies: ["OpenAPIKit", "Yams"]), + + .target( + name: "OpenAPIKitCompat", + dependencies: ["OpenAPIKit30", "OpenAPIKit"]), ], swiftLanguageVersions: [ .v5 ] ) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift new file mode 100644 index 000000000..64ef1bca6 --- /dev/null +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -0,0 +1,144 @@ +// +// Compat30To31.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + + +import OpenAPIKit +import OpenAPIKit30 + +private typealias OpenAPI31 = OpenAPIKit.OpenAPI +private typealias OpenAPI30 = OpenAPIKit30.OpenAPI + +public extension OpenAPIKit30.OpenAPI.Document { + func `convert`(to version: OpenAPIKit.OpenAPI.Document.Version) -> OpenAPIKit.OpenAPI.Document { + switch version { + case .v3_1_0: + return self.to31() + } + } +} + +private extension OpenAPIKit30.OpenAPI.Document { + func to31() -> OpenAPI31.Document { + let info = OpenAPI31.Document.Info( + title: info.title, version: info.version + ) + + let servers = servers.map { $0.to31() } + + let paths = paths.mapValues { $0.to31() } + + return OpenAPI31.Document( + info: info, + servers: servers, + paths: paths, + components: components.to31() + ) + } +} + +private extension OpenAPIKit30.OpenAPI.Server { + func to31() -> OpenAPI31.Server { + + let variables = variables.mapValues { variable in + OpenAPI31.Server.Variable( + enum: variable.enum, + default: variable.default, + description: variable.description, + vendorExtensions: variable.vendorExtensions + ) + } + + return OpenAPI31.Server( + urlTemplate: urlTemplate, + description: description, + variables: variables, + vendorExtensions: vendorExtensions + ) + } +} + +private extension OpenAPIKit30.OpenAPI.PathItem { + func to31() -> OpenAPI31.PathItem { + OpenAPI31.PathItem( + summary: <#T##String?#> + ) + } +} + +private extension OpenAPIKit30.OpenAPI.SecurityScheme { + func to31() -> OpenAPI31.SecurityScheme { + OpenAPI31.SecurityScheme( + type: <#T##OpenAPI.SecurityScheme.SecurityType#> + ) + } +} + +private extension OpenAPIKit30.JSONSchema { + func to31() -> OpenAPIKit.JSONSchema { + let schema: OpenAPIKit.JSONSchema.Schema + + switch value { + case .boolean(let core): + schema = .boolean( + .init( + format: core.format, + required: core.required, + nullable: core.nullable, + permissions: core.permissions, + deprecated: core.deprecated, + title: core.title, + description: core.description, + discriminator: core.discriminator, + externalDocs: <#T##OpenAPI.ExternalDocumentation?#> + ) + ) + case .number(_, _): + <#code#> + case .integer(_, _): + <#code#> + case .string(_, _): + <#code#> + case .object(_, _): + <#code#> + case .array(_, _): + <#code#> + case .all(of: let of, core: let core): + <#code#> + case .one(of: let of, core: let core): + <#code#> + case .any(of: let of, core: let core): + <#code#> + case .not(_, core: let core): + <#code#> + case .reference(_, _): + <#code#> + case .fragment(_): + <#code#> + } + + OpenAPIKit.JSONSchema( + schema: schema + ) + } +} + +private extension OpenAPIKit30.OpenAPI.Components { + func to31() -> OpenAPI31.Components { + OpenAPI31.Components( + schemas: schemas.mapValues { $0.to31() }, + responses: responses.mapValues { $0.to31() }, + parameters: parameters.mapValues { $0.to31() }, + examples: examples.mapValues { $0.to31() }, + requestBodies: requestBodies.mapValues { $0.to31() }, + headers: headers.mapValues { $0.to31() }, + securitySchemes: securitySchemes.mapValues { $0.to31() }, + links: links.mapValues { $0.to31() }, + callbacks: callbacks.mapValues { $0.to31() }, + vendorExtensions: vendorExtensions.mapValues { $0.to31() } + ) + } +} From 2cfe6e24161a41c214fb8c408b128067c22cb140 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:18:45 -0600 Subject: [PATCH 02/30] Alias stuff into desired namespaces in re-export file. --- Sources/OpenAPIKit/_CoreReExport.swift | 28 +++++++++++++++++++++++- Sources/OpenAPIKit30/_CoreReExport.swift | 28 +++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index c342da20f..1fecc086a 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -8,7 +8,33 @@ @_exported import struct OpenAPIKitCore.AnyCodable @_exported import struct OpenAPIKitCore.CodingPathError @_exported import enum OpenAPIKitCore.Either -@_exported import enum OpenAPIKitCore.OpenAPI @_exported import protocol OpenAPIKitCore.OpenAPIError @_exported import struct OpenAPIKitCore.OrderedDictionary @_exported import struct OpenAPIKitCore.URLTemplate + +import OpenAPIKitCore + +public extension OpenAPI { + typealias HttpMethod = OpenAPIKitCore.HttpMethod + typealias ContentType = OpenAPIKitCore.ContentType + typealias Error = OpenAPIKitCore.Error + typealias Warning = OpenAPIKitCore.Warning + typealias Path = OpenAPIKitCore.Path + typealias ComponentKey = OpenAPIKitCore.ComponentKey + typealias Discriminator = OpenAPIKitCore.Discriminator + typealias ExternalDocumentation = OpenAPIKitCore.ExternalDocumentation +} + +public extension JSONSchema { + typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions +} + +public extension JSONTypeFormat { + typealias AnyFormat = OpenAPIKitCore.AnyFormat + typealias BooleanFormat = OpenAPIKitCore.BooleanFormat + typealias ObjectFormat = OpenAPIKitCore.ObjectFormat + typealias ArrayFormat = OpenAPIKitCore.ArrayFormat + typealias NumberFormat = OpenAPIKitCore.NumberFormat + typealias IntegerFormat = OpenAPIKitCore.IntegerFormat + typealias StringFormat = OpenAPIKitCore.StringFormat +} diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index c342da20f..1fecc086a 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -8,7 +8,33 @@ @_exported import struct OpenAPIKitCore.AnyCodable @_exported import struct OpenAPIKitCore.CodingPathError @_exported import enum OpenAPIKitCore.Either -@_exported import enum OpenAPIKitCore.OpenAPI @_exported import protocol OpenAPIKitCore.OpenAPIError @_exported import struct OpenAPIKitCore.OrderedDictionary @_exported import struct OpenAPIKitCore.URLTemplate + +import OpenAPIKitCore + +public extension OpenAPI { + typealias HttpMethod = OpenAPIKitCore.HttpMethod + typealias ContentType = OpenAPIKitCore.ContentType + typealias Error = OpenAPIKitCore.Error + typealias Warning = OpenAPIKitCore.Warning + typealias Path = OpenAPIKitCore.Path + typealias ComponentKey = OpenAPIKitCore.ComponentKey + typealias Discriminator = OpenAPIKitCore.Discriminator + typealias ExternalDocumentation = OpenAPIKitCore.ExternalDocumentation +} + +public extension JSONSchema { + typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions +} + +public extension JSONTypeFormat { + typealias AnyFormat = OpenAPIKitCore.AnyFormat + typealias BooleanFormat = OpenAPIKitCore.BooleanFormat + typealias ObjectFormat = OpenAPIKitCore.ObjectFormat + typealias ArrayFormat = OpenAPIKitCore.ArrayFormat + typealias NumberFormat = OpenAPIKitCore.NumberFormat + typealias IntegerFormat = OpenAPIKitCore.IntegerFormat + typealias StringFormat = OpenAPIKitCore.StringFormat +} From 395b6c0e413493ad2b3683c636fabbe8903ba2d0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:20:03 -0600 Subject: [PATCH 03/30] Move Discriminator into core --- Sources/OpenAPIKit30/Discriminator.swift | 53 ------------------- .../Discriminator.swift | 34 ++++++------ 2 files changed, 15 insertions(+), 72 deletions(-) delete mode 100644 Sources/OpenAPIKit30/Discriminator.swift rename Sources/{OpenAPIKit => OpenAPIKitCore}/Discriminator.swift (51%) diff --git a/Sources/OpenAPIKit30/Discriminator.swift b/Sources/OpenAPIKit30/Discriminator.swift deleted file mode 100644 index e157f0266..000000000 --- a/Sources/OpenAPIKit30/Discriminator.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Discriminator.swift -// -// -// Created by Mathew Polzin on 10/6/19. -// - -import OpenAPIKitCore - -extension OpenAPI { - /// OpenAPI Spec "Disciminator Object" - /// - /// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). - public struct Discriminator: Equatable { - public let propertyName: String - public let mapping: [String: String]? - - public init(propertyName: String, - mapping: [String: String]? = nil) { - self.propertyName = propertyName - self.mapping = mapping - } - } -} - -// MARK: - Codable - -extension OpenAPI.Discriminator: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(propertyName, forKey: .propertyName) - try container.encodeIfPresent(mapping, forKey: .mapping) - } -} - -extension OpenAPI.Discriminator: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - propertyName = try container.decode(String.self, forKey: .propertyName) - mapping = try container.decodeIfPresent([String: String].self, forKey: .mapping) - } -} - -extension OpenAPI.Discriminator { - private enum CodingKeys: String, CodingKey { - case propertyName - case mapping - } -} - -extension OpenAPI.Discriminator: Validatable {} diff --git a/Sources/OpenAPIKit/Discriminator.swift b/Sources/OpenAPIKitCore/Discriminator.swift similarity index 51% rename from Sources/OpenAPIKit/Discriminator.swift rename to Sources/OpenAPIKitCore/Discriminator.swift index e157f0266..b06e98f9d 100644 --- a/Sources/OpenAPIKit/Discriminator.swift +++ b/Sources/OpenAPIKitCore/Discriminator.swift @@ -5,27 +5,23 @@ // Created by Mathew Polzin on 10/6/19. // -import OpenAPIKitCore - -extension OpenAPI { - /// OpenAPI Spec "Disciminator Object" - /// - /// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). - public struct Discriminator: Equatable { - public let propertyName: String - public let mapping: [String: String]? - - public init(propertyName: String, - mapping: [String: String]? = nil) { - self.propertyName = propertyName - self.mapping = mapping - } +/// OpenAPI Spec "Disciminator Object" +/// +/// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). +public struct Discriminator: Equatable { + public let propertyName: String + public let mapping: [String: String]? + + public init(propertyName: String, + mapping: [String: String]? = nil) { + self.propertyName = propertyName + self.mapping = mapping } } // MARK: - Codable -extension OpenAPI.Discriminator: Encodable { +extension Discriminator: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -34,7 +30,7 @@ extension OpenAPI.Discriminator: Encodable { } } -extension OpenAPI.Discriminator: Decodable { +extension Discriminator: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -43,11 +39,11 @@ extension OpenAPI.Discriminator: Decodable { } } -extension OpenAPI.Discriminator { +extension Discriminator { private enum CodingKeys: String, CodingKey { case propertyName case mapping } } -extension OpenAPI.Discriminator: Validatable {} +extension Discriminator: Validatable {} From 5f68fcc903b356f51469e71be5f5ed4dbb7233ab Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:21:18 -0600 Subject: [PATCH 04/30] recreate OpenAPI namespace in each version. --- Sources/OpenAPIKit/OpenAPI.swift | 9 +++++++++ Sources/OpenAPIKit30/OpenAPI.swift | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 Sources/OpenAPIKit/OpenAPI.swift create mode 100644 Sources/OpenAPIKit30/OpenAPI.swift diff --git a/Sources/OpenAPIKit/OpenAPI.swift b/Sources/OpenAPIKit/OpenAPI.swift new file mode 100644 index 000000000..f56c419b7 --- /dev/null +++ b/Sources/OpenAPIKit/OpenAPI.swift @@ -0,0 +1,9 @@ +// +// OpenAPI.swift +// +// +// Created by Mathew Polzin on 6/22/19. +// + +/// The OpenAPI namespace +public enum OpenAPI {} diff --git a/Sources/OpenAPIKit30/OpenAPI.swift b/Sources/OpenAPIKit30/OpenAPI.swift new file mode 100644 index 000000000..f56c419b7 --- /dev/null +++ b/Sources/OpenAPIKit30/OpenAPI.swift @@ -0,0 +1,9 @@ +// +// OpenAPI.swift +// +// +// Created by Mathew Polzin on 6/22/19. +// + +/// The OpenAPI namespace +public enum OpenAPI {} From 28ecfa333dac8378248a077fddd3f43a6e1dbf6c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:22:48 -0600 Subject: [PATCH 05/30] Move Path type into core. --- Sources/OpenAPIKit/Path Item/PathItem.swift | 31 ---------------- Sources/OpenAPIKit30/Path Item/PathItem.swift | 31 ---------------- Sources/OpenAPIKitCore/Path.swift | 35 +++++++++++++++++++ 3 files changed, 35 insertions(+), 62 deletions(-) create mode 100644 Sources/OpenAPIKitCore/Path.swift diff --git a/Sources/OpenAPIKit/Path Item/PathItem.swift b/Sources/OpenAPIKit/Path Item/PathItem.swift index 2ea0b5bdc..326d1dea0 100644 --- a/Sources/OpenAPIKit/Path Item/PathItem.swift +++ b/Sources/OpenAPIKit/Path Item/PathItem.swift @@ -7,37 +7,6 @@ import OpenAPIKitCore -extension OpenAPI { - /// OpenAPI Spec "Paths Object" path field pattern support. - /// - /// See [OpenAPI Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#paths-object) - /// and [OpenAPI Patterned Fields](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#patterned-fields). - public struct Path: RawRepresentable, Equatable, Hashable { - public let components: [String] - - public init(_ components: [String]) { - self.components = components - } - - public init(rawValue: String) { - let pathComponents = rawValue.split(separator: "/").map(String.init) - components = pathComponents.count > 0 && pathComponents[0].isEmpty - ? Array(pathComponents.dropFirst()) - : pathComponents - } - - public var rawValue: String { - return "/\(components.joined(separator: "/"))" - } - } -} - -extension OpenAPI.Path: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self.init(rawValue: value) - } -} - extension OpenAPI { /// OpenAPI Spec "Path Item Object" /// diff --git a/Sources/OpenAPIKit30/Path Item/PathItem.swift b/Sources/OpenAPIKit30/Path Item/PathItem.swift index 15d3fc3ec..c9c5e9c89 100644 --- a/Sources/OpenAPIKit30/Path Item/PathItem.swift +++ b/Sources/OpenAPIKit30/Path Item/PathItem.swift @@ -7,37 +7,6 @@ import OpenAPIKitCore -extension OpenAPI { - /// OpenAPI Spec "Paths Object" path field pattern support. - /// - /// See [OpenAPI Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#paths-object) - /// and [OpenAPI Patterned Fields](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#patterned-fields). - public struct Path: RawRepresentable, Equatable, Hashable { - public let components: [String] - - public init(_ components: [String]) { - self.components = components - } - - public init(rawValue: String) { - let pathComponents = rawValue.split(separator: "/").map(String.init) - components = pathComponents.count > 0 && pathComponents[0].isEmpty - ? Array(pathComponents.dropFirst()) - : pathComponents - } - - public var rawValue: String { - return "/\(components.joined(separator: "/"))" - } - } -} - -extension OpenAPI.Path: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self.init(rawValue: value) - } -} - extension OpenAPI { /// OpenAPI Spec "Path Item Object" /// diff --git a/Sources/OpenAPIKitCore/Path.swift b/Sources/OpenAPIKitCore/Path.swift new file mode 100644 index 000000000..d729ecb1f --- /dev/null +++ b/Sources/OpenAPIKitCore/Path.swift @@ -0,0 +1,35 @@ +// +// Path.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + +/// OpenAPI Spec "Paths Object" path field pattern support. +/// +/// See [OpenAPI Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#paths-object) +/// and [OpenAPI Patterned Fields](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#patterned-fields). +public struct Path: RawRepresentable, Equatable, Hashable { + public let components: [String] + + public init(_ components: [String]) { + self.components = components + } + + public init(rawValue: String) { + let pathComponents = rawValue.split(separator: "/").map(String.init) + components = pathComponents.count > 0 && pathComponents[0].isEmpty + ? Array(pathComponents.dropFirst()) + : pathComponents + } + + public var rawValue: String { + return "/\(components.joined(separator: "/"))" + } +} + +extension Path: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(rawValue: value) + } +} From 10045c77f1a2d1b83ec924a45b15ac80bae2f1a3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:25:48 -0600 Subject: [PATCH 06/30] Move ComponentKey type into core. --- .../Components Object/Components.swift | 58 ---------------- .../Components Object/Components.swift | 58 ---------------- Sources/OpenAPIKitCore/ComponentKey.swift | 67 +++++++++++++++++++ 3 files changed, 67 insertions(+), 116 deletions(-) create mode 100644 Sources/OpenAPIKitCore/ComponentKey.swift diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index b818eb338..599e5788b 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -72,64 +72,6 @@ extension OpenAPI { } extension OpenAPI { - /// A key for one of the component dictionaries. - /// - /// These keys must match the regex - /// `^[a-zA-Z0-9\.\-_]+$`. - public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider { - public let rawValue: String - - public init(stringLiteral value: StringLiteralType) { - self.rawValue = value - } - - public init?(rawValue: String) { - guard !rawValue.isEmpty else { - return nil - } - var allowedCharacters = CharacterSet.alphanumerics - allowedCharacters.insert(charactersIn: "-_.") - guard CharacterSet(charactersIn: rawValue).isSubset(of: allowedCharacters) else { - return nil - } - self.rawValue = rawValue - } - - public static func problem(with proposedString: String) -> String? { - if Self(rawValue: proposedString) == nil { - return "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(proposedString)' does not.." - } - return nil - } - - public init(from decoder: Decoder) throws { - let rawValue = try decoder.singleValueContainer().decode(String.self) - guard let key = Self(rawValue: rawValue) else { - throw InconsistencyError( - subjectName: "Component Key", - details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", - codingPath: decoder.codingPath - ) - } - self = key - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - // we check for consistency on encode because a string literal - // may result in an invalid component key being constructed. - guard Self(rawValue: rawValue) != nil else { - throw InconsistencyError( - subjectName: "Component Key", - details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", - codingPath: container.codingPath - ) - } - - try container.encode(rawValue) - } - } public typealias ComponentDictionary = OrderedDictionary } diff --git a/Sources/OpenAPIKit30/Components Object/Components.swift b/Sources/OpenAPIKit30/Components Object/Components.swift index b90f8b053..54d8d701f 100644 --- a/Sources/OpenAPIKit30/Components Object/Components.swift +++ b/Sources/OpenAPIKit30/Components Object/Components.swift @@ -68,64 +68,6 @@ extension OpenAPI { } extension OpenAPI { - /// A key for one of the component dictionaries. - /// - /// These keys must match the regex - /// `^[a-zA-Z0-9\.\-_]+$`. - public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider { - public let rawValue: String - - public init(stringLiteral value: StringLiteralType) { - self.rawValue = value - } - - public init?(rawValue: String) { - guard !rawValue.isEmpty else { - return nil - } - var allowedCharacters = CharacterSet.alphanumerics - allowedCharacters.insert(charactersIn: "-_.") - guard CharacterSet(charactersIn: rawValue).isSubset(of: allowedCharacters) else { - return nil - } - self.rawValue = rawValue - } - - public static func problem(with proposedString: String) -> String? { - if Self(rawValue: proposedString) == nil { - return "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(proposedString)' does not.." - } - return nil - } - - public init(from decoder: Decoder) throws { - let rawValue = try decoder.singleValueContainer().decode(String.self) - guard let key = Self(rawValue: rawValue) else { - throw InconsistencyError( - subjectName: "Component Key", - details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", - codingPath: decoder.codingPath - ) - } - self = key - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - // we check for consistency on encode because a string literal - // may result in an invalid component key being constructed. - guard Self(rawValue: rawValue) != nil else { - throw InconsistencyError( - subjectName: "Component Key", - details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", - codingPath: container.codingPath - ) - } - - try container.encode(rawValue) - } - } public typealias ComponentDictionary = OrderedDictionary } diff --git a/Sources/OpenAPIKitCore/ComponentKey.swift b/Sources/OpenAPIKitCore/ComponentKey.swift new file mode 100644 index 000000000..7774f41bf --- /dev/null +++ b/Sources/OpenAPIKitCore/ComponentKey.swift @@ -0,0 +1,67 @@ +// +// ComponentKey.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + +import Foundation + +/// A key for one of the component dictionaries. +/// +/// These keys must match the regex +/// `^[a-zA-Z0-9\.\-_]+$`. +public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider { + public let rawValue: String + + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } + + public init?(rawValue: String) { + guard !rawValue.isEmpty else { + return nil + } + var allowedCharacters = CharacterSet.alphanumerics + allowedCharacters.insert(charactersIn: "-_.") + guard CharacterSet(charactersIn: rawValue).isSubset(of: allowedCharacters) else { + return nil + } + self.rawValue = rawValue + } + + public static func problem(with proposedString: String) -> String? { + if Self(rawValue: proposedString) == nil { + return "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(proposedString)' does not.." + } + return nil + } + + public init(from decoder: Decoder) throws { + let rawValue = try decoder.singleValueContainer().decode(String.self) + guard let key = Self(rawValue: rawValue) else { + throw InconsistencyError( + subjectName: "Component Key", + details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", + codingPath: decoder.codingPath + ) + } + self = key + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + // we check for consistency on encode because a string literal + // may result in an invalid component key being constructed. + guard Self(rawValue: rawValue) != nil else { + throw InconsistencyError( + subjectName: "Component Key", + details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", + codingPath: container.codingPath + ) + } + + try container.encode(rawValue) + } + } From 96ef49edde0f1e5e3bfaa74c1c5865001671bd24 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:30:00 -0600 Subject: [PATCH 07/30] Move schema formats and permissions types into core. --- .../OpenAPIKit/Schema Object/JSONSchema.swift | 24 +- .../Schema Object/JSONSchemaContext.swift | 19 -- .../Schema Object/TypesAndFormats.swift | 313 +++--------------- .../Schema Object/JSONSchema.swift | 24 +- .../Schema Object/JSONSchemaContext.swift | 19 -- .../Schema Object/TypesAndFormats.swift | 313 +++--------------- .../JSONSchemaPermissions.swift | 12 + Sources/OpenAPIKitCore/JSONTypeFormat.swift | 270 +++++++++++++++ 8 files changed, 380 insertions(+), 614 deletions(-) create mode 100644 Sources/OpenAPIKitCore/JSONSchemaPermissions.swift create mode 100644 Sources/OpenAPIKitCore/JSONTypeFormat.swift diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift index cebab9ae1..266f3995b 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift @@ -1003,7 +1003,7 @@ extension JSONSchema { format: JSONTypeFormat.BooleanFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1036,7 +1036,7 @@ extension JSONSchema { format: JSONTypeFormat.BooleanFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1073,7 +1073,7 @@ extension JSONSchema { format: JSONTypeFormat.AnyFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1106,7 +1106,7 @@ extension JSONSchema { format: JSONTypeFormat.AnyFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1146,7 +1146,7 @@ extension JSONSchema { format: JSONTypeFormat.StringFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1187,7 +1187,7 @@ extension JSONSchema { format: JSONTypeFormat.StringFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1230,7 +1230,7 @@ extension JSONSchema { format: JSONTypeFormat.NumberFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1271,7 +1271,7 @@ extension JSONSchema { format: JSONTypeFormat.NumberFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1314,7 +1314,7 @@ extension JSONSchema { format: JSONTypeFormat.IntegerFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1355,7 +1355,7 @@ extension JSONSchema { format: JSONTypeFormat.IntegerFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1398,7 +1398,7 @@ extension JSONSchema { format: JSONTypeFormat.ObjectFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1446,7 +1446,7 @@ extension JSONSchema { format: JSONTypeFormat.ArrayFormat = .unspecified, required: Bool = true, nullable: Bool = false, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index c19d7a094..75be083b7 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -217,25 +217,6 @@ extension JSONSchema { self.defaultValue = defaultValue self.examples = examples.map(AnyCodable.init) } - - public enum Permissions: String, Codable { - case readOnly - case writeOnly - case readWrite - - public init( - _ permissions: CoreContext.Permissions - ) { - switch permissions { - case .readOnly: - self = .readOnly - case .writeOnly: - self = .writeOnly - case .readWrite: - self = .readWrite - } - } - } } } diff --git a/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift b/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift index 91e2a02c0..34b95e95f 100644 --- a/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift +++ b/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift @@ -121,296 +121,57 @@ public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, var jsonType: JSONType { get } } -extension JSONTypeFormat { - /// A format used when no type is known or any type is allowed. - /// - /// There are no built-in formats that do not have an associated - /// type, but it is still important to be able to specify a format without - /// a type. This can come into play when writing fragments of schemas - /// to be combined later. - public enum AnyFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = AnyCodable - - public static var unspecified: AnyFormat { - return .generic - } - - public var jsonType: JSONType { - return .object - } - } - - /// The allowed "format" properties for `.boolean` schemas. - public enum BooleanFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Bool - - public static var unspecified: BooleanFormat { - return .generic - } - - public var jsonType: JSONType { - return .boolean - } - } - - /// The allowed "format" properties for `.object` schemas. - public enum ObjectFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = AnyCodable - - public static var unspecified: ObjectFormat { - return .generic - } - - public var jsonType: JSONType { - return .object - } - } - - /// The allowed "format" properties for `.array` schemas. - public enum ArrayFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = [AnyCodable] - - public static var unspecified: ArrayFormat { - return .generic - } - - public var jsonType: JSONType { - return .array - } - } - - /// The allowed "format" properties for `.number` schemas. - public enum NumberFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case float - case double - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .float: return "float" - case .double: return "double" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "float": self = .float - case "double": self = .double - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Double - - public static var unspecified: NumberFormat { - return .generic - } - - public var jsonType: JSONType { - return .number - } +/// A format used when no type is known or any type is allowed. +/// +/// There are no built-in formats that do not have an associated +/// type, but it is still important to be able to specify a format without +/// a type. This can come into play when writing fragments of schemas +/// to be combined later. +extension JSONTypeFormat.AnyFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .object } +} - /// The allowed "format" properties for `.integer` schemas. - public enum IntegerFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case int32 - case int64 - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .int32: return "int32" - case .int64: return "int64" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "int32": self = .int32 - case "int64": self = .int64 - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Int - - public static var unspecified: IntegerFormat { - return .generic - } - - public var jsonType: JSONType { - return .integer - } +/// The allowed "format" properties for `.boolean` schemas. +extension JSONTypeFormat.BooleanFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .boolean } +} - /// The allowed "format" properties for `.string` schemas. - public enum StringFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case byte - case binary - case date - /// A string instance is valid against this attribute if it is a valid - /// date representation as defined by - /// https://tools.ietf.org/html/rfc3339#section-5.6 - case dateTime - case password - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .byte: return "byte" - case .binary: return "binary" - case .date: return "date" - case .dateTime: return "date-time" - case .password: return "password" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "byte": self = .byte - case "binary": self = .binary - case "date": self = .date - case "date-time": self = .dateTime - case "password": self = .password - default: self = .other(rawValue) - } - } - - public typealias SwiftType = String - - public static var unspecified: StringFormat { - return .generic - } - - public var jsonType: JSONType { - return .string - } +/// The allowed "format" properties for `.object` schemas. +extension JSONTypeFormat.ObjectFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .object } } -extension JSONTypeFormat.StringFormat { - /// Popular non-standard "format" properties for `.string` schemas. - /// - /// Specify with e.g. `.string(format: .extended(.uuid))` - public enum Extended: String, Equatable { - case uuid = "uuid" - case email = "email" - case hostname = "hostname" - case ipv4 = "ipv4" - case ipv6 = "ipv6" - /// A string instance is valid against this attribute if it is a valid - /// URI, according to - /// https://tools.ietf.org/html/rfc3986 - case uri = "uri" - /// A string instance is valid against this attribute if it is a valid - /// URI, according to - /// https://tools.ietf.org/html/rfc3986 - case uriReference = "uriref" +/// The allowed "format" properties for `.array` schemas. +extension JSONTypeFormat.ArrayFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .array } +} - public static func extended(_ format: Extended) -> Self { - return .other(format.rawValue) +/// The allowed "format" properties for `.number` schemas. +extension JSONTypeFormat.NumberFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .number } } -extension JSONTypeFormat.IntegerFormat { - /// Popular non-standard "format" properties for `.integer` schemas. - /// - /// Specify with e.g. `.integer(format: .extended(.uint32))` - public enum Extended: String, Equatable { - case uint32 = "uint32" - case uint64 = "uint64" +/// The allowed "format" properties for `.integer` schemas. +extension JSONTypeFormat.IntegerFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .integer } +} - public static func extended(_ format: Extended) -> Self { - return .other(format.rawValue) +/// The allowed "format" properties for `.string` schemas. +extension JSONTypeFormat.StringFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .string } } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift index 25a9d98fe..b1f1b03b6 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift @@ -911,7 +911,7 @@ extension JSONSchema { format: JSONTypeFormat.BooleanFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -944,7 +944,7 @@ extension JSONSchema { format: JSONTypeFormat.BooleanFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -981,7 +981,7 @@ extension JSONSchema { format: JSONTypeFormat.AnyFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1014,7 +1014,7 @@ extension JSONSchema { format: JSONTypeFormat.AnyFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1054,7 +1054,7 @@ extension JSONSchema { format: JSONTypeFormat.StringFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1095,7 +1095,7 @@ extension JSONSchema { format: JSONTypeFormat.StringFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1138,7 +1138,7 @@ extension JSONSchema { format: JSONTypeFormat.NumberFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1179,7 +1179,7 @@ extension JSONSchema { format: JSONTypeFormat.NumberFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1222,7 +1222,7 @@ extension JSONSchema { format: JSONTypeFormat.IntegerFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1263,7 +1263,7 @@ extension JSONSchema { format: JSONTypeFormat.IntegerFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1306,7 +1306,7 @@ extension JSONSchema { format: JSONTypeFormat.ObjectFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, @@ -1354,7 +1354,7 @@ extension JSONSchema { format: JSONTypeFormat.ArrayFormat = .unspecified, required: Bool = true, nullable: Bool? = nil, - permissions: JSONSchema.CoreContext.Permissions? = nil, + permissions: Permissions? = nil, deprecated: Bool? = nil, title: String? = nil, description: String? = nil, diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift index 35dd54702..62c2523b5 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift @@ -210,25 +210,6 @@ extension JSONSchema { self.defaultValue = defaultValue self.example = AnyCodable(example) } - - public enum Permissions: String, Codable { - case readOnly - case writeOnly - case readWrite - - public init( - _ permissions: CoreContext.Permissions - ) { - switch permissions { - case .readOnly: - self = .readOnly - case .writeOnly: - self = .writeOnly - case .readWrite: - self = .readWrite - } - } - } } } diff --git a/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift b/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift index 6416ce5c7..ba71ad631 100644 --- a/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift +++ b/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift @@ -115,296 +115,57 @@ public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, var jsonType: JSONType { get } } -extension JSONTypeFormat { - /// A format used when no type is known or any type is allowed. - /// - /// There are no built-in formats that do not have an associated - /// type, but it is still important to be able to specify a format without - /// a type. This can come into play when writing fragments of schemas - /// to be combined later. - public enum AnyFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = AnyCodable - - public static var unspecified: AnyFormat { - return .generic - } - - public var jsonType: JSONType { - return .object - } - } - - /// The allowed "format" properties for `.boolean` schemas. - public enum BooleanFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Bool - - public static var unspecified: BooleanFormat { - return .generic - } - - public var jsonType: JSONType { - return .boolean - } - } - - /// The allowed "format" properties for `.object` schemas. - public enum ObjectFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = AnyCodable - - public static var unspecified: ObjectFormat { - return .generic - } - - public var jsonType: JSONType { - return .object - } - } - - /// The allowed "format" properties for `.array` schemas. - public enum ArrayFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = [AnyCodable] - - public static var unspecified: ArrayFormat { - return .generic - } - - public var jsonType: JSONType { - return .array - } - } - - /// The allowed "format" properties for `.number` schemas. - public enum NumberFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case float - case double - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .float: return "float" - case .double: return "double" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "float": self = .float - case "double": self = .double - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Double - - public static var unspecified: NumberFormat { - return .generic - } - - public var jsonType: JSONType { - return .number - } +/// A format used when no type is known or any type is allowed. +/// +/// There are no built-in formats that do not have an associated +/// type, but it is still important to be able to specify a format without +/// a type. This can come into play when writing fragments of schemas +/// to be combined later. +extension JSONTypeFormat.AnyFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .object } +} - /// The allowed "format" properties for `.integer` schemas. - public enum IntegerFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case int32 - case int64 - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .int32: return "int32" - case .int64: return "int64" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "int32": self = .int32 - case "int64": self = .int64 - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Int - - public static var unspecified: IntegerFormat { - return .generic - } - - public var jsonType: JSONType { - return .integer - } +/// The allowed "format" properties for `.boolean` schemas. +extension JSONTypeFormat.BooleanFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .boolean } +} - /// The allowed "format" properties for `.string` schemas. - public enum StringFormat: RawRepresentable, Equatable, OpenAPIFormat { - case generic - case byte - case binary - case date - /// A string instance is valid against this attribute if it is a valid - /// date representation as defined by - /// https://tools.ietf.org/html/rfc3339#section-5.6 - case dateTime - case password - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .byte: return "byte" - case .binary: return "binary" - case .date: return "date" - case .dateTime: return "date-time" - case .password: return "password" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "byte": self = .byte - case "binary": self = .binary - case "date": self = .date - case "date-time": self = .dateTime - case "password": self = .password - default: self = .other(rawValue) - } - } - - public typealias SwiftType = String - - public static var unspecified: StringFormat { - return .generic - } - - public var jsonType: JSONType { - return .string - } +/// The allowed "format" properties for `.object` schemas. +extension JSONTypeFormat.ObjectFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .object } } -extension JSONTypeFormat.StringFormat { - /// Popular non-standard "format" properties for `.string` schemas. - /// - /// Specify with e.g. `.string(format: .extended(.uuid))` - public enum Extended: String, Equatable { - case uuid = "uuid" - case email = "email" - case hostname = "hostname" - case ipv4 = "ipv4" - case ipv6 = "ipv6" - /// A string instance is valid against this attribute if it is a valid - /// URI, according to - /// https://tools.ietf.org/html/rfc3986 - case uri = "uri" - /// A string instance is valid against this attribute if it is a valid - /// URI, according to - /// https://tools.ietf.org/html/rfc3986 - case uriReference = "uriref" +/// The allowed "format" properties for `.array` schemas. +extension JSONTypeFormat.ArrayFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .array } +} - public static func extended(_ format: Extended) -> Self { - return .other(format.rawValue) +/// The allowed "format" properties for `.number` schemas. +extension JSONTypeFormat.NumberFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .number } } -extension JSONTypeFormat.IntegerFormat { - /// Popular non-standard "format" properties for `.integer` schemas. - /// - /// Specify with e.g. `.integer(format: .extended(.uint32))` - public enum Extended: String, Equatable { - case uint32 = "uint32" - case uint64 = "uint64" +/// The allowed "format" properties for `.integer` schemas. +extension JSONTypeFormat.IntegerFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .integer } +} - public static func extended(_ format: Extended) -> Self { - return .other(format.rawValue) +/// The allowed "format" properties for `.string` schemas. +extension JSONTypeFormat.StringFormat: OpenAPIFormat { + public var jsonType: JSONType { + return .string } } diff --git a/Sources/OpenAPIKitCore/JSONSchemaPermissions.swift b/Sources/OpenAPIKitCore/JSONSchemaPermissions.swift new file mode 100644 index 000000000..6df1c99b2 --- /dev/null +++ b/Sources/OpenAPIKitCore/JSONSchemaPermissions.swift @@ -0,0 +1,12 @@ +// +// JSONSchemaPermissions.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + +public enum JSONSchemaPermissions: String, Codable { + case readOnly + case writeOnly + case readWrite +} diff --git a/Sources/OpenAPIKitCore/JSONTypeFormat.swift b/Sources/OpenAPIKitCore/JSONTypeFormat.swift new file mode 100644 index 000000000..65def782e --- /dev/null +++ b/Sources/OpenAPIKitCore/JSONTypeFormat.swift @@ -0,0 +1,270 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + + +/// A format used when no type is known or any type is allowed. +/// +/// There are no built-in formats that do not have an associated +/// type, but it is still important to be able to specify a format without +/// a type. This can come into play when writing fragments of schemas +/// to be combined later. +public enum AnyFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = AnyCodable + + public static var unspecified: AnyFormat { + return .generic + } +} + +/// The allowed "format" properties for `.boolean` schemas. +public enum BooleanFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = Bool + + public static var unspecified: BooleanFormat { + return .generic + } +} + +/// The allowed "format" properties for `.object` schemas. +public enum ObjectFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = AnyCodable + + public static var unspecified: ObjectFormat { + return .generic + } +} + +/// The allowed "format" properties for `.array` schemas. +public enum ArrayFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = [AnyCodable] + + public static var unspecified: ArrayFormat { + return .generic + } +} + +/// The allowed "format" properties for `.number` schemas. +public enum NumberFormat: RawRepresentable, Equatable { + case generic + case float + case double + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .float: return "float" + case .double: return "double" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + case "float": self = .float + case "double": self = .double + default: self = .other(rawValue) + } + } + + public typealias SwiftType = Double + + public static var unspecified: NumberFormat { + return .generic + } +} + +/// The allowed "format" properties for `.integer` schemas. +public enum IntegerFormat: RawRepresentable, Equatable { + case generic + case int32 + case int64 + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .int32: return "int32" + case .int64: return "int64" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + case "int32": self = .int32 + case "int64": self = .int64 + default: self = .other(rawValue) + } + } + + public typealias SwiftType = Int + + public static var unspecified: IntegerFormat { + return .generic + } +} + +/// The allowed "format" properties for `.string` schemas. +public enum StringFormat: RawRepresentable, Equatable { + case generic + case byte + case binary + case date + /// A string instance is valid against this attribute if it is a valid + /// date representation as defined by + /// https://tools.ietf.org/html/rfc3339#section-5.6 + case dateTime + case password + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .byte: return "byte" + case .binary: return "binary" + case .date: return "date" + case .dateTime: return "date-time" + case .password: return "password" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + case "byte": self = .byte + case "binary": self = .binary + case "date": self = .date + case "date-time": self = .dateTime + case "password": self = .password + default: self = .other(rawValue) + } + } + + public typealias SwiftType = String + + public static var unspecified: StringFormat { + return .generic + } +} + +extension StringFormat { + /// Popular non-standard "format" properties for `.string` schemas. + /// + /// Specify with e.g. `.string(format: .extended(.uuid))` + public enum Extended: String, Equatable { + case uuid = "uuid" + case email = "email" + case hostname = "hostname" + case ipv4 = "ipv4" + case ipv6 = "ipv6" + /// A string instance is valid against this attribute if it is a valid + /// URI, according to + /// https://tools.ietf.org/html/rfc3986 + case uri = "uri" + /// A string instance is valid against this attribute if it is a valid + /// URI, according to + /// https://tools.ietf.org/html/rfc3986 + case uriReference = "uriref" + } + + public static func extended(_ format: Extended) -> Self { + return .other(format.rawValue) + } +} + +extension IntegerFormat { + /// Popular non-standard "format" properties for `.integer` schemas. + /// + /// Specify with e.g. `.integer(format: .extended(.uint32))` + public enum Extended: String, Equatable { + case uint32 = "uint32" + case uint64 = "uint64" + } + + public static func extended(_ format: Extended) -> Self { + return .other(format.rawValue) + } +} From cc7688b595a5bf12fd74bb7804f249beb2e9c850 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:31:39 -0600 Subject: [PATCH 08/30] Move core ContentType type out of OpenAPI namespace. --- Tests/OpenAPIKitCoreTests/ContentTypeTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift b/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift index 515c863ab..f60bebe6c 100644 --- a/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift +++ b/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift @@ -10,7 +10,7 @@ import XCTest final class ContentTypeTests: XCTestCase { func test_badContentType() { - let bad = OpenAPI.ContentType(rawValue: "not a content type") + let bad = ContentType(rawValue: "not a content type") XCTAssertEqual( bad?.warnings[0].localizedDescription, "\'not a content type\' could not be parsed as a Content Type. Content Types should have the format \'/\'" @@ -18,7 +18,7 @@ final class ContentTypeTests: XCTestCase { } func test_contentTypeStringReflexivity() { - let types: [OpenAPI.ContentType] = [ + let types: [ContentType] = [ .bmp, .css, .csv, @@ -57,24 +57,24 @@ final class ContentTypeTests: XCTestCase { ] for type in types { - XCTAssertEqual(type, OpenAPI.ContentType(rawValue: type.rawValue)) + XCTAssertEqual(type, ContentType(rawValue: type.rawValue)) } } func test_goodParam() { - let type = OpenAPI.ContentType.init(rawValue: "text/html; charset=utf8") + let type = ContentType.init(rawValue: "text/html; charset=utf8") XCTAssertEqual(type?.warnings.count, 0) XCTAssertEqual(type?.rawValue, "text/html; charset=utf8") } func test_multipleParams() { - let type = OpenAPI.ContentType.init(rawValue: "my/type; some=thing; another=else") + let type = ContentType.init(rawValue: "my/type; some=thing; another=else") XCTAssertEqual(type?.warnings.count, 0) XCTAssert(type?.rawValue == "my/type; another=else; some=thing") } func test_oneBadParam() { - let type = OpenAPI.ContentType.init(rawValue: "my/type; some: thing; another=else") + let type = ContentType.init(rawValue: "my/type; some: thing; another=else") XCTAssertEqual(type?.warnings.count, 1) XCTAssertEqual(type?.warnings.first?.localizedDescription, "Could not parse a Content Type parameter from \' some: thing\'") XCTAssert(type?.rawValue == "my/type; another=else") From 505f3c6812db3336ce163f86569f5f8139b18647 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 17 Dec 2022 23:32:25 -0600 Subject: [PATCH 09/30] remove OpenAPI namespace from core. --- Sources/OpenAPIKitCore/ContentType.swift | 146 +++++++++--------- ...rDecodeNoTypesMatchedErrorExtensions.swift | 4 +- .../OpenAPIDecodingErrors.swift | 2 +- .../OpenAPIError.swift | 82 +++++----- .../OpenAPIWarning.swift | 68 ++++---- .../RequestDecodingError.swift | 6 +- Sources/OpenAPIKitCore/HttpMethod.swift | 32 ++-- Sources/OpenAPIKitCore/OpenAPI.swift | 9 -- .../OrderedDictionary/OrderedDictionary.swift | 4 +- 9 files changed, 168 insertions(+), 185 deletions(-) delete mode 100644 Sources/OpenAPIKitCore/OpenAPI.swift diff --git a/Sources/OpenAPIKitCore/ContentType.swift b/Sources/OpenAPIKitCore/ContentType.swift index 24a547ebb..ffa276e56 100644 --- a/Sources/OpenAPIKitCore/ContentType.swift +++ b/Sources/OpenAPIKitCore/ContentType.swift @@ -5,97 +5,95 @@ // Created by Mathew Polzin on 12/29/19. // -extension OpenAPI { - /// The Content Type of an API request or response body. - public struct ContentType: Codable, Equatable, Hashable, RawRepresentable, HasWarnings { - internal let underlyingType: Builtin - public let warnings: [OpenAPI.Warning] +/// The Content Type of an API request or response body. +public struct ContentType: Codable, Equatable, Hashable, RawRepresentable, HasWarnings { + internal let underlyingType: Builtin + public let warnings: [Warning] - /// The type and subtype of the content type. This is everything except for - /// any parameters that are also attached. - /// - /// If you want the full content type string, use `rawValue`. - public var typeAndSubtype: String { underlyingType.rawValue } + /// The type and subtype of the content type. This is everything except for + /// any parameters that are also attached. + /// + /// If you want the full content type string, use `rawValue`. + public var typeAndSubtype: String { underlyingType.rawValue } - /// Key/Value pairs serialized as parameters for the content type. - /// - /// For exmaple, in "`text/plain; charset=UTF-8`" "charset" is - /// the name of a parameter with the value "UTF-8". - /// - /// OpenAPIKit will always encode these parameters with the keys in - /// alphabetical order. - public var parameters: [String: String] + /// Key/Value pairs serialized as parameters for the content type. + /// + /// For exmaple, in "`text/plain; charset=UTF-8`" "charset" is + /// the name of a parameter with the value "UTF-8". + /// + /// OpenAPIKit will always encode these parameters with the keys in + /// alphabetical order. + public var parameters: [String: String] - /// The full raw string value of the content type and any of its parameters. - /// - /// If you only want the type & subtype without any parameters, use - /// `typeAndSubtype`. - public var rawValue: String { - let rawParams = parameters - .sorted(by: { $0.0 < $1.0 }) - .map { (name, value) in "; \(name)=\(value)" } - .joined() + /// The full raw string value of the content type and any of its parameters. + /// + /// If you only want the type & subtype without any parameters, use + /// `typeAndSubtype`. + public var rawValue: String { + let rawParams = parameters + .sorted(by: { $0.0 < $1.0 }) + .map { (name, value) in "; \(name)=\(value)" } + .joined() - return typeAndSubtype + rawParams - } + return typeAndSubtype + rawParams + } - public init?(rawValue: String) { - let parts = rawValue.split(separator: ";") - let type = parts.first.map(String.init) ?? rawValue + public init?(rawValue: String) { + let parts = rawValue.split(separator: ";") + let type = parts.first.map(String.init) ?? rawValue - var warnings = [OpenAPI.Warning]() - var params = [String:String]() - for part in parts.dropFirst() { - switch Self.parseParameter(part) { - case .success(let (name, value)): - params[name] = value - case .failure(let warning): - warnings.append(warning) - } + var warnings = [Warning]() + var params = [String:String]() + for part in parts.dropFirst() { + switch Self.parseParameter(part) { + case .success(let (name, value)): + params[name] = value + case .failure(let warning): + warnings.append(warning) } + } - parameters = params + parameters = params - if let underlying = Builtin.init(rawValue: type) { - underlyingType = underlying - } else { - underlyingType = .other(type) - warnings.append( - .message( - "'\(rawValue)' could not be parsed as a Content Type. Content Types should have the format '/'" - ) + if let underlying = Builtin.init(rawValue: type) { + underlyingType = underlying + } else { + underlyingType = .other(type) + warnings.append( + .message( + "'\(rawValue)' could not be parsed as a Content Type. Content Types should have the format '/'" ) - } - self.warnings = warnings + ) } + self.warnings = warnings + } - private static func parseParameter(_ param: String.SubSequence) -> Result<(String, String), OpenAPI.Warning> { - let parts = param.split(separator: "=") + private static func parseParameter(_ param: String.SubSequence) -> Result<(String, String), Warning> { + let parts = param.split(separator: "=") - guard parts.count == 2, - let name = parts.first, - let value = parts.last else { - return .failure(.message("Could not parse a Content Type parameter from '\(param)'")) - } + guard parts.count == 2, + let name = parts.first, + let value = parts.last else { + return .failure(.message("Could not parse a Content Type parameter from '\(param)'")) + } - return .success( - ( - String(name.trimmingCharacters(in: .whitespaces)), - String(value.trimmingCharacters(in: .whitespaces)) - ) + return .success( + ( + String(name.trimmingCharacters(in: .whitespaces)), + String(value.trimmingCharacters(in: .whitespaces)) ) - } + ) + } - internal init(_ builtin: Builtin) { - underlyingType = builtin - parameters = [:] - warnings = [] - } + internal init(_ builtin: Builtin) { + underlyingType = builtin + parameters = [:] + warnings = [] } } // convenience constructors -public extension OpenAPI.ContentType { +public extension ContentType { /// Bitmap image static let bmp: Self = .init(.bmp) static let css: Self = .init(.css) @@ -157,7 +155,7 @@ public extension OpenAPI.ContentType { static let any: Self = .init(.any) } -extension OpenAPI.ContentType { +extension ContentType { // This internal representation makes it easier to ensure that the popular // builtin types supported are fully covered in their rawValue implementation. internal enum Builtin: Codable, Equatable, Hashable { @@ -223,7 +221,7 @@ extension OpenAPI.ContentType { } } -extension OpenAPI.ContentType.Builtin: RawRepresentable { +extension ContentType.Builtin: RawRepresentable { public var rawValue: String { switch self { case .bmp: return "image/bmp" @@ -316,4 +314,4 @@ extension OpenAPI.ContentType.Builtin: RawRepresentable { } } -extension OpenAPI.ContentType: Validatable {} +extension ContentType: Validatable {} diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/EitherDecodeNoTypesMatchedErrorExtensions.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/EitherDecodeNoTypesMatchedErrorExtensions.swift index 71f3f9950..4cb250295 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/EitherDecodeNoTypesMatchedErrorExtensions.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/EitherDecodeNoTypesMatchedErrorExtensions.swift @@ -39,10 +39,10 @@ public extension EitherDecodeNoTypesMatchedError { // If the intention was not to use a reference, this error will be superfluous. let error1 = isRefKeyNotFoundError(failure1) ? nil - : OpenAPI.Error(from: failure1.error.replacingPath(with: failure1.codingPath(relativeTo: codingPath))).localizedDescription + : Error(from: failure1.error.replacingPath(with: failure1.codingPath(relativeTo: codingPath))).localizedDescription let error2 = isRefKeyNotFoundError(failure2) ? nil - : OpenAPI.Error(from: failure2.error.replacingPath(with: failure2.codingPath(relativeTo: codingPath))).localizedDescription + : Error(from: failure2.error.replacingPath(with: failure2.codingPath(relativeTo: codingPath))).localizedDescription let details1 = error1 .map { "\(String(describing: failure1.type)) could not be decoded because:\n\($0)" } diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift index 86273409b..781569ef0 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 2/23/20. // -extension OpenAPI.Error { +extension Error { // Just creating a namespace public enum Decoding {} } diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIError.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIError.swift index 5ab249203..544be57e3 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIError.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIError.swift @@ -5,52 +5,50 @@ // Created by Mathew Polzin on 3/30/20. // -extension OpenAPI { - /// An `OpenAPI.Error` can be constructed from any error thrown while decoding - /// an OpenAPI document. This wrapper provides a superior human-readable error - /// and a human readable coding path. - /// - /// Example: - /// - /// do { - /// document = try JSONDecoder().decode(OpenAPI.Document.self, from: ...) - /// } catch let error { - /// let prettyError = OpenAPI.Error(from: error) - /// print(prettyError.localizedDescription) - /// print(prettyError.codingPathString) - /// } - /// - public struct Error: Swift.Error, CustomStringConvertible { - - public let localizedDescription: String - public let codingPath: [CodingKey] - public let underlyingError: Swift.Error - - public var codingPathString: String { codingPath.stringValue } - - public init(from underlyingError: Swift.Error) { - self.underlyingError = underlyingError - if let openAPIError = underlyingError as? OpenAPIError { +/// An `OpenAPI.Error` can be constructed from any error thrown while decoding +/// an OpenAPI document. This wrapper provides a superior human-readable error +/// and a human readable coding path. +/// +/// Example: +/// +/// do { +/// document = try JSONDecoder().decode(OpenAPI.Document.self, from: ...) +/// } catch let error { +/// let prettyError = OpenAPI.Error(from: error) +/// print(prettyError.localizedDescription) +/// print(prettyError.codingPathString) +/// } +/// +public struct Error: Swift.Error, CustomStringConvertible { + + public let localizedDescription: String + public let codingPath: [CodingKey] + public let underlyingError: Swift.Error + + public var codingPathString: String { codingPath.stringValue } + + public init(from underlyingError: Swift.Error) { + self.underlyingError = underlyingError + if let openAPIError = underlyingError as? OpenAPIError { + localizedDescription = openAPIError.localizedDescription + codingPath = openAPIError.codingPath + + } else if let decodingError = underlyingError as? Swift.DecodingError { + + if let openAPIError = decodingError.underlyingError as? OpenAPIError { localizedDescription = openAPIError.localizedDescription codingPath = openAPIError.codingPath - - } else if let decodingError = underlyingError as? Swift.DecodingError { - - if let openAPIError = decodingError.underlyingError as? OpenAPIError { - localizedDescription = openAPIError.localizedDescription - codingPath = openAPIError.codingPath - } else { - let wrappedError = DecodingErrorWrapper(decodingError: decodingError) - localizedDescription = wrappedError.localizedDescription - codingPath = wrappedError.codingPath - } - } else { - localizedDescription = underlyingError.localizedDescription - codingPath = [] + let wrappedError = DecodingErrorWrapper(decodingError: decodingError) + localizedDescription = wrappedError.localizedDescription + codingPath = wrappedError.codingPath } - } - public var description: String { localizedDescription } + } else { + localizedDescription = underlyingError.localizedDescription + codingPath = [] + } } + + public var description: String { localizedDescription } } diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIWarning.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIWarning.swift index cb4862c79..21aa92406 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIWarning.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIWarning.swift @@ -5,54 +5,52 @@ // Created by Mathew Polzin on 12/19/21. // -extension OpenAPI { - public enum Warning: Swift.Error { - case underlyingError(OpenAPIError) - case message(String) +public enum Warning: Swift.Error { + case underlyingError(OpenAPIError) + case message(String) - public var underlyingError: OpenAPIError? { - switch self { - case .underlyingError(let error): return error - case .message: return nil - } + public var underlyingError: OpenAPIError? { + switch self { + case .underlyingError(let error): return error + case .message: return nil } + } - public var subjectName: String? { - switch self { - case .underlyingError(let err): return err.subjectName - default: return nil - } + public var subjectName: String? { + switch self { + case .underlyingError(let err): return err.subjectName + default: return nil } + } - public var contextString: String? { - switch self { - case .underlyingError(let err): return err.contextString - default: return nil - } + public var contextString: String? { + switch self { + case .underlyingError(let err): return err.contextString + default: return nil } + } - public var errorCategory: ErrorCategory? { - switch self { - case .underlyingError(let err): return err.errorCategory - default: return nil - } + public var errorCategory: ErrorCategory? { + switch self { + case .underlyingError(let err): return err.errorCategory + default: return nil } + } - public var codingPath: [CodingKey]? { - switch self { - case .underlyingError(let err): return err.codingPath - default: return nil - } + public var codingPath: [CodingKey]? { + switch self { + case .underlyingError(let err): return err.codingPath + default: return nil } + } - /// Get a human readable string value of the coding path. - public var codingPathString : String? { - return codingPath?.stringValue - } + /// Get a human readable string value of the coding path. + public var codingPathString : String? { + return codingPath?.stringValue } } -extension OpenAPI.Warning: CustomStringConvertible { +extension Warning: CustomStringConvertible { /// Description of warning. public var localizedDescription: String { switch self { @@ -67,5 +65,5 @@ extension OpenAPI.Warning: CustomStringConvertible { } public protocol HasWarnings { - var warnings: [OpenAPI.Warning] { get } + var warnings: [Warning] { get } } diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift index 623e53edc..af8d921ba 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 2/28/20. // -extension OpenAPI.Error.Decoding { +extension Error.Decoding { public struct Request: OpenAPIError { public let context: Context internal let relativeCodingPath: [CodingKey] @@ -23,7 +23,7 @@ extension OpenAPI.Error.Decoding { } } -extension OpenAPI.Error.Decoding.Request { +extension Error.Decoding.Request { public var subjectName: String { switch context { case .inconsistency(let error): @@ -91,7 +91,7 @@ extension OpenAPI.Error.Decoding.Request { } } -extension OpenAPI.Error.Decoding.Request: DiggingError { +extension Error.Decoding.Request: DiggingError { public init(unwrapping error: Swift.DecodingError) { if let decodingError = error.underlyingError as? Swift.DecodingError { self = Self(unwrapping: decodingError) diff --git a/Sources/OpenAPIKitCore/HttpMethod.swift b/Sources/OpenAPIKitCore/HttpMethod.swift index 6165d72d0..803fa4632 100644 --- a/Sources/OpenAPIKitCore/HttpMethod.swift +++ b/Sources/OpenAPIKitCore/HttpMethod.swift @@ -5,21 +5,19 @@ // Created by Mathew Polzin on 12/29/19. // -extension OpenAPI { - /// Represents the HTTP methods supported by the - /// OpenAPI Specification. - /// - /// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object) because the supported - /// HTTP methods are enumerated as properties on that - /// object. - public enum HttpMethod: String, CaseIterable { - case get = "GET" - case post = "POST" - case patch = "PATCH" - case put = "PUT" - case delete = "DELETE" - case head = "HEAD" - case options = "OPTIONS" - case trace = "TRACE" - } +/// Represents the HTTP methods supported by the +/// OpenAPI Specification. +/// +/// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object) because the supported +/// HTTP methods are enumerated as properties on that +/// object. +public enum HttpMethod: String, CaseIterable { + case get = "GET" + case post = "POST" + case patch = "PATCH" + case put = "PUT" + case delete = "DELETE" + case head = "HEAD" + case options = "OPTIONS" + case trace = "TRACE" } diff --git a/Sources/OpenAPIKitCore/OpenAPI.swift b/Sources/OpenAPIKitCore/OpenAPI.swift deleted file mode 100644 index f56c419b7..000000000 --- a/Sources/OpenAPIKitCore/OpenAPI.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// OpenAPI.swift -// -// -// Created by Mathew Polzin on 6/22/19. -// - -/// The OpenAPI namespace -public enum OpenAPI {} diff --git a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift index defcfa43c..8779f30ec 100644 --- a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift @@ -21,9 +21,9 @@ public struct OrderedDictionary: HasWarnings where Key: Hashable { private var orderedKeys: [Key] private var unorderedHash: [Key: Value] - private var _warnings: [OpenAPI.Warning] + private var _warnings: [Warning] - public var warnings: [OpenAPI.Warning] { _warnings } + public var warnings: [Warning] { _warnings } public init() { orderedKeys = [] From 40fdf5d225231f411ccbb7802e3ba816cdf49e9e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 18 Dec 2022 14:50:28 -0600 Subject: [PATCH 10/30] remove external documentation alias from a different codepath exploration. wrap up some permissions changes. --- .../OpenAPIKit/Schema Object/JSONSchema+Combining.swift | 7 +++---- Sources/OpenAPIKit/_CoreReExport.swift | 1 - .../OpenAPIKit30/Schema Object/JSONSchema+Combining.swift | 7 +++---- Sources/OpenAPIKit30/_CoreReExport.swift | 1 - .../Schema Object/SchemaFragmentCombiningTests.swift | 2 +- .../Schema Object/SchemaFragmentCombiningTests.swift | 2 +- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift index 752f99c49..93686b192 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift @@ -286,7 +286,7 @@ extension JSONSchema.CoreContext where Format == JSONTypeFormat.AnyFormat { format: newFormat, required: required, nullable: nullable, - permissions: _permissions.map(OtherContext.Permissions.init), + permissions: _permissions, deprecated: _deprecated, title: title, description: description, @@ -312,7 +312,7 @@ extension JSONSchema.CoreContext { if let conflict = conflicting(_permissions, other._permissions) { throw JSONSchemaResolutionError(.inconsistency("A schema cannot be both \(conflict.0.rawValue) and \(conflict.1.rawValue).")) } - let newPermissions: JSONSchema.CoreContext.Permissions? + let newPermissions: JSONSchema.Permissions? if _permissions == nil && other._permissions == nil { newPermissions = nil } else { @@ -577,13 +577,12 @@ extension JSONSchema.CoreContext { guard let newFormat = NewFormat(rawValue: format.rawValue) else { throw JSONSchemaResolutionError(.inconsistency("Tried to create a \(NewFormat.self) from the incompatible format value: \(format.rawValue)")) } - let newPermissions = _permissions.map(JSONSchema.CoreContext.Permissions.init) return .init( format: newFormat, required: required, nullable: nullable, - permissions: newPermissions, + permissions: _permissions, deprecated: _deprecated, title: title, description: description, diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index 1fecc086a..099878280 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -22,7 +22,6 @@ public extension OpenAPI { typealias Path = OpenAPIKitCore.Path typealias ComponentKey = OpenAPIKitCore.ComponentKey typealias Discriminator = OpenAPIKitCore.Discriminator - typealias ExternalDocumentation = OpenAPIKitCore.ExternalDocumentation } public extension JSONSchema { diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift index 9d61dd767..c56fb60ed 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift @@ -260,7 +260,7 @@ extension JSONSchema.CoreContext where Format == JSONTypeFormat.AnyFormat { format: newFormat, required: required, nullable: _nullable, - permissions: _permissions.map(OtherContext.Permissions.init), + permissions: _permissions, deprecated: _deprecated, title: title, description: description, @@ -286,7 +286,7 @@ extension JSONSchema.CoreContext { if let conflict = conflicting(_permissions, other._permissions) { throw JSONSchemaResolutionError(.inconsistency("A schema cannot be both \(conflict.0.rawValue) and \(conflict.1.rawValue).")) } - let newPermissions: JSONSchema.CoreContext.Permissions? + let newPermissions: JSONSchema.Permissions? if _permissions == nil && other._permissions == nil { newPermissions = nil } else { @@ -557,13 +557,12 @@ extension JSONSchema.CoreContext { guard let newFormat = NewFormat(rawValue: format.rawValue) else { throw JSONSchemaResolutionError(.inconsistency("Tried to create a \(NewFormat.self) from the incompatible format value: \(format.rawValue)")) } - let newPermissions = _permissions.map(JSONSchema.CoreContext.Permissions.init) return .init( format: newFormat, required: required, nullable: _nullable, - permissions: newPermissions, + permissions: _permissions, deprecated: _deprecated, title: title, description: description, diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index 1fecc086a..099878280 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -22,7 +22,6 @@ public extension OpenAPI { typealias Path = OpenAPIKitCore.Path typealias ComponentKey = OpenAPIKitCore.ComponentKey typealias Discriminator = OpenAPIKitCore.Discriminator - typealias ExternalDocumentation = OpenAPIKitCore.ExternalDocumentation } public extension JSONSchema { diff --git a/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift b/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift index d8f2a8366..264f9c2a2 100644 --- a/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift +++ b/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift @@ -1244,7 +1244,7 @@ extension JSONSchema.CoreContext { format: NewFormat(rawValue: format.rawValue)!, required: required, nullable: nullable, - permissions: JSONSchema.CoreContext.Permissions(permissions), + permissions: permissions, deprecated: deprecated, title: title, description: description, diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentCombiningTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentCombiningTests.swift index 1adc1ad8d..47cd1af52 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentCombiningTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentCombiningTests.swift @@ -1313,7 +1313,7 @@ extension JSONSchema.CoreContext { format: NewFormat(rawValue: format.rawValue)!, required: required, nullable: nullable, - permissions: JSONSchema.CoreContext.Permissions(permissions), + permissions: permissions, deprecated: deprecated, title: title, description: description, From 3cf544e17f9e6716a38bb94b2a830cb9632d3787 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 18 Dec 2022 15:21:17 -0600 Subject: [PATCH 11/30] Move OAuthFlows and SecurityScheme Location into core. Add decodeURLAsString helper to core. --- .../OpenAPIKit/Security/SecurityScheme.swift | 15 +- Sources/OpenAPIKit/_CoreReExport.swift | 5 + .../OpenAPIKit30/Security/OAuthFlows.swift | 275 ------------------ .../Security/SecurityScheme.swift | 9 +- Sources/OpenAPIKit30/_CoreReExport.swift | 5 + .../OAuthFlows.swift | 107 ++++--- Sources/OpenAPIKitCore/SecurityScheme.swift | 14 + .../Utility/Container+DecodeURLAsString.swift | 38 +++ 8 files changed, 118 insertions(+), 350 deletions(-) delete mode 100644 Sources/OpenAPIKit30/Security/OAuthFlows.swift rename Sources/{OpenAPIKit/Security => OpenAPIKitCore}/OAuthFlows.swift (67%) create mode 100644 Sources/OpenAPIKitCore/SecurityScheme.swift create mode 100644 Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 0003db2b1..c39b4f9c4 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -52,20 +52,14 @@ extension OpenAPI { public static func mutualTLS(description: String? = nil) -> SecurityScheme { return .init(type: .mutualTLS, description: description) } - + public enum SecurityType: Equatable { - case apiKey(name: String, location: Location) + case apiKey(name: String, location: SecuritySchemeLocation) case http(scheme: String, bearerFormat: String?) case oauth2(flows: OAuthFlows) case openIdConnect(openIdConnectUrl: URL) case mutualTLS } - - public enum Location: String, Codable, Equatable { - case query - case header - case cookie - } } } @@ -89,7 +83,7 @@ extension OpenAPI.SecurityScheme.SecurityType { case .openIdConnect: return .openIdConnect case .mutualTLS: - return .mutualTLS + return .mutualTLS } } } @@ -271,6 +265,3 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { return self } } - -extension OpenAPI.SecurityScheme.Location: Validatable {} -extension OpenAPI.SecurityScheme.SecurityType.Name: Validatable {} diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index 099878280..b16880bfd 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -22,6 +22,11 @@ public extension OpenAPI { typealias Path = OpenAPIKitCore.Path typealias ComponentKey = OpenAPIKitCore.ComponentKey typealias Discriminator = OpenAPIKitCore.Discriminator + typealias OAuthFlows = OpenAPIKitCore.OAuthFlows +} + +public extension OpenAPI.SecurityScheme { + typealias Location = OpenAPIKitCore.SecuritySchemeLocation } public extension JSONSchema { diff --git a/Sources/OpenAPIKit30/Security/OAuthFlows.swift b/Sources/OpenAPIKit30/Security/OAuthFlows.swift deleted file mode 100644 index 8d24ed999..000000000 --- a/Sources/OpenAPIKit30/Security/OAuthFlows.swift +++ /dev/null @@ -1,275 +0,0 @@ -// -// OAuthFlows.swift -// -// -// Created by Mathew Polzin on 1/23/20. -// - -import OpenAPIKitCore -import Foundation - -extension OpenAPI { - /// OpenAPI Spec "Oauth Flows Object" - /// - /// See [OpenAPI Oauth Flows Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object). - public struct OAuthFlows: Equatable { - public let implicit: Implicit? - public let password: Password? - public let clientCredentials: ClientCredentials? - public let authorizationCode: AuthorizationCode? - - public init( - implicit: Implicit? = nil, - password: Password? = nil, - clientCredentials: ClientCredentials? = nil, - authorizationCode: AuthorizationCode? = nil - ) { - self.implicit = implicit - self.password = password - self.clientCredentials = clientCredentials - self.authorizationCode = authorizationCode - } - } -} - -extension OpenAPI.OAuthFlows { - public typealias Scope = String - public typealias ScopeDescription = String - - public struct CommonFields: Equatable { - public let refreshUrl: URL? - public let scopes: OrderedDictionary - } - - @dynamicMemberLookup - public struct Implicit: Equatable { - private let common: CommonFields - public let authorizationUrl: URL - - public init(authorizationUrl: URL, refreshUrl: URL? = nil, scopes: OrderedDictionary) { - self.authorizationUrl = authorizationUrl - common = .init(refreshUrl: refreshUrl, scopes: scopes) - } - - public subscript(dynamicMember path: KeyPath) -> T { - return common[keyPath: path] - } - } - - @dynamicMemberLookup - public struct Password: Equatable { - private let common: CommonFields - public let tokenUrl: URL - - public init(tokenUrl: URL, refreshUrl: URL? = nil, scopes: OrderedDictionary) { - self.tokenUrl = tokenUrl - common = .init(refreshUrl: refreshUrl, scopes: scopes) - } - - public subscript(dynamicMember path: KeyPath) -> T { - return common[keyPath: path] - } - } - - @dynamicMemberLookup - public struct ClientCredentials: Equatable { - private let common: CommonFields - public let tokenUrl: URL - - public init(tokenUrl: URL, refreshUrl: URL? = nil, scopes: OrderedDictionary) { - self.tokenUrl = tokenUrl - common = .init(refreshUrl: refreshUrl, scopes: scopes) - } - - public subscript(dynamicMember path: KeyPath) -> T { - return common[keyPath: path] - } - } - - @dynamicMemberLookup - public struct AuthorizationCode: Equatable { - private let common: CommonFields - public let authorizationUrl: URL - public let tokenUrl: URL - - public init(authorizationUrl: URL, tokenUrl: URL, refreshUrl: URL? = nil, scopes: OrderedDictionary) { - self.authorizationUrl = authorizationUrl - self.tokenUrl = tokenUrl - common = .init(refreshUrl: refreshUrl, scopes: scopes) - } - - public subscript(dynamicMember path: KeyPath) -> T { - return common[keyPath: path] - } - } -} - -// MARK: - Codable -extension OpenAPI.OAuthFlows { - private enum CodingKeys: String, CodingKey { - case implicit - case password - case clientCredentials - case authorizationCode - } -} - -extension OpenAPI.OAuthFlows.CommonFields { - private enum CodingKeys: String, CodingKey { - case refreshUrl - case scopes - } -} - -extension OpenAPI.OAuthFlows.Implicit { - private enum CodingKeys: String, CodingKey { - case authorizationUrl - } -} - -extension OpenAPI.OAuthFlows.Password { - private enum CodingKeys: String, CodingKey { - case tokenUrl - } -} - -extension OpenAPI.OAuthFlows.ClientCredentials { - private enum CodingKeys: String, CodingKey { - case tokenUrl - } -} - -extension OpenAPI.OAuthFlows.AuthorizationCode { - private enum CodingKeys: String, CodingKey { - case authorizationUrl - case tokenUrl - } -} - -extension OpenAPI.OAuthFlows: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(implicit, forKey: .implicit) - try container.encodeIfPresent(password, forKey: .password) - try container.encodeIfPresent(clientCredentials, forKey: .clientCredentials) - try container.encodeIfPresent(authorizationCode, forKey: .authorizationCode) - } -} - -extension OpenAPI.OAuthFlows: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - implicit = try container.decodeIfPresent(OpenAPI.OAuthFlows.Implicit.self, forKey: .implicit) - password = try container.decodeIfPresent(OpenAPI.OAuthFlows.Password.self, forKey: .password) - clientCredentials = try container.decodeIfPresent(OpenAPI.OAuthFlows.ClientCredentials.self, forKey: .clientCredentials) - authorizationCode = try container.decodeIfPresent(OpenAPI.OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode) - } -} - -extension OpenAPI.OAuthFlows.CommonFields: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(refreshUrl?.absoluteString, forKey: .refreshUrl) - try container.encode(scopes, forKey: .scopes) - } -} - -extension OpenAPI.OAuthFlows.CommonFields: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - refreshUrl = try container.decodeURLAsStringIfPresent(forKey: .refreshUrl) - scopes = try container.decode(OrderedDictionary.self, forKey: .scopes) - } -} - -extension OpenAPI.OAuthFlows.Implicit: Encodable { - public func encode(to encoder: Encoder) throws { - try common.encode(to: encoder) - - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(authorizationUrl.absoluteString, forKey: .authorizationUrl) - } -} - -extension OpenAPI.OAuthFlows.Implicit: Decodable { - public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) - - let container = try decoder.container(keyedBy: CodingKeys.self) - - authorizationUrl = try container.decodeURLAsString(forKey: .authorizationUrl) - } -} - -extension OpenAPI.OAuthFlows.Password: Encodable { - public func encode(to encoder: Encoder) throws { - try common.encode(to: encoder) - - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(tokenUrl.absoluteString, forKey: .tokenUrl) - } -} - -extension OpenAPI.OAuthFlows.Password: Decodable { - public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) - - let container = try decoder.container(keyedBy: CodingKeys.self) - - tokenUrl = try container.decodeURLAsString(forKey: .tokenUrl) - } -} - -extension OpenAPI.OAuthFlows.ClientCredentials: Encodable { - public func encode(to encoder: Encoder) throws { - try common.encode(to: encoder) - - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(tokenUrl.absoluteString, forKey: .tokenUrl) - } -} - -extension OpenAPI.OAuthFlows.ClientCredentials: Decodable { - public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) - - let container = try decoder.container(keyedBy: CodingKeys.self) - - tokenUrl = try container.decodeURLAsString(forKey: .tokenUrl) - } -} - -extension OpenAPI.OAuthFlows.AuthorizationCode: Encodable { - public func encode(to encoder: Encoder) throws { - try common.encode(to: encoder) - - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(tokenUrl.absoluteString, forKey: .tokenUrl) - try container.encode(authorizationUrl.absoluteString, forKey: .authorizationUrl) - } -} - -extension OpenAPI.OAuthFlows.AuthorizationCode: Decodable { - public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) - - let container = try decoder.container(keyedBy: CodingKeys.self) - - tokenUrl = try container.decodeURLAsString(forKey: .tokenUrl) - authorizationUrl = try container.decodeURLAsString(forKey: .authorizationUrl) - } -} - -extension OpenAPI.OAuthFlows: Validatable {} -extension OpenAPI.OAuthFlows.Implicit: Validatable {} -extension OpenAPI.OAuthFlows.Password: Validatable {} -extension OpenAPI.OAuthFlows.ClientCredentials: Validatable {} -extension OpenAPI.OAuthFlows.AuthorizationCode: Validatable {} diff --git a/Sources/OpenAPIKit30/Security/SecurityScheme.swift b/Sources/OpenAPIKit30/Security/SecurityScheme.swift index 86e2ac496..ed40ec404 100644 --- a/Sources/OpenAPIKit30/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit30/Security/SecurityScheme.swift @@ -50,17 +50,11 @@ extension OpenAPI { } public enum SecurityType: Equatable { - case apiKey(name: String, location: Location) + case apiKey(name: String, location: SecuritySchemeLocation) case http(scheme: String, bearerFormat: String?) case oauth2(flows: OAuthFlows) case openIdConnect(openIdConnectUrl: URL) } - - public enum Location: String, Codable, Equatable { - case query - case header - case cookie - } } } @@ -249,5 +243,4 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { } } -extension OpenAPI.SecurityScheme.Location: Validatable {} extension OpenAPI.SecurityScheme.SecurityType.Name: Validatable {} diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index 099878280..b16880bfd 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -22,6 +22,11 @@ public extension OpenAPI { typealias Path = OpenAPIKitCore.Path typealias ComponentKey = OpenAPIKitCore.ComponentKey typealias Discriminator = OpenAPIKitCore.Discriminator + typealias OAuthFlows = OpenAPIKitCore.OAuthFlows +} + +public extension OpenAPI.SecurityScheme { + typealias Location = OpenAPIKitCore.SecuritySchemeLocation } public extension JSONSchema { diff --git a/Sources/OpenAPIKit/Security/OAuthFlows.swift b/Sources/OpenAPIKitCore/OAuthFlows.swift similarity index 67% rename from Sources/OpenAPIKit/Security/OAuthFlows.swift rename to Sources/OpenAPIKitCore/OAuthFlows.swift index 8d24ed999..ea3930674 100644 --- a/Sources/OpenAPIKit/Security/OAuthFlows.swift +++ b/Sources/OpenAPIKitCore/OAuthFlows.swift @@ -5,34 +5,31 @@ // Created by Mathew Polzin on 1/23/20. // -import OpenAPIKitCore import Foundation -extension OpenAPI { - /// OpenAPI Spec "Oauth Flows Object" - /// - /// See [OpenAPI Oauth Flows Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object). - public struct OAuthFlows: Equatable { - public let implicit: Implicit? - public let password: Password? - public let clientCredentials: ClientCredentials? - public let authorizationCode: AuthorizationCode? - - public init( - implicit: Implicit? = nil, - password: Password? = nil, - clientCredentials: ClientCredentials? = nil, - authorizationCode: AuthorizationCode? = nil - ) { - self.implicit = implicit - self.password = password - self.clientCredentials = clientCredentials - self.authorizationCode = authorizationCode - } +/// OpenAPI Spec "Oauth Flows Object" +/// +/// See [OpenAPI Oauth Flows Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object). +public struct OAuthFlows: Equatable { + public let implicit: Implicit? + public let password: Password? + public let clientCredentials: ClientCredentials? + public let authorizationCode: AuthorizationCode? + + public init( + implicit: Implicit? = nil, + password: Password? = nil, + clientCredentials: ClientCredentials? = nil, + authorizationCode: AuthorizationCode? = nil + ) { + self.implicit = implicit + self.password = password + self.clientCredentials = clientCredentials + self.authorizationCode = authorizationCode } } -extension OpenAPI.OAuthFlows { +extension OAuthFlows { public typealias Scope = String public typealias ScopeDescription = String @@ -105,7 +102,7 @@ extension OpenAPI.OAuthFlows { } // MARK: - Codable -extension OpenAPI.OAuthFlows { +extension OAuthFlows { private enum CodingKeys: String, CodingKey { case implicit case password @@ -114,39 +111,39 @@ extension OpenAPI.OAuthFlows { } } -extension OpenAPI.OAuthFlows.CommonFields { +extension OAuthFlows.CommonFields { private enum CodingKeys: String, CodingKey { case refreshUrl case scopes } } -extension OpenAPI.OAuthFlows.Implicit { +extension OAuthFlows.Implicit { private enum CodingKeys: String, CodingKey { case authorizationUrl } } -extension OpenAPI.OAuthFlows.Password { +extension OAuthFlows.Password { private enum CodingKeys: String, CodingKey { case tokenUrl } } -extension OpenAPI.OAuthFlows.ClientCredentials { +extension OAuthFlows.ClientCredentials { private enum CodingKeys: String, CodingKey { case tokenUrl } } -extension OpenAPI.OAuthFlows.AuthorizationCode { +extension OAuthFlows.AuthorizationCode { private enum CodingKeys: String, CodingKey { case authorizationUrl case tokenUrl } } -extension OpenAPI.OAuthFlows: Encodable { +extension OAuthFlows: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -157,18 +154,18 @@ extension OpenAPI.OAuthFlows: Encodable { } } -extension OpenAPI.OAuthFlows: Decodable { +extension OAuthFlows: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - implicit = try container.decodeIfPresent(OpenAPI.OAuthFlows.Implicit.self, forKey: .implicit) - password = try container.decodeIfPresent(OpenAPI.OAuthFlows.Password.self, forKey: .password) - clientCredentials = try container.decodeIfPresent(OpenAPI.OAuthFlows.ClientCredentials.self, forKey: .clientCredentials) - authorizationCode = try container.decodeIfPresent(OpenAPI.OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode) + implicit = try container.decodeIfPresent(OAuthFlows.Implicit.self, forKey: .implicit) + password = try container.decodeIfPresent(OAuthFlows.Password.self, forKey: .password) + clientCredentials = try container.decodeIfPresent(OAuthFlows.ClientCredentials.self, forKey: .clientCredentials) + authorizationCode = try container.decodeIfPresent(OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode) } } -extension OpenAPI.OAuthFlows.CommonFields: Encodable { +extension OAuthFlows.CommonFields: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -177,16 +174,16 @@ extension OpenAPI.OAuthFlows.CommonFields: Encodable { } } -extension OpenAPI.OAuthFlows.CommonFields: Decodable { +extension OAuthFlows.CommonFields: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) refreshUrl = try container.decodeURLAsStringIfPresent(forKey: .refreshUrl) - scopes = try container.decode(OrderedDictionary.self, forKey: .scopes) + scopes = try container.decode(OrderedDictionary.self, forKey: .scopes) } } -extension OpenAPI.OAuthFlows.Implicit: Encodable { +extension OAuthFlows.Implicit: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -196,9 +193,9 @@ extension OpenAPI.OAuthFlows.Implicit: Encodable { } } -extension OpenAPI.OAuthFlows.Implicit: Decodable { +extension OAuthFlows.Implicit: Decodable { public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) + common = try OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -206,7 +203,7 @@ extension OpenAPI.OAuthFlows.Implicit: Decodable { } } -extension OpenAPI.OAuthFlows.Password: Encodable { +extension OAuthFlows.Password: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -216,9 +213,9 @@ extension OpenAPI.OAuthFlows.Password: Encodable { } } -extension OpenAPI.OAuthFlows.Password: Decodable { +extension OAuthFlows.Password: Decodable { public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) + common = try OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -226,7 +223,7 @@ extension OpenAPI.OAuthFlows.Password: Decodable { } } -extension OpenAPI.OAuthFlows.ClientCredentials: Encodable { +extension OAuthFlows.ClientCredentials: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -236,9 +233,9 @@ extension OpenAPI.OAuthFlows.ClientCredentials: Encodable { } } -extension OpenAPI.OAuthFlows.ClientCredentials: Decodable { +extension OAuthFlows.ClientCredentials: Decodable { public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) + common = try OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -246,7 +243,7 @@ extension OpenAPI.OAuthFlows.ClientCredentials: Decodable { } } -extension OpenAPI.OAuthFlows.AuthorizationCode: Encodable { +extension OAuthFlows.AuthorizationCode: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -257,9 +254,9 @@ extension OpenAPI.OAuthFlows.AuthorizationCode: Encodable { } } -extension OpenAPI.OAuthFlows.AuthorizationCode: Decodable { +extension OAuthFlows.AuthorizationCode: Decodable { public init(from decoder: Decoder) throws { - common = try OpenAPI.OAuthFlows.CommonFields(from: decoder) + common = try OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -268,8 +265,8 @@ extension OpenAPI.OAuthFlows.AuthorizationCode: Decodable { } } -extension OpenAPI.OAuthFlows: Validatable {} -extension OpenAPI.OAuthFlows.Implicit: Validatable {} -extension OpenAPI.OAuthFlows.Password: Validatable {} -extension OpenAPI.OAuthFlows.ClientCredentials: Validatable {} -extension OpenAPI.OAuthFlows.AuthorizationCode: Validatable {} +extension OAuthFlows: Validatable {} +extension OAuthFlows.Implicit: Validatable {} +extension OAuthFlows.Password: Validatable {} +extension OAuthFlows.ClientCredentials: Validatable {} +extension OAuthFlows.AuthorizationCode: Validatable {} diff --git a/Sources/OpenAPIKitCore/SecurityScheme.swift b/Sources/OpenAPIKitCore/SecurityScheme.swift new file mode 100644 index 000000000..841aea8e7 --- /dev/null +++ b/Sources/OpenAPIKitCore/SecurityScheme.swift @@ -0,0 +1,14 @@ +// +// SecurityScheme.swift +// +// +// Created by Mathew Polzin on 12/18/22. +// + +public enum SecuritySchemeLocation: String, Codable, Equatable { + case query + case header + case cookie +} + +extension SecuritySchemeLocation: Validatable {} diff --git a/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift b/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift new file mode 100644 index 000000000..a95f78732 --- /dev/null +++ b/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift @@ -0,0 +1,38 @@ +// +// Container+DecodeURLAsString.swift +// +// +// Created by Mathew Polzin on 7/5/20. +// + +import OpenAPIKitCore +import Foundation + +extension KeyedDecodingContainerProtocol { + internal func decodeURLAsString(forKey key: Self.Key) throws -> URL { + let string = try decode(String.self, forKey: key) + guard let url = URL(string: string) else { + throw InconsistencyError( + subjectName: key.stringValue, + details: "If specified, must be a valid URL", + codingPath: codingPath + ) + } + return url + } + + internal func decodeURLAsStringIfPresent(forKey key: Self.Key) throws -> URL? { + guard let string = try decodeIfPresent(String.self, forKey: key) else { + return nil + } + + guard let url = URL(string: string) else { + throw InconsistencyError( + subjectName: key.stringValue, + details: "If specified, must be a valid URL", + codingPath: codingPath + ) + } + return url + } +} From 5d50faa265477d1be679b8d039508c2b3af525fd Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 18 Dec 2022 15:24:02 -0600 Subject: [PATCH 12/30] Get compat layer compiling so that tests can be run. --- Sources/OpenAPIKitCompat/Compat30To31.swift | 125 +++++++++++--------- 1 file changed, 72 insertions(+), 53 deletions(-) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 64ef1bca6..75d8bdf71 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -63,82 +63,101 @@ private extension OpenAPIKit30.OpenAPI.Server { private extension OpenAPIKit30.OpenAPI.PathItem { func to31() -> OpenAPI31.PathItem { + // TODO: finish filling out constructor with all optional arguments. OpenAPI31.PathItem( - summary: <#T##String?#> + summary: summary ) } } +private extension OpenAPIKit30.OpenAPI.SecurityScheme.SecurityType { + func to31() -> OpenAPI31.SecurityScheme.SecurityType { + switch self { + case .apiKey(name: let name, location: let location): + return .apiKey(name: name, location: location) + case .http(scheme: let scheme, bearerFormat: let bearerFormat): + return .http(scheme: scheme, bearerFormat: bearerFormat) + case .oauth2(flows: let flows): + return .oauth2(flows: flows) + case .openIdConnect(openIdConnectUrl: let openIdConnectUrl): + return .openIdConnect(openIdConnectUrl: openIdConnectUrl) + } + } +} + private extension OpenAPIKit30.OpenAPI.SecurityScheme { func to31() -> OpenAPI31.SecurityScheme { + // TODO: finish filling out constructor with all optional arguments. OpenAPI31.SecurityScheme( - type: <#T##OpenAPI.SecurityScheme.SecurityType#> + type: type.to31() ) } } private extension OpenAPIKit30.JSONSchema { func to31() -> OpenAPIKit.JSONSchema { - let schema: OpenAPIKit.JSONSchema.Schema - - switch value { - case .boolean(let core): - schema = .boolean( - .init( - format: core.format, - required: core.required, - nullable: core.nullable, - permissions: core.permissions, - deprecated: core.deprecated, - title: core.title, - description: core.description, - discriminator: core.discriminator, - externalDocs: <#T##OpenAPI.ExternalDocumentation?#> - ) - ) - case .number(_, _): - <#code#> - case .integer(_, _): - <#code#> - case .string(_, _): - <#code#> - case .object(_, _): - <#code#> - case .array(_, _): - <#code#> - case .all(of: let of, core: let core): - <#code#> - case .one(of: let of, core: let core): - <#code#> - case .any(of: let of, core: let core): - <#code#> - case .not(_, core: let core): - <#code#> - case .reference(_, _): - <#code#> - case .fragment(_): - <#code#> - } - +// let schema: OpenAPIKit.JSONSchema.Schema + +// switch value { +// case .boolean(let core): +// schema = .boolean( +// .init( +// format: core.format, +// required: core.required, +// nullable: core.nullable, +// permissions: core.permissions, +// deprecated: core.deprecated, +// title: core.title, +// description: core.description, +// discriminator: core.discriminator, +// externalDocs: <#T##OpenAPI.ExternalDocumentation?#> +// ) +// ) +// case .number(_, _): +// <#code#> +// case .integer(_, _): +// <#code#> +// case .string(_, _): +// <#code#> +// case .object(_, _): +// <#code#> +// case .array(_, _): +// <#code#> +// case .all(of: let of, core: let core): +// <#code#> +// case .one(of: let of, core: let core): +// <#code#> +// case .any(of: let of, core: let core): +// <#code#> +// case .not(_, core: let core): +// <#code#> +// case .reference(_, _): +// <#code#> +// case .fragment(_): +// <#code#> +// } + + // TODO: finish filling out constructor, replacing the null schema. OpenAPIKit.JSONSchema( - schema: schema + schema: .null // schema ) } } private extension OpenAPIKit30.OpenAPI.Components { func to31() -> OpenAPI31.Components { + // TODO: finish filling out constructor with all optional arguments. OpenAPI31.Components( schemas: schemas.mapValues { $0.to31() }, - responses: responses.mapValues { $0.to31() }, - parameters: parameters.mapValues { $0.to31() }, - examples: examples.mapValues { $0.to31() }, - requestBodies: requestBodies.mapValues { $0.to31() }, - headers: headers.mapValues { $0.to31() }, - securitySchemes: securitySchemes.mapValues { $0.to31() }, - links: links.mapValues { $0.to31() }, - callbacks: callbacks.mapValues { $0.to31() }, - vendorExtensions: vendorExtensions.mapValues { $0.to31() } +// responses: responses.mapValues { $0.to31() }, +// parameters: parameters.mapValues { $0.to31() }, +// examples: examples.mapValues { $0.to31() }, +// requestBodies: requestBodies.mapValues { $0.to31() }, +// headers: headers.mapValues { $0.to31() }, + securitySchemes: securitySchemes.mapValues { $0.to31() } +// links: links.mapValues { $0.to31() }, +// callbacks: callbacks.mapValues { $0.to31() }, +// vendorExtensions: vendorExtensions.mapValues { $0.to31() } ) } } From bc55887059011ce65d9088e112b6045462c3d3c9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 19 Dec 2022 01:08:49 -0600 Subject: [PATCH 13/30] move schema context style type and response status code type to core. continue implementing conversion functions for compatibility layer. --- .../Parameter/ParameterSchemaContext.swift | 56 ++- Sources/OpenAPIKit/Response/Response.swift | 103 ------ Sources/OpenAPIKit/_CoreReExport.swift | 8 + .../Parameter/ParameterSchemaContext.swift | 58 ++- Sources/OpenAPIKit30/Response/Response.swift | 103 ------ Sources/OpenAPIKit30/_CoreReExport.swift | 8 + Sources/OpenAPIKitCompat/Compat30To31.swift | 338 ++++++++++++++++-- Sources/OpenAPIKitCore/Either/Either.swift | 20 ++ .../ParameterSchemaContextStyle.swift | 16 + .../OpenAPIKitCore/ResponseStatusCode.swift | 106 ++++++ 10 files changed, 518 insertions(+), 298 deletions(-) create mode 100644 Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift create mode 100644 Sources/OpenAPIKitCore/ResponseStatusCode.swift diff --git a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift index 583396bee..3af3e8ff2 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift @@ -127,41 +127,31 @@ extension OpenAPI.Parameter { } } -extension OpenAPI.Parameter.SchemaContext { - public enum Style: String, CaseIterable, Codable { - case form - case simple - case matrix - case label - case spaceDelimited - case pipeDelimited - case deepObject - - /// Get the default `Style` for the given location - /// per the OpenAPI Specification. - /// - /// See the `style` fixed field under - /// [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). - public static func `default`(for location: OpenAPI.Parameter.Context) -> Self { - switch location { - case .query: - return .form - case .cookie: - return .form - case .path: - return .simple - case .header: - return .simple - } +extension OpenAPI.Parameter.SchemaContext.Style { + /// Get the default `Style` for the given location + /// per the OpenAPI Specification. + /// + /// See the `style` fixed field under + /// [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). + public static func `default`(for location: OpenAPI.Parameter.Context) -> Self { + switch location { + case .query: + return .form + case .cookie: + return .form + case .path: + return .simple + case .header: + return .simple } + } - internal var defaultExplode: Bool { - switch self { - case .form: - return true - default: - return false - } + internal var defaultExplode: Bool { + switch self { + case .form: + return true + default: + return false } } } diff --git a/Sources/OpenAPIKit/Response/Response.swift b/Sources/OpenAPIKit/Response/Response.swift index bd940397f..f85a83e2f 100644 --- a/Sources/OpenAPIKit/Response/Response.swift +++ b/Sources/OpenAPIKit/Response/Response.swift @@ -68,109 +68,6 @@ extension OrderedDictionary where Key == OpenAPI.Response.StatusCode { } } -// MARK: - Status Code -extension OpenAPI.Response { - /// An HTTP Status code or status code range. - /// - /// OpenAPI supports one of the following as a key in the [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object): - /// - A `default` entry. - /// - A specific status code. - /// - A status code range. - /// - /// The `.default` case is used for a default entry. - /// - /// You can use integer literals to specify an exact status code. - /// - /// Status code ranges are named in the `StatusCode.Range` enum. For example, the "1XX" range (100-199) can be written as either `.range(.information)` or as `.range(.init(rawValue: "1XX"))`. - public struct StatusCode: RawRepresentable, Equatable, Hashable, HasWarnings { - public typealias RawValue = String - - public let warnings: [OpenAPI.Warning] - - public var value: Code - - internal init(value: Code) { - self.value = value - warnings = [] - } - - public static let `default`: Self = .init(value: .default) - public static func range(_ range: Range) -> Self { .init(value: .range(range)) } - public static func status(code: Int) -> Self { .init(value: .status(code: code)) } - - public enum Code: Equatable, Hashable { - case `default` - case range(Range) - case status(code: Int) - } - - public enum Range: String { - /// Status Code `100-199` - case information = "1XX" - /// Status Code `200-299` - case success = "2XX" - /// Status Code `300-399` - case redirect = "3XX" - /// Status Code `400-499` - case clientError = "4XX" - /// Status Code `500-599` - case serverError = "5XX" - } - - public var rawValue: String { - switch value { - case .default: - return "default" - - case .range(let range): - return range.rawValue - - case .status(code: let code): - return String(code) - } - } - - public var isSuccess: Bool { - switch value { - case .range(.success), .status(code: 200..<300): - return true - case .range, .status, .default: - return false - } - } - - public init?(rawValue: String) { - if let val = Int(rawValue) { - value = .status(code: val) - warnings = [] - } else if rawValue == "default" { - value = .default - warnings = [] - } else if let range = Range(rawValue: rawValue.uppercased()) { - value = .range(range) - warnings = [] - } else if rawValue.contains("/"), - let first = (rawValue.split(separator: "/")).first, - let fallback = Self(rawValue: String(first)) { - value = fallback.value - warnings = [ - .message("Found non-compliant Status Code '\(rawValue)' but was able to parse as \(first)") - ] - } else { - return nil - } - } - } -} - -extension OpenAPI.Response.StatusCode: ExpressibleByIntegerLiteral { - - public init(integerLiteral value: Int) { - self.value = .status(code: value) - warnings = [] - } -} - // MARK: `Either` convenience methods extension Either where A == OpenAPI.Reference, B == OpenAPI.Response { diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index b16880bfd..ecc4570d9 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -29,6 +29,14 @@ public extension OpenAPI.SecurityScheme { typealias Location = OpenAPIKitCore.SecuritySchemeLocation } +public extension OpenAPI.Parameter.SchemaContext { + typealias Style = ParameterSchemaContextStyle +} + +public extension OpenAPI.Response { + typealias StatusCode = OpenAPIKitCore.ResponseStatusCode +} + public extension JSONSchema { typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions } diff --git a/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift index a330cb896..e44df7313 100644 --- a/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift @@ -128,41 +128,31 @@ extension OpenAPI.Parameter { } } -extension OpenAPI.Parameter.SchemaContext { - public enum Style: String, CaseIterable, Codable { - case form - case simple - case matrix - case label - case spaceDelimited - case pipeDelimited - case deepObject - - /// Get the default `Style` for the given location - /// per the OpenAPI Specification. - /// - /// See the `style` fixed field under - /// [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). - public static func `default`(for location: OpenAPI.Parameter.Context) -> Self { - switch location { - case .query: - return .form - case .cookie: - return .form - case .path: - return .simple - case .header: - return .simple - } +extension OpenAPI.Parameter.SchemaContext.Style { + /// Get the default `Style` for the given location + /// per the OpenAPI Specification. + /// + /// See the `style` fixed field under + /// [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). + public static func `default`(for location: OpenAPI.Parameter.Context) -> Self { + switch location { + case .query: + return .form + case .cookie: + return .form + case .path: + return .simple + case .header: + return .simple } - - internal var defaultExplode: Bool { - switch self { - case .form: - return true - default: - return false - } + } + + internal var defaultExplode: Bool { + switch self { + case .form: + return true + default: + return false } } } diff --git a/Sources/OpenAPIKit30/Response/Response.swift b/Sources/OpenAPIKit30/Response/Response.swift index 02edee5da..56e057ddc 100644 --- a/Sources/OpenAPIKit30/Response/Response.swift +++ b/Sources/OpenAPIKit30/Response/Response.swift @@ -68,109 +68,6 @@ extension OrderedDictionary where Key == OpenAPI.Response.StatusCode { } } -// MARK: - Status Code -extension OpenAPI.Response { - /// An HTTP Status code or status code range. - /// - /// OpenAPI supports one of the following as a key in the [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object): - /// - A `default` entry. - /// - A specific status code. - /// - A status code range. - /// - /// The `.default` case is used for a default entry. - /// - /// You can use integer literals to specify an exact status code. - /// - /// Status code ranges are named in the `StatusCode.Range` enum. For example, the "1XX" range (100-199) can be written as either `.range(.information)` or as `.range(.init(rawValue: "1XX"))`. - public struct StatusCode: RawRepresentable, Equatable, Hashable, HasWarnings { - public typealias RawValue = String - - public let warnings: [OpenAPI.Warning] - - public var value: Code - - internal init(value: Code) { - self.value = value - warnings = [] - } - - public static let `default`: Self = .init(value: .default) - public static func range(_ range: Range) -> Self { .init(value: .range(range)) } - public static func status(code: Int) -> Self { .init(value: .status(code: code)) } - - public enum Code: Equatable, Hashable { - case `default` - case range(Range) - case status(code: Int) - } - - public enum Range: String { - /// Status Code `100-199` - case information = "1XX" - /// Status Code `200-299` - case success = "2XX" - /// Status Code `300-399` - case redirect = "3XX" - /// Status Code `400-499` - case clientError = "4XX" - /// Status Code `500-599` - case serverError = "5XX" - } - - public var rawValue: String { - switch value { - case .default: - return "default" - - case .range(let range): - return range.rawValue - - case .status(code: let code): - return String(code) - } - } - - public var isSuccess: Bool { - switch value { - case .range(.success), .status(code: 200..<300): - return true - case .range, .status, .default: - return false - } - } - - public init?(rawValue: String) { - if let val = Int(rawValue) { - value = .status(code: val) - warnings = [] - } else if rawValue == "default" { - value = .default - warnings = [] - } else if let range = Range(rawValue: rawValue.uppercased()) { - value = .range(range) - warnings = [] - } else if rawValue.contains("/"), - let first = (rawValue.split(separator: "/")).first, - let fallback = Self(rawValue: String(first)) { - value = fallback.value - warnings = [ - .message("Found non-compliant Status Code '\(rawValue)' but was able to parse as \(first)") - ] - } else { - return nil - } - } - } -} - -extension OpenAPI.Response.StatusCode: ExpressibleByIntegerLiteral { - - public init(integerLiteral value: Int) { - self.value = .status(code: value) - warnings = [] - } -} - // MARK: `Either` convenience methods extension Either where A == JSONReference, B == OpenAPI.Response { diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index b16880bfd..ecc4570d9 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -29,6 +29,14 @@ public extension OpenAPI.SecurityScheme { typealias Location = OpenAPIKitCore.SecuritySchemeLocation } +public extension OpenAPI.Parameter.SchemaContext { + typealias Style = ParameterSchemaContextStyle +} + +public extension OpenAPI.Response { + typealias StatusCode = OpenAPIKitCore.ResponseStatusCode +} + public extension JSONSchema { typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions } diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 75d8bdf71..7ad73040a 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -5,7 +5,6 @@ // Created by Mathew Polzin on 12/17/22. // - import OpenAPIKit import OpenAPIKit30 @@ -21,8 +20,13 @@ public extension OpenAPIKit30.OpenAPI.Document { } } -private extension OpenAPIKit30.OpenAPI.Document { - func to31() -> OpenAPI31.Document { +private protocol To31 { + associatedtype Destination + func to31() -> Destination +} + +extension OpenAPIKit30.OpenAPI.Document: To31 { + fileprivate func to31() -> OpenAPI31.Document { let info = OpenAPI31.Document.Info( title: info.title, version: info.version ) @@ -31,17 +35,26 @@ private extension OpenAPIKit30.OpenAPI.Document { let paths = paths.mapValues { $0.to31() } + let security = security.map { $0.to31() } + + let tags = tags?.map { $0.to31() } + return OpenAPI31.Document( + openAPIVersion: .v3_1_0, info: info, servers: servers, paths: paths, - components: components.to31() + components: components.to31(), + security: security, + tags: tags, + externalDocs: externalDocs?.to31(), + vendorExtensions: vendorExtensions ) } } -private extension OpenAPIKit30.OpenAPI.Server { - func to31() -> OpenAPI31.Server { +extension OpenAPIKit30.OpenAPI.Server: To31 { + fileprivate func to31() -> OpenAPI31.Server { let variables = variables.mapValues { variable in OpenAPI31.Server.Variable( @@ -61,17 +74,291 @@ private extension OpenAPIKit30.OpenAPI.Server { } } -private extension OpenAPIKit30.OpenAPI.PathItem { - func to31() -> OpenAPI31.PathItem { +extension OpenAPIKit30.OpenAPI.Header: To31 { + fileprivate func to31() -> OpenAPI31.Header { + let newSchemaOrContent: Either + switch schemaOrContent { + case .a(let context): + newSchemaOrContent = .a(context.to31()) + case .b(let contentMap): + newSchemaOrContent = .b(contentMap.mapValues { $0.to31() }) + } + + return OpenAPI31.Header( + schemaOrContent: newSchemaOrContent, + description: description, + required: `required`, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.Parameter.Context: To31 { + fileprivate func to31() -> OpenAPI31.Parameter.Context { + switch self { + case .query(required: let required, allowEmptyValue: let allowEmptyValue): + return .query(required: required, allowEmptyValue: allowEmptyValue) + case .header(required: let required): + return .header(required: required) + case .path: + return .path + case .cookie(required: let required): + return .cookie(required: required) + } + } +} + +extension OpenAPIKit30.OpenAPI.Example: To31 { + fileprivate func to31() -> OpenAPI31.Example { + OpenAPI31.Example( + summary: summary, + description: description, + value: value, + vendorExtensions: vendorExtensions + ) + } +} + +extension Either: To31 where A: To31, B: To31 { + fileprivate func to31() -> Either { + switch self { + case .a(let a): + return .a(a.to31()) + case .b(let b): + return .b(b.to31()) + } + } +} + +fileprivate func eitherRefTo31(_ either: Either, T>) -> Either, U> where T: To31, T.Destination == U { + switch either { + case .a(let a): + return .a(.init(a.to31())) + case .b(let b): + return .b(b.to31()) + } +} + +extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { + fileprivate func to31() -> OpenAPI31.Parameter.SchemaContext { + let examples = examples?.mapValues(eitherRefTo31) + switch schema { + case .a(let ref): + if let examples { + return OpenAPI31.Parameter.SchemaContext( + schemaReference: .init(ref.to31()), + style: style, + allowReserved: allowReserved, + examples: examples + ) + } else { + return OpenAPI31.Parameter.SchemaContext( + schemaReference: .init(ref.to31()), + style: style, + allowReserved: allowReserved, + example: example + ) + } + case .b(let schema): + if let examples { + return OpenAPI31.Parameter.SchemaContext( + schema.to31(), + style: style, + allowReserved: allowReserved, + examples: examples + ) + } else { + return OpenAPI31.Parameter.SchemaContext( + schema.to31(), + style: style, + allowReserved: allowReserved, + example: example + ) + } + } + } +} + +extension OpenAPIKit30.OpenAPI.Content.Encoding: To31 { + fileprivate func to31() -> OpenAPI31.Content.Encoding { + OpenAPI31.Content.Encoding( + contentType: contentType, + headers: headers?.mapValues(eitherRefTo31), + style: style, + explode: explode, + allowReserved: allowReserved + ) + } +} + +extension OpenAPIKit30.OpenAPI.Content: To31 { + fileprivate func to31() -> OpenAPI31.Content { + let examples = examples?.mapValues(eitherRefTo31) + if let examples { + return OpenAPI31.Content( + schema: schema.map(eitherRefTo31), + examples: examples, + encoding: encoding?.mapValues { $0.to31() }, + vendorExtensions: vendorExtensions + ) + } else { + return OpenAPI31.Content( + schema: schema.map(eitherRefTo31), + example: example, + encoding: encoding?.mapValues { $0.to31() }, + vendorExtensions: vendorExtensions + ) + } + } +} + +extension OpenAPIKit30.OpenAPI.Parameter: To31 { + fileprivate func to31() -> OpenAPI31.Parameter { + let newSchemaOrContent: Either + switch schemaOrContent { + case .a(let context): + newSchemaOrContent = .a(context.to31()) + case .b(let contentMap): + newSchemaOrContent = .b(contentMap.mapValues { $0.to31() }) + } + + return OpenAPI31.Parameter( + name: name, + context: context.to31(), + schemaOrContent: newSchemaOrContent, + description: description, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.RuntimeExpression: To31 { + fileprivate func to31() -> OpenAPI31.Run { + + } +} + +extension OpenAPIKit30.OpenAPI.Link: To31 { + fileprivate func to31() -> OpenAPI31.Link { + return OpenAPI31.Link( + operation: operation, + parameters: parameters.mapValues { parameter in parameter.mapFirst { $0.to31() }}, + requestBody: requestBody?.mapFirst { $0.to31() }, + description: description, + server: server?.to31(), + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.Response: To31 { + fileprivate func to31() -> OpenAPI31.Response { + OpenAPI31.Response( + description: description, + headers: headers?.mapValues(eitherRefTo31), + content: content.mapValues { $0.to31() }, + links: links.mapValues(eitherRefTo31), + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.Operation: To31 { + fileprivate func to31() -> OpenAPI31.Operation { // TODO: finish filling out constructor with all optional arguments. - OpenAPI31.PathItem( - summary: summary + OpenAPI31.Operation( + tags: tags, + summary: summary, + description: description, + externalDocs: externalDocs?.to31(), + operationId: operationId, + parameters: parameters.map(eitherRefTo31), +// requestBody: eitherRefTo31(requestBody), + responses: responses.mapValues(eitherRefTo31), +// callbacks: callbacks.mapValues(eitherRefTo31), + deprecated: deprecated, +// security: , + servers: servers?.map { $0.to31() }, + vendorExtensions: vendorExtensions ) } } -private extension OpenAPIKit30.OpenAPI.SecurityScheme.SecurityType { - func to31() -> OpenAPI31.SecurityScheme.SecurityType { +extension OpenAPIKit30.OpenAPI.PathItem: To31 { + fileprivate func to31() -> OpenAPI31.PathItem { + let servers = servers?.map { $0.to31() } + + let parameters = parameters.map(eitherRefTo31) + + return OpenAPI31.PathItem( + summary: summary, + description: description, + servers: servers, + parameters: parameters, + get: `get`?.to31(), + put: put?.to31(), + post: post?.to31(), + delete: delete?.to31(), + options: options?.to31(), + head: head?.to31(), + patch: patch?.to31(), + trace: trace?.to31(), + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.SecurityRequirement: To31 { + fileprivate func to31() -> OpenAPI31.SecurityRequirement { + var result = [OpenAPIKit.JSONReference: [String]]() + for (key, value) in self { + result[key.to31()] = value + } + return result + } +} + +private extension OpenAPIKit30.JSONReference { + func to31() -> OpenAPIKit.JSONReference { + switch self { + case .internal(let ref): + switch ref { + case .component(name: let name): + return .internal(.component(name: name)) + case .path(let path): + return .internal(.path(.init(rawValue: path.rawValue))) + } + case .external(let url): + return OpenAPIKit.JSONReference.external(url) + } + } +} + +extension OpenAPIKit30.OpenAPI.Tag: To31 { + fileprivate func to31() -> OpenAPI31.Tag { + OpenAPI31.Tag( + name: name, + description: description, + externalDocs: externalDocs?.to31(), + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.ExternalDocumentation: To31 { + fileprivate func to31() -> OpenAPI31.ExternalDocumentation { + OpenAPI31.ExternalDocumentation( + description: description, + url: url, + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.SecurityScheme.SecurityType: To31 { + fileprivate func to31() -> OpenAPI31.SecurityScheme.SecurityType { switch self { case .apiKey(name: let name, location: let location): return .apiKey(name: name, location: location) @@ -85,17 +372,18 @@ private extension OpenAPIKit30.OpenAPI.SecurityScheme.SecurityType { } } -private extension OpenAPIKit30.OpenAPI.SecurityScheme { - func to31() -> OpenAPI31.SecurityScheme { - // TODO: finish filling out constructor with all optional arguments. +extension OpenAPIKit30.OpenAPI.SecurityScheme: To31 { + fileprivate func to31() -> OpenAPI31.SecurityScheme { OpenAPI31.SecurityScheme( - type: type.to31() + type: type.to31(), + description: description, + vendorExtensions: vendorExtensions ) } } -private extension OpenAPIKit30.JSONSchema { - func to31() -> OpenAPIKit.JSONSchema { +extension OpenAPIKit30.JSONSchema: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema { // let schema: OpenAPIKit.JSONSchema.Schema // switch value { @@ -144,20 +432,20 @@ private extension OpenAPIKit30.JSONSchema { } } -private extension OpenAPIKit30.OpenAPI.Components { - func to31() -> OpenAPI31.Components { +extension OpenAPIKit30.OpenAPI.Components: To31 { + fileprivate func to31() -> OpenAPI31.Components { // TODO: finish filling out constructor with all optional arguments. OpenAPI31.Components( schemas: schemas.mapValues { $0.to31() }, // responses: responses.mapValues { $0.to31() }, -// parameters: parameters.mapValues { $0.to31() }, -// examples: examples.mapValues { $0.to31() }, + parameters: parameters.mapValues { $0.to31() }, + examples: examples.mapValues { $0.to31() }, // requestBodies: requestBodies.mapValues { $0.to31() }, -// headers: headers.mapValues { $0.to31() }, - securitySchemes: securitySchemes.mapValues { $0.to31() } + headers: headers.mapValues { $0.to31() }, + securitySchemes: securitySchemes.mapValues { $0.to31() }, // links: links.mapValues { $0.to31() }, // callbacks: callbacks.mapValues { $0.to31() }, -// vendorExtensions: vendorExtensions.mapValues { $0.to31() } + vendorExtensions: vendorExtensions ) } } diff --git a/Sources/OpenAPIKitCore/Either/Either.swift b/Sources/OpenAPIKitCore/Either/Either.swift index b45ebe4c5..13eddae32 100644 --- a/Sources/OpenAPIKitCore/Either/Either.swift +++ b/Sources/OpenAPIKitCore/Either/Either.swift @@ -55,3 +55,23 @@ public enum Either { } extension Either: Equatable where A: Equatable, B: Equatable {} + +extension Either { + public func mapFirst(_ transform: (A) -> T) -> Either { + switch self { + case .a(let a): + return .a(transform(a)) + case .b(let b): + return .b(b) + } + } + + public func mapSecond(_ transform: (B) -> T) -> Either { + switch self { + case .a(let a): + return .a(a) + case .b(let b): + return .b(transform(b)) + } + } +} diff --git a/Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift b/Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift new file mode 100644 index 000000000..2e1758c7f --- /dev/null +++ b/Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift @@ -0,0 +1,16 @@ +// +// ParameterSchemaContextStyle.swift +// +// +// Created by Mathew Polzin on 12/18/22. +// + +public enum ParameterSchemaContextStyle: String, CaseIterable, Codable { + case form + case simple + case matrix + case label + case spaceDelimited + case pipeDelimited + case deepObject +} diff --git a/Sources/OpenAPIKitCore/ResponseStatusCode.swift b/Sources/OpenAPIKitCore/ResponseStatusCode.swift new file mode 100644 index 000000000..1695246db --- /dev/null +++ b/Sources/OpenAPIKitCore/ResponseStatusCode.swift @@ -0,0 +1,106 @@ +// +// ResponseStatusCode.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +/// An HTTP Status code or status code range. +/// +/// OpenAPI supports one of the following as a key in the [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object): +/// - A `default` entry. +/// - A specific status code. +/// - A status code range. +/// +/// The `.default` case is used for a default entry. +/// +/// You can use integer literals to specify an exact status code. +/// +/// Status code ranges are named in the `StatusCode.Range` enum. For example, the "1XX" range (100-199) can be written as either `.range(.information)` or as `.range(.init(rawValue: "1XX"))`. +public struct ResponseStatusCode: RawRepresentable, Equatable, Hashable, HasWarnings { + public typealias RawValue = String + + public let warnings: [Warning] + + public var value: Code + + internal init(value: Code) { + self.value = value + warnings = [] + } + + public static let `default`: Self = .init(value: .default) + public static func range(_ range: Range) -> Self { .init(value: .range(range)) } + public static func status(code: Int) -> Self { .init(value: .status(code: code)) } + + public enum Code: Equatable, Hashable { + case `default` + case range(Range) + case status(code: Int) + } + + public enum Range: String { + /// Status Code `100-199` + case information = "1XX" + /// Status Code `200-299` + case success = "2XX" + /// Status Code `300-399` + case redirect = "3XX" + /// Status Code `400-499` + case clientError = "4XX" + /// Status Code `500-599` + case serverError = "5XX" + } + + public var rawValue: String { + switch value { + case .default: + return "default" + + case .range(let range): + return range.rawValue + + case .status(code: let code): + return String(code) + } + } + + public var isSuccess: Bool { + switch value { + case .range(.success), .status(code: 200..<300): + return true + case .range, .status, .default: + return false + } + } + + public init?(rawValue: String) { + if let val = Int(rawValue) { + value = .status(code: val) + warnings = [] + } else if rawValue == "default" { + value = .default + warnings = [] + } else if let range = Range(rawValue: rawValue.uppercased()) { + value = .range(range) + warnings = [] + } else if rawValue.contains("/"), + let first = (rawValue.split(separator: "/")).first, + let fallback = Self(rawValue: String(first)) { + value = fallback.value + warnings = [ + .message("Found non-compliant Status Code '\(rawValue)' but was able to parse as \(first)") + ] + } else { + return nil + } + } +} + +extension ResponseStatusCode: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: Int) { + self.value = .status(code: value) + warnings = [] + } +} From f153e14a59f2efd77492a794029ecd605c05e8e8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 19 Dec 2022 20:26:46 -0600 Subject: [PATCH 14/30] move either map functions into compat module. move callback url type into core. continue implementing compat module. --- Sources/OpenAPIKit/Callbacks.swift | 56 ----- Sources/OpenAPIKit/_CoreReExport.swift | 1 + Sources/OpenAPIKit30/Callbacks.swift | 57 ----- Sources/OpenAPIKit30/_CoreReExport.swift | 1 + Sources/OpenAPIKitCompat/Compat30To31.swift | 238 ++++++++++++++------ Sources/OpenAPIKitCompat/Either+Map.swift | 28 +++ Sources/OpenAPIKitCore/CallbackURL.swift | 64 ++++++ Sources/OpenAPIKitCore/Either/Either.swift | 20 -- 8 files changed, 264 insertions(+), 201 deletions(-) create mode 100644 Sources/OpenAPIKitCompat/Either+Map.swift create mode 100644 Sources/OpenAPIKitCore/CallbackURL.swift diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 676bd9dfd..3b0e61dc4 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -10,46 +10,6 @@ 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. The OpenAPI Spec "Callback Object." /// @@ -60,19 +20,3 @@ extension OpenAPI { /// A map of named collections of Callback Objects (`OpenAPI.Callbacks`). public typealias CallbacksMap = OrderedDictionary, Callbacks>> } - -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/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index ecc4570d9..0341c1eed 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -23,6 +23,7 @@ public extension OpenAPI { typealias ComponentKey = OpenAPIKitCore.ComponentKey typealias Discriminator = OpenAPIKitCore.Discriminator typealias OAuthFlows = OpenAPIKitCore.OAuthFlows + typealias CallbackURL = OpenAPIKitCore.CallbackURL } public extension OpenAPI.SecurityScheme { diff --git a/Sources/OpenAPIKit30/Callbacks.swift b/Sources/OpenAPIKit30/Callbacks.swift index beff994bc..6309e7713 100644 --- a/Sources/OpenAPIKit30/Callbacks.swift +++ b/Sources/OpenAPIKit30/Callbacks.swift @@ -9,47 +9,6 @@ import OpenAPIKitCore 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. /// @@ -60,19 +19,3 @@ extension OpenAPI { /// A map of named collections of Callback Objects (`OpenAPI.Callbacks`). public typealias CallbacksMap = OrderedDictionary, Callbacks>> } - -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/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index ecc4570d9..0341c1eed 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -23,6 +23,7 @@ public extension OpenAPI { typealias ComponentKey = OpenAPIKitCore.ComponentKey typealias Discriminator = OpenAPIKitCore.Discriminator typealias OAuthFlows = OpenAPIKitCore.OAuthFlows + typealias CallbackURL = OpenAPIKitCore.CallbackURL } public extension OpenAPI.SecurityScheme { diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 7ad73040a..61675e38b 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -234,9 +234,35 @@ extension OpenAPIKit30.OpenAPI.Parameter: To31 { } } +extension OpenAPIKit30.OpenAPI.RuntimeExpression.Source: To31 { + fileprivate func to31() -> OpenAPI31.RuntimeExpression.Source { + switch self { + case .header(name: let name): + return .header(name: name) + case .query(name: let name): + return .query(name: name) + case .path(name: let name): + return .path(name: name) + case .body(let ref): + return .body(ref?.to31()) + } + } +} + extension OpenAPIKit30.OpenAPI.RuntimeExpression: To31 { - fileprivate func to31() -> OpenAPI31.Run { - + fileprivate func to31() -> OpenAPI31.RuntimeExpression { + switch self { + case .url: + return .url + case .method: + return .method + case .statusCode: + return .statusCode + case .request(let source): + return .request(source.to31()) + case .response(let source): + return .response(source.to31()) + } } } @@ -265,27 +291,62 @@ extension OpenAPIKit30.OpenAPI.Response: To31 { } } -extension OpenAPIKit30.OpenAPI.Operation: To31 { - fileprivate func to31() -> OpenAPI31.Operation { - // TODO: finish filling out constructor with all optional arguments. - OpenAPI31.Operation( - tags: tags, - summary: summary, +extension OpenAPIKit30.OpenAPI.Request: To31 { + fileprivate func to31() -> OpenAPI31.Request { + OpenAPI31.Request( description: description, - externalDocs: externalDocs?.to31(), - operationId: operationId, - parameters: parameters.map(eitherRefTo31), -// requestBody: eitherRefTo31(requestBody), - responses: responses.mapValues(eitherRefTo31), -// callbacks: callbacks.mapValues(eitherRefTo31), - deprecated: deprecated, -// security: , - servers: servers?.map { $0.to31() }, + content: content.mapValues { $0.to31() }, + required: `required`, vendorExtensions: vendorExtensions ) } } +extension OpenAPIKit30.OpenAPI.Callbacks: To31 { + fileprivate func to31() -> OpenAPI31.Callbacks { + self.mapValues { (pathItem: OpenAPI30.PathItem) in + .b(pathItem.to31()) + } + } +} + +extension OpenAPIKit30.OpenAPI.Operation: To31 { + fileprivate func to31() -> OpenAPI31.Operation { + if let requestBody { + return OpenAPI31.Operation( + tags: tags, + summary: summary, + description: description, + externalDocs: externalDocs?.to31(), + operationId: operationId, + parameters: parameters.map(eitherRefTo31), + requestBody: eitherRefTo31(requestBody), + responses: responses.mapValues(eitherRefTo31), + callbacks: callbacks.mapValues(eitherRefTo31), + deprecated: deprecated, + security: security?.map { $0.to31() }, + servers: servers?.map { $0.to31() }, + vendorExtensions: vendorExtensions + ) + } else { + return OpenAPI31.Operation( + tags: tags, + summary: summary, + description: description, + externalDocs: externalDocs?.to31(), + operationId: operationId, + parameters: parameters.map(eitherRefTo31), + responses: responses.mapValues(eitherRefTo31), + callbacks: callbacks.mapValues(eitherRefTo31), + deprecated: deprecated, + security: security?.map { $0.to31() }, + servers: servers?.map { $0.to31() }, + vendorExtensions: vendorExtensions + ) + } + } +} + extension OpenAPIKit30.OpenAPI.PathItem: To31 { fileprivate func to31() -> OpenAPI31.PathItem { let servers = servers?.map { $0.to31() } @@ -320,16 +381,22 @@ extension OpenAPIKit30.OpenAPI.SecurityRequirement: To31 { } } +private extension OpenAPIKit30.JSONReference.InternalReference { + func to31() -> OpenAPIKit.JSONReference.InternalReference { + switch self { + case .component(name: let name): + return .component(name: name) + case .path(let path): + return .path(.init(rawValue: path.rawValue)) + } + } +} + private extension OpenAPIKit30.JSONReference { func to31() -> OpenAPIKit.JSONReference { switch self { case .internal(let ref): - switch ref { - case .component(name: let name): - return .internal(.component(name: name)) - case .path(let path): - return .internal(.path(.init(rawValue: path.rawValue))) - } + return .internal(ref.to31()) case .external(let url): return OpenAPIKit.JSONReference.external(url) } @@ -382,48 +449,84 @@ extension OpenAPIKit30.OpenAPI.SecurityScheme: To31 { } } +extension OpenAPIKit30.JSONTypeFormat: To31 { + fileprivate func to31() -> OpenAPIKit.JSONTypeFormat { + switch self { + case .boolean(let f): + return .boolean(f) + case .object(let f): + return .object(f) + case .array(let f): + return .array(f) + case .number(let f): + return .number(f) + case .integer(let f): + return .integer(f) + case .string(let f): + return .string(f) + } + } +} + +extension OpenAPIKit30.JSONSchema.CoreContext: To31 where Format: OpenAPIKit.OpenAPIFormat { + fileprivate func to31() -> OpenAPIKit.JSONSchema.CoreContext { + OpenAPIKit.JSONSchema.CoreContext( + format: format, + required: `required`, + nullable: nullable, + permissions: permissions, + deprecated: deprecated, + title: title, + description: description, + discriminator: discriminator, + externalDocs: externalDocs?.to31(), + allowedValues: allowedValues, + defaultValue: defaultValue, + examples: [example].compactMap { $0 } + ) + } +} + +extension OpenAPIKit30.JSONSchema.NumericContext: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema.NumericContext { + OpenAPIKit.JSONSchema.NumericContext( + multipleOf: multipleOf, + maximum: maximum, + minimum: minimum + ) + } +} + extension OpenAPIKit30.JSONSchema: To31 { fileprivate func to31() -> OpenAPIKit.JSONSchema { -// let schema: OpenAPIKit.JSONSchema.Schema - -// switch value { -// case .boolean(let core): -// schema = .boolean( -// .init( -// format: core.format, -// required: core.required, -// nullable: core.nullable, -// permissions: core.permissions, -// deprecated: core.deprecated, -// title: core.title, -// description: core.description, -// discriminator: core.discriminator, -// externalDocs: <#T##OpenAPI.ExternalDocumentation?#> -// ) -// ) -// case .number(_, _): -// <#code#> -// case .integer(_, _): -// <#code#> -// case .string(_, _): -// <#code#> -// case .object(_, _): -// <#code#> -// case .array(_, _): -// <#code#> -// case .all(of: let of, core: let core): -// <#code#> -// case .one(of: let of, core: let core): -// <#code#> -// case .any(of: let of, core: let core): -// <#code#> -// case .not(_, core: let core): -// <#code#> -// case .reference(_, _): -// <#code#> -// case .fragment(_): -// <#code#> -// } + let schema: OpenAPIKit.JSONSchema.Schema + + switch value { + case .boolean(let core): + schema = .boolean(core.to31()) + case .number(let core, let numeric): + schema = .number(core.to31(), <#T##JSONSchema.NumericContext#>) + case .integer(_, _): + <#code#> + case .string(_, _): + <#code#> + case .object(_, _): + <#code#> + case .array(_, _): + <#code#> + case .all(of: let of, core: let core): + <#code#> + case .one(of: let of, core: let core): + <#code#> + case .any(of: let of, core: let core): + <#code#> + case .not(_, core: let core): + <#code#> + case .reference(_, _): + <#code#> + case .fragment(_): + <#code#> + } // TODO: finish filling out constructor, replacing the null schema. OpenAPIKit.JSONSchema( @@ -434,17 +537,16 @@ extension OpenAPIKit30.JSONSchema: To31 { extension OpenAPIKit30.OpenAPI.Components: To31 { fileprivate func to31() -> OpenAPI31.Components { - // TODO: finish filling out constructor with all optional arguments. OpenAPI31.Components( schemas: schemas.mapValues { $0.to31() }, -// responses: responses.mapValues { $0.to31() }, + responses: responses.mapValues { $0.to31() }, parameters: parameters.mapValues { $0.to31() }, examples: examples.mapValues { $0.to31() }, -// requestBodies: requestBodies.mapValues { $0.to31() }, + requestBodies: requestBodies.mapValues { $0.to31() }, headers: headers.mapValues { $0.to31() }, securitySchemes: securitySchemes.mapValues { $0.to31() }, -// links: links.mapValues { $0.to31() }, -// callbacks: callbacks.mapValues { $0.to31() }, + links: links.mapValues { $0.to31() }, + callbacks: callbacks.mapValues { $0.to31() }, vendorExtensions: vendorExtensions ) } diff --git a/Sources/OpenAPIKitCompat/Either+Map.swift b/Sources/OpenAPIKitCompat/Either+Map.swift new file mode 100644 index 000000000..2ec2efc22 --- /dev/null +++ b/Sources/OpenAPIKitCompat/Either+Map.swift @@ -0,0 +1,28 @@ +// +// Either+Map.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +import OpenAPIKitCore + +extension Either { + internal func mapFirst(_ transform: (A) -> T) -> Either { + switch self { + case .a(let a): + return .a(transform(a)) + case .b(let b): + return .b(b) + } + } + + internal func mapSecond(_ transform: (B) -> T) -> Either { + switch self { + case .a(let a): + return .a(a) + case .b(let b): + return .b(transform(b)) + } + } +} diff --git a/Sources/OpenAPIKitCore/CallbackURL.swift b/Sources/OpenAPIKitCore/CallbackURL.swift new file mode 100644 index 000000000..f6fce4253 --- /dev/null +++ b/Sources/OpenAPIKitCore/CallbackURL.swift @@ -0,0 +1,64 @@ +// +// CallbackURL.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +import Foundation + +/// 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) + } +} + +extension CallbackURL: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(rawValue) + } +} + +extension CallbackURL: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + template = try container.decode(URLTemplate.self) + } +} diff --git a/Sources/OpenAPIKitCore/Either/Either.swift b/Sources/OpenAPIKitCore/Either/Either.swift index 13eddae32..b45ebe4c5 100644 --- a/Sources/OpenAPIKitCore/Either/Either.swift +++ b/Sources/OpenAPIKitCore/Either/Either.swift @@ -55,23 +55,3 @@ public enum Either { } extension Either: Equatable where A: Equatable, B: Equatable {} - -extension Either { - public func mapFirst(_ transform: (A) -> T) -> Either { - switch self { - case .a(let a): - return .a(transform(a)) - case .b(let b): - return .b(b) - } - } - - public func mapSecond(_ transform: (B) -> T) -> Either { - switch self { - case .a(let a): - return .a(a) - case .b(let b): - return .b(transform(b)) - } - } -} From 3188674c565fb010346ecfceb485af948cab4004 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 19 Dec 2022 21:02:55 -0600 Subject: [PATCH 15/30] Move the simple (not compound) json schema contexts into core. finish filling out the compat layer for 3.0 to 3.1 --- .../Schema Object/JSONSchema+Combining.swift | 16 +- .../Schema Object/JSONSchemaContext.swift | 322 ----------------- Sources/OpenAPIKit/_CoreReExport.swift | 4 + .../Schema Object/JSONSchema+Combining.swift | 16 +- .../Schema Object/JSONSchemaContext.swift | 315 ---------------- Sources/OpenAPIKit30/_CoreReExport.swift | 4 + Sources/OpenAPIKitCompat/Compat30To31.swift | 65 ++-- .../JSONSchemaSimpleContexts.swift | 339 ++++++++++++++++++ 8 files changed, 401 insertions(+), 680 deletions(-) create mode 100644 Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift index 93686b192..70ad22e74 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift @@ -424,7 +424,7 @@ extension JSONSchema.IntegerContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return .init( + return ._init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -454,7 +454,7 @@ extension JSONSchema.NumericContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return .init( + return ._init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -467,7 +467,7 @@ extension JSONSchema.StringContext { if let conflict = conflicting(maxLength, other.maxLength) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "maxLength", original: String(conflict.0), new: String(conflict.1))) } - if let conflict = conflicting(_minLength, other._minLength) { + if let conflict = conflicting(StringContext._minLength(self), StringContext._minLength(other)) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "minLength", original: String(conflict.0), new: String(conflict.1))) } if let conflict = conflicting(pattern, other.pattern) { @@ -476,7 +476,7 @@ extension JSONSchema.StringContext { // explicitly declaring these constants one at a time // helps the type checker a lot. let newMaxLength = maxLength ?? other.maxLength - let newMinLength = _minLength ?? other._minLength + let newMinLength = StringContext._minLength(self) ?? StringContext._minLength(other) let newPattern = pattern ?? other.pattern return .init( maxLength: newMaxLength, @@ -612,7 +612,7 @@ extension JSONSchema.IntegerContext { throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return .init( + return ._init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum @@ -637,7 +637,7 @@ extension JSONSchema.NumericContext { throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return .init( + return ._init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum @@ -647,7 +647,7 @@ extension JSONSchema.NumericContext { extension JSONSchema.StringContext { internal func validatedContext() throws -> JSONSchema.StringContext { - if let minimum = _minLength { + if let minimum = StringContext._minLength(self) { guard minimum >= 0 else { throw JSONSchemaResolutionError(.inconsistency("String minimum length (\(minimum) cannot be less than 0")) } @@ -659,7 +659,7 @@ extension JSONSchema.StringContext { } return .init( maxLength: maxLength, - minLength: _minLength, + minLength: StringContext._minLength(self), pattern: pattern ) } diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index 75be083b7..f362c86e4 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -389,157 +389,6 @@ extension JSONSchema.CoreContext { // MARK: - Specific Contexts extension JSONSchema { - /// The context that only applies to `.number` schemas. - /// - /// - Note: Although integers are numbers, `integer` - /// schemas have their own context type. An - /// `IntegerContext` _can_ be asked for the - /// `NumericContext` that would describe it via its - /// `numericContext` property. - public struct NumericContext: Equatable { - public struct Bound: Equatable { - public let value: Double - public let exclusive: Bool - - internal static let defaultExclusion: Bool = false - } - - /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. - public let multipleOf: Double? - - public let maximum: Bound? - public let minimum: Bound? - - public init( - multipleOf: Double? = nil, - maximum: (Double, exclusive: Bool)? = nil, - minimum: (Double, exclusive: Bool)? = nil - ) { - self.multipleOf = multipleOf - self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - } - - internal init( - multipleOf: Double?, - maximum: Bound?, - minimum: Bound? - ) { - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - } - - /// The context that only applies to `.integer` schemas. - public struct IntegerContext: Equatable { - public struct Bound: Equatable { - public let value: Int - public let exclusive: Bool - - internal static let defaultExclusion: Bool = false - } - - /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. - public let multipleOf: Int? - - public let maximum: Bound? - public let minimum: Bound? - - public init( - multipleOf: Int? = nil, - maximum: (Int, exclusive: Bool)? = nil, - minimum: (Int, exclusive: Bool)? = nil - ) { - self.multipleOf = multipleOf - self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - } - - /// Create an `IntegerContext` from the given `NumericContext`. - /// - /// This will only succeed if all properties of the `NumericContext` are - /// integers. - public init?(from numericContext: NumericContext) { - let multipleOf: Int? - if let numericMultipleOf = numericContext.multipleOf { - guard let intMultipleOf = Int(exactly: numericMultipleOf) else { - return nil - } - multipleOf = intMultipleOf - } else { - multipleOf = nil - } - - let maximum: Bound? - if let numericMax = numericContext.maximum { - guard let intMaxValue = Int(exactly: numericMax.value) else { - return nil - } - maximum = Bound(value: intMaxValue, exclusive: numericMax.exclusive) - } else { - maximum = nil - } - - let minimum: Bound? - if let numericMin = numericContext.minimum { - guard let intMinValue = Int(exactly: numericMin.value) else { - return nil - } - minimum = Bound(value: intMinValue, exclusive: numericMin.exclusive) - } else { - minimum = nil - } - - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - internal init( - multipleOf: Int?, - maximum: Bound?, - minimum: Bound? - ) { - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - /// Get the `NumericContext` that describes this - /// `IntegerContext`. - public var numericContext: NumericContext { - return .init( - multipleOf: multipleOf.map(Double.init), - maximum: maximum.map { (Double($0.value), exclusive: $0.exclusive) }, - minimum: minimum.map { (Double($0.value), exclusive: $0.exclusive) } - ) - } - } - - /// The context that only applies to `.string` schemas. - public struct StringContext: Equatable { - public let maxLength: Int? - let _minLength: Int? - - public var minLength: Int { - return _minLength ?? 0 - } - - /// Regular expression - public let pattern: String? - - public init( - maxLength: Int? = nil, - minLength: Int? = nil, - pattern: String? = nil - ) { - self.maxLength = maxLength - self._minLength = minLength - self.pattern = pattern - } - } - /// The context that only applies to `.array` schemas. public struct ArrayContext: Equatable { /// A JSON Type Node that describes @@ -639,23 +488,6 @@ extension JSONSchema { self._minProperties = minProperties } } - - /// The context that only applies to `.reference` schemas. - public struct ReferenceContext: Equatable { - public let required: Bool - - public init(required: Bool = true) { - self.required = required - } - - public func requiredContext() -> ReferenceContext { - return .init(required: true) - } - - public func optionalContext() -> ReferenceContext { - return .init(required: false) - } - } } // MARK: - Codable @@ -813,160 +645,6 @@ extension JSONSchema.CoreContext: Decodable { } } -extension JSONSchema.NumericContext { - internal enum CodingKeys: String, CodingKey { - case multipleOf - case maximum - case exclusiveMaximum - case minimum - case exclusiveMinimum - } -} - -extension JSONSchema.NumericContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(multipleOf, forKey: .multipleOf) - - if let max = maximum { - if max.exclusive { - try container.encode(max.value, forKey: .exclusiveMaximum) - } else { - try container.encode(max.value, forKey: .maximum) - } - } - - if let min = minimum { - if min.exclusive { - try container.encode(min.value, forKey: .exclusiveMinimum) - } else { - try container.encode(min.value, forKey: .minimum) - } - } - } -} - -extension JSONSchema.NumericContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) - - if let exclusiveMaximum = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum) { - maximum = Bound(value: exclusiveMaximum, exclusive: true) - } else { - maximum = try container.decodeIfPresent(Double.self, forKey: .maximum) - .map { Bound(value: $0, exclusive: false) } - } - - if let exclusiveMinimum = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum) { - minimum = Bound(value: exclusiveMinimum, exclusive: true) - } else { - minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) - .map { Bound(value: $0, exclusive: false) } - } - } -} - -extension JSONSchema.IntegerContext { - internal enum CodingKeys: String, CodingKey { - case multipleOf - case maximum - case exclusiveMaximum - case minimum - case exclusiveMinimum - } -} - -extension JSONSchema.IntegerContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(multipleOf, forKey: .multipleOf) - - if let max = maximum { - if max.exclusive { - try container.encode(max.value, forKey: .exclusiveMaximum) - } else { - try container.encode(max.value, forKey: .maximum) - } - } - - if let min = minimum { - if min.exclusive { - try container.encode(min.value, forKey: .exclusiveMinimum) - } else { - try container.encode(min.value, forKey: .minimum) - } - } - } -} - -extension JSONSchema.IntegerContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) - - // the following acrobatics thanks to some libraries (namely Yams) not - // being willing to decode floating point representations of whole numbers - // as integer values. - let exclusiveMaximumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum) - let exclusiveMinimumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum) - - let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) - let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) - - func boundFromAttempt(_ attempt: Double?, max: Bool, exclusive: Bool) throws -> Bound? { - return try attempt.map { floatVal in - guard let integer = Int(exactly: floatVal) else { - throw InconsistencyError( - subjectName: max ? "maximum" : "minimum", - details: "Expected an Integer literal but found a floating point value", - codingPath: decoder.codingPath - ) - } - return Bound(value: integer, exclusive: exclusive) - } - } - - maximum = try boundFromAttempt(exclusiveMaximumAttempt, max: true, exclusive: true) - ?? boundFromAttempt(maximumAttempt, max: true, exclusive: false) - - minimum = try boundFromAttempt(exclusiveMinimumAttempt, max: false, exclusive: true) - ?? boundFromAttempt(minimumAttempt, max: false, exclusive: false) - } -} - -extension JSONSchema.StringContext { - internal enum CodingKeys: String, CodingKey { - case maxLength - case minLength - case pattern - } -} - -extension JSONSchema.StringContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(maxLength, forKey: .maxLength) - try container.encodeIfPresent(_minLength, forKey: .minLength) - try container.encodeIfPresent(pattern, forKey: .pattern) - } -} - -extension JSONSchema.StringContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) - _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) - pattern = try container.decodeIfPresent(String.self, forKey: .pattern) - } -} - extension JSONSchema.ArrayContext { internal enum CodingKeys: String, CodingKey { case items diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index 0341c1eed..ee9112ae6 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -40,6 +40,10 @@ public extension OpenAPI.Response { public extension JSONSchema { typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions + typealias NumericContext = OpenAPIKitCore.NumericContext + typealias IntegerContext = OpenAPIKitCore.IntegerContext + typealias StringContext = OpenAPIKitCore.StringContext + typealias ReferenceContext = OpenAPIKitCore.ReferenceContext } public extension JSONTypeFormat { diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift index c56fb60ed..95d1c4c37 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift @@ -404,7 +404,7 @@ extension JSONSchema.IntegerContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return .init( + return ._init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -434,7 +434,7 @@ extension JSONSchema.NumericContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return .init( + return ._init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -447,7 +447,7 @@ extension JSONSchema.StringContext { if let conflict = conflicting(maxLength, other.maxLength) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "maxLength", original: String(conflict.0), new: String(conflict.1))) } - if let conflict = conflicting(_minLength, other._minLength) { + if let conflict = conflicting(StringContext._minLength(self), StringContext._minLength(other)) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "minLength", original: String(conflict.0), new: String(conflict.1))) } if let conflict = conflicting(pattern, other.pattern) { @@ -456,7 +456,7 @@ extension JSONSchema.StringContext { // explicitly declaring these constants one at a time // helps the type checker a lot. let newMaxLength = maxLength ?? other.maxLength - let newMinLength = _minLength ?? other._minLength + let newMinLength = StringContext._minLength(self) ?? StringContext._minLength(other) let newPattern = pattern ?? other.pattern return .init( maxLength: newMaxLength, @@ -592,7 +592,7 @@ extension JSONSchema.IntegerContext { throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return .init( + return ._init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum @@ -617,7 +617,7 @@ extension JSONSchema.NumericContext { throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return .init( + return ._init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum @@ -627,7 +627,7 @@ extension JSONSchema.NumericContext { extension JSONSchema.StringContext { internal func validatedContext() throws -> JSONSchema.StringContext { - if let minimum = _minLength { + if let minimum = StringContext._minLength(self) { guard minimum >= 0 else { throw JSONSchemaResolutionError(.inconsistency("String minimum length (\(minimum) cannot be less than 0")) } @@ -639,7 +639,7 @@ extension JSONSchema.StringContext { } return .init( maxLength: maxLength, - minLength: _minLength, + minLength: StringContext._minLength(self), pattern: pattern ) } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift index 62c2523b5..669e6501c 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift @@ -346,157 +346,6 @@ extension JSONSchema.CoreContext { // MARK: - Specific Contexts extension JSONSchema { - /// The context that only applies to `.number` schemas. - /// - /// - Note: Although integers are numbers, `integer` - /// schemas have their own context type. An - /// `IntegerContext` _can_ be asked for the - /// `NumericContext` that would describe it via its - /// `numericContext` property. - public struct NumericContext: Equatable { - public struct Bound: Equatable { - public let value: Double - public let exclusive: Bool - - internal static let defaultExclusion: Bool = false - } - - /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. - public let multipleOf: Double? - - public let maximum: Bound? - public let minimum: Bound? - - public init( - multipleOf: Double? = nil, - maximum: (Double, exclusive: Bool)? = nil, - minimum: (Double, exclusive: Bool)? = nil - ) { - self.multipleOf = multipleOf - self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - } - - internal init( - multipleOf: Double?, - maximum: Bound?, - minimum: Bound? - ) { - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - } - - /// The context that only applies to `.integer` schemas. - public struct IntegerContext: Equatable { - public struct Bound: Equatable { - public let value: Int - public let exclusive: Bool - - internal static let defaultExclusion: Bool = false - } - - /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. - public let multipleOf: Int? - - public let maximum: Bound? - public let minimum: Bound? - - public init( - multipleOf: Int? = nil, - maximum: (Int, exclusive: Bool)? = nil, - minimum: (Int, exclusive: Bool)? = nil - ) { - self.multipleOf = multipleOf - self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - } - - /// Create an `IntegerContext` from the given `NumericContext`. - /// - /// This will only succeed if all properties of the `NumericContext` are - /// integers. - public init?(from numericContext: NumericContext) { - let multipleOf: Int? - if let numericMultipleOf = numericContext.multipleOf { - guard let intMultipleOf = Int(exactly: numericMultipleOf) else { - return nil - } - multipleOf = intMultipleOf - } else { - multipleOf = nil - } - - let maximum: Bound? - if let numericMax = numericContext.maximum { - guard let intMaxValue = Int(exactly: numericMax.value) else { - return nil - } - maximum = Bound(value: intMaxValue, exclusive: numericMax.exclusive) - } else { - maximum = nil - } - - let minimum: Bound? - if let numericMin = numericContext.minimum { - guard let intMinValue = Int(exactly: numericMin.value) else { - return nil - } - minimum = Bound(value: intMinValue, exclusive: numericMin.exclusive) - } else { - minimum = nil - } - - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - internal init( - multipleOf: Int?, - maximum: Bound?, - minimum: Bound? - ) { - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - /// Get the `NumericContext` that describes this - /// `IntegerContext`. - public var numericContext: NumericContext { - return .init( - multipleOf: multipleOf.map(Double.init), - maximum: maximum.map { (Double($0.value), exclusive: $0.exclusive) }, - minimum: minimum.map { (Double($0.value), exclusive: $0.exclusive) } - ) - } - } - - /// The context that only applies to `.string` schemas. - public struct StringContext: Equatable { - public let maxLength: Int? - let _minLength: Int? - - public var minLength: Int { - return _minLength ?? 0 - } - - /// Regular expression - public let pattern: String? - - public init( - maxLength: Int? = nil, - minLength: Int? = nil, - pattern: String? = nil - ) { - self.maxLength = maxLength - self._minLength = minLength - self.pattern = pattern - } - } - /// The context that only applies to `.array` schemas. public struct ArrayContext: Equatable { /// A JSON Type Node that describes @@ -596,23 +445,6 @@ extension JSONSchema { self._minProperties = minProperties } } - - /// The context that only applies to `.reference` schemas. - public struct ReferenceContext: Equatable { - public let required: Bool - - public init(required: Bool = true) { - self.required = required - } - - public func requiredContext() -> ReferenceContext { - return .init(required: true) - } - - public func optionalContext() -> ReferenceContext { - return .init(required: false) - } - } } // MARK: - Codable @@ -739,153 +571,6 @@ extension JSONSchema.CoreContext: Decodable { } } -extension JSONSchema.NumericContext { - internal enum CodingKeys: String, CodingKey { - case multipleOf - case maximum - case exclusiveMaximum - case minimum - case exclusiveMinimum - } -} - -extension JSONSchema.NumericContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(multipleOf, forKey: .multipleOf) - - if let max = maximum { - try container.encode(max.value, forKey: .maximum) - if max.exclusive { - try container.encode(true, forKey: .exclusiveMaximum) - } - } - - if let min = minimum { - try container.encode(min.value, forKey: .minimum) - if min.exclusive { - try container.encode(true, forKey: .exclusiveMinimum) - } - } - } -} - -extension JSONSchema.NumericContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) - - let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? Bound.defaultExclusion - let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? Bound.defaultExclusion - - maximum = (try container.decodeIfPresent(Double.self, forKey: .maximum)) - .map { Bound(value: $0, exclusive: exclusiveMaximum) } - minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) - .map { Bound(value: $0, exclusive: exclusiveMinimum) } - } -} - -extension JSONSchema.IntegerContext { - internal enum CodingKeys: String, CodingKey { - case multipleOf - case maximum - case exclusiveMaximum - case minimum - case exclusiveMinimum - } -} - -extension JSONSchema.IntegerContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(multipleOf, forKey: .multipleOf) - - if let max = maximum { - try container.encode(max.value, forKey: .maximum) - if max.exclusive { - try container.encode(true, forKey: .exclusiveMaximum) - } - } - - if let min = minimum { - try container.encode(min.value, forKey: .minimum) - if min.exclusive { - try container.encode(true, forKey: .exclusiveMinimum) - } - } - } -} - -extension JSONSchema.IntegerContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) - - let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? false - let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? false - - // the following acrobatics thanks to some libraries (namely Yams) not - // being willing to decode floating point representations of whole numbers - // as integer values. - let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) - let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) - - maximum = try maximumAttempt.map { floatMax in - guard let integer = Int(exactly: floatMax) else { - throw InconsistencyError( - subjectName: "maximum", - details: "Expected an Integer literal but found a floating point value", - codingPath: decoder.codingPath - ) - } - return integer - }.map { Bound(value: $0, exclusive: exclusiveMaximum) } - - minimum = try minimumAttempt.map { floatMin in - guard let integer = Int(exactly: floatMin) else { - throw InconsistencyError( - subjectName: "minimum", - details: "Expected an Integer literal but found a floating point value", - codingPath: decoder.codingPath - ) - } - return integer - }.map { Bound(value: $0, exclusive: exclusiveMinimum) } - } -} - -extension JSONSchema.StringContext { - internal enum CodingKeys: String, CodingKey { - case maxLength - case minLength - case pattern - } -} - -extension JSONSchema.StringContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(maxLength, forKey: .maxLength) - try container.encodeIfPresent(_minLength, forKey: .minLength) - try container.encodeIfPresent(pattern, forKey: .pattern) - } -} - -extension JSONSchema.StringContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) - _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) - pattern = try container.decodeIfPresent(String.self, forKey: .pattern) - } -} - extension JSONSchema.ArrayContext { internal enum CodingKeys: String, CodingKey { case items diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index 0341c1eed..ee9112ae6 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -40,6 +40,10 @@ public extension OpenAPI.Response { public extension JSONSchema { typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions + typealias NumericContext = OpenAPIKitCore.NumericContext + typealias IntegerContext = OpenAPIKitCore.IntegerContext + typealias StringContext = OpenAPIKitCore.StringContext + typealias ReferenceContext = OpenAPIKitCore.ReferenceContext } public extension JSONTypeFormat { diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 61675e38b..a69b0bb7a 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -487,12 +487,24 @@ extension OpenAPIKit30.JSONSchema.CoreContext: To31 where Format: OpenAPIKit.Ope } } -extension OpenAPIKit30.JSONSchema.NumericContext: To31 { - fileprivate func to31() -> OpenAPIKit.JSONSchema.NumericContext { - OpenAPIKit.JSONSchema.NumericContext( - multipleOf: multipleOf, - maximum: maximum, - minimum: minimum +extension OpenAPIKit30.JSONSchema.ArrayContext: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema.ArrayContext { + OpenAPIKit.JSONSchema.ArrayContext( + items: items.map { $0.to31() }, + maxItems: maxItems, + minItems: minItems, + uniqueItems: uniqueItems + ) + } +} + +extension OpenAPIKit30.JSONSchema.ObjectContext: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema.ObjectContext { + OpenAPIKit.JSONSchema.ObjectContext( + properties: properties.mapValues { $0.to31() }, + additionalProperties: additionalProperties?.mapSecond { $0.to31() }, + maxProperties: maxProperties, + minProperties: minProperties ) } } @@ -505,32 +517,31 @@ extension OpenAPIKit30.JSONSchema: To31 { case .boolean(let core): schema = .boolean(core.to31()) case .number(let core, let numeric): - schema = .number(core.to31(), <#T##JSONSchema.NumericContext#>) - case .integer(_, _): - <#code#> - case .string(_, _): - <#code#> - case .object(_, _): - <#code#> - case .array(_, _): - <#code#> + schema = .number(core.to31(), numeric) + case .integer(let core, let integral): + schema = .integer(core.to31(), integral) + case .string(let core, let stringy): + schema = .string(core.to31(), stringy) + case .object(let core, let objective): + schema = .object(core.to31(), objective.to31()) + case .array(let core, let listy): + schema = .array(core.to31(), listy.to31()) case .all(of: let of, core: let core): - <#code#> + schema = .all(of: of.map { $0.to31() }, core: core.to31()) case .one(of: let of, core: let core): - <#code#> + schema = .one(of: of.map { $0.to31() }, core: core.to31()) case .any(of: let of, core: let core): - <#code#> - case .not(_, core: let core): - <#code#> - case .reference(_, _): - <#code#> - case .fragment(_): - <#code#> + schema = .any(of: of.map { $0.to31() }, core: core.to31()) + case .not(let not, core: let core): + schema = .not(not.to31(), core: core.to31()) + case .reference(let ref, let context): + schema = .reference(ref.to31(), context) + case .fragment(let core): + schema = .fragment(core.to31()) } - // TODO: finish filling out constructor, replacing the null schema. - OpenAPIKit.JSONSchema( - schema: .null // schema + return OpenAPIKit.JSONSchema( + schema: schema ) } } diff --git a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift new file mode 100644 index 000000000..5c5d6de2d --- /dev/null +++ b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift @@ -0,0 +1,339 @@ +// +// JSONSchemaSimpleContexts.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +/// The context that only applies to `.number` schemas. +/// +/// - Note: Although integers are numbers, `integer` +/// schemas have their own context type. An +/// `IntegerContext` _can_ be asked for the +/// `NumericContext` that would describe it via its +/// `numericContext` property. +public struct NumericContext: Equatable { + public struct Bound: Equatable { + public let value: Double + public let exclusive: Bool + + internal static let defaultExclusion: Bool = false + } + + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. + public let multipleOf: Double? + + public let maximum: Bound? + public let minimum: Bound? + + public init( + multipleOf: Double? = nil, + maximum: (Double, exclusive: Bool)? = nil, + minimum: (Double, exclusive: Bool)? = nil + ) { + self.multipleOf = multipleOf + self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + } + + internal init( + multipleOf: Double?, + maximum: Bound?, + minimum: Bound? + ) { + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + public static func _init( + multipleOf: Double?, + maximum: Bound?, + minimum: Bound? + ) -> NumericContext { .init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) } +} + +/// The context that only applies to `.integer` schemas. +public struct IntegerContext: Equatable { + public struct Bound: Equatable { + public let value: Int + public let exclusive: Bool + + internal static let defaultExclusion: Bool = false + } + + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. + public let multipleOf: Int? + + public let maximum: Bound? + public let minimum: Bound? + + public init( + multipleOf: Int? = nil, + maximum: (Int, exclusive: Bool)? = nil, + minimum: (Int, exclusive: Bool)? = nil + ) { + self.multipleOf = multipleOf + self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + } + + /// Create an `IntegerContext` from the given `NumericContext`. + /// + /// This will only succeed if all properties of the `NumericContext` are + /// integers. + public init?(from numericContext: NumericContext) { + let multipleOf: Int? + if let numericMultipleOf = numericContext.multipleOf { + guard let intMultipleOf = Int(exactly: numericMultipleOf) else { + return nil + } + multipleOf = intMultipleOf + } else { + multipleOf = nil + } + + let maximum: Bound? + if let numericMax = numericContext.maximum { + guard let intMaxValue = Int(exactly: numericMax.value) else { + return nil + } + maximum = Bound(value: intMaxValue, exclusive: numericMax.exclusive) + } else { + maximum = nil + } + + let minimum: Bound? + if let numericMin = numericContext.minimum { + guard let intMinValue = Int(exactly: numericMin.value) else { + return nil + } + minimum = Bound(value: intMinValue, exclusive: numericMin.exclusive) + } else { + minimum = nil + } + + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + internal init( + multipleOf: Int?, + maximum: Bound?, + minimum: Bound? + ) { + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + public static func _init( + multipleOf: Int?, + maximum: Bound?, + minimum: Bound? + ) -> IntegerContext { .init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) } + + /// Get the `NumericContext` that describes this + /// `IntegerContext`. + public var numericContext: NumericContext { + return .init( + multipleOf: multipleOf.map(Double.init), + maximum: maximum.map { (Double($0.value), exclusive: $0.exclusive) }, + minimum: minimum.map { (Double($0.value), exclusive: $0.exclusive) } + ) + } +} + +/// The context that only applies to `.string` schemas. +public struct StringContext: Equatable { + public let maxLength: Int? + let _minLength: Int? + + public var minLength: Int { + return _minLength ?? 0 + } + + /// Regular expression + public let pattern: String? + + public init( + maxLength: Int? = nil, + minLength: Int? = nil, + pattern: String? = nil + ) { + self.maxLength = maxLength + self._minLength = minLength + self.pattern = pattern + } + + // we make the following a static function so it doesn't muddy the namespace while auto-completing on a value. + public static func _minLength(_ context: StringContext) -> Int? { + return context._minLength + } +} + + +/// The context that only applies to `.reference` schemas. +public struct ReferenceContext: Equatable { + public let required: Bool + + public init(required: Bool = true) { + self.required = required + } + + public func requiredContext() -> ReferenceContext { + return .init(required: true) + } + + public func optionalContext() -> ReferenceContext { + return .init(required: false) + } +} + +extension NumericContext { + public enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } +} + +extension NumericContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(multipleOf, forKey: .multipleOf) + + if let max = maximum { + try container.encode(max.value, forKey: .maximum) + if max.exclusive { + try container.encode(true, forKey: .exclusiveMaximum) + } + } + + if let min = minimum { + try container.encode(min.value, forKey: .minimum) + if min.exclusive { + try container.encode(true, forKey: .exclusiveMinimum) + } + } + } +} + +extension NumericContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) + + let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? Bound.defaultExclusion + let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? Bound.defaultExclusion + + maximum = (try container.decodeIfPresent(Double.self, forKey: .maximum)) + .map { Bound(value: $0, exclusive: exclusiveMaximum) } + minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) + .map { Bound(value: $0, exclusive: exclusiveMinimum) } + } +} + +extension IntegerContext { + public enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } +} + +extension IntegerContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(multipleOf, forKey: .multipleOf) + + if let max = maximum { + try container.encode(max.value, forKey: .maximum) + if max.exclusive { + try container.encode(true, forKey: .exclusiveMaximum) + } + } + + if let min = minimum { + try container.encode(min.value, forKey: .minimum) + if min.exclusive { + try container.encode(true, forKey: .exclusiveMinimum) + } + } + } +} + +extension IntegerContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) + + let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? false + let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? false + + // the following acrobatics thanks to some libraries (namely Yams) not + // being willing to decode floating point representations of whole numbers + // as integer values. + let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) + let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) + + maximum = try maximumAttempt.map { floatMax in + guard let integer = Int(exactly: floatMax) else { + throw InconsistencyError( + subjectName: "maximum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return integer + }.map { Bound(value: $0, exclusive: exclusiveMaximum) } + + minimum = try minimumAttempt.map { floatMin in + guard let integer = Int(exactly: floatMin) else { + throw InconsistencyError( + subjectName: "minimum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return integer + }.map { Bound(value: $0, exclusive: exclusiveMinimum) } + } +} + +extension StringContext { + public enum CodingKeys: String, CodingKey { + case maxLength + case minLength + case pattern + } +} + +extension StringContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(maxLength, forKey: .maxLength) + try container.encodeIfPresent(_minLength, forKey: .minLength) + try container.encodeIfPresent(pattern, forKey: .pattern) + } +} + +extension StringContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) + _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) + pattern = try container.decodeIfPresent(String.self, forKey: .pattern) + } +} From 596076464e4995727d74a5b1d91c31ebfdbab237 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 19 Dec 2022 21:26:42 -0600 Subject: [PATCH 16/30] Add test target and first test for compat layer. fix bug with conversion of document info. --- Package.swift | 3 ++ Sources/OpenAPIKitCompat/Compat30To31.swift | 40 ++++++++++++++--- .../DocumentConversionTests.swift | 44 +++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift diff --git a/Package.swift b/Package.swift index 982d0a01f..d93c76bb4 100644 --- a/Package.swift +++ b/Package.swift @@ -68,6 +68,9 @@ let package = Package( .target( name: "OpenAPIKitCompat", dependencies: ["OpenAPIKit30", "OpenAPIKit"]), + .testTarget( + name: "OpenAPIKitCompatTests", + dependencies: ["OpenAPIKitCompat"]) ], swiftLanguageVersions: [ .v5 ] ) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index a69b0bb7a..f6674ac3f 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -27,10 +27,6 @@ private protocol To31 { extension OpenAPIKit30.OpenAPI.Document: To31 { fileprivate func to31() -> OpenAPI31.Document { - let info = OpenAPI31.Document.Info( - title: info.title, version: info.version - ) - let servers = servers.map { $0.to31() } let paths = paths.mapValues { $0.to31() } @@ -41,7 +37,7 @@ extension OpenAPIKit30.OpenAPI.Document: To31 { return OpenAPI31.Document( openAPIVersion: .v3_1_0, - info: info, + info: info.to31(), servers: servers, paths: paths, components: components.to31(), @@ -53,6 +49,40 @@ extension OpenAPIKit30.OpenAPI.Document: To31 { } } +extension OpenAPIKit30.OpenAPI.Document.Info: To31 { + fileprivate func to31() -> OpenAPI31.Document.Info { + OpenAPI31.Document.Info( + title: title, + description: description, + termsOfService: termsOfService, + contact: contact?.to31(), + license: license?.to31(), + version: version, + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.Document.Info.License: To31 { + fileprivate func to31() -> OpenAPI31.Document.Info.License { + OpenAPI31.Document.Info.License( + name: name, + url: url, + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPIKit30.OpenAPI.Document.Info.Contact: To31 { + fileprivate func to31() -> OpenAPI31.Document.Info.Contact { + OpenAPI31.Document.Info.Contact( + name: name, + url: url, + email: email + ) + } +} + extension OpenAPIKit30.OpenAPI.Server: To31 { fileprivate func to31() -> OpenAPI31.Server { diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift new file mode 100644 index 000000000..8245fa444 --- /dev/null +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -0,0 +1,44 @@ +// +// DocumentConversionTests.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +import OpenAPIKit30 +import OpenAPIKit +import OpenAPIKitCompat +import XCTest + +final class DocumentConversionTests: XCTestCase { + func test_barebonesDocument() { + let oldDoc = OpenAPIKit30.OpenAPI.Document( + info: .init(title: "Hello World", version: "1.0.1"), + servers: [], + paths: [:], + components: .noComponents + ) + + let newDoc = oldDoc.convert(to: .v3_1_0) + + assertEqual(newDoc, oldDoc) + } +} + +fileprivate func assertEqual(_ newDoc: OpenAPIKit.OpenAPI.Document, _ oldDoc: OpenAPIKit30.OpenAPI.Document) { + XCTAssertEqual(newDoc.info.title, oldDoc.info.title) + XCTAssertEqual(newDoc.info.version, oldDoc.info.version) + XCTAssertEqual(newDoc.info.vendorExtensions, oldDoc.info.vendorExtensions) + XCTAssertEqual(newDoc.info.description, oldDoc.info.description) + XCTAssertEqual(newDoc.info.contact?.name, oldDoc.info.contact?.name) + XCTAssertEqual(newDoc.info.contact?.url, oldDoc.info.contact?.url) + XCTAssertEqual(newDoc.info.contact?.email, oldDoc.info.contact?.email) + XCTAssertEqual(newDoc.info.contact?.vendorExtensions, oldDoc.info.contact?.vendorExtensions) + XCTAssertEqual(newDoc.info.termsOfService, oldDoc.info.termsOfService) + XCTAssertEqual(newDoc.info.license?.name, oldDoc.info.license?.name) + XCTAssertEqual(newDoc.info.license?.identifier, oldDoc.info.license?.url.map(OpenAPIKit.OpenAPI.Document.Info.License.Identifier.url)) + XCTAssertEqual(newDoc.info.license?.vendorExtensions, oldDoc.info.license?.vendorExtensions) + XCTAssertNil(newDoc.info.summary) + + // TODO: test the rest of equality. +} From 644a089f0e4d8872e998aa33ae4b7b8f93518bbe Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 19 Dec 2022 21:35:26 -0600 Subject: [PATCH 17/30] another test and another bug fix. --- Sources/OpenAPIKitCompat/Compat30To31.swift | 3 ++- .../DocumentConversionTests.swift | 27 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index f6674ac3f..fdbc8964f 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -78,7 +78,8 @@ extension OpenAPIKit30.OpenAPI.Document.Info.Contact: To31 { OpenAPI31.Document.Info.Contact( name: name, url: url, - email: email + email: email, + vendorExtensions: vendorExtensions ) } } diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 8245fa444..0383c413a 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -21,11 +21,34 @@ final class DocumentConversionTests: XCTestCase { let newDoc = oldDoc.convert(to: .v3_1_0) - assertEqual(newDoc, oldDoc) + assertEqualOldToNew(newDoc, oldDoc) } + + func test_fullInfo() { + let oldDoc = OpenAPIKit30.OpenAPI.Document( + info: .init( + title: "Hello World", + description: "described", + termsOfService: URL(string: "https://website.com"), + contact: .init(name: "Me", url: URL(string: "https://website.com"), email: "me@website.com", vendorExtensions: ["x-test": 1]), + license: .init(name: "MIT-not", url: URL(string: "https://website.com"), vendorExtensions: ["x-two": 2.0]), + version: "1.0.1", + vendorExtensions: ["x-good" : "yeah"] + ), + servers: [], + paths: [:], + components: .noComponents + ) + + let newDoc = oldDoc.convert(to: .v3_1_0) + + assertEqualOldToNew(newDoc, oldDoc) + } + + // TODO: more tests } -fileprivate func assertEqual(_ newDoc: OpenAPIKit.OpenAPI.Document, _ oldDoc: OpenAPIKit30.OpenAPI.Document) { +fileprivate func assertEqualOldToNew(_ newDoc: OpenAPIKit.OpenAPI.Document, _ oldDoc: OpenAPIKit30.OpenAPI.Document) { XCTAssertEqual(newDoc.info.title, oldDoc.info.title) XCTAssertEqual(newDoc.info.version, oldDoc.info.version) XCTAssertEqual(newDoc.info.vendorExtensions, oldDoc.info.vendorExtensions) From a12fbad475dc0981899f75db8eb41f68b6ffeaf3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 19 Dec 2022 21:42:30 -0600 Subject: [PATCH 18/30] fix import warning --- Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift b/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift index a95f78732..787a05f60 100644 --- a/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift +++ b/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift @@ -5,7 +5,6 @@ // Created by Mathew Polzin on 7/5/20. // -import OpenAPIKitCore import Foundation extension KeyedDecodingContainerProtocol { From 75dbfbdf3247e941dbed5146435203bfb7b268f3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 20 Dec 2022 00:03:04 -0600 Subject: [PATCH 19/30] See if its easy to get compilation for Swift 5.1 --- Sources/OpenAPIKitCompat/Compat30To31.swift | 53 ++++++++------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index fdbc8964f..a49240441 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -27,22 +27,14 @@ private protocol To31 { extension OpenAPIKit30.OpenAPI.Document: To31 { fileprivate func to31() -> OpenAPI31.Document { - let servers = servers.map { $0.to31() } - - let paths = paths.mapValues { $0.to31() } - - let security = security.map { $0.to31() } - - let tags = tags?.map { $0.to31() } - - return OpenAPI31.Document( + OpenAPI31.Document( openAPIVersion: .v3_1_0, info: info.to31(), - servers: servers, - paths: paths, + servers: servers.map { $0.to31() }, + paths: paths.mapValues { $0.to31() }, components: components.to31(), - security: security, - tags: tags, + security: security.map { $0.to31() }, + tags: tags?.map { $0.to31() }, externalDocs: externalDocs?.to31(), vendorExtensions: vendorExtensions ) @@ -87,7 +79,7 @@ extension OpenAPIKit30.OpenAPI.Document.Info.Contact: To31 { extension OpenAPIKit30.OpenAPI.Server: To31 { fileprivate func to31() -> OpenAPI31.Server { - let variables = variables.mapValues { variable in + let newVariables = variables.mapValues { variable in OpenAPI31.Server.Variable( enum: variable.enum, default: variable.default, @@ -99,7 +91,7 @@ extension OpenAPIKit30.OpenAPI.Server: To31 { return OpenAPI31.Server( urlTemplate: urlTemplate, description: description, - variables: variables, + variables: newVariables, vendorExtensions: vendorExtensions ) } @@ -173,15 +165,15 @@ fileprivate func eitherRefTo31(_ either: Either OpenAPI31.Parameter.SchemaContext { - let examples = examples?.mapValues(eitherRefTo31) + let newExamples = examples?.mapValues(eitherRefTo31) switch schema { case .a(let ref): - if let examples { + if let newExamples = newExamples { return OpenAPI31.Parameter.SchemaContext( schemaReference: .init(ref.to31()), style: style, allowReserved: allowReserved, - examples: examples + examples: newExamples ) } else { return OpenAPI31.Parameter.SchemaContext( @@ -192,12 +184,12 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { ) } case .b(let schema): - if let examples { + if let newExamples = newExamples { return OpenAPI31.Parameter.SchemaContext( schema.to31(), style: style, allowReserved: allowReserved, - examples: examples + examples: newExamples ) } else { return OpenAPI31.Parameter.SchemaContext( @@ -225,11 +217,10 @@ extension OpenAPIKit30.OpenAPI.Content.Encoding: To31 { extension OpenAPIKit30.OpenAPI.Content: To31 { fileprivate func to31() -> OpenAPI31.Content { - let examples = examples?.mapValues(eitherRefTo31) - if let examples { + if let newExamples = examples?.mapValues(eitherRefTo31) { return OpenAPI31.Content( schema: schema.map(eitherRefTo31), - examples: examples, + examples: newExamples, encoding: encoding?.mapValues { $0.to31() }, vendorExtensions: vendorExtensions ) @@ -299,7 +290,7 @@ extension OpenAPIKit30.OpenAPI.RuntimeExpression: To31 { extension OpenAPIKit30.OpenAPI.Link: To31 { fileprivate func to31() -> OpenAPI31.Link { - return OpenAPI31.Link( + OpenAPI31.Link( operation: operation, parameters: parameters.mapValues { parameter in parameter.mapFirst { $0.to31() }}, requestBody: requestBody?.mapFirst { $0.to31() }, @@ -343,7 +334,7 @@ extension OpenAPIKit30.OpenAPI.Callbacks: To31 { extension OpenAPIKit30.OpenAPI.Operation: To31 { fileprivate func to31() -> OpenAPI31.Operation { - if let requestBody { + if let newRequestBody = requestBody { return OpenAPI31.Operation( tags: tags, summary: summary, @@ -351,7 +342,7 @@ extension OpenAPIKit30.OpenAPI.Operation: To31 { externalDocs: externalDocs?.to31(), operationId: operationId, parameters: parameters.map(eitherRefTo31), - requestBody: eitherRefTo31(requestBody), + requestBody: eitherRefTo31(newRequestBody), responses: responses.mapValues(eitherRefTo31), callbacks: callbacks.mapValues(eitherRefTo31), deprecated: deprecated, @@ -380,15 +371,11 @@ extension OpenAPIKit30.OpenAPI.Operation: To31 { extension OpenAPIKit30.OpenAPI.PathItem: To31 { fileprivate func to31() -> OpenAPI31.PathItem { - let servers = servers?.map { $0.to31() } - - let parameters = parameters.map(eitherRefTo31) - - return OpenAPI31.PathItem( + OpenAPI31.PathItem( summary: summary, description: description, - servers: servers, - parameters: parameters, + servers: servers?.map { $0.to31() }, + parameters: parameters.map(eitherRefTo31), get: `get`?.to31(), put: put?.to31(), post: post?.to31(), From 783950724ec17f21a491b4d1aba1c489bec77009 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 20 Dec 2022 00:26:00 -0600 Subject: [PATCH 20/30] Add newer Swift versions to test matrix. --- .github/workflows/codecov.yml | 2 +- .github/workflows/tests.yml | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2b9461a74..564bedc70 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -5,7 +5,7 @@ on: [pull_request] jobs: codecov: container: - image: swift:5.5 + image: swift:5.7 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27ee0bdfe..66c5acb78 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,6 +62,16 @@ jobs: - swift:5.5-focal - swift:5.5-centos8 - swift:5.5-amazonlinux2 + - swift:5.6-xenial + - swift:5.6-bionic + - swift:5.6-focal + - swift:5.6-centos8 + - swift:5.6-amazonlinux2 + - swift:5.7-xenial + - swift:5.7-bionic + - swift:5.7-focal + - swift:5.7-centos8 + - swift:5.7-amazonlinux2 - swiftlang/swift:nightly-xenial - swiftlang/swift:nightly-bionic - swiftlang/swift:nightly-focal @@ -74,10 +84,10 @@ jobs: - name: Run tests run: swift test osx: - runs-on: macOS-11 + runs-on: macOS-12 steps: - name: Select latest available Xcode - uses: maxim-lobanov/setup-xcode@v1.2.3 + uses: maxim-lobanov/setup-xcode@v1.5.1 with: xcode-version: latest - name: Checkout code From 9fb5ef00b0e59fe64843dde89c15d49a2d21a5f4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 20 Dec 2022 00:26:31 -0600 Subject: [PATCH 21/30] Move integer and numeric json schema context encoding and decoding back out of core because it differs for OpenAPI 3 vs 3.1 --- .../Schema Object/JSONSchemaContext.swift | 132 ++++++++++++++++ .../Schema Object/JSONSchemaContext.swift | 123 +++++++++++++++ .../JSONSchemaSimpleContexts.swift | 141 +++--------------- 3 files changed, 275 insertions(+), 121 deletions(-) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index f362c86e4..cf26afd32 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -645,6 +645,138 @@ extension JSONSchema.CoreContext: Decodable { } } +extension JSONSchema.NumericContext { + internal enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } +} + +extension JSONSchema.NumericContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(multipleOf, forKey: .multipleOf) + + if let max = maximum { + if max.exclusive { + try container.encode(max.value, forKey: .exclusiveMaximum) + } else { + try container.encode(max.value, forKey: .maximum) + } + } + + if let min = minimum { + if min.exclusive { + try container.encode(min.value, forKey: .exclusiveMinimum) + } else { + try container.encode(min.value, forKey: .minimum) + } + } + } +} + +extension JSONSchema.NumericContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) + + let maximum: Bound? + if let exclusiveMaximum = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum) { + maximum = Bound._init(value: exclusiveMaximum, exclusive: true) + } else { + maximum = try container.decodeIfPresent(Double.self, forKey: .maximum) + .map { Bound._init(value: $0, exclusive: false) } + } + + let minimum: Bound? + if let exclusiveMinimum = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum) { + minimum = Bound._init(value: exclusiveMinimum, exclusive: true) + } else { + minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) + .map { Bound._init(value: $0, exclusive: false) } + } + + self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) + } +} + +extension JSONSchema.IntegerContext { + internal enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } +} + +extension JSONSchema.IntegerContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(multipleOf, forKey: .multipleOf) + + if let max = maximum { + if max.exclusive { + try container.encode(max.value, forKey: .exclusiveMaximum) + } else { + try container.encode(max.value, forKey: .maximum) + } + } + + if let min = minimum { + if min.exclusive { + try container.encode(min.value, forKey: .exclusiveMinimum) + } else { + try container.encode(min.value, forKey: .minimum) + } + } + } +} + +extension JSONSchema.IntegerContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) + + // the following acrobatics thanks to some libraries (namely Yams) not + // being willing to decode floating point representations of whole numbers + // as integer values. + let exclusiveMaximumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum) + let exclusiveMinimumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum) + + let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) + let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) + + func boundFromAttempt(_ attempt: Double?, max: Bool, exclusive: Bool) throws -> Bound? { + return try attempt.map { floatVal in + guard let integer = Int(exactly: floatVal) else { + throw InconsistencyError( + subjectName: max ? "maximum" : "minimum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return Bound._init(value: integer, exclusive: exclusive) + } + } + + let maximum = try boundFromAttempt(exclusiveMaximumAttempt, max: true, exclusive: true) + ?? boundFromAttempt(maximumAttempt, max: true, exclusive: false) + + let minimum = try boundFromAttempt(exclusiveMinimumAttempt, max: false, exclusive: true) + ?? boundFromAttempt(minimumAttempt, max: false, exclusive: false) + + self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) + } +} + extension JSONSchema.ArrayContext { internal enum CodingKeys: String, CodingKey { case items diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift index 669e6501c..2559e189b 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift @@ -571,6 +571,129 @@ extension JSONSchema.CoreContext: Decodable { } } +extension JSONSchema.NumericContext { + public enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } +} + +extension JSONSchema.NumericContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(multipleOf, forKey: .multipleOf) + + if let max = maximum { + try container.encode(max.value, forKey: .maximum) + if max.exclusive { + try container.encode(true, forKey: .exclusiveMaximum) + } + } + + if let min = minimum { + try container.encode(min.value, forKey: .minimum) + if min.exclusive { + try container.encode(true, forKey: .exclusiveMinimum) + } + } + } +} + +extension JSONSchema.NumericContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) + + let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? Bound._defaultExclusion + let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? Bound._defaultExclusion + + let maximum = (try container.decodeIfPresent(Double.self, forKey: .maximum)) + .map { Bound._init(value: $0, exclusive: exclusiveMaximum) } + let minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) + .map { Bound._init(value: $0, exclusive: exclusiveMinimum) } + + self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) + } +} + +extension JSONSchema.IntegerContext { + public enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } +} + +extension JSONSchema.IntegerContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(multipleOf, forKey: .multipleOf) + + if let max = maximum { + try container.encode(max.value, forKey: .maximum) + if max.exclusive { + try container.encode(true, forKey: .exclusiveMaximum) + } + } + + if let min = minimum { + try container.encode(min.value, forKey: .minimum) + if min.exclusive { + try container.encode(true, forKey: .exclusiveMinimum) + } + } + } +} + +extension JSONSchema.IntegerContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) + + let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? false + let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? false + + // the following acrobatics thanks to some libraries (namely Yams) not + // being willing to decode floating point representations of whole numbers + // as integer values. + let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) + let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) + + let maximum = try maximumAttempt.map { floatMax in + guard let integer = Int(exactly: floatMax) else { + throw InconsistencyError( + subjectName: "maximum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return integer + }.map { Bound._init(value: $0, exclusive: exclusiveMaximum) } + + let minimum = try minimumAttempt.map { floatMin in + guard let integer = Int(exactly: floatMin) else { + throw InconsistencyError( + subjectName: "minimum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return integer + }.map { Bound._init(value: $0, exclusive: exclusiveMinimum) } + + self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) + } +} + extension JSONSchema.ArrayContext { internal enum CodingKeys: String, CodingKey { case items diff --git a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift index 5c5d6de2d..550b4117c 100644 --- a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift +++ b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift @@ -17,7 +17,16 @@ public struct NumericContext: Equatable { public let value: Double public let exclusive: Bool - internal static let defaultExclusion: Bool = false + public static let _defaultExclusion: Bool = false + + internal init(value: Double, exclusive: Bool) { + self.value = value + self.exclusive = exclusive + } + + public static func _init(value: Double, exclusive: Bool) -> Bound { + .init(value: value, exclusive: exclusive) + } } /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. @@ -59,7 +68,16 @@ public struct IntegerContext: Equatable { public let value: Int public let exclusive: Bool - internal static let defaultExclusion: Bool = false + public static let _defaultExclusion: Bool = false + + internal init(value: Int, exclusive: Bool) { + self.value = value + self.exclusive = exclusive + } + + public static func _init(value: Int, exclusive: Bool) -> Bound { + .init(value: value, exclusive: exclusive) + } } /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. @@ -191,125 +209,6 @@ public struct ReferenceContext: Equatable { } } -extension NumericContext { - public enum CodingKeys: String, CodingKey { - case multipleOf - case maximum - case exclusiveMaximum - case minimum - case exclusiveMinimum - } -} - -extension NumericContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(multipleOf, forKey: .multipleOf) - - if let max = maximum { - try container.encode(max.value, forKey: .maximum) - if max.exclusive { - try container.encode(true, forKey: .exclusiveMaximum) - } - } - - if let min = minimum { - try container.encode(min.value, forKey: .minimum) - if min.exclusive { - try container.encode(true, forKey: .exclusiveMinimum) - } - } - } -} - -extension NumericContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) - - let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? Bound.defaultExclusion - let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? Bound.defaultExclusion - - maximum = (try container.decodeIfPresent(Double.self, forKey: .maximum)) - .map { Bound(value: $0, exclusive: exclusiveMaximum) } - minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) - .map { Bound(value: $0, exclusive: exclusiveMinimum) } - } -} - -extension IntegerContext { - public enum CodingKeys: String, CodingKey { - case multipleOf - case maximum - case exclusiveMaximum - case minimum - case exclusiveMinimum - } -} - -extension IntegerContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(multipleOf, forKey: .multipleOf) - - if let max = maximum { - try container.encode(max.value, forKey: .maximum) - if max.exclusive { - try container.encode(true, forKey: .exclusiveMaximum) - } - } - - if let min = minimum { - try container.encode(min.value, forKey: .minimum) - if min.exclusive { - try container.encode(true, forKey: .exclusiveMinimum) - } - } - } -} - -extension IntegerContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) - - let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? false - let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? false - - // the following acrobatics thanks to some libraries (namely Yams) not - // being willing to decode floating point representations of whole numbers - // as integer values. - let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) - let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) - - maximum = try maximumAttempt.map { floatMax in - guard let integer = Int(exactly: floatMax) else { - throw InconsistencyError( - subjectName: "maximum", - details: "Expected an Integer literal but found a floating point value", - codingPath: decoder.codingPath - ) - } - return integer - }.map { Bound(value: $0, exclusive: exclusiveMaximum) } - - minimum = try minimumAttempt.map { floatMin in - guard let integer = Int(exactly: floatMin) else { - throw InconsistencyError( - subjectName: "minimum", - details: "Expected an Integer literal but found a floating point value", - codingPath: decoder.codingPath - ) - } - return integer - }.map { Bound(value: $0, exclusive: exclusiveMinimum) } - } -} - extension StringContext { public enum CodingKeys: String, CodingKey { case maxLength From 4d7eb3b27dc5724b2952559e248497d828a166d6 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 20 Dec 2022 00:33:53 -0600 Subject: [PATCH 22/30] fix version matric images. --- .github/workflows/tests.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 66c5acb78..c11514285 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,20 +62,19 @@ jobs: - swift:5.5-focal - swift:5.5-centos8 - swift:5.5-amazonlinux2 - - swift:5.6-xenial - swift:5.6-bionic - swift:5.6-focal - - swift:5.6-centos8 + - swift:5.6-centos7 - swift:5.6-amazonlinux2 - - swift:5.7-xenial - swift:5.7-bionic - swift:5.7-focal - - swift:5.7-centos8 + - swift:5.7-jammpy + - swift:5.7-centos7 - swift:5.7-amazonlinux2 - - swiftlang/swift:nightly-xenial - swiftlang/swift:nightly-bionic - swiftlang/swift:nightly-focal - - swiftlang/swift:nightly-centos8 + - swiftlang/swift:nightly-jammy + - swiftlang/swift:nightly-centos7 - swiftlang/swift:nightly-amazonlinux2 container: ${{ matrix.image }} steps: From d7c387670291f7986c855d878d85a63b9c8d0d2a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 20 Dec 2022 01:01:50 -0600 Subject: [PATCH 23/30] move the rest of the numeric and integer context types back out of core. --- .../Schema Object/JSONSchema+Combining.swift | 8 +- .../Schema Object/JSONSchemaContext.swift | 152 +++++++++++++++-- Sources/OpenAPIKit/_CoreReExport.swift | 2 - .../Schema Object/JSONSchema+Combining.swift | 8 +- .../Schema Object/JSONSchemaContext.swift | 156 +++++++++++++++-- Sources/OpenAPIKit30/_CoreReExport.swift | 2 - Sources/OpenAPIKitCompat/Compat30To31.swift | 24 ++- .../JSONSchemaSimpleContexts.swift | 158 ------------------ 8 files changed, 307 insertions(+), 203 deletions(-) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift index 70ad22e74..32e1005f5 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift @@ -424,7 +424,7 @@ extension JSONSchema.IntegerContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return ._init( + return .init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -454,7 +454,7 @@ extension JSONSchema.NumericContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return ._init( + return .init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -612,7 +612,7 @@ extension JSONSchema.IntegerContext { throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return ._init( + return .init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum @@ -637,7 +637,7 @@ extension JSONSchema.NumericContext { throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return ._init( + return .init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index cf26afd32..ede3a4d65 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -389,6 +389,134 @@ extension JSONSchema.CoreContext { // MARK: - Specific Contexts extension JSONSchema { + /// The context that only applies to `.number` schemas. + /// + /// - Note: Although integers are numbers, `integer` + /// schemas have their own context type. An + /// `IntegerContext` _can_ be asked for the + /// `NumericContext` that would describe it via its + /// `numericContext` property. + public struct NumericContext: Equatable { + public struct Bound: Equatable { + public let value: Double + public let exclusive: Bool + + internal static let defaultExclusion: Bool = false + } + + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. + public let multipleOf: Double? + + public let maximum: Bound? + public let minimum: Bound? + + public init( + multipleOf: Double? = nil, + maximum: (Double, exclusive: Bool)? = nil, + minimum: (Double, exclusive: Bool)? = nil + ) { + self.multipleOf = multipleOf + self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + } + + internal init( + multipleOf: Double?, + maximum: Bound?, + minimum: Bound? + ) { + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + } + + /// The context that only applies to `.integer` schemas. + public struct IntegerContext: Equatable { + public struct Bound: Equatable { + public let value: Int + public let exclusive: Bool + + internal static let defaultExclusion: Bool = false + } + + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. + public let multipleOf: Int? + + public let maximum: Bound? + public let minimum: Bound? + + public init( + multipleOf: Int? = nil, + maximum: (Int, exclusive: Bool)? = nil, + minimum: (Int, exclusive: Bool)? = nil + ) { + self.multipleOf = multipleOf + self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + } + + /// Create an `IntegerContext` from the given `NumericContext`. + /// + /// This will only succeed if all properties of the `NumericContext` are + /// integers. + public init?(from numericContext: NumericContext) { + let multipleOf: Int? + if let numericMultipleOf = numericContext.multipleOf { + guard let intMultipleOf = Int(exactly: numericMultipleOf) else { + return nil + } + multipleOf = intMultipleOf + } else { + multipleOf = nil + } + + let maximum: Bound? + if let numericMax = numericContext.maximum { + guard let intMaxValue = Int(exactly: numericMax.value) else { + return nil + } + maximum = Bound(value: intMaxValue, exclusive: numericMax.exclusive) + } else { + maximum = nil + } + + let minimum: Bound? + if let numericMin = numericContext.minimum { + guard let intMinValue = Int(exactly: numericMin.value) else { + return nil + } + minimum = Bound(value: intMinValue, exclusive: numericMin.exclusive) + } else { + minimum = nil + } + + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + internal init( + multipleOf: Int?, + maximum: Bound?, + minimum: Bound? + ) { + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + /// Get the `NumericContext` that describes this + /// `IntegerContext`. + public var numericContext: NumericContext { + return .init( + multipleOf: multipleOf.map(Double.init), + maximum: maximum.map { (Double($0.value), exclusive: $0.exclusive) }, + minimum: minimum.map { (Double($0.value), exclusive: $0.exclusive) } + ) + } + } + /// The context that only applies to `.array` schemas. public struct ArrayContext: Equatable { /// A JSON Type Node that describes @@ -683,25 +811,21 @@ extension JSONSchema.NumericContext: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) + multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) - let maximum: Bound? if let exclusiveMaximum = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum) { - maximum = Bound._init(value: exclusiveMaximum, exclusive: true) + maximum = Bound(value: exclusiveMaximum, exclusive: true) } else { maximum = try container.decodeIfPresent(Double.self, forKey: .maximum) - .map { Bound._init(value: $0, exclusive: false) } + .map { Bound(value: $0, exclusive: false) } } - let minimum: Bound? if let exclusiveMinimum = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum) { - minimum = Bound._init(value: exclusiveMinimum, exclusive: true) + minimum = Bound(value: exclusiveMinimum, exclusive: true) } else { minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) - .map { Bound._init(value: $0, exclusive: false) } + .map { Bound(value: $0, exclusive: false) } } - - self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) } } @@ -743,7 +867,7 @@ extension JSONSchema.IntegerContext: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) + multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) // the following acrobatics thanks to some libraries (namely Yams) not // being willing to decode floating point representations of whole numbers @@ -763,17 +887,15 @@ extension JSONSchema.IntegerContext: Decodable { codingPath: decoder.codingPath ) } - return Bound._init(value: integer, exclusive: exclusive) + return Bound(value: integer, exclusive: exclusive) } } - let maximum = try boundFromAttempt(exclusiveMaximumAttempt, max: true, exclusive: true) + maximum = try boundFromAttempt(exclusiveMaximumAttempt, max: true, exclusive: true) ?? boundFromAttempt(maximumAttempt, max: true, exclusive: false) - let minimum = try boundFromAttempt(exclusiveMinimumAttempt, max: false, exclusive: true) + minimum = try boundFromAttempt(exclusiveMinimumAttempt, max: false, exclusive: true) ?? boundFromAttempt(minimumAttempt, max: false, exclusive: false) - - self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) } } diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index ee9112ae6..d7de02b1b 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -40,8 +40,6 @@ public extension OpenAPI.Response { public extension JSONSchema { typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions - typealias NumericContext = OpenAPIKitCore.NumericContext - typealias IntegerContext = OpenAPIKitCore.IntegerContext typealias StringContext = OpenAPIKitCore.StringContext typealias ReferenceContext = OpenAPIKitCore.ReferenceContext } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift index 95d1c4c37..3a2c417bb 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift @@ -404,7 +404,7 @@ extension JSONSchema.IntegerContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return ._init( + return .init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -434,7 +434,7 @@ extension JSONSchema.NumericContext { let newMultipleOf = multipleOf ?? other.multipleOf let newMaximum = maximum ?? other.maximum let newMinimum = minimum ?? other.minimum - return ._init( + return .init( multipleOf: newMultipleOf, maximum: newMaximum, minimum: newMinimum @@ -592,7 +592,7 @@ extension JSONSchema.IntegerContext { throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return ._init( + return .init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum @@ -617,7 +617,7 @@ extension JSONSchema.NumericContext { throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(min.value) cannot be higher than maximum (\(max.value)")) } } - return ._init( + return .init( multipleOf: multipleOf, maximum: maximum, minimum: validatedMinimum diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift index 2559e189b..9c7a99d0a 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift @@ -346,6 +346,134 @@ extension JSONSchema.CoreContext { // MARK: - Specific Contexts extension JSONSchema { + /// The context that only applies to `.number` schemas. + /// + /// - Note: Although integers are numbers, `integer` + /// schemas have their own context type. An + /// `IntegerContext` _can_ be asked for the + /// `NumericContext` that would describe it via its + /// `numericContext` property. + public struct NumericContext: Equatable { + public struct Bound: Equatable { + public let value: Double + public let exclusive: Bool + + internal static let defaultExclusion: Bool = false + } + + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. + public let multipleOf: Double? + + public let maximum: Bound? + public let minimum: Bound? + + public init( + multipleOf: Double? = nil, + maximum: (Double, exclusive: Bool)? = nil, + minimum: (Double, exclusive: Bool)? = nil + ) { + self.multipleOf = multipleOf + self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + } + + internal init( + multipleOf: Double?, + maximum: Bound?, + minimum: Bound? + ) { + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + } + + /// The context that only applies to `.integer` schemas. + public struct IntegerContext: Equatable { + public struct Bound: Equatable { + public let value: Int + public let exclusive: Bool + + internal static let defaultExclusion: Bool = false + } + + /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. + public let multipleOf: Int? + + public let maximum: Bound? + public let minimum: Bound? + + public init( + multipleOf: Int? = nil, + maximum: (Int, exclusive: Bool)? = nil, + minimum: (Int, exclusive: Bool)? = nil + ) { + self.multipleOf = multipleOf + self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } + } + + /// Create an `IntegerContext` from the given `NumericContext`. + /// + /// This will only succeed if all properties of the `NumericContext` are + /// integers. + public init?(from numericContext: NumericContext) { + let multipleOf: Int? + if let numericMultipleOf = numericContext.multipleOf { + guard let intMultipleOf = Int(exactly: numericMultipleOf) else { + return nil + } + multipleOf = intMultipleOf + } else { + multipleOf = nil + } + + let maximum: Bound? + if let numericMax = numericContext.maximum { + guard let intMaxValue = Int(exactly: numericMax.value) else { + return nil + } + maximum = Bound(value: intMaxValue, exclusive: numericMax.exclusive) + } else { + maximum = nil + } + + let minimum: Bound? + if let numericMin = numericContext.minimum { + guard let intMinValue = Int(exactly: numericMin.value) else { + return nil + } + minimum = Bound(value: intMinValue, exclusive: numericMin.exclusive) + } else { + minimum = nil + } + + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + internal init( + multipleOf: Int?, + maximum: Bound?, + minimum: Bound? + ) { + self.multipleOf = multipleOf + self.maximum = maximum + self.minimum = minimum + } + + /// Get the `NumericContext` that describes this + /// `IntegerContext`. + public var numericContext: NumericContext { + return .init( + multipleOf: multipleOf.map(Double.init), + maximum: maximum.map { (Double($0.value), exclusive: $0.exclusive) }, + minimum: minimum.map { (Double($0.value), exclusive: $0.exclusive) } + ) + } + } + /// The context that only applies to `.array` schemas. public struct ArrayContext: Equatable { /// A JSON Type Node that describes @@ -607,17 +735,15 @@ extension JSONSchema.NumericContext: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) + multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) - let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? Bound._defaultExclusion - let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? Bound._defaultExclusion + let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? Bound.defaultExclusion + let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? Bound.defaultExclusion - let maximum = (try container.decodeIfPresent(Double.self, forKey: .maximum)) - .map { Bound._init(value: $0, exclusive: exclusiveMaximum) } - let minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) - .map { Bound._init(value: $0, exclusive: exclusiveMinimum) } - - self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) + maximum = (try container.decodeIfPresent(Double.self, forKey: .maximum)) + .map { Bound(value: $0, exclusive: exclusiveMaximum) } + minimum = (try container.decodeIfPresent(Double.self, forKey: .minimum)) + .map { Bound(value: $0, exclusive: exclusiveMinimum) } } } @@ -657,7 +783,7 @@ extension JSONSchema.IntegerContext: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) + multipleOf = try container.decodeIfPresent(Int.self, forKey: .multipleOf) let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? false let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? false @@ -668,7 +794,7 @@ extension JSONSchema.IntegerContext: Decodable { let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) - let maximum = try maximumAttempt.map { floatMax in + maximum = try maximumAttempt.map { floatMax in guard let integer = Int(exactly: floatMax) else { throw InconsistencyError( subjectName: "maximum", @@ -677,9 +803,9 @@ extension JSONSchema.IntegerContext: Decodable { ) } return integer - }.map { Bound._init(value: $0, exclusive: exclusiveMaximum) } + }.map { Bound(value: $0, exclusive: exclusiveMaximum) } - let minimum = try minimumAttempt.map { floatMin in + minimum = try minimumAttempt.map { floatMin in guard let integer = Int(exactly: floatMin) else { throw InconsistencyError( subjectName: "minimum", @@ -688,9 +814,7 @@ extension JSONSchema.IntegerContext: Decodable { ) } return integer - }.map { Bound._init(value: $0, exclusive: exclusiveMinimum) } - - self = ._init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) + }.map { Bound(value: $0, exclusive: exclusiveMinimum) } } } diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index ee9112ae6..d7de02b1b 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -40,8 +40,6 @@ public extension OpenAPI.Response { public extension JSONSchema { typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions - typealias NumericContext = OpenAPIKitCore.NumericContext - typealias IntegerContext = OpenAPIKitCore.IntegerContext typealias StringContext = OpenAPIKitCore.StringContext typealias ReferenceContext = OpenAPIKitCore.ReferenceContext } diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index a49240441..aff05338d 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -505,6 +505,26 @@ extension OpenAPIKit30.JSONSchema.CoreContext: To31 where Format: OpenAPIKit.Ope } } +extension OpenAPIKit30.JSONSchema.NumericContext: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema.NumericContext { + OpenAPIKit.JSONSchema.NumericContext( + multipleOf: multipleOf, + maximum: maximum.map { ($0.value, $0.exclusive) }, + minimum: minimum.map { ($0.value, $0.exclusive) } + ) + } +} + +extension OpenAPIKit30.JSONSchema.IntegerContext: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema.IntegerContext { + OpenAPIKit.JSONSchema.IntegerContext( + multipleOf: multipleOf, + maximum: maximum.map { ($0.value, $0.exclusive) }, + minimum: minimum.map { ($0.value, $0.exclusive) } + ) + } +} + extension OpenAPIKit30.JSONSchema.ArrayContext: To31 { fileprivate func to31() -> OpenAPIKit.JSONSchema.ArrayContext { OpenAPIKit.JSONSchema.ArrayContext( @@ -535,9 +555,9 @@ extension OpenAPIKit30.JSONSchema: To31 { case .boolean(let core): schema = .boolean(core.to31()) case .number(let core, let numeric): - schema = .number(core.to31(), numeric) + schema = .number(core.to31(), numeric.to31()) case .integer(let core, let integral): - schema = .integer(core.to31(), integral) + schema = .integer(core.to31(), integral.to31()) case .string(let core, let stringy): schema = .string(core.to31(), stringy) case .object(let core, let objective): diff --git a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift index 550b4117c..d4a9b58da 100644 --- a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift +++ b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift @@ -5,164 +5,6 @@ // Created by Mathew Polzin on 12/19/22. // -/// The context that only applies to `.number` schemas. -/// -/// - Note: Although integers are numbers, `integer` -/// schemas have their own context type. An -/// `IntegerContext` _can_ be asked for the -/// `NumericContext` that would describe it via its -/// `numericContext` property. -public struct NumericContext: Equatable { - public struct Bound: Equatable { - public let value: Double - public let exclusive: Bool - - public static let _defaultExclusion: Bool = false - - internal init(value: Double, exclusive: Bool) { - self.value = value - self.exclusive = exclusive - } - - public static func _init(value: Double, exclusive: Bool) -> Bound { - .init(value: value, exclusive: exclusive) - } - } - - /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. - public let multipleOf: Double? - - public let maximum: Bound? - public let minimum: Bound? - - public init( - multipleOf: Double? = nil, - maximum: (Double, exclusive: Bool)? = nil, - minimum: (Double, exclusive: Bool)? = nil - ) { - self.multipleOf = multipleOf - self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - } - - internal init( - multipleOf: Double?, - maximum: Bound?, - minimum: Bound? - ) { - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - public static func _init( - multipleOf: Double?, - maximum: Bound?, - minimum: Bound? - ) -> NumericContext { .init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) } -} - -/// The context that only applies to `.integer` schemas. -public struct IntegerContext: Equatable { - public struct Bound: Equatable { - public let value: Int - public let exclusive: Bool - - public static let _defaultExclusion: Bool = false - - internal init(value: Int, exclusive: Bool) { - self.value = value - self.exclusive = exclusive - } - - public static func _init(value: Int, exclusive: Bool) -> Bound { - .init(value: value, exclusive: exclusive) - } - } - - /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. - public let multipleOf: Int? - - public let maximum: Bound? - public let minimum: Bound? - - public init( - multipleOf: Int? = nil, - maximum: (Int, exclusive: Bool)? = nil, - minimum: (Int, exclusive: Bool)? = nil - ) { - self.multipleOf = multipleOf - self.maximum = maximum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - self.minimum = minimum.map { Bound(value: $0.0, exclusive: $0.exclusive) } - } - - /// Create an `IntegerContext` from the given `NumericContext`. - /// - /// This will only succeed if all properties of the `NumericContext` are - /// integers. - public init?(from numericContext: NumericContext) { - let multipleOf: Int? - if let numericMultipleOf = numericContext.multipleOf { - guard let intMultipleOf = Int(exactly: numericMultipleOf) else { - return nil - } - multipleOf = intMultipleOf - } else { - multipleOf = nil - } - - let maximum: Bound? - if let numericMax = numericContext.maximum { - guard let intMaxValue = Int(exactly: numericMax.value) else { - return nil - } - maximum = Bound(value: intMaxValue, exclusive: numericMax.exclusive) - } else { - maximum = nil - } - - let minimum: Bound? - if let numericMin = numericContext.minimum { - guard let intMinValue = Int(exactly: numericMin.value) else { - return nil - } - minimum = Bound(value: intMinValue, exclusive: numericMin.exclusive) - } else { - minimum = nil - } - - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - internal init( - multipleOf: Int?, - maximum: Bound?, - minimum: Bound? - ) { - self.multipleOf = multipleOf - self.maximum = maximum - self.minimum = minimum - } - - public static func _init( - multipleOf: Int?, - maximum: Bound?, - minimum: Bound? - ) -> IntegerContext { .init(multipleOf: multipleOf, maximum: maximum, minimum: minimum) } - - /// Get the `NumericContext` that describes this - /// `IntegerContext`. - public var numericContext: NumericContext { - return .init( - multipleOf: multipleOf.map(Double.init), - maximum: maximum.map { (Double($0.value), exclusive: $0.exclusive) }, - minimum: minimum.map { (Double($0.value), exclusive: $0.exclusive) } - ) - } -} - /// The context that only applies to `.string` schemas. public struct StringContext: Equatable { public let maxLength: Int? From d806ad4d93e3acfb0decb3449ef1dd132ddc4209 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 20 Dec 2022 01:03:02 -0600 Subject: [PATCH 24/30] fix typo in jammy image name. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c11514285..fce2c718e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,7 @@ jobs: - swift:5.6-amazonlinux2 - swift:5.7-bionic - swift:5.7-focal - - swift:5.7-jammpy + - swift:5.7-jammy - swift:5.7-centos7 - swift:5.7-amazonlinux2 - swiftlang/swift:nightly-bionic From 7b57a4a99ddb56c3634cd3a184e8b5c6b1021a7c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 21 Dec 2022 22:11:53 -0600 Subject: [PATCH 25/30] remove centos images cuz swift package manager doesn't even work on that image. --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fce2c718e..7b83d1b24 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,17 +64,14 @@ jobs: - swift:5.5-amazonlinux2 - swift:5.6-bionic - swift:5.6-focal - - swift:5.6-centos7 - swift:5.6-amazonlinux2 - swift:5.7-bionic - swift:5.7-focal - swift:5.7-jammy - - swift:5.7-centos7 - swift:5.7-amazonlinux2 - swiftlang/swift:nightly-bionic - swiftlang/swift:nightly-focal - swiftlang/swift:nightly-jammy - - swiftlang/swift:nightly-centos7 - swiftlang/swift:nightly-amazonlinux2 container: ${{ matrix.image }} steps: From f5a0fe080f90b61f9734b90ecb9e8fbe5712865c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 21 Dec 2022 22:58:16 -0600 Subject: [PATCH 26/30] a tad more test coverage. --- .../DocumentConversionTests.swift | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 0383c413a..80ce55519 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -13,6 +13,7 @@ import XCTest final class DocumentConversionTests: XCTestCase { func test_barebonesDocument() { let oldDoc = OpenAPIKit30.OpenAPI.Document( + openAPIVersion: .v3_0_3, info: .init(title: "Hello World", version: "1.0.1"), servers: [], paths: [:], @@ -21,6 +22,22 @@ final class DocumentConversionTests: XCTestCase { let newDoc = oldDoc.convert(to: .v3_1_0) + assertEqualOldToNew(newDoc, oldDoc) + XCTAssertEqual(newDoc.openAPIVersion, .v3_1_0) + } + + func test_vendorExtensionsOnDoc() { + let oldDoc = OpenAPIKit30.OpenAPI.Document( + openAPIVersion: .v3_0_3, + info: .init(title: "Hello World", version: "1.0.1"), + servers: [], + paths: [:], + components: .noComponents, + vendorExtensions: ["x-doc": "document"] + ) + + let newDoc = oldDoc.convert(to: .v3_1_0) + assertEqualOldToNew(newDoc, oldDoc) } @@ -45,10 +62,37 @@ final class DocumentConversionTests: XCTestCase { assertEqualOldToNew(newDoc, oldDoc) } + func test_servers() { + let oldDoc = OpenAPIKit30.OpenAPI.Document( + info: .init(title: "Hello", version: "1.0.0"), + servers: [ + .init( + url: URL(string: "https://website.com")!, + description: "ok", + variables: ["x-ok": .init(default: "1.0")], + vendorExtensions: ["x-cool": 1.5] + ), + .init( + urlTemplate: .init(url: URL(string: "https://website.com")!), + description: "hi", + variables: ["hello": .init(enum: ["1"], default: "1", description: "described", vendorExtensions: ["x-hi": "hello"])], + vendorExtensions: ["x-test": 2] + ) + ], + paths: [:], + components: .noComponents + ) + + let newDoc = oldDoc.convert(to: .v3_1_0) + + assertEqualOldToNew(newDoc, oldDoc) + } + // TODO: more tests } fileprivate func assertEqualOldToNew(_ newDoc: OpenAPIKit.OpenAPI.Document, _ oldDoc: OpenAPIKit30.OpenAPI.Document) { + // INFO XCTAssertEqual(newDoc.info.title, oldDoc.info.title) XCTAssertEqual(newDoc.info.version, oldDoc.info.version) XCTAssertEqual(newDoc.info.vendorExtensions, oldDoc.info.vendorExtensions) @@ -63,5 +107,37 @@ fileprivate func assertEqualOldToNew(_ newDoc: OpenAPIKit.OpenAPI.Document, _ ol XCTAssertEqual(newDoc.info.license?.vendorExtensions, oldDoc.info.license?.vendorExtensions) XCTAssertNil(newDoc.info.summary) + // SERVERS + XCTAssertEqual(newDoc.servers.count, oldDoc.servers.count) + for (newServer, oldServer) in zip(newDoc.servers, oldDoc.servers) { + assertEqualOldToNew(newServer, oldServer) + } + + // PATHS + + // COMPONENTS + + // SECURITY + + // TAGS + + // EXTERNAL DOCS + + // VENDOR EXTENSIONS + XCTAssertEqual(newDoc.vendorExtensions, oldDoc.vendorExtensions) + // TODO: test the rest of equality. } + +fileprivate func assertEqualOldToNew(_ newServer: OpenAPIKit.OpenAPI.Server, _ oldServer: OpenAPIKit30.OpenAPI.Server) { + XCTAssertEqual(newServer.urlTemplate, oldServer.urlTemplate) + XCTAssertEqual(newServer.description, oldServer.description) + XCTAssertEqual(newServer.vendorExtensions, oldServer.vendorExtensions) + for (key, newVariable) in newServer.variables { + let oldVariable = oldServer.variables[key] + XCTAssertEqual(newVariable.description, oldVariable?.description) + XCTAssertEqual(newVariable.`enum`, oldVariable?.`enum`) + XCTAssertEqual(newVariable.`default`, oldVariable?.`default`) + XCTAssertEqual(newVariable.vendorExtensions, oldVariable?.vendorExtensions) + } +} From acacf966aecdea23ebb5b46489bbc98beff32782 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 24 Dec 2022 13:36:13 -0600 Subject: [PATCH 27/30] move parameter context location into core. --- .../OpenAPIKit/Parameter/ParameterContext.swift | 9 --------- Sources/OpenAPIKit/_CoreReExport.swift | 4 ++++ .../OpenAPIKit30/Parameter/ParameterContext.swift | 8 -------- Sources/OpenAPIKit30/_CoreReExport.swift | 4 ++++ .../OpenAPIKitCore/ParameterContextLocation.swift | 15 +++++++++++++++ 5 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 Sources/OpenAPIKitCore/ParameterContextLocation.swift diff --git a/Sources/OpenAPIKit/Parameter/ParameterContext.swift b/Sources/OpenAPIKit/Parameter/ParameterContext.swift index 8d148f466..13119e83b 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterContext.swift @@ -73,13 +73,6 @@ extension OpenAPI.Parameter { } extension OpenAPI.Parameter.Context { - public enum Location: String, CaseIterable, Codable { - case query - case header - case path - case cookie - } - public var location: Location { switch self { case .query: @@ -93,5 +86,3 @@ extension OpenAPI.Parameter.Context { } } } - -extension OpenAPI.Parameter.Context.Location: Validatable {} diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index d7de02b1b..cb6ed0247 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -30,6 +30,10 @@ public extension OpenAPI.SecurityScheme { typealias Location = OpenAPIKitCore.SecuritySchemeLocation } +public extension OpenAPI.Parameter.Context { + typealias Location = OpenAPIKitCore.ParameterContextLocation +} + public extension OpenAPI.Parameter.SchemaContext { typealias Style = ParameterSchemaContextStyle } diff --git a/Sources/OpenAPIKit30/Parameter/ParameterContext.swift b/Sources/OpenAPIKit30/Parameter/ParameterContext.swift index 8d148f466..abb134563 100644 --- a/Sources/OpenAPIKit30/Parameter/ParameterContext.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterContext.swift @@ -73,13 +73,6 @@ extension OpenAPI.Parameter { } extension OpenAPI.Parameter.Context { - public enum Location: String, CaseIterable, Codable { - case query - case header - case path - case cookie - } - public var location: Location { switch self { case .query: @@ -94,4 +87,3 @@ extension OpenAPI.Parameter.Context { } } -extension OpenAPI.Parameter.Context.Location: Validatable {} diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index d7de02b1b..cb6ed0247 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -30,6 +30,10 @@ public extension OpenAPI.SecurityScheme { typealias Location = OpenAPIKitCore.SecuritySchemeLocation } +public extension OpenAPI.Parameter.Context { + typealias Location = OpenAPIKitCore.ParameterContextLocation +} + public extension OpenAPI.Parameter.SchemaContext { typealias Style = ParameterSchemaContextStyle } diff --git a/Sources/OpenAPIKitCore/ParameterContextLocation.swift b/Sources/OpenAPIKitCore/ParameterContextLocation.swift new file mode 100644 index 000000000..4c6e2e38f --- /dev/null +++ b/Sources/OpenAPIKitCore/ParameterContextLocation.swift @@ -0,0 +1,15 @@ +// +// ParameterContextLocation.swift +// +// +// Created by Mathew Polzin on 12/24/22. +// + +public enum ParameterContextLocation: String, CaseIterable, Codable { + case query + case header + case path + case cookie +} + +extension ParameterContextLocation: Validatable {} From a9917966651a35d4204e03fb9358a695b211347e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 24 Dec 2022 13:36:27 -0600 Subject: [PATCH 28/30] Add some more test coverage for compat layer. --- .../DocumentConversionTests.swift | 185 +++++++++++++++++- 1 file changed, 176 insertions(+), 9 deletions(-) diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 80ce55519..7aff305db 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -11,7 +11,7 @@ import OpenAPIKitCompat import XCTest final class DocumentConversionTests: XCTestCase { - func test_barebonesDocument() { + func test_barebonesDocument() throws { let oldDoc = OpenAPIKit30.OpenAPI.Document( openAPIVersion: .v3_0_3, info: .init(title: "Hello World", version: "1.0.1"), @@ -22,11 +22,11 @@ final class DocumentConversionTests: XCTestCase { let newDoc = oldDoc.convert(to: .v3_1_0) - assertEqualOldToNew(newDoc, oldDoc) + try assertEqualOldToNew(newDoc, oldDoc) XCTAssertEqual(newDoc.openAPIVersion, .v3_1_0) } - func test_vendorExtensionsOnDoc() { + func test_vendorExtensionsOnDoc() throws { let oldDoc = OpenAPIKit30.OpenAPI.Document( openAPIVersion: .v3_0_3, info: .init(title: "Hello World", version: "1.0.1"), @@ -38,10 +38,10 @@ final class DocumentConversionTests: XCTestCase { let newDoc = oldDoc.convert(to: .v3_1_0) - assertEqualOldToNew(newDoc, oldDoc) + try assertEqualOldToNew(newDoc, oldDoc) } - func test_fullInfo() { + func test_fullInfo() throws { let oldDoc = OpenAPIKit30.OpenAPI.Document( info: .init( title: "Hello World", @@ -59,10 +59,10 @@ final class DocumentConversionTests: XCTestCase { let newDoc = oldDoc.convert(to: .v3_1_0) - assertEqualOldToNew(newDoc, oldDoc) + try assertEqualOldToNew(newDoc, oldDoc) } - func test_servers() { + func test_servers() throws { let oldDoc = OpenAPIKit30.OpenAPI.Document( info: .init(title: "Hello", version: "1.0.0"), servers: [ @@ -85,13 +85,112 @@ final class DocumentConversionTests: XCTestCase { let newDoc = oldDoc.convert(to: .v3_1_0) - assertEqualOldToNew(newDoc, oldDoc) + try assertEqualOldToNew(newDoc, oldDoc) + } + + func test_paths() throws { + let params: OpenAPIKit30.OpenAPI.Parameter.Array = [ + .a(.external(URL(string: "https://welcome.com")!)), + .a(.component(named: "test")), + .parameter(.init(name: "test", context: .query, schema: .string)) + ] + + let externalDocs = OpenAPIKit30.OpenAPI.ExternalDocumentation( + description: "hello", + url: URL(string: "https://website.com")!, + vendorExtensions: ["x-hi": 3] + ) + + let request = OpenAPIKit30.OpenAPI.Request( + description: "describble", + content: [ + .json: .init(schema: .string, example: "{\"hi\": 1}", encoding: ["utf8": .init()]) + ], + required: true, + vendorExtensions: ["x-tend": "ed"] + ) + + let response = OpenAPIKit30.OpenAPI.Response( + description: "hello", + headers: ["Content-Type": .header(.init(schema: .string))], + content: [.json: .init(schema: .string)], + links: ["link1": .link(operationId: "link1")] + ) + + let callbacks: OpenAPIKit30.OpenAPI.Callbacks = [ + .init(url: URL(string: "https://website.com")!): .init(summary: "hello") + ] + + let server = OpenAPIKit30.OpenAPI.Server( + url: URL(string: "https://website.com")!, + description: "ok", + variables: ["x-ok": .init(default: "1.0")], + vendorExtensions: ["x-cool": 1.5] + ) + + let securityRequirement: OpenAPIKit30.OpenAPI.SecurityRequirement = [ + .component(named: "security"): ["hello"] + ] + + let operation = OpenAPIKit30.OpenAPI.Operation( + tags: ["hello"], + summary: "sum", + description: "described", + externalDocs: externalDocs, + operationId: "ident", + parameters: params, + requestBody: .request(request), + responses: [200: .b(response)], + callbacks: ["callback": .b(callbacks)], + deprecated: true, + security: [securityRequirement], + servers: [server], + vendorExtensions: ["x-hello": 101] + ) + + let oldDoc = OpenAPIKit30.OpenAPI.Document( + info: .init(title: "Hello", version: "1.0.0"), + servers: [], + paths: [ + "/hello/world": .init( + summary: "sum", + description: "described", + servers: [ + .init( + url: URL(string: "https://website.com")!, + description: "ok", + variables: ["x-ok": .init(default: "1.0")], + vendorExtensions: ["x-cool": 1.5] + ) + ], + parameters: [ + .a(.external(URL(string: "https://welcome.com")!)), + .a(.internal(.component(name: "test"))), + .parameter(.init(name: "test", context: .query, schema: .string)) + ], + get: operation, + put: operation, + post: operation, + delete: operation, + options: operation, + head: operation, + patch: operation, + trace: operation, + vendorExtensions: ["x-test": 123] + ) + ], + components: .noComponents + ) + + let newDoc = oldDoc.convert(to: .v3_1_0) + + try assertEqualOldToNew(newDoc, oldDoc) } // TODO: more tests } -fileprivate func assertEqualOldToNew(_ newDoc: OpenAPIKit.OpenAPI.Document, _ oldDoc: OpenAPIKit30.OpenAPI.Document) { +fileprivate func assertEqualOldToNew(_ newDoc: OpenAPIKit.OpenAPI.Document, _ oldDoc: OpenAPIKit30.OpenAPI.Document) throws { // INFO XCTAssertEqual(newDoc.info.title, oldDoc.info.title) XCTAssertEqual(newDoc.info.version, oldDoc.info.version) @@ -114,6 +213,11 @@ fileprivate func assertEqualOldToNew(_ newDoc: OpenAPIKit.OpenAPI.Document, _ ol } // PATHS + XCTAssertEqual(newDoc.paths.count, oldDoc.paths.count) + for (path, newPathItem) in newDoc.paths { + let oldPathItem = try XCTUnwrap(oldDoc.paths[path]) + try assertEqualOldToNew(newPathItem, oldPathItem) + } // COMPONENTS @@ -141,3 +245,66 @@ fileprivate func assertEqualOldToNew(_ newServer: OpenAPIKit.OpenAPI.Server, _ o XCTAssertEqual(newVariable.vendorExtensions, oldVariable?.vendorExtensions) } } + +fileprivate func assertEqualOldToNew(_ newPathItem: OpenAPIKit.OpenAPI.PathItem, _ oldPathItem: OpenAPIKit30.OpenAPI.PathItem) throws { + XCTAssertEqual(newPathItem.summary, oldPathItem.summary) + XCTAssertEqual(newPathItem.description, oldPathItem.description) + if let newServers = newPathItem.servers { + let oldServers = try XCTUnwrap(oldPathItem.servers) + for (newServer, oldServer) in zip(newServers, oldServers) { + assertEqualOldToNew(newServer, oldServer) + } + } + for (newParameter, oldParameter) in zip(newPathItem.parameters, oldPathItem.parameters) { + switch (newParameter, oldParameter) { + case (.a(let ref), .a(let ref2)): + XCTAssertNil(ref.summary) + XCTAssertNil(ref.description) + XCTAssertEqual(ref.jsonReference.absoluteString, ref2.absoluteString) + case (.b(let param), .b(let param2)): + XCTAssertEqual(param.name, param2.name) + assertEqualOldToNew(param.context, param2.context) + XCTAssertEqual(param.description, param2.description) + XCTAssertEqual(param.deprecated, param2.deprecated) + XCTAssertEqual(param.vendorExtensions, param2.vendorExtensions) + XCTAssertEqual(param.required, param2.required) + default: + XCTFail("Parameters are not equal because one is a reference and the other is not: \(newParameter) / \(oldParameter)") + } + } + try assertEqualOldToNew(newPathItem.get, oldPathItem.get) + try assertEqualOldToNew(newPathItem.put, oldPathItem.put) + try assertEqualOldToNew(newPathItem.post, oldPathItem.post) + try assertEqualOldToNew(newPathItem.delete, oldPathItem.delete) + try assertEqualOldToNew(newPathItem.options, oldPathItem.options) + try assertEqualOldToNew(newPathItem.head, oldPathItem.head) + try assertEqualOldToNew(newPathItem.patch, oldPathItem.patch) + try assertEqualOldToNew(newPathItem.trace, oldPathItem.trace) + + XCTAssertEqual(newPathItem.vendorExtensions, oldPathItem.vendorExtensions) +} + +fileprivate func assertEqualOldToNew(_ newParamContext: OpenAPIKit.OpenAPI.Parameter.Context, _ oldParamContext: OpenAPIKit30.OpenAPI.Parameter.Context) { + switch (newParamContext, oldParamContext) { + case (.query(required: let req, allowEmptyValue: let empty), .query(required: let req2, allowEmptyValue: let empty2)): + XCTAssertEqual(req, req2) + XCTAssertEqual(empty, empty2) + case (.header(required: let req), .header(required: let req2)): + XCTAssertEqual(req, req2) + case (.path, .path): + break + case (.cookie(required: let req), .cookie(required: let req2)): + XCTAssertEqual(req, req2) + default: + XCTFail("Parameter contexts are not equal. \(newParamContext) / \(oldParamContext)") + } +} + +fileprivate func assertEqualOldToNew(_ newOperation: OpenAPIKit.OpenAPI.Operation?, _ oldOperation: OpenAPIKit30.OpenAPI.Operation?) throws { + if let newOp = newOperation { + let oldOp = try XCTUnwrap(oldOperation) + // TODO + } else { + XCTAssertNil(oldOperation) + } +} From 80e029235ed6536e4cabfeebd31df4e97a1e3005 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 24 Dec 2022 16:58:21 -0600 Subject: [PATCH 29/30] new namespace in Core to reduce noise in global namespace. --- .../Schema Object/JSONSchema+Combining.swift | 8 +- .../OpenAPIKit/Security/SecurityScheme.swift | 2 +- Sources/OpenAPIKit/_CoreReExport.swift | 42 +-- .../Schema Object/JSONSchema+Combining.swift | 8 +- .../Security/SecurityScheme.swift | 2 +- Sources/OpenAPIKit30/_CoreReExport.swift | 42 +-- Sources/OpenAPIKitCore/CallbackURL.swift | 64 ----- Sources/OpenAPIKitCore/ComponentKey.swift | 67 ----- Sources/OpenAPIKitCore/HttpMethod.swift | 23 -- .../JSONSchemaPermissions.swift | 12 - .../JSONSchemaSimpleContexts.swift | 80 ------ Sources/OpenAPIKitCore/JSONTypeFormat.swift | 270 ----------------- .../ParameterContextLocation.swift | 15 - .../ParameterSchemaContextStyle.swift | 16 -- Sources/OpenAPIKitCore/Path.swift | 35 --- .../OpenAPIKitCore/ResponseStatusCode.swift | 106 ------- Sources/OpenAPIKitCore/SecurityScheme.swift | 14 - Sources/OpenAPIKitCore/Shared.swift | 11 + .../OpenAPIKitCore/Shared/CallbackURL.swift | 66 +++++ .../OpenAPIKitCore/Shared/ComponentKey.swift | 69 +++++ .../{ => Shared}/ContentType.swift | 146 +++++----- .../{ => Shared}/Discriminator.swift | 32 ++- .../OpenAPIKitCore/Shared/HttpMethod.swift | 25 ++ .../Shared/JSONSchemaPermissions.swift | 14 + .../Shared/JSONSchemaSimpleContexts.swift | 82 ++++++ .../Shared/JSONTypeFormat.swift | 272 ++++++++++++++++++ .../{ => Shared}/OAuthFlows.swift | 106 +++---- .../Shared/ParameterContextLocation.swift | 17 ++ .../Shared/ParameterSchemaContextStyle.swift | 18 ++ Sources/OpenAPIKitCore/Shared/Path.swift | 37 +++ .../Shared/ResponseStatusCode.swift | 108 +++++++ .../Shared/SecurityScheme.swift | 16 ++ .../ContentTypeTests.swift | 12 +- 33 files changed, 938 insertions(+), 899 deletions(-) delete mode 100644 Sources/OpenAPIKitCore/CallbackURL.swift delete mode 100644 Sources/OpenAPIKitCore/ComponentKey.swift delete mode 100644 Sources/OpenAPIKitCore/HttpMethod.swift delete mode 100644 Sources/OpenAPIKitCore/JSONSchemaPermissions.swift delete mode 100644 Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift delete mode 100644 Sources/OpenAPIKitCore/JSONTypeFormat.swift delete mode 100644 Sources/OpenAPIKitCore/ParameterContextLocation.swift delete mode 100644 Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift delete mode 100644 Sources/OpenAPIKitCore/Path.swift delete mode 100644 Sources/OpenAPIKitCore/ResponseStatusCode.swift delete mode 100644 Sources/OpenAPIKitCore/SecurityScheme.swift create mode 100644 Sources/OpenAPIKitCore/Shared.swift create mode 100644 Sources/OpenAPIKitCore/Shared/CallbackURL.swift create mode 100644 Sources/OpenAPIKitCore/Shared/ComponentKey.swift rename Sources/OpenAPIKitCore/{ => Shared}/ContentType.swift (68%) rename Sources/OpenAPIKitCore/{ => Shared}/Discriminator.swift (52%) create mode 100644 Sources/OpenAPIKitCore/Shared/HttpMethod.swift create mode 100644 Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift create mode 100644 Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift create mode 100644 Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift rename Sources/OpenAPIKitCore/{ => Shared}/OAuthFlows.swift (68%) create mode 100644 Sources/OpenAPIKitCore/Shared/ParameterContextLocation.swift create mode 100644 Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift create mode 100644 Sources/OpenAPIKitCore/Shared/Path.swift create mode 100644 Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift create mode 100644 Sources/OpenAPIKitCore/Shared/SecurityScheme.swift diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift index 32e1005f5..288a01392 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift @@ -467,7 +467,7 @@ extension JSONSchema.StringContext { if let conflict = conflicting(maxLength, other.maxLength) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "maxLength", original: String(conflict.0), new: String(conflict.1))) } - if let conflict = conflicting(StringContext._minLength(self), StringContext._minLength(other)) { + if let conflict = conflicting(Self._minLength(self), Self._minLength(other)) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "minLength", original: String(conflict.0), new: String(conflict.1))) } if let conflict = conflicting(pattern, other.pattern) { @@ -476,7 +476,7 @@ extension JSONSchema.StringContext { // explicitly declaring these constants one at a time // helps the type checker a lot. let newMaxLength = maxLength ?? other.maxLength - let newMinLength = StringContext._minLength(self) ?? StringContext._minLength(other) + let newMinLength = Self._minLength(self) ?? Self._minLength(other) let newPattern = pattern ?? other.pattern return .init( maxLength: newMaxLength, @@ -647,7 +647,7 @@ extension JSONSchema.NumericContext { extension JSONSchema.StringContext { internal func validatedContext() throws -> JSONSchema.StringContext { - if let minimum = StringContext._minLength(self) { + if let minimum = Self._minLength(self) { guard minimum >= 0 else { throw JSONSchemaResolutionError(.inconsistency("String minimum length (\(minimum) cannot be less than 0")) } @@ -659,7 +659,7 @@ extension JSONSchema.StringContext { } return .init( maxLength: maxLength, - minLength: StringContext._minLength(self), + minLength: Self._minLength(self), pattern: pattern ) } diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index c39b4f9c4..20e3b0391 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -54,7 +54,7 @@ extension OpenAPI { } public enum SecurityType: Equatable { - case apiKey(name: String, location: SecuritySchemeLocation) + case apiKey(name: String, location: Location) case http(scheme: String, bearerFormat: String?) case oauth2(flows: OAuthFlows) case openIdConnect(openIdConnectUrl: URL) diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index cb6ed0247..00447c26e 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -15,45 +15,45 @@ import OpenAPIKitCore public extension OpenAPI { - typealias HttpMethod = OpenAPIKitCore.HttpMethod - typealias ContentType = OpenAPIKitCore.ContentType + typealias HttpMethod = OpenAPIKitCore.Shared.HttpMethod + typealias ContentType = OpenAPIKitCore.Shared.ContentType typealias Error = OpenAPIKitCore.Error typealias Warning = OpenAPIKitCore.Warning - typealias Path = OpenAPIKitCore.Path - typealias ComponentKey = OpenAPIKitCore.ComponentKey - typealias Discriminator = OpenAPIKitCore.Discriminator - typealias OAuthFlows = OpenAPIKitCore.OAuthFlows - typealias CallbackURL = OpenAPIKitCore.CallbackURL + typealias Path = OpenAPIKitCore.Shared.Path + typealias ComponentKey = OpenAPIKitCore.Shared.ComponentKey + typealias Discriminator = OpenAPIKitCore.Shared.Discriminator + typealias OAuthFlows = OpenAPIKitCore.Shared.OAuthFlows + typealias CallbackURL = OpenAPIKitCore.Shared.CallbackURL } public extension OpenAPI.SecurityScheme { - typealias Location = OpenAPIKitCore.SecuritySchemeLocation + typealias Location = OpenAPIKitCore.Shared.SecuritySchemeLocation } public extension OpenAPI.Parameter.Context { - typealias Location = OpenAPIKitCore.ParameterContextLocation + typealias Location = OpenAPIKitCore.Shared.ParameterContextLocation } public extension OpenAPI.Parameter.SchemaContext { - typealias Style = ParameterSchemaContextStyle + typealias Style = OpenAPIKitCore.Shared.ParameterSchemaContextStyle } public extension OpenAPI.Response { - typealias StatusCode = OpenAPIKitCore.ResponseStatusCode + typealias StatusCode = OpenAPIKitCore.Shared.ResponseStatusCode } public extension JSONSchema { - typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions - typealias StringContext = OpenAPIKitCore.StringContext - typealias ReferenceContext = OpenAPIKitCore.ReferenceContext + typealias Permissions = OpenAPIKitCore.Shared.JSONSchemaPermissions + typealias StringContext = OpenAPIKitCore.Shared.StringContext + typealias ReferenceContext = OpenAPIKitCore.Shared.ReferenceContext } public extension JSONTypeFormat { - typealias AnyFormat = OpenAPIKitCore.AnyFormat - typealias BooleanFormat = OpenAPIKitCore.BooleanFormat - typealias ObjectFormat = OpenAPIKitCore.ObjectFormat - typealias ArrayFormat = OpenAPIKitCore.ArrayFormat - typealias NumberFormat = OpenAPIKitCore.NumberFormat - typealias IntegerFormat = OpenAPIKitCore.IntegerFormat - typealias StringFormat = OpenAPIKitCore.StringFormat + typealias AnyFormat = OpenAPIKitCore.Shared.AnyFormat + typealias BooleanFormat = OpenAPIKitCore.Shared.BooleanFormat + typealias ObjectFormat = OpenAPIKitCore.Shared.ObjectFormat + typealias ArrayFormat = OpenAPIKitCore.Shared.ArrayFormat + typealias NumberFormat = OpenAPIKitCore.Shared.NumberFormat + typealias IntegerFormat = OpenAPIKitCore.Shared.IntegerFormat + typealias StringFormat = OpenAPIKitCore.Shared.StringFormat } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift index 3a2c417bb..79adb97c1 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift @@ -447,7 +447,7 @@ extension JSONSchema.StringContext { if let conflict = conflicting(maxLength, other.maxLength) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "maxLength", original: String(conflict.0), new: String(conflict.1))) } - if let conflict = conflicting(StringContext._minLength(self), StringContext._minLength(other)) { + if let conflict = conflicting(Self._minLength(self), Self._minLength(other)) { throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "minLength", original: String(conflict.0), new: String(conflict.1))) } if let conflict = conflicting(pattern, other.pattern) { @@ -456,7 +456,7 @@ extension JSONSchema.StringContext { // explicitly declaring these constants one at a time // helps the type checker a lot. let newMaxLength = maxLength ?? other.maxLength - let newMinLength = StringContext._minLength(self) ?? StringContext._minLength(other) + let newMinLength = Self._minLength(self) ?? Self._minLength(other) let newPattern = pattern ?? other.pattern return .init( maxLength: newMaxLength, @@ -627,7 +627,7 @@ extension JSONSchema.NumericContext { extension JSONSchema.StringContext { internal func validatedContext() throws -> JSONSchema.StringContext { - if let minimum = StringContext._minLength(self) { + if let minimum = Self._minLength(self) { guard minimum >= 0 else { throw JSONSchemaResolutionError(.inconsistency("String minimum length (\(minimum) cannot be less than 0")) } @@ -639,7 +639,7 @@ extension JSONSchema.StringContext { } return .init( maxLength: maxLength, - minLength: StringContext._minLength(self), + minLength: Self._minLength(self), pattern: pattern ) } diff --git a/Sources/OpenAPIKit30/Security/SecurityScheme.swift b/Sources/OpenAPIKit30/Security/SecurityScheme.swift index ed40ec404..9d9cc7a62 100644 --- a/Sources/OpenAPIKit30/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit30/Security/SecurityScheme.swift @@ -50,7 +50,7 @@ extension OpenAPI { } public enum SecurityType: Equatable { - case apiKey(name: String, location: SecuritySchemeLocation) + case apiKey(name: String, location: Location) case http(scheme: String, bearerFormat: String?) case oauth2(flows: OAuthFlows) case openIdConnect(openIdConnectUrl: URL) diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index cb6ed0247..00447c26e 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -15,45 +15,45 @@ import OpenAPIKitCore public extension OpenAPI { - typealias HttpMethod = OpenAPIKitCore.HttpMethod - typealias ContentType = OpenAPIKitCore.ContentType + typealias HttpMethod = OpenAPIKitCore.Shared.HttpMethod + typealias ContentType = OpenAPIKitCore.Shared.ContentType typealias Error = OpenAPIKitCore.Error typealias Warning = OpenAPIKitCore.Warning - typealias Path = OpenAPIKitCore.Path - typealias ComponentKey = OpenAPIKitCore.ComponentKey - typealias Discriminator = OpenAPIKitCore.Discriminator - typealias OAuthFlows = OpenAPIKitCore.OAuthFlows - typealias CallbackURL = OpenAPIKitCore.CallbackURL + typealias Path = OpenAPIKitCore.Shared.Path + typealias ComponentKey = OpenAPIKitCore.Shared.ComponentKey + typealias Discriminator = OpenAPIKitCore.Shared.Discriminator + typealias OAuthFlows = OpenAPIKitCore.Shared.OAuthFlows + typealias CallbackURL = OpenAPIKitCore.Shared.CallbackURL } public extension OpenAPI.SecurityScheme { - typealias Location = OpenAPIKitCore.SecuritySchemeLocation + typealias Location = OpenAPIKitCore.Shared.SecuritySchemeLocation } public extension OpenAPI.Parameter.Context { - typealias Location = OpenAPIKitCore.ParameterContextLocation + typealias Location = OpenAPIKitCore.Shared.ParameterContextLocation } public extension OpenAPI.Parameter.SchemaContext { - typealias Style = ParameterSchemaContextStyle + typealias Style = OpenAPIKitCore.Shared.ParameterSchemaContextStyle } public extension OpenAPI.Response { - typealias StatusCode = OpenAPIKitCore.ResponseStatusCode + typealias StatusCode = OpenAPIKitCore.Shared.ResponseStatusCode } public extension JSONSchema { - typealias Permissions = OpenAPIKitCore.JSONSchemaPermissions - typealias StringContext = OpenAPIKitCore.StringContext - typealias ReferenceContext = OpenAPIKitCore.ReferenceContext + typealias Permissions = OpenAPIKitCore.Shared.JSONSchemaPermissions + typealias StringContext = OpenAPIKitCore.Shared.StringContext + typealias ReferenceContext = OpenAPIKitCore.Shared.ReferenceContext } public extension JSONTypeFormat { - typealias AnyFormat = OpenAPIKitCore.AnyFormat - typealias BooleanFormat = OpenAPIKitCore.BooleanFormat - typealias ObjectFormat = OpenAPIKitCore.ObjectFormat - typealias ArrayFormat = OpenAPIKitCore.ArrayFormat - typealias NumberFormat = OpenAPIKitCore.NumberFormat - typealias IntegerFormat = OpenAPIKitCore.IntegerFormat - typealias StringFormat = OpenAPIKitCore.StringFormat + typealias AnyFormat = OpenAPIKitCore.Shared.AnyFormat + typealias BooleanFormat = OpenAPIKitCore.Shared.BooleanFormat + typealias ObjectFormat = OpenAPIKitCore.Shared.ObjectFormat + typealias ArrayFormat = OpenAPIKitCore.Shared.ArrayFormat + typealias NumberFormat = OpenAPIKitCore.Shared.NumberFormat + typealias IntegerFormat = OpenAPIKitCore.Shared.IntegerFormat + typealias StringFormat = OpenAPIKitCore.Shared.StringFormat } diff --git a/Sources/OpenAPIKitCore/CallbackURL.swift b/Sources/OpenAPIKitCore/CallbackURL.swift deleted file mode 100644 index f6fce4253..000000000 --- a/Sources/OpenAPIKitCore/CallbackURL.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// CallbackURL.swift -// -// -// Created by Mathew Polzin on 12/19/22. -// - -import Foundation - -/// 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) - } -} - -extension CallbackURL: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(rawValue) - } -} - -extension CallbackURL: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - template = try container.decode(URLTemplate.self) - } -} diff --git a/Sources/OpenAPIKitCore/ComponentKey.swift b/Sources/OpenAPIKitCore/ComponentKey.swift deleted file mode 100644 index 7774f41bf..000000000 --- a/Sources/OpenAPIKitCore/ComponentKey.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// ComponentKey.swift -// -// -// Created by Mathew Polzin on 12/17/22. -// - -import Foundation - -/// A key for one of the component dictionaries. -/// -/// These keys must match the regex -/// `^[a-zA-Z0-9\.\-_]+$`. -public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider { - public let rawValue: String - - public init(stringLiteral value: StringLiteralType) { - self.rawValue = value - } - - public init?(rawValue: String) { - guard !rawValue.isEmpty else { - return nil - } - var allowedCharacters = CharacterSet.alphanumerics - allowedCharacters.insert(charactersIn: "-_.") - guard CharacterSet(charactersIn: rawValue).isSubset(of: allowedCharacters) else { - return nil - } - self.rawValue = rawValue - } - - public static func problem(with proposedString: String) -> String? { - if Self(rawValue: proposedString) == nil { - return "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(proposedString)' does not.." - } - return nil - } - - public init(from decoder: Decoder) throws { - let rawValue = try decoder.singleValueContainer().decode(String.self) - guard let key = Self(rawValue: rawValue) else { - throw InconsistencyError( - subjectName: "Component Key", - details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", - codingPath: decoder.codingPath - ) - } - self = key - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - // we check for consistency on encode because a string literal - // may result in an invalid component key being constructed. - guard Self(rawValue: rawValue) != nil else { - throw InconsistencyError( - subjectName: "Component Key", - details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", - codingPath: container.codingPath - ) - } - - try container.encode(rawValue) - } - } diff --git a/Sources/OpenAPIKitCore/HttpMethod.swift b/Sources/OpenAPIKitCore/HttpMethod.swift deleted file mode 100644 index 803fa4632..000000000 --- a/Sources/OpenAPIKitCore/HttpMethod.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// HttpMethod.swift -// -// -// Created by Mathew Polzin on 12/29/19. -// - -/// Represents the HTTP methods supported by the -/// OpenAPI Specification. -/// -/// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object) because the supported -/// HTTP methods are enumerated as properties on that -/// object. -public enum HttpMethod: String, CaseIterable { - case get = "GET" - case post = "POST" - case patch = "PATCH" - case put = "PUT" - case delete = "DELETE" - case head = "HEAD" - case options = "OPTIONS" - case trace = "TRACE" -} diff --git a/Sources/OpenAPIKitCore/JSONSchemaPermissions.swift b/Sources/OpenAPIKitCore/JSONSchemaPermissions.swift deleted file mode 100644 index 6df1c99b2..000000000 --- a/Sources/OpenAPIKitCore/JSONSchemaPermissions.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// JSONSchemaPermissions.swift -// -// -// Created by Mathew Polzin on 12/17/22. -// - -public enum JSONSchemaPermissions: String, Codable { - case readOnly - case writeOnly - case readWrite -} diff --git a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift b/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift deleted file mode 100644 index d4a9b58da..000000000 --- a/Sources/OpenAPIKitCore/JSONSchemaSimpleContexts.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// JSONSchemaSimpleContexts.swift -// -// -// Created by Mathew Polzin on 12/19/22. -// - -/// The context that only applies to `.string` schemas. -public struct StringContext: Equatable { - public let maxLength: Int? - let _minLength: Int? - - public var minLength: Int { - return _minLength ?? 0 - } - - /// Regular expression - public let pattern: String? - - public init( - maxLength: Int? = nil, - minLength: Int? = nil, - pattern: String? = nil - ) { - self.maxLength = maxLength - self._minLength = minLength - self.pattern = pattern - } - - // we make the following a static function so it doesn't muddy the namespace while auto-completing on a value. - public static func _minLength(_ context: StringContext) -> Int? { - return context._minLength - } -} - - -/// The context that only applies to `.reference` schemas. -public struct ReferenceContext: Equatable { - public let required: Bool - - public init(required: Bool = true) { - self.required = required - } - - public func requiredContext() -> ReferenceContext { - return .init(required: true) - } - - public func optionalContext() -> ReferenceContext { - return .init(required: false) - } -} - -extension StringContext { - public enum CodingKeys: String, CodingKey { - case maxLength - case minLength - case pattern - } -} - -extension StringContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(maxLength, forKey: .maxLength) - try container.encodeIfPresent(_minLength, forKey: .minLength) - try container.encodeIfPresent(pattern, forKey: .pattern) - } -} - -extension StringContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) - _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) - pattern = try container.decodeIfPresent(String.self, forKey: .pattern) - } -} diff --git a/Sources/OpenAPIKitCore/JSONTypeFormat.swift b/Sources/OpenAPIKitCore/JSONTypeFormat.swift deleted file mode 100644 index 65def782e..000000000 --- a/Sources/OpenAPIKitCore/JSONTypeFormat.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// File.swift -// -// -// Created by Mathew Polzin on 12/17/22. -// - - -/// A format used when no type is known or any type is allowed. -/// -/// There are no built-in formats that do not have an associated -/// type, but it is still important to be able to specify a format without -/// a type. This can come into play when writing fragments of schemas -/// to be combined later. -public enum AnyFormat: RawRepresentable, Equatable { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = AnyCodable - - public static var unspecified: AnyFormat { - return .generic - } -} - -/// The allowed "format" properties for `.boolean` schemas. -public enum BooleanFormat: RawRepresentable, Equatable { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Bool - - public static var unspecified: BooleanFormat { - return .generic - } -} - -/// The allowed "format" properties for `.object` schemas. -public enum ObjectFormat: RawRepresentable, Equatable { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = AnyCodable - - public static var unspecified: ObjectFormat { - return .generic - } -} - -/// The allowed "format" properties for `.array` schemas. -public enum ArrayFormat: RawRepresentable, Equatable { - case generic - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - default: self = .other(rawValue) - } - } - - public typealias SwiftType = [AnyCodable] - - public static var unspecified: ArrayFormat { - return .generic - } -} - -/// The allowed "format" properties for `.number` schemas. -public enum NumberFormat: RawRepresentable, Equatable { - case generic - case float - case double - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .float: return "float" - case .double: return "double" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "float": self = .float - case "double": self = .double - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Double - - public static var unspecified: NumberFormat { - return .generic - } -} - -/// The allowed "format" properties for `.integer` schemas. -public enum IntegerFormat: RawRepresentable, Equatable { - case generic - case int32 - case int64 - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .int32: return "int32" - case .int64: return "int64" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "int32": self = .int32 - case "int64": self = .int64 - default: self = .other(rawValue) - } - } - - public typealias SwiftType = Int - - public static var unspecified: IntegerFormat { - return .generic - } -} - -/// The allowed "format" properties for `.string` schemas. -public enum StringFormat: RawRepresentable, Equatable { - case generic - case byte - case binary - case date - /// A string instance is valid against this attribute if it is a valid - /// date representation as defined by - /// https://tools.ietf.org/html/rfc3339#section-5.6 - case dateTime - case password - case other(String) - - public var rawValue: String { - switch self { - case .generic: return "" - case .byte: return "byte" - case .binary: return "binary" - case .date: return "date" - case .dateTime: return "date-time" - case .password: return "password" - case .other(let other): - return other - } - } - - public init(rawValue: String) { - switch rawValue { - case "": self = .generic - case "byte": self = .byte - case "binary": self = .binary - case "date": self = .date - case "date-time": self = .dateTime - case "password": self = .password - default: self = .other(rawValue) - } - } - - public typealias SwiftType = String - - public static var unspecified: StringFormat { - return .generic - } -} - -extension StringFormat { - /// Popular non-standard "format" properties for `.string` schemas. - /// - /// Specify with e.g. `.string(format: .extended(.uuid))` - public enum Extended: String, Equatable { - case uuid = "uuid" - case email = "email" - case hostname = "hostname" - case ipv4 = "ipv4" - case ipv6 = "ipv6" - /// A string instance is valid against this attribute if it is a valid - /// URI, according to - /// https://tools.ietf.org/html/rfc3986 - case uri = "uri" - /// A string instance is valid against this attribute if it is a valid - /// URI, according to - /// https://tools.ietf.org/html/rfc3986 - case uriReference = "uriref" - } - - public static func extended(_ format: Extended) -> Self { - return .other(format.rawValue) - } -} - -extension IntegerFormat { - /// Popular non-standard "format" properties for `.integer` schemas. - /// - /// Specify with e.g. `.integer(format: .extended(.uint32))` - public enum Extended: String, Equatable { - case uint32 = "uint32" - case uint64 = "uint64" - } - - public static func extended(_ format: Extended) -> Self { - return .other(format.rawValue) - } -} diff --git a/Sources/OpenAPIKitCore/ParameterContextLocation.swift b/Sources/OpenAPIKitCore/ParameterContextLocation.swift deleted file mode 100644 index 4c6e2e38f..000000000 --- a/Sources/OpenAPIKitCore/ParameterContextLocation.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ParameterContextLocation.swift -// -// -// Created by Mathew Polzin on 12/24/22. -// - -public enum ParameterContextLocation: String, CaseIterable, Codable { - case query - case header - case path - case cookie -} - -extension ParameterContextLocation: Validatable {} diff --git a/Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift b/Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift deleted file mode 100644 index 2e1758c7f..000000000 --- a/Sources/OpenAPIKitCore/ParameterSchemaContextStyle.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ParameterSchemaContextStyle.swift -// -// -// Created by Mathew Polzin on 12/18/22. -// - -public enum ParameterSchemaContextStyle: String, CaseIterable, Codable { - case form - case simple - case matrix - case label - case spaceDelimited - case pipeDelimited - case deepObject -} diff --git a/Sources/OpenAPIKitCore/Path.swift b/Sources/OpenAPIKitCore/Path.swift deleted file mode 100644 index d729ecb1f..000000000 --- a/Sources/OpenAPIKitCore/Path.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Path.swift -// -// -// Created by Mathew Polzin on 12/17/22. -// - -/// OpenAPI Spec "Paths Object" path field pattern support. -/// -/// See [OpenAPI Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#paths-object) -/// and [OpenAPI Patterned Fields](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#patterned-fields). -public struct Path: RawRepresentable, Equatable, Hashable { - public let components: [String] - - public init(_ components: [String]) { - self.components = components - } - - public init(rawValue: String) { - let pathComponents = rawValue.split(separator: "/").map(String.init) - components = pathComponents.count > 0 && pathComponents[0].isEmpty - ? Array(pathComponents.dropFirst()) - : pathComponents - } - - public var rawValue: String { - return "/\(components.joined(separator: "/"))" - } -} - -extension Path: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self.init(rawValue: value) - } -} diff --git a/Sources/OpenAPIKitCore/ResponseStatusCode.swift b/Sources/OpenAPIKitCore/ResponseStatusCode.swift deleted file mode 100644 index 1695246db..000000000 --- a/Sources/OpenAPIKitCore/ResponseStatusCode.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// ResponseStatusCode.swift -// -// -// Created by Mathew Polzin on 12/19/22. -// - -/// An HTTP Status code or status code range. -/// -/// OpenAPI supports one of the following as a key in the [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object): -/// - A `default` entry. -/// - A specific status code. -/// - A status code range. -/// -/// The `.default` case is used for a default entry. -/// -/// You can use integer literals to specify an exact status code. -/// -/// Status code ranges are named in the `StatusCode.Range` enum. For example, the "1XX" range (100-199) can be written as either `.range(.information)` or as `.range(.init(rawValue: "1XX"))`. -public struct ResponseStatusCode: RawRepresentable, Equatable, Hashable, HasWarnings { - public typealias RawValue = String - - public let warnings: [Warning] - - public var value: Code - - internal init(value: Code) { - self.value = value - warnings = [] - } - - public static let `default`: Self = .init(value: .default) - public static func range(_ range: Range) -> Self { .init(value: .range(range)) } - public static func status(code: Int) -> Self { .init(value: .status(code: code)) } - - public enum Code: Equatable, Hashable { - case `default` - case range(Range) - case status(code: Int) - } - - public enum Range: String { - /// Status Code `100-199` - case information = "1XX" - /// Status Code `200-299` - case success = "2XX" - /// Status Code `300-399` - case redirect = "3XX" - /// Status Code `400-499` - case clientError = "4XX" - /// Status Code `500-599` - case serverError = "5XX" - } - - public var rawValue: String { - switch value { - case .default: - return "default" - - case .range(let range): - return range.rawValue - - case .status(code: let code): - return String(code) - } - } - - public var isSuccess: Bool { - switch value { - case .range(.success), .status(code: 200..<300): - return true - case .range, .status, .default: - return false - } - } - - public init?(rawValue: String) { - if let val = Int(rawValue) { - value = .status(code: val) - warnings = [] - } else if rawValue == "default" { - value = .default - warnings = [] - } else if let range = Range(rawValue: rawValue.uppercased()) { - value = .range(range) - warnings = [] - } else if rawValue.contains("/"), - let first = (rawValue.split(separator: "/")).first, - let fallback = Self(rawValue: String(first)) { - value = fallback.value - warnings = [ - .message("Found non-compliant Status Code '\(rawValue)' but was able to parse as \(first)") - ] - } else { - return nil - } - } -} - -extension ResponseStatusCode: ExpressibleByIntegerLiteral { - - public init(integerLiteral value: Int) { - self.value = .status(code: value) - warnings = [] - } -} diff --git a/Sources/OpenAPIKitCore/SecurityScheme.swift b/Sources/OpenAPIKitCore/SecurityScheme.swift deleted file mode 100644 index 841aea8e7..000000000 --- a/Sources/OpenAPIKitCore/SecurityScheme.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SecurityScheme.swift -// -// -// Created by Mathew Polzin on 12/18/22. -// - -public enum SecuritySchemeLocation: String, Codable, Equatable { - case query - case header - case cookie -} - -extension SecuritySchemeLocation: Validatable {} diff --git a/Sources/OpenAPIKitCore/Shared.swift b/Sources/OpenAPIKitCore/Shared.swift new file mode 100644 index 000000000..5976827a3 --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared.swift @@ -0,0 +1,11 @@ +// +// Shared.swift +// +// +// Created by Mathew Polzin on 12/24/22. +// + +/// A Core namespace for OpenAPI types that are shared by multiple OpenAPI standard versions. +public enum Shared { + +} diff --git a/Sources/OpenAPIKitCore/Shared/CallbackURL.swift b/Sources/OpenAPIKitCore/Shared/CallbackURL.swift new file mode 100644 index 000000000..5e2d6295d --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/CallbackURL.swift @@ -0,0 +1,66 @@ +// +// CallbackURL.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +import Foundation + +extension Shared { + /// 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) + } + } +} + +extension Shared.CallbackURL: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(rawValue) + } +} + +extension Shared.CallbackURL: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + template = try container.decode(URLTemplate.self) + } +} diff --git a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift new file mode 100644 index 000000000..f8ff7f48e --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift @@ -0,0 +1,69 @@ +// +// ComponentKey.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + +import Foundation + +extension Shared { + /// A key for one of the component dictionaries. + /// + /// These keys must match the regex + /// `^[a-zA-Z0-9\.\-_]+$`. + public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider { + public let rawValue: String + + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } + + public init?(rawValue: String) { + guard !rawValue.isEmpty else { + return nil + } + var allowedCharacters = CharacterSet.alphanumerics + allowedCharacters.insert(charactersIn: "-_.") + guard CharacterSet(charactersIn: rawValue).isSubset(of: allowedCharacters) else { + return nil + } + self.rawValue = rawValue + } + + public static func problem(with proposedString: String) -> String? { + if Self(rawValue: proposedString) == nil { + return "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(proposedString)' does not.." + } + return nil + } + + public init(from decoder: Decoder) throws { + let rawValue = try decoder.singleValueContainer().decode(String.self) + guard let key = Self(rawValue: rawValue) else { + throw InconsistencyError( + subjectName: "Component Key", + details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", + codingPath: decoder.codingPath + ) + } + self = key + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + // we check for consistency on encode because a string literal + // may result in an invalid component key being constructed. + guard Self(rawValue: rawValue) != nil else { + throw InconsistencyError( + subjectName: "Component Key", + details: "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '\(rawValue)' does not..", + codingPath: container.codingPath + ) + } + + try container.encode(rawValue) + } + } +} diff --git a/Sources/OpenAPIKitCore/ContentType.swift b/Sources/OpenAPIKitCore/Shared/ContentType.swift similarity index 68% rename from Sources/OpenAPIKitCore/ContentType.swift rename to Sources/OpenAPIKitCore/Shared/ContentType.swift index ffa276e56..bf81f267a 100644 --- a/Sources/OpenAPIKitCore/ContentType.swift +++ b/Sources/OpenAPIKitCore/Shared/ContentType.swift @@ -5,95 +5,97 @@ // Created by Mathew Polzin on 12/29/19. // -/// The Content Type of an API request or response body. -public struct ContentType: Codable, Equatable, Hashable, RawRepresentable, HasWarnings { - internal let underlyingType: Builtin - public let warnings: [Warning] +extension Shared { + /// The Content Type of an API request or response body. + public struct ContentType: Codable, Equatable, Hashable, RawRepresentable, HasWarnings { + internal let underlyingType: Builtin + public let warnings: [Warning] - /// The type and subtype of the content type. This is everything except for - /// any parameters that are also attached. - /// - /// If you want the full content type string, use `rawValue`. - public var typeAndSubtype: String { underlyingType.rawValue } + /// The type and subtype of the content type. This is everything except for + /// any parameters that are also attached. + /// + /// If you want the full content type string, use `rawValue`. + public var typeAndSubtype: String { underlyingType.rawValue } - /// Key/Value pairs serialized as parameters for the content type. - /// - /// For exmaple, in "`text/plain; charset=UTF-8`" "charset" is - /// the name of a parameter with the value "UTF-8". - /// - /// OpenAPIKit will always encode these parameters with the keys in - /// alphabetical order. - public var parameters: [String: String] + /// Key/Value pairs serialized as parameters for the content type. + /// + /// For exmaple, in "`text/plain; charset=UTF-8`" "charset" is + /// the name of a parameter with the value "UTF-8". + /// + /// OpenAPIKit will always encode these parameters with the keys in + /// alphabetical order. + public var parameters: [String: String] - /// The full raw string value of the content type and any of its parameters. - /// - /// If you only want the type & subtype without any parameters, use - /// `typeAndSubtype`. - public var rawValue: String { - let rawParams = parameters - .sorted(by: { $0.0 < $1.0 }) - .map { (name, value) in "; \(name)=\(value)" } - .joined() + /// The full raw string value of the content type and any of its parameters. + /// + /// If you only want the type & subtype without any parameters, use + /// `typeAndSubtype`. + public var rawValue: String { + let rawParams = parameters + .sorted(by: { $0.0 < $1.0 }) + .map { (name, value) in "; \(name)=\(value)" } + .joined() - return typeAndSubtype + rawParams - } + return typeAndSubtype + rawParams + } - public init?(rawValue: String) { - let parts = rawValue.split(separator: ";") - let type = parts.first.map(String.init) ?? rawValue + public init?(rawValue: String) { + let parts = rawValue.split(separator: ";") + let type = parts.first.map(String.init) ?? rawValue - var warnings = [Warning]() - var params = [String:String]() - for part in parts.dropFirst() { - switch Self.parseParameter(part) { - case .success(let (name, value)): - params[name] = value - case .failure(let warning): - warnings.append(warning) + var warnings = [Warning]() + var params = [String:String]() + for part in parts.dropFirst() { + switch Self.parseParameter(part) { + case .success(let (name, value)): + params[name] = value + case .failure(let warning): + warnings.append(warning) + } } - } - parameters = params + parameters = params - if let underlying = Builtin.init(rawValue: type) { - underlyingType = underlying - } else { - underlyingType = .other(type) - warnings.append( - .message( - "'\(rawValue)' could not be parsed as a Content Type. Content Types should have the format '/'" + if let underlying = Builtin.init(rawValue: type) { + underlyingType = underlying + } else { + underlyingType = .other(type) + warnings.append( + .message( + "'\(rawValue)' could not be parsed as a Content Type. Content Types should have the format '/'" + ) ) - ) + } + self.warnings = warnings } - self.warnings = warnings - } - private static func parseParameter(_ param: String.SubSequence) -> Result<(String, String), Warning> { - let parts = param.split(separator: "=") + private static func parseParameter(_ param: String.SubSequence) -> Result<(String, String), Warning> { + let parts = param.split(separator: "=") - guard parts.count == 2, - let name = parts.first, - let value = parts.last else { - return .failure(.message("Could not parse a Content Type parameter from '\(param)'")) - } + guard parts.count == 2, + let name = parts.first, + let value = parts.last else { + return .failure(.message("Could not parse a Content Type parameter from '\(param)'")) + } - return .success( - ( - String(name.trimmingCharacters(in: .whitespaces)), - String(value.trimmingCharacters(in: .whitespaces)) + return .success( + ( + String(name.trimmingCharacters(in: .whitespaces)), + String(value.trimmingCharacters(in: .whitespaces)) + ) ) - ) - } + } - internal init(_ builtin: Builtin) { - underlyingType = builtin - parameters = [:] - warnings = [] + internal init(_ builtin: Builtin) { + underlyingType = builtin + parameters = [:] + warnings = [] + } } } // convenience constructors -public extension ContentType { +public extension Shared.ContentType { /// Bitmap image static let bmp: Self = .init(.bmp) static let css: Self = .init(.css) @@ -155,7 +157,7 @@ public extension ContentType { static let any: Self = .init(.any) } -extension ContentType { +extension Shared.ContentType { // This internal representation makes it easier to ensure that the popular // builtin types supported are fully covered in their rawValue implementation. internal enum Builtin: Codable, Equatable, Hashable { @@ -221,7 +223,7 @@ extension ContentType { } } -extension ContentType.Builtin: RawRepresentable { +extension Shared.ContentType.Builtin: RawRepresentable { public var rawValue: String { switch self { case .bmp: return "image/bmp" @@ -314,4 +316,4 @@ extension ContentType.Builtin: RawRepresentable { } } -extension ContentType: Validatable {} +extension Shared.ContentType: Validatable {} diff --git a/Sources/OpenAPIKitCore/Discriminator.swift b/Sources/OpenAPIKitCore/Shared/Discriminator.swift similarity index 52% rename from Sources/OpenAPIKitCore/Discriminator.swift rename to Sources/OpenAPIKitCore/Shared/Discriminator.swift index b06e98f9d..fe66f71ab 100644 --- a/Sources/OpenAPIKitCore/Discriminator.swift +++ b/Sources/OpenAPIKitCore/Shared/Discriminator.swift @@ -5,23 +5,25 @@ // Created by Mathew Polzin on 10/6/19. // -/// OpenAPI Spec "Disciminator Object" -/// -/// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). -public struct Discriminator: Equatable { - public let propertyName: String - public let mapping: [String: String]? - - public init(propertyName: String, - mapping: [String: String]? = nil) { - self.propertyName = propertyName - self.mapping = mapping +extension Shared { + /// OpenAPI Spec "Disciminator Object" + /// + /// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). + public struct Discriminator: Equatable { + public let propertyName: String + public let mapping: [String: String]? + + public init(propertyName: String, + mapping: [String: String]? = nil) { + self.propertyName = propertyName + self.mapping = mapping + } } } // MARK: - Codable -extension Discriminator: Encodable { +extension Shared.Discriminator: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -30,7 +32,7 @@ extension Discriminator: Encodable { } } -extension Discriminator: Decodable { +extension Shared.Discriminator: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -39,11 +41,11 @@ extension Discriminator: Decodable { } } -extension Discriminator { +extension Shared.Discriminator { private enum CodingKeys: String, CodingKey { case propertyName case mapping } } -extension Discriminator: Validatable {} +extension Shared.Discriminator: Validatable {} diff --git a/Sources/OpenAPIKitCore/Shared/HttpMethod.swift b/Sources/OpenAPIKitCore/Shared/HttpMethod.swift new file mode 100644 index 000000000..316a15ddd --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/HttpMethod.swift @@ -0,0 +1,25 @@ +// +// HttpMethod.swift +// +// +// Created by Mathew Polzin on 12/29/19. +// + +extension Shared { + /// Represents the HTTP methods supported by the + /// OpenAPI Specification. + /// + /// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object) because the supported + /// HTTP methods are enumerated as properties on that + /// object. + public enum HttpMethod: String, CaseIterable { + case get = "GET" + case post = "POST" + case patch = "PATCH" + case put = "PUT" + case delete = "DELETE" + case head = "HEAD" + case options = "OPTIONS" + case trace = "TRACE" + } +} diff --git a/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift b/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift new file mode 100644 index 000000000..c6363d646 --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift @@ -0,0 +1,14 @@ +// +// JSONSchemaPermissions.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + +extension Shared { + public enum JSONSchemaPermissions: String, Codable { + case readOnly + case writeOnly + case readWrite + } +} diff --git a/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift b/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift new file mode 100644 index 000000000..0e65dd39f --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift @@ -0,0 +1,82 @@ +// +// JSONSchemaSimpleContexts.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +extension Shared { + /// The context that only applies to `.string` schemas. + public struct StringContext: Equatable { + public let maxLength: Int? + let _minLength: Int? + + public var minLength: Int { + return _minLength ?? 0 + } + + /// Regular expression + public let pattern: String? + + public init( + maxLength: Int? = nil, + minLength: Int? = nil, + pattern: String? = nil + ) { + self.maxLength = maxLength + self._minLength = minLength + self.pattern = pattern + } + + // we make the following a static function so it doesn't muddy the namespace while auto-completing on a value. + public static func _minLength(_ context: StringContext) -> Int? { + return context._minLength + } + } + + + /// The context that only applies to `.reference` schemas. + public struct ReferenceContext: Equatable { + public let required: Bool + + public init(required: Bool = true) { + self.required = required + } + + public func requiredContext() -> ReferenceContext { + return .init(required: true) + } + + public func optionalContext() -> ReferenceContext { + return .init(required: false) + } + } +} + +extension Shared.StringContext { + public enum CodingKeys: String, CodingKey { + case maxLength + case minLength + case pattern + } +} + +extension Shared.StringContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(maxLength, forKey: .maxLength) + try container.encodeIfPresent(_minLength, forKey: .minLength) + try container.encodeIfPresent(pattern, forKey: .pattern) + } +} + +extension Shared.StringContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) + _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) + pattern = try container.decodeIfPresent(String.self, forKey: .pattern) + } +} diff --git a/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift b/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift new file mode 100644 index 000000000..15bca0aa8 --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift @@ -0,0 +1,272 @@ +// +// JSONTypeFormat.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + + +extension Shared { + /// A format used when no type is known or any type is allowed. + /// + /// There are no built-in formats that do not have an associated + /// type, but it is still important to be able to specify a format without + /// a type. This can come into play when writing fragments of schemas + /// to be combined later. + public enum AnyFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = AnyCodable + + public static var unspecified: AnyFormat { + return .generic + } + } + + /// The allowed "format" properties for `.boolean` schemas. + public enum BooleanFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = Bool + + public static var unspecified: BooleanFormat { + return .generic + } + } + + /// The allowed "format" properties for `.object` schemas. + public enum ObjectFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = AnyCodable + + public static var unspecified: ObjectFormat { + return .generic + } + } + + /// The allowed "format" properties for `.array` schemas. + public enum ArrayFormat: RawRepresentable, Equatable { + case generic + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + default: self = .other(rawValue) + } + } + + public typealias SwiftType = [AnyCodable] + + public static var unspecified: ArrayFormat { + return .generic + } + } + + /// The allowed "format" properties for `.number` schemas. + public enum NumberFormat: RawRepresentable, Equatable { + case generic + case float + case double + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .float: return "float" + case .double: return "double" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + case "float": self = .float + case "double": self = .double + default: self = .other(rawValue) + } + } + + public typealias SwiftType = Double + + public static var unspecified: NumberFormat { + return .generic + } + } + + /// The allowed "format" properties for `.integer` schemas. + public enum IntegerFormat: RawRepresentable, Equatable { + case generic + case int32 + case int64 + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .int32: return "int32" + case .int64: return "int64" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + case "int32": self = .int32 + case "int64": self = .int64 + default: self = .other(rawValue) + } + } + + public typealias SwiftType = Int + + public static var unspecified: IntegerFormat { + return .generic + } + } + + /// The allowed "format" properties for `.string` schemas. + public enum StringFormat: RawRepresentable, Equatable { + case generic + case byte + case binary + case date + /// A string instance is valid against this attribute if it is a valid + /// date representation as defined by + /// https://tools.ietf.org/html/rfc3339#section-5.6 + case dateTime + case password + case other(String) + + public var rawValue: String { + switch self { + case .generic: return "" + case .byte: return "byte" + case .binary: return "binary" + case .date: return "date" + case .dateTime: return "date-time" + case .password: return "password" + case .other(let other): + return other + } + } + + public init(rawValue: String) { + switch rawValue { + case "": self = .generic + case "byte": self = .byte + case "binary": self = .binary + case "date": self = .date + case "date-time": self = .dateTime + case "password": self = .password + default: self = .other(rawValue) + } + } + + public typealias SwiftType = String + + public static var unspecified: StringFormat { + return .generic + } + } +} + +extension Shared.StringFormat { + /// Popular non-standard "format" properties for `.string` schemas. + /// + /// Specify with e.g. `.string(format: .extended(.uuid))` + public enum Extended: String, Equatable { + case uuid = "uuid" + case email = "email" + case hostname = "hostname" + case ipv4 = "ipv4" + case ipv6 = "ipv6" + /// A string instance is valid against this attribute if it is a valid + /// URI, according to + /// https://tools.ietf.org/html/rfc3986 + case uri = "uri" + /// A string instance is valid against this attribute if it is a valid + /// URI, according to + /// https://tools.ietf.org/html/rfc3986 + case uriReference = "uriref" + } + + public static func extended(_ format: Extended) -> Self { + return .other(format.rawValue) + } +} + +extension Shared.IntegerFormat { + /// Popular non-standard "format" properties for `.integer` schemas. + /// + /// Specify with e.g. `.integer(format: .extended(.uint32))` + public enum Extended: String, Equatable { + case uint32 = "uint32" + case uint64 = "uint64" + } + + public static func extended(_ format: Extended) -> Self { + return .other(format.rawValue) + } +} diff --git a/Sources/OpenAPIKitCore/OAuthFlows.swift b/Sources/OpenAPIKitCore/Shared/OAuthFlows.swift similarity index 68% rename from Sources/OpenAPIKitCore/OAuthFlows.swift rename to Sources/OpenAPIKitCore/Shared/OAuthFlows.swift index ea3930674..1a8253abe 100644 --- a/Sources/OpenAPIKitCore/OAuthFlows.swift +++ b/Sources/OpenAPIKitCore/Shared/OAuthFlows.swift @@ -7,29 +7,31 @@ import Foundation -/// OpenAPI Spec "Oauth Flows Object" -/// -/// See [OpenAPI Oauth Flows Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object). -public struct OAuthFlows: Equatable { - public let implicit: Implicit? - public let password: Password? - public let clientCredentials: ClientCredentials? - public let authorizationCode: AuthorizationCode? - - public init( - implicit: Implicit? = nil, - password: Password? = nil, - clientCredentials: ClientCredentials? = nil, - authorizationCode: AuthorizationCode? = nil - ) { - self.implicit = implicit - self.password = password - self.clientCredentials = clientCredentials - self.authorizationCode = authorizationCode +extension Shared { + /// OpenAPI Spec "Oauth Flows Object" + /// + /// See [OpenAPI Oauth Flows Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object). + public struct OAuthFlows: Equatable { + public let implicit: Implicit? + public let password: Password? + public let clientCredentials: ClientCredentials? + public let authorizationCode: AuthorizationCode? + + public init( + implicit: Implicit? = nil, + password: Password? = nil, + clientCredentials: ClientCredentials? = nil, + authorizationCode: AuthorizationCode? = nil + ) { + self.implicit = implicit + self.password = password + self.clientCredentials = clientCredentials + self.authorizationCode = authorizationCode + } } } -extension OAuthFlows { +extension Shared.OAuthFlows { public typealias Scope = String public typealias ScopeDescription = String @@ -102,7 +104,7 @@ extension OAuthFlows { } // MARK: - Codable -extension OAuthFlows { +extension Shared.OAuthFlows { private enum CodingKeys: String, CodingKey { case implicit case password @@ -111,39 +113,39 @@ extension OAuthFlows { } } -extension OAuthFlows.CommonFields { +extension Shared.OAuthFlows.CommonFields { private enum CodingKeys: String, CodingKey { case refreshUrl case scopes } } -extension OAuthFlows.Implicit { +extension Shared.OAuthFlows.Implicit { private enum CodingKeys: String, CodingKey { case authorizationUrl } } -extension OAuthFlows.Password { +extension Shared.OAuthFlows.Password { private enum CodingKeys: String, CodingKey { case tokenUrl } } -extension OAuthFlows.ClientCredentials { +extension Shared.OAuthFlows.ClientCredentials { private enum CodingKeys: String, CodingKey { case tokenUrl } } -extension OAuthFlows.AuthorizationCode { +extension Shared.OAuthFlows.AuthorizationCode { private enum CodingKeys: String, CodingKey { case authorizationUrl case tokenUrl } } -extension OAuthFlows: Encodable { +extension Shared.OAuthFlows: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -154,18 +156,18 @@ extension OAuthFlows: Encodable { } } -extension OAuthFlows: Decodable { +extension Shared.OAuthFlows: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - implicit = try container.decodeIfPresent(OAuthFlows.Implicit.self, forKey: .implicit) - password = try container.decodeIfPresent(OAuthFlows.Password.self, forKey: .password) - clientCredentials = try container.decodeIfPresent(OAuthFlows.ClientCredentials.self, forKey: .clientCredentials) - authorizationCode = try container.decodeIfPresent(OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode) + implicit = try container.decodeIfPresent(Shared.OAuthFlows.Implicit.self, forKey: .implicit) + password = try container.decodeIfPresent(Shared.OAuthFlows.Password.self, forKey: .password) + clientCredentials = try container.decodeIfPresent(Shared.OAuthFlows.ClientCredentials.self, forKey: .clientCredentials) + authorizationCode = try container.decodeIfPresent(Shared.OAuthFlows.AuthorizationCode.self, forKey: .authorizationCode) } } -extension OAuthFlows.CommonFields: Encodable { +extension Shared.OAuthFlows.CommonFields: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -174,16 +176,16 @@ extension OAuthFlows.CommonFields: Encodable { } } -extension OAuthFlows.CommonFields: Decodable { +extension Shared.OAuthFlows.CommonFields: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) refreshUrl = try container.decodeURLAsStringIfPresent(forKey: .refreshUrl) - scopes = try container.decode(OrderedDictionary.self, forKey: .scopes) + scopes = try container.decode(OrderedDictionary.self, forKey: .scopes) } } -extension OAuthFlows.Implicit: Encodable { +extension Shared.OAuthFlows.Implicit: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -193,9 +195,9 @@ extension OAuthFlows.Implicit: Encodable { } } -extension OAuthFlows.Implicit: Decodable { +extension Shared.OAuthFlows.Implicit: Decodable { public init(from decoder: Decoder) throws { - common = try OAuthFlows.CommonFields(from: decoder) + common = try Shared.OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -203,7 +205,7 @@ extension OAuthFlows.Implicit: Decodable { } } -extension OAuthFlows.Password: Encodable { +extension Shared.OAuthFlows.Password: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -213,9 +215,9 @@ extension OAuthFlows.Password: Encodable { } } -extension OAuthFlows.Password: Decodable { +extension Shared.OAuthFlows.Password: Decodable { public init(from decoder: Decoder) throws { - common = try OAuthFlows.CommonFields(from: decoder) + common = try Shared.OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -223,7 +225,7 @@ extension OAuthFlows.Password: Decodable { } } -extension OAuthFlows.ClientCredentials: Encodable { +extension Shared.OAuthFlows.ClientCredentials: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -233,9 +235,9 @@ extension OAuthFlows.ClientCredentials: Encodable { } } -extension OAuthFlows.ClientCredentials: Decodable { +extension Shared.OAuthFlows.ClientCredentials: Decodable { public init(from decoder: Decoder) throws { - common = try OAuthFlows.CommonFields(from: decoder) + common = try Shared.OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -243,7 +245,7 @@ extension OAuthFlows.ClientCredentials: Decodable { } } -extension OAuthFlows.AuthorizationCode: Encodable { +extension Shared.OAuthFlows.AuthorizationCode: Encodable { public func encode(to encoder: Encoder) throws { try common.encode(to: encoder) @@ -254,9 +256,9 @@ extension OAuthFlows.AuthorizationCode: Encodable { } } -extension OAuthFlows.AuthorizationCode: Decodable { +extension Shared.OAuthFlows.AuthorizationCode: Decodable { public init(from decoder: Decoder) throws { - common = try OAuthFlows.CommonFields(from: decoder) + common = try Shared.OAuthFlows.CommonFields(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -265,8 +267,8 @@ extension OAuthFlows.AuthorizationCode: Decodable { } } -extension OAuthFlows: Validatable {} -extension OAuthFlows.Implicit: Validatable {} -extension OAuthFlows.Password: Validatable {} -extension OAuthFlows.ClientCredentials: Validatable {} -extension OAuthFlows.AuthorizationCode: Validatable {} +extension Shared.OAuthFlows: Validatable {} +extension Shared.OAuthFlows.Implicit: Validatable {} +extension Shared.OAuthFlows.Password: Validatable {} +extension Shared.OAuthFlows.ClientCredentials: Validatable {} +extension Shared.OAuthFlows.AuthorizationCode: Validatable {} diff --git a/Sources/OpenAPIKitCore/Shared/ParameterContextLocation.swift b/Sources/OpenAPIKitCore/Shared/ParameterContextLocation.swift new file mode 100644 index 000000000..1a36cc8e0 --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/ParameterContextLocation.swift @@ -0,0 +1,17 @@ +// +// ParameterContextLocation.swift +// +// +// Created by Mathew Polzin on 12/24/22. +// + +extension Shared { + public enum ParameterContextLocation: String, CaseIterable, Codable { + case query + case header + case path + case cookie + } +} + +extension Shared.ParameterContextLocation: Validatable {} diff --git a/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift b/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift new file mode 100644 index 000000000..e28e9c5be --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift @@ -0,0 +1,18 @@ +// +// ParameterSchemaContextStyle.swift +// +// +// Created by Mathew Polzin on 12/18/22. +// + +extension Shared { + public enum ParameterSchemaContextStyle: String, CaseIterable, Codable { + case form + case simple + case matrix + case label + case spaceDelimited + case pipeDelimited + case deepObject + } +} diff --git a/Sources/OpenAPIKitCore/Shared/Path.swift b/Sources/OpenAPIKitCore/Shared/Path.swift new file mode 100644 index 000000000..f951b481f --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/Path.swift @@ -0,0 +1,37 @@ +// +// Path.swift +// +// +// Created by Mathew Polzin on 12/17/22. +// + +extension Shared { + /// OpenAPI Spec "Paths Object" path field pattern support. + /// + /// See [OpenAPI Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#paths-object) + /// and [OpenAPI Patterned Fields](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#patterned-fields). + public struct Path: RawRepresentable, Equatable, Hashable { + public let components: [String] + + public init(_ components: [String]) { + self.components = components + } + + public init(rawValue: String) { + let pathComponents = rawValue.split(separator: "/").map(String.init) + components = pathComponents.count > 0 && pathComponents[0].isEmpty + ? Array(pathComponents.dropFirst()) + : pathComponents + } + + public var rawValue: String { + return "/\(components.joined(separator: "/"))" + } + } +} + +extension Shared.Path: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(rawValue: value) + } +} diff --git a/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift b/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift new file mode 100644 index 000000000..9c69aeb9e --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift @@ -0,0 +1,108 @@ +// +// ResponseStatusCode.swift +// +// +// Created by Mathew Polzin on 12/19/22. +// + +extension Shared { + /// An HTTP Status code or status code range. + /// + /// OpenAPI supports one of the following as a key in the [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object): + /// - A `default` entry. + /// - A specific status code. + /// - A status code range. + /// + /// The `.default` case is used for a default entry. + /// + /// You can use integer literals to specify an exact status code. + /// + /// Status code ranges are named in the `StatusCode.Range` enum. For example, the "1XX" range (100-199) can be written as either `.range(.information)` or as `.range(.init(rawValue: "1XX"))`. + public struct ResponseStatusCode: RawRepresentable, Equatable, Hashable, HasWarnings { + public typealias RawValue = String + + public let warnings: [Warning] + + public var value: Code + + internal init(value: Code) { + self.value = value + warnings = [] + } + + public static let `default`: Self = .init(value: .default) + public static func range(_ range: Range) -> Self { .init(value: .range(range)) } + public static func status(code: Int) -> Self { .init(value: .status(code: code)) } + + public enum Code: Equatable, Hashable { + case `default` + case range(Range) + case status(code: Int) + } + + public enum Range: String { + /// Status Code `100-199` + case information = "1XX" + /// Status Code `200-299` + case success = "2XX" + /// Status Code `300-399` + case redirect = "3XX" + /// Status Code `400-499` + case clientError = "4XX" + /// Status Code `500-599` + case serverError = "5XX" + } + + public var rawValue: String { + switch value { + case .default: + return "default" + + case .range(let range): + return range.rawValue + + case .status(code: let code): + return String(code) + } + } + + public var isSuccess: Bool { + switch value { + case .range(.success), .status(code: 200..<300): + return true + case .range, .status, .default: + return false + } + } + + public init?(rawValue: String) { + if let val = Int(rawValue) { + value = .status(code: val) + warnings = [] + } else if rawValue == "default" { + value = .default + warnings = [] + } else if let range = Range(rawValue: rawValue.uppercased()) { + value = .range(range) + warnings = [] + } else if rawValue.contains("/"), + let first = (rawValue.split(separator: "/")).first, + let fallback = Self(rawValue: String(first)) { + value = fallback.value + warnings = [ + .message("Found non-compliant Status Code '\(rawValue)' but was able to parse as \(first)") + ] + } else { + return nil + } + } + } +} + +extension Shared.ResponseStatusCode: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: Int) { + self.value = .status(code: value) + warnings = [] + } +} diff --git a/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift b/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift new file mode 100644 index 000000000..4c24567b5 --- /dev/null +++ b/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift @@ -0,0 +1,16 @@ +// +// SecurityScheme.swift +// +// +// Created by Mathew Polzin on 12/18/22. +// + +extension Shared { + public enum SecuritySchemeLocation: String, Codable, Equatable { + case query + case header + case cookie + } +} + +extension Shared.SecuritySchemeLocation: Validatable {} diff --git a/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift b/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift index f60bebe6c..12f5b900e 100644 --- a/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift +++ b/Tests/OpenAPIKitCoreTests/ContentTypeTests.swift @@ -10,7 +10,7 @@ import XCTest final class ContentTypeTests: XCTestCase { func test_badContentType() { - let bad = ContentType(rawValue: "not a content type") + let bad = Shared.ContentType(rawValue: "not a content type") XCTAssertEqual( bad?.warnings[0].localizedDescription, "\'not a content type\' could not be parsed as a Content Type. Content Types should have the format \'/\'" @@ -18,7 +18,7 @@ final class ContentTypeTests: XCTestCase { } func test_contentTypeStringReflexivity() { - let types: [ContentType] = [ + let types: [Shared.ContentType] = [ .bmp, .css, .csv, @@ -57,24 +57,24 @@ final class ContentTypeTests: XCTestCase { ] for type in types { - XCTAssertEqual(type, ContentType(rawValue: type.rawValue)) + XCTAssertEqual(type, Shared.ContentType(rawValue: type.rawValue)) } } func test_goodParam() { - let type = ContentType.init(rawValue: "text/html; charset=utf8") + let type = Shared.ContentType.init(rawValue: "text/html; charset=utf8") XCTAssertEqual(type?.warnings.count, 0) XCTAssertEqual(type?.rawValue, "text/html; charset=utf8") } func test_multipleParams() { - let type = ContentType.init(rawValue: "my/type; some=thing; another=else") + let type = Shared.ContentType.init(rawValue: "my/type; some=thing; another=else") XCTAssertEqual(type?.warnings.count, 0) XCTAssert(type?.rawValue == "my/type; another=else; some=thing") } func test_oneBadParam() { - let type = ContentType.init(rawValue: "my/type; some: thing; another=else") + let type = Shared.ContentType.init(rawValue: "my/type; some: thing; another=else") XCTAssertEqual(type?.warnings.count, 1) XCTAssertEqual(type?.warnings.first?.localizedDescription, "Could not parse a Content Type parameter from \' some: thing\'") XCTAssert(type?.rawValue == "my/type; another=else") From d96f819964a665438c15134465d334d4d3446034 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 25 Dec 2022 20:02:15 -0600 Subject: [PATCH 30/30] filling out more of the compat layer tests --- .../DocumentConversionTests.swift | 249 +++++++++++++++++- 1 file changed, 238 insertions(+), 11 deletions(-) diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 7aff305db..76f67ddf9 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -246,16 +246,8 @@ fileprivate func assertEqualOldToNew(_ newServer: OpenAPIKit.OpenAPI.Server, _ o } } -fileprivate func assertEqualOldToNew(_ newPathItem: OpenAPIKit.OpenAPI.PathItem, _ oldPathItem: OpenAPIKit30.OpenAPI.PathItem) throws { - XCTAssertEqual(newPathItem.summary, oldPathItem.summary) - XCTAssertEqual(newPathItem.description, oldPathItem.description) - if let newServers = newPathItem.servers { - let oldServers = try XCTUnwrap(oldPathItem.servers) - for (newServer, oldServer) in zip(newServers, oldServers) { - assertEqualOldToNew(newServer, oldServer) - } - } - for (newParameter, oldParameter) in zip(newPathItem.parameters, oldPathItem.parameters) { +fileprivate func assertEqualOldToNew(_ newParamArray: OpenAPIKit.OpenAPI.Parameter.Array, _ oldParamArray: OpenAPIKit30.OpenAPI.Parameter.Array) { + for (newParameter, oldParameter) in zip(newParamArray, oldParamArray) { switch (newParameter, oldParameter) { case (.a(let ref), .a(let ref2)): XCTAssertNil(ref.summary) @@ -272,6 +264,18 @@ fileprivate func assertEqualOldToNew(_ newPathItem: OpenAPIKit.OpenAPI.PathItem, XCTFail("Parameters are not equal because one is a reference and the other is not: \(newParameter) / \(oldParameter)") } } +} + +fileprivate func assertEqualOldToNew(_ newPathItem: OpenAPIKit.OpenAPI.PathItem, _ oldPathItem: OpenAPIKit30.OpenAPI.PathItem) throws { + XCTAssertEqual(newPathItem.summary, oldPathItem.summary) + XCTAssertEqual(newPathItem.description, oldPathItem.description) + if let newServers = newPathItem.servers { + let oldServers = try XCTUnwrap(oldPathItem.servers) + for (newServer, oldServer) in zip(newServers, oldServers) { + assertEqualOldToNew(newServer, oldServer) + } + } + assertEqualOldToNew(newPathItem.parameters, oldPathItem.parameters) try assertEqualOldToNew(newPathItem.get, oldPathItem.get) try assertEqualOldToNew(newPathItem.put, oldPathItem.put) try assertEqualOldToNew(newPathItem.post, oldPathItem.post) @@ -303,8 +307,231 @@ fileprivate func assertEqualOldToNew(_ newParamContext: OpenAPIKit.OpenAPI.Param fileprivate func assertEqualOldToNew(_ newOperation: OpenAPIKit.OpenAPI.Operation?, _ oldOperation: OpenAPIKit30.OpenAPI.Operation?) throws { if let newOp = newOperation { let oldOp = try XCTUnwrap(oldOperation) - // TODO + + XCTAssertEqual(newOp.tags, oldOp.tags) + XCTAssertEqual(newOp.summary, oldOp.summary) + XCTAssertEqual(newOp.description, oldOp.description) + try assertEqualOldToNew(newOp.externalDocs, oldOp.externalDocs) + XCTAssertEqual(newOp.operationId, oldOp.operationId) + assertEqualOldToNew(newOp.parameters, oldOp.parameters) + if let newRequest = newOp.requestBody { + let oldRequest = try XCTUnwrap(oldOp.requestBody) + switch (newRequest, oldRequest) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let req1), .b(let req2)): + try assertEqualOldToNew(req1, req2) + default: + XCTFail("One request was a reference and the other was not. \(newRequest) / \(oldRequest)") + } + } else { + XCTAssertNil(oldOp.requestBody) + } + assertEqualOldToNew(newOp.responses, oldOp.responses) + try assertEqualOldToNew(newOp.callbacks, oldOp.callbacks) + XCTAssertEqual(newOp.deprecated, oldOp.deprecated) + if let newSecurity = newOp.security { + let oldSecurity = try XCTUnwrap(oldOp.security) + + for (newSecurityReq, oldSecurityReq) in zip(newSecurity, oldSecurity) { + try assertEqualOldToNew(newSecurityReq, oldSecurityReq) + } + } else { + XCTAssertNil(oldOp.security) + } + if let newServers = newOp.servers { + let oldServers = try XCTUnwrap(oldOp.servers) + + for (newServer, oldServer) in zip(newServers, oldServers) { + assertEqualOldToNew(newServer, oldServer) + } + } else { + XCTAssertNil(oldOp.servers) + } + XCTAssertEqual(newOp.vendorExtensions, oldOp.vendorExtensions) } else { XCTAssertNil(oldOperation) } } + +fileprivate func assertEqualOldToNew(_ newSecurityReq: OpenAPIKit.OpenAPI.SecurityRequirement, _ oldSecurityReq: OpenAPIKit30.OpenAPI.SecurityRequirement) throws { + for (ref, strs) in newSecurityReq { + switch ref { + case .internal(let internalRef): + let maybeOldRefInternal = OpenAPIKit30.JSONReference.InternalReference(rawValue: internalRef.rawValue) + let oldRefInternal = try XCTUnwrap(maybeOldRefInternal) + let oldRef = OpenAPIKit30.JSONReference.internal(oldRefInternal) + let oldStrs = oldSecurityReq[oldRef] + XCTAssertEqual(strs, oldStrs) + case .external(let external): + let oldStrs = oldSecurityReq[.external(external)] + XCTAssertEqual(strs, oldStrs) + } + } +} + +fileprivate func assertEqualOldToNew(_ newRequest: OpenAPIKit.OpenAPI.Request, _ oldRequest: OpenAPIKit30.OpenAPI.Request) throws { + XCTAssertEqual(newRequest.description, oldRequest.description) + try assertEqualOldToNew(newRequest.content, oldRequest.content) + XCTAssertEqual(newRequest.required, oldRequest.required) + XCTAssertEqual(newRequest.vendorExtensions, oldRequest.vendorExtensions) +} + +fileprivate func assertEqualOldToNew(_ newContentMap: OpenAPIKit.OpenAPI.Content.Map, _ oldContentMap: OpenAPIKit30.OpenAPI.Content.Map) throws { + for ((newCt, newContent), (oldCt, oldContent)) in zip(newContentMap, oldContentMap) { + XCTAssertEqual(newCt, oldCt) + switch (newContent.schema, oldContent.schema) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let schema1), .b(let schema2)): + assertEqualOldToNew(schema1, schema2) + default: + XCTFail("Found one reference and one schema. \(String(describing: newContent.schema)) / \(String(describing: oldContent.schema))") + } + XCTAssertEqual(newContent.example, oldContent.example) + if let newContentExamplesRef = newContent.examples { + let oldContentExamplesRef = try XCTUnwrap(oldContent.examples) + for ((newKey, newExampleRef), (oldKey, oldExampleRef)) in zip(newContentExamplesRef, oldContentExamplesRef) { + XCTAssertEqual(newKey, oldKey) + switch (newExampleRef, oldExampleRef) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let example1), .b(let example2)): + assertEqualOldToNew(example1, example2) + default: + XCTFail("Found one reference and one example. \(newExampleRef) / \(oldExampleRef)") + } + } + } else { + XCTAssertNil(oldContent.examples) + } + if let newEncodingRef = newContent.encoding { + let oldEncodingRef = try XCTUnwrap(oldContent.encoding) + for ((newKey, newEncoding), (oldKey, oldEncoding)) in zip(newEncodingRef, oldEncodingRef) { + XCTAssertEqual(newKey, oldKey) + try assertEqualOldToNew(newEncoding, oldEncoding) + } + } else { + XCTAssertNil(oldContent.encoding) + } + XCTAssertEqual(newContent.vendorExtensions, oldContent.vendorExtensions) + } +} + +fileprivate func assertEqualOldToNew(_ newSchema: OpenAPIKit.JSONSchema, _ oldSchema: OpenAPIKit30.JSONSchema) { + // TODO +} + +fileprivate func assertEqualOldToNew(_ newExample: OpenAPIKit.OpenAPI.Example, _ oldExample: OpenAPIKit30.OpenAPI.Example) { + XCTAssertEqual(newExample.summary, oldExample.summary) + XCTAssertEqual(newExample.description, oldExample.description) + XCTAssertEqual(newExample.value, oldExample.value) + XCTAssertEqual(newExample.vendorExtensions, oldExample.vendorExtensions) +} + +fileprivate func assertEqualOldToNew(_ newEncoding: OpenAPIKit.OpenAPI.Content.Encoding, _ oldEncoding: OpenAPIKit30.OpenAPI.Content.Encoding) throws { + XCTAssertEqual(newEncoding.contentType, oldEncoding.contentType) + if let newEncodingHeaders = newEncoding.headers { + let oldEncodingHeaders = try XCTUnwrap(oldEncoding.headers) + for ((newKey, newHeader), (oldKey, oldHeader)) in zip(newEncodingHeaders, oldEncodingHeaders) { + XCTAssertEqual(newKey, oldKey) + switch (newHeader, oldHeader) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let header1), .b(let header2)): + try assertEqualOldToNew(header1, header2) + default: + XCTFail("Found one reference and one header. \(newHeader) / \(oldHeader)") + } + } + } else { + XCTAssertNil(oldEncoding.headers) + } + XCTAssertEqual(newEncoding.style, oldEncoding.style) + XCTAssertEqual(newEncoding.explode, oldEncoding.explode) + XCTAssertEqual(newEncoding.allowReserved, oldEncoding.allowReserved) +} + +fileprivate func assertEqualOldToNew(_ newHeader: OpenAPIKit.OpenAPI.Header, _ oldHeader: OpenAPIKit30.OpenAPI.Header) throws { + XCTAssertEqual(newHeader.description, oldHeader.description) + XCTAssertEqual(newHeader.required, oldHeader.required) + XCTAssertEqual(newHeader.deprecated, oldHeader.deprecated) + switch (newHeader.schemaOrContent, oldHeader.schemaOrContent) { + case (.a(let schema1), .a(let schema2)): + try assertEqualOldToNew(schema1, schema2) + case (.b(let content1), .b(let content2)): + try assertEqualOldToNew(content1, content2) + default: + XCTFail("Found one schema and one content map. \(newHeader.schemaOrContent) / \(oldHeader.schemaOrContent)") + } + XCTAssertEqual(newHeader.vendorExtensions, oldHeader.vendorExtensions) +} + +fileprivate func assertEqualOldToNew(_ newSchemaContext: OpenAPIKit.OpenAPI.Parameter.SchemaContext, _ oldSchemaContext: OpenAPIKit30.OpenAPI.Parameter.SchemaContext) throws { + XCTAssertEqual(newSchemaContext.style, oldSchemaContext.style) + XCTAssertEqual(newSchemaContext.explode, oldSchemaContext.explode) + XCTAssertEqual(newSchemaContext.allowReserved, oldSchemaContext.allowReserved) + switch (newSchemaContext.schema, oldSchemaContext.schema) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let schema1), .b(let schema2)): + assertEqualOldToNew(schema1, schema2) + default: + XCTFail("Found one reference and one schema. \(newSchemaContext.schema) / \(oldSchemaContext.schema)") + } + XCTAssertEqual(newSchemaContext.example, oldSchemaContext.example) + if let newExamplesRef = newSchemaContext.examples { + let oldExamplesRef = try XCTUnwrap(oldSchemaContext.examples) + for ((newKey, newExampleRef), (oldKey, oldExampleRef)) in zip(newExamplesRef, oldExamplesRef) { + XCTAssertEqual(newKey, oldKey) + switch (newExampleRef, oldExampleRef) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let example1), .b(let example2)): + assertEqualOldToNew(example1, example2) + default: + XCTFail("Found one reference and one example. \(newExampleRef) / \(oldExampleRef)") + } + } + } else { + XCTAssertNil(oldSchemaContext.examples) + } +} + +fileprivate func assertEqualOldToNew(_ newResponseMap: OpenAPIKit.OpenAPI.Response.Map, _ oldResponseMap: OpenAPIKit30.OpenAPI.Response.Map) { + // TODO +} + +fileprivate func assertEqualOldToNew(_ newCallbacksMap: OpenAPIKit.OpenAPI.CallbacksMap, _ oldCallbacksMap: OpenAPIKit30.OpenAPI.CallbacksMap) throws { + XCTAssertEqual(newCallbacksMap.count, oldCallbacksMap.count) + for (key, ref) in newCallbacksMap { + let oldRef = try XCTUnwrap(oldCallbacksMap[key]) + switch (ref, oldRef) { + case (.a(let ref1), .a(let ref2)): + XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) + case (.b(let callback1), .b(let callback2)): + for (url, pathItemRef) in callback1 { + let pathItem2 = try XCTUnwrap(callback2[url]) + switch pathItemRef { + case .a(_): + XCTFail("Found a reference in OpenAPI 3.1 document where OpenAPI 3.0 does not support references!") + case .b(let pathItem): + try assertEqualOldToNew(pathItem, pathItem2) + } + } + default: + XCTFail("Found reference to a callbacks object in one document and actual callbacks object in the other. \(ref) / \(oldRef)") + } + } +} + +fileprivate func assertEqualOldToNew(_ newExternalDocs: OpenAPIKit.OpenAPI.ExternalDocumentation?, _ oldExternalDocs: OpenAPIKit30.OpenAPI.ExternalDocumentation?) throws { + if let newDocs = newExternalDocs { + let oldDocs = try XCTUnwrap(oldExternalDocs) + XCTAssertEqual(newDocs.description, oldDocs.description) + XCTAssertEqual(newDocs.url, oldDocs.url) + XCTAssertEqual(newDocs.vendorExtensions, oldDocs.vendorExtensions) + } else { + XCTAssertNil(oldExternalDocs) + } +}