diff --git a/Sources/OpenAPIKit/Content/Content.swift b/Sources/OpenAPIKit/Content/Content.swift index ac87d8f1b..51a32fb92 100644 --- a/Sources/OpenAPIKit/Content/Content.swift +++ b/Sources/OpenAPIKit/Content/Content.swift @@ -192,7 +192,7 @@ extension OpenAPI { self.schema = schemaValue } self.examples = examples - self.example = examples.flatMap(Self.firstExample(from:)) + self.example = examples.flatMap(Self.firstExampleValue(from:)) self.encoding = encoding.map(Either.a) self.vendorExtensions = vendorExtensions @@ -209,7 +209,7 @@ extension OpenAPI { ) { self.schema = .reference(schemaReference.jsonReference) self.examples = examples - self.example = examples.flatMap(Self.firstExample(from:)) + self.example = examples.flatMap(Self.firstExampleValue(from:)) self.encoding = encoding.map(Either.a) self.vendorExtensions = vendorExtensions @@ -228,7 +228,7 @@ extension OpenAPI { self.schema = schema self.itemSchema = itemSchema self.examples = examples - self.example = examples.flatMap(Self.firstExample(from:)) + self.example = examples.flatMap(Self.firstExampleValue(from:)) self.encoding = encoding.map(Either.a) self.vendorExtensions = vendorExtensions @@ -246,7 +246,7 @@ extension OpenAPI { self.schema = nil self.itemSchema = itemSchema self.examples = examples - self.example = examples.flatMap(Self.firstExample(from:)) + self.example = examples.flatMap(Self.firstExampleValue(from:)) self.encoding = encoding.map(Either.a) self.vendorExtensions = vendorExtensions @@ -265,7 +265,7 @@ extension OpenAPI { self.schema = nil self.itemSchema = itemSchema self.examples = examples - self.example = examples.flatMap(Self.firstExample(from:)) + self.example = examples.flatMap(Self.firstExampleValue(from:)) if itemEncoding != nil || prefixEncoding != [] { self.encoding = .b(.init(prefixEncoding: prefixEncoding, itemEncoding: itemEncoding)) } else { @@ -336,19 +336,19 @@ extension OpenAPI.Content { /// /// Operates on a dictionary with values that may be either /// an Example or a reference to and example. - internal static func firstExample(from exampleDict: OpenAPI.Example.Map) -> AnyCodable? { + internal static func firstExampleValue(from exampleDict: OpenAPI.Example.Map) -> AnyCodable? { return exampleDict .lazy - .compactMap { $0.value.exampleValue?.value?.codableValue } + .compactMap { (_, exampleOrRef) in exampleOrRef.exampleValue?.value?.value } .first } /// Pulls the first example found in the example dictionary /// given. - internal static func firstExample(from exampleDict: OrderedDictionary) -> AnyCodable? { + internal static func firstExampleValue(from exampleDict: OrderedDictionary) -> AnyCodable? { return exampleDict .lazy - .compactMap { $0.value.value?.codableValue } + .compactMap { (_, example) in example.value?.value } .first } } @@ -434,7 +434,7 @@ extension OpenAPI.Content: Decodable { } else { let examplesMap = try container.decodeIfPresent(OpenAPI.Example.Map.self, forKey: .examples) examples = examplesMap - example = examplesMap.flatMap(Self.firstExample(from:)) + example = examplesMap.flatMap(Self.firstExampleValue(from:)) } vendorExtensions = try Self.extensions(from: decoder) diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index 9a0016298..9160b5b35 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -52,7 +52,7 @@ public struct DereferencedContent: Equatable { } self.examples = examples - self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + self.example = examples.flatMap(OpenAPI.Content.firstExampleValue(from:)) ?? content.example switch content.encoding { diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift deleted file mode 100644 index ca2906881..000000000 --- a/Sources/OpenAPIKit/Example.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// Example.swift -// -// -// Created by Mathew Polzin on 10/6/19. -// - -import OpenAPIKitCore -import Foundation - -extension OpenAPI { - /// OpenAPI Spec "Example Object" - /// - /// See [OpenAPI Example Object](https://spec.openapis.org/oas/v3.1.1.html#example-object). - public struct Example: Equatable, CodableVendorExtendable, Sendable { - public let summary: String? - public let description: String? - - /// Represents the OpenAPI `externalValue` as a URL _or_ - /// the OpenAPI `value` as `AnyCodable`. - public let value: Either? - - /// Dictionary of vendor extensions. - /// - /// These should be of the form: - /// `[ "x-extensionKey": ]` - /// where the values are anything codable. - public var vendorExtensions: [String: AnyCodable] - - public init( - summary: String? = nil, - description: String? = nil, - value: Either? = nil, - vendorExtensions: [String: AnyCodable] = [:] - ) { - self.summary = summary - self.description = description - self.value = value - self.vendorExtensions = vendorExtensions - } - } -} - -extension OpenAPI.Example { - public typealias Map = OrderedDictionary, OpenAPI.Example>> -} - -// MARK: - Either Convenience -extension Either where A == OpenAPI.Reference, B == OpenAPI.Example { - /// Construct an `Example`. - public static func example( - summary: String? = nil, - description: String? = nil, - value: Either? = nil, - vendorExtensions: [String: AnyCodable] = [:] - ) -> Self { - return .b( - .init( - summary: summary, - description: description, - value: value, - vendorExtensions: vendorExtensions - ) - ) - } -} - -// MARK: - Describable & Summarizable - -extension OpenAPI.Example : OpenAPISummarizable { - public func overriddenNonNil(summary: String?) -> OpenAPI.Example { - guard let summary = summary else { return self } - return OpenAPI.Example( - summary: summary, - description: description, - value: value, - vendorExtensions: vendorExtensions - ) - } - - public func overriddenNonNil(description: String?) -> OpenAPI.Example { - guard let description = description else { return self } - return OpenAPI.Example( - summary: summary, - description: description, - value: value, - vendorExtensions: vendorExtensions - ) - } -} - -// MARK: - Codable -extension OpenAPI.Example: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(summary, forKey: .summary) - try container.encodeIfPresent(description, forKey: .description) - - switch value { - case .a(let url): - try container.encode(url.absoluteURL, forKey: .externalValue) - case .b(let example): - try container.encode(example, forKey: .value) - case nil: - break - } - - if VendorExtensionsConfiguration.isEnabled(for: encoder) { - try encodeExtensions(to: &container) - } - } -} - -extension OpenAPI.Example: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - guard !(container.contains(.externalValue) && container.contains(.value)) else { - throw GenericError( - subjectName: "example value", - details: "Found both `value` and `externalValue` keys in an Example. You must specify one or the other.", - codingPath: container.codingPath - ) - } - - if let externalValue = try container.decodeURLAsStringIfPresent(forKey: .externalValue) { - value = .a(externalValue) - } else if let internalValue = try container.decodeIfPresent(AnyCodable.self, forKey: .value) { - value = .b(internalValue) - } else { - value = nil - } - - summary = try container.decodeIfPresent(String.self, forKey: .summary) - description = try container.decodeIfPresent(String.self, forKey: .description) - - vendorExtensions = try Self.extensions(from: decoder) - } -} - -extension OpenAPI.Example { - internal enum CodingKeys: ExtendableCodingKey { - case summary - case description - case value - case externalValue - case extended(String) - - static var allBuiltinKeys: [CodingKeys] { - return [.summary, .description, .value, .externalValue] - } - - static func extendedKey(for value: String) -> CodingKeys { - return .extended(value) - } - - init?(stringValue: String) { - switch stringValue { - case "summary": - self = .summary - case "description": - self = .description - case "value": - self = .value - case "externalValue": - self = .externalValue - default: - self = .extendedKey(for: stringValue) - } - } - - var stringValue: String { - switch self { - case .summary: - return "summary" - case .description: - return "description" - case .value: - return "value" - case .externalValue: - return "externalValue" - case .extended(let key): - return key - } - } - } -} - -// MARK: - LocallyDereferenceable -extension OpenAPI.Example: LocallyDereferenceable { - /// Examples do not contain any references but for convenience - /// they can be "dereferenced" to themselves. - public func _dereferenced( - in components: OpenAPI.Components, - following references: Set, - dereferencedFromComponentNamed name: String? - ) throws -> OpenAPI.Example{ - var vendorExtensions = self.vendorExtensions - if let name { - vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) - } - - return .init( - summary: self.summary, - description: self.description, - value: self.value, - vendorExtensions: vendorExtensions - ) - } -} - -extension OpenAPI.Example: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { - return (self, .init(), []) - } -} - -extension OpenAPI.Example: Validatable {} diff --git a/Sources/OpenAPIKit/Example/Example.swift b/Sources/OpenAPIKit/Example/Example.swift new file mode 100644 index 000000000..094a7ce1a --- /dev/null +++ b/Sources/OpenAPIKit/Example/Example.swift @@ -0,0 +1,368 @@ +// +// Example.swift +// +// +// Created by Mathew Polzin on 10/6/19. +// + +import OpenAPIKitCore +import Foundation + +extension OpenAPI { + /// OpenAPI Spec "Example Object" + /// + /// See [OpenAPI Example Object](https://spec.openapis.org/oas/v3.2.0.html#example-object). + public struct Example: Equatable, CodableVendorExtendable, Sendable { + public let summary: String? + public let description: String? + + /// Stores the OpenAPI `dataValue`, `serializedValue`, `externalValue`, + /// and `value` fields. + public let value: OpenAPI.Example.Value? + + /// Dictionary of vendor extensions. + /// + /// These should be of the form: + /// `[ "x-extensionKey": ]` + /// where the values are anything codable. + public var vendorExtensions: [String: AnyCodable] + + public var dataValue: AnyCodable? { value?.dataValue } + public var serializedValue: String? { value?.serializedValue } + public var externalValue: URL? { value?.externalValue } + + public var legacyValue: AnyCodable? { value?.legacyValue } + public var dataOrLegacyValue: AnyCodable? { value?.value } + + @available(*, deprecated, message: "This initializer populates the deprecated 'value' field, use init(summary:description:dataValue:serializedValue:vendorExtensions:) or init(summary:description:dataValue:externalValue:vendorExtensions:) instead.") + public init( + summary: String? = nil, + description: String? = nil, + value: Either?, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.summary = summary + self.description = description + switch value { + case .a(let url): self.value = .value(data: nil, serialized: .b(url)) + case .b(let value): self.value = .legacy(value) + case nil: self.value = nil + } + self.vendorExtensions = vendorExtensions + } + + public init( + summary: String? = nil, + description: String? = nil, + legacyValue: Either?, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.summary = summary + self.description = description + switch legacyValue { + case .a(let url): self.value = .value(data: nil, serialized: .b(url)) + case .b(let value): self.value = .legacy(value) + case nil: self.value = nil + } + self.vendorExtensions = vendorExtensions + } + + public init( + summary: String? = nil, + description: String? = nil, + value: Value?, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.summary = summary + self.description = description + self.value = value + self.vendorExtensions = vendorExtensions + } + + public init( + summary: String? = nil, + description: String? = nil, + dataValue: AnyCodable? = nil, + serializedValue: String? = nil, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.summary = summary + self.description = description + if dataValue != nil || serializedValue != nil { + self.value = .value(data: dataValue, serialized: serializedValue.map(Either.a)) + } else { + self.value = nil + } + self.vendorExtensions = vendorExtensions + } + + public init( + summary: String? = nil, + description: String? = nil, + dataValue: AnyCodable? = nil, + externalValue: URL?, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.summary = summary + self.description = description + if dataValue != nil || externalValue != nil { + self.value = .value(data: dataValue, serialized: externalValue.map(Either.b)) + } else { + self.value = nil + } + self.vendorExtensions = vendorExtensions + } + } +} + +extension OpenAPI.Example { + public typealias Map = OrderedDictionary, OpenAPI.Example>> +} + +// MARK: - Either Convenience +extension Either where A == OpenAPI.Reference, B == OpenAPI.Example { + @available(*, deprecated, message: "This function populates the deprecated 'value' field, use .value(summary:description:dataValue:serializedValue:vendorExtensions:) or .value(summary:description:dataValue:externalValue:vendorExtensions:) instead.") + public static func example( + summary: String? = nil, + description: String? = nil, + value: Either?, + vendorExtensions: [String: AnyCodable] = [:] + ) -> Self { + return .b( + .init( + summary: summary, + description: description, + legacyValue: value, + vendorExtensions: vendorExtensions + ) + ) + } + + public static func example( + summary: String? = nil, + description: String? = nil, + dataValue: AnyCodable? = nil, + serializedValue: String? = nil, + vendorExtensions: [String: AnyCodable] = [:] + ) -> Self { + return .b( + .init( + summary: summary, + description: description, + dataValue: dataValue, + serializedValue: serializedValue, + vendorExtensions: vendorExtensions + ) + ) + } + + public static func example( + summary: String? = nil, + description: String? = nil, + dataValue: AnyCodable? = nil, + externalValue: URL?, + vendorExtensions: [String: AnyCodable] = [:] + ) -> Self { + return .b( + .init( + summary: summary, + description: description, + dataValue: dataValue, + externalValue: externalValue, + vendorExtensions: vendorExtensions + ) + ) + } +} + +// MARK: - Describable & Summarizable + +extension OpenAPI.Example : OpenAPISummarizable { + public func overriddenNonNil(summary: String?) -> OpenAPI.Example { + guard let summary = summary else { return self } + return OpenAPI.Example( + summary: summary, + description: description, + value: value, + vendorExtensions: vendorExtensions + ) + } + + public func overriddenNonNil(description: String?) -> OpenAPI.Example { + guard let description = description else { return self } + return OpenAPI.Example( + summary: summary, + description: description, + value: value, + vendorExtensions: vendorExtensions + ) + } +} + +// MARK: - Codable +extension OpenAPI.Example: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(summary, forKey: .summary) + try container.encodeIfPresent(description, forKey: .description) + + switch value { + case .legacy(let value): + try container.encode(value, forKey: .value) + case .value(data: let dataValue, serialized: let serialized): + try container.encodeIfPresent(dataValue, forKey: .dataValue) + switch serialized { + case .a(let serializedValue): + try container.encode(serializedValue, forKey: .serializedValue) + case .b(let externalValue): + try container.encode(externalValue.absoluteURL, forKey: .externalValue) + case nil: + break + } + case nil: + break; + } + + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } + } +} + +extension OpenAPI.Example: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let value = try container.decodeIfPresent(AnyCodable.self, forKey: .value) { + guard !(container.contains(.externalValue) || container.contains(.dataValue) || container.contains(.serializedValue)) else { + throw GenericError( + subjectName: "example value", + details: "Found both `value` and one of `externalValue`, `dataValue`, or `serializedValue` keys in an Example. `value` cannot be used with these other keys.", + codingPath: container.codingPath + ) + } + + self.value = .legacy(value) + } else { + + let dataValue = try container.decodeIfPresent(AnyCodable.self, forKey: .dataValue) + if let externalValue = try container.decodeURLAsStringIfPresent(forKey: .externalValue) { + guard !(container.contains(.serializedValue)) else { + throw GenericError( + subjectName: "example value", + details: "Found both `externalValue` and `serializedValue` keys in an Example. These fields are mutually exclusive.", + codingPath: container.codingPath + ) + } + + self.value = .value(data: dataValue, serialized: .b(externalValue)) + } else if let serializedValue = try container.decodeIfPresent(String.self, forKey: .serializedValue) { + self.value = .value(data: dataValue, serialized: .a(serializedValue)) + } else { + self.value = dataValue.map { .value(data: $0, serialized: nil) } + } + } + + summary = try container.decodeIfPresent(String.self, forKey: .summary) + description = try container.decodeIfPresent(String.self, forKey: .description) + + vendorExtensions = try Self.extensions(from: decoder) + } +} + +extension OpenAPI.Example { + internal enum CodingKeys: ExtendableCodingKey { + case summary + case description + case dataValue + case serializedValue + case value + case externalValue + case extended(String) + + static var allBuiltinKeys: [CodingKeys] { + return [ + .summary, + .description, + .dataValue, + .serializedValue, + .value, + .externalValue + ] + } + + static func extendedKey(for value: String) -> CodingKeys { + return .extended(value) + } + + init?(stringValue: String) { + switch stringValue { + case "summary": + self = .summary + case "description": + self = .description + case "dataValue": + self = .dataValue + case "serializedValue": + self = .serializedValue + case "value": + self = .value + case "externalValue": + self = .externalValue + default: + self = .extendedKey(for: stringValue) + } + } + + var stringValue: String { + switch self { + case .summary: + return "summary" + case .description: + return "description" + case .dataValue: + return "dataValue" + case .serializedValue: + return "serializedValue" + case .value: + return "value" + case .externalValue: + return "externalValue" + case .extended(let key): + return key + } + } + } +} + +// MARK: - LocallyDereferenceable +extension OpenAPI.Example: LocallyDereferenceable { + /// Examples do not contain any references but for convenience + /// they can be "dereferenced" to themselves. + public func _dereferenced( + in components: OpenAPI.Components, + following references: Set, + dereferencedFromComponentNamed name: String? + ) throws -> OpenAPI.Example{ + var vendorExtensions = self.vendorExtensions + if let name { + vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) + } + + return .init( + summary: self.summary, + description: self.description, + value: self.value, + vendorExtensions: vendorExtensions + ) + } +} + +extension OpenAPI.Example: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} + +extension OpenAPI.Example: Validatable {} diff --git a/Sources/OpenAPIKit/Example/ExampleValue.swift b/Sources/OpenAPIKit/Example/ExampleValue.swift new file mode 100644 index 000000000..f8dae0dad --- /dev/null +++ b/Sources/OpenAPIKit/Example/ExampleValue.swift @@ -0,0 +1,88 @@ +// +// ExampleValue.swift +// + +import Foundation + +extension OpenAPI.Example { + /// OpenAPI Spec "Example Object" `datValue`, `serializedValue`, + /// `externalValue`, and `value` fields get represented by this type in + /// order to guard against forbidden combinations of those fields. + /// + /// The `dataValue` and `serializedValue` fields were added in OAS 3.2.0; + /// for OAS 3.1.x documents, use of these fields will produce a warning + /// upon document validation. + /// + /// See [OpenAPI Example Object](https://spec.openapis.org/oas/v3.2.0.html#example-object). + /// + /// The fields can be used in the following combinations: + /// |-------------------------------------------------------------| + /// | dataValue | serializedValue | externalValue | value | + /// |-------------|-------------------|-----------------|---------| + /// | + | | | | + /// | + | + | | | + /// | + | | + | | + /// | | + | | | + /// | | | + | | + /// | | | | + | + /// |-------------------------------------------------------------| + /// + /// **Examples:** + /// + /// // dataValue + serializedValue (xml) + /// Value.value(data: ["name": "Frank"], serialized: .a("Frank")) + /// + /// // dataValue + externalValue + /// Value.value(data: ["name": "Susan"], serialized: .b(URL(string: "https://website.com/examples/name.xml")!)) + /// + /// // externalValue + /// Value.value(data: nil, serialized: .b(URL(string: "https://website.com/examples/name.xml")!)) + /// + /// // value + /// Value.legacy(["name": "Sam"]) + /// + public enum Value: Equatable, Sendable { + case legacy(AnyCodable) + case value(data: AnyCodable?, serialized: Either?) + } +} + +extension OpenAPI.Example.Value { + /// The OpenAPI Spec `value` or `dataValue` if either is specified. If you + /// need to differentiate between the two fields, `switch` on the `Value` + /// instead or use the `legacyValue` or `dataValue` accessors. + public var value: AnyCodable? { + switch self { + case .legacy(let value), .value(data: let .some(value), serialized: _): value + default: nil + } + } + + public var legacyValue: AnyCodable? { + switch self { + case .legacy(let value): value + default: nil + } + } + + public var dataValue: AnyCodable? { + switch self { + case .value(data: let .some(value), serialized: _): value + default: nil + } + } + + public var serializedValue: String? { + switch self { + case .value(data: _, serialized: let .a(value)): value + default: nil + } + } + + public var externalValue: URL? { + switch self { + case .value(data: _, serialized: let .b(value)): value + default: nil + } + } +} diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index a101c1653..66a4ac06a 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -47,7 +47,7 @@ public struct DereferencedSchemaContext: Equatable { .mapValues { try $0._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil) } self.examples = examples - self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + self.example = examples.flatMap(OpenAPI.Content.firstExampleValue(from:)) ?? schemaContext.example self.underlyingSchemaContext = schemaContext diff --git a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift index c7b3b02fd..1c0448f3c 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift @@ -93,7 +93,7 @@ extension OpenAPI.Parameter { self.allowReserved = allowReserved self.schema = .init(schema) self.examples = examples - self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + self.example = examples.flatMap(OpenAPI.Content.firstExampleValue(from:)) self.conditionalWarnings = style.conditionalWarnings } @@ -106,7 +106,7 @@ extension OpenAPI.Parameter { self.allowReserved = allowReserved self.schema = .init(schema) self.examples = examples - self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + self.example = examples.flatMap(OpenAPI.Content.firstExampleValue(from:)) self.explode = style.defaultExplode @@ -123,7 +123,7 @@ extension OpenAPI.Parameter { self.allowReserved = allowReserved self.schema = .init(schemaReference) self.examples = examples - self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + self.example = examples.flatMap(OpenAPI.Content.firstExampleValue(from:)) self.conditionalWarnings = style.conditionalWarnings } @@ -136,7 +136,7 @@ extension OpenAPI.Parameter { self.allowReserved = allowReserved self.schema = .init(schemaReference) self.examples = examples - self.example = examples.flatMap(OpenAPI.Content.firstExample(from:)) + self.example = examples.flatMap(OpenAPI.Content.firstExampleValue(from:)) self.explode = style.defaultExplode @@ -321,7 +321,7 @@ extension OpenAPI.Parameter.SchemaContext { } else { let examplesMap = try container.decodeIfPresent(OpenAPI.Example.Map.self, forKey: .examples) examples = examplesMap - example = examplesMap.flatMap(OpenAPI.Content.firstExample(from:)) + example = examplesMap.flatMap(OpenAPI.Content.firstExampleValue(from:)) } self.conditionalWarnings = style.conditionalWarnings diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 91e8b257f..506f70378 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -138,10 +138,16 @@ extension OpenAPIKit30.OpenAPI.Parameter.Context { extension OpenAPIKit30.OpenAPI.Example: To31 { fileprivate func to31() -> OpenAPIKit.OpenAPI.Example { - OpenAPIKit.OpenAPI.Example( + let newValue: OpenAPIKit.OpenAPI.Example.Value? = value.map { value in + switch value { + case .a(let url): .value(data: nil, serialized: .b(url)) + case .b(let data): .legacy(data) + } + } + return OpenAPIKit.OpenAPI.Example( summary: summary, description: description, - value: value, + value: newValue, vendorExtensions: vendorExtensions ) } diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 01b7fd6ae..896531e5a 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -1237,8 +1237,15 @@ fileprivate func assertEqualNewToOld(_ newStringContext: OpenAPIKit.JSONSchema.S fileprivate func assertEqualNewToOld(_ 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) + + XCTAssertNil(newExample.serializedValue) + XCTAssertNil(newExample.dataValue) + switch oldExample.value { + case .a(let url): XCTAssertEqual(url, newExample.externalValue) + case .b(let data): XCTAssertEqual(data, newExample.value?.value) + case nil: XCTAssertNil(newExample.value) + } } fileprivate func assertEqualNewToOld(_ newEncoding: OpenAPIKit.OpenAPI.Content.Encoding, _ oldEncoding: OpenAPIKit30.OpenAPI.Content.Encoding) throws { diff --git a/Tests/OpenAPIKitTests/ComponentsTests.swift b/Tests/OpenAPIKitTests/ComponentsTests.swift index 31ae5e77e..1aaa86cdd 100644 --- a/Tests/OpenAPIKitTests/ComponentsTests.swift +++ b/Tests/OpenAPIKitTests/ComponentsTests.swift @@ -38,7 +38,7 @@ final class ComponentsTests: XCTestCase { "three": .parameter(.init(name: "hi", context: .query(content: [:]))) ], examples: [ - "four": .example(.init(value: .init(URL(string: "http://address.com")!))) + "four": .example(.init(legacyValue: .init(URL(string: "http://address.com")!))) ], requestBodies: [ "five": .request(.init(content: [:])) @@ -88,7 +88,7 @@ final class ComponentsTests: XCTestCase { "three": .init(name: "hi", context: .query(content: [:])) ], examples: [ - "four": .init(value: .init(URL(string: "http://address.com")!)) + "four": .init(legacyValue: .init(URL(string: "http://address.com")!)) ], requestBodies: [ "five": .init(content: [:]) @@ -233,7 +233,7 @@ final class ComponentsTests: XCTestCase { "three": .init(name: "hello", context: .query(schema: .string)) ], examples: [ - "four": .init(value: .init(URL(string: "hello.com/hello")!)) + "four": .init(legacyValue: .init(URL(string: "hello.com/hello")!)) ], requestBodies: [ "five": .init(content: [:]) @@ -275,7 +275,7 @@ final class ComponentsTests: XCTestCase { XCTAssertEqual(components[ref1], .string) XCTAssertEqual(components[ref2], .init(description: "hello", content: [:])) XCTAssertEqual(components[ref3], .init(name: "hello", context: .query(schema: .string))) - XCTAssertEqual(components[ref4], .init(value: .init(URL(string: "hello.com/hello")!))) + XCTAssertEqual(components[ref4], .init(legacyValue: .init(URL(string: "hello.com/hello")!))) XCTAssertEqual(components[ref5], .init(content: [:])) XCTAssertEqual(components[ref6], .init(schema: .string)) XCTAssertEqual(components[ref7], .apiKey(name: "hello", location: .cookie)) @@ -484,7 +484,7 @@ extension ComponentsTests { "three": .init(name: "hi", context: .query(content: [:])) ], examples: [ - "four": .init(value: .init(URL(string: "http://address.com")!)) + "four": .init(legacyValue: .init(URL(string: "http://address.com")!)) ], requestBodies: [ "five": .init(content: [:]) @@ -723,7 +723,7 @@ extension ComponentsTests { "three": .init(name: "hi", context: .query(content: [:])) ], examples: [ - "four": .init(value: .init(URL(string: "http://address.com")!)) + "four": .init(legacyValue: .init(URL(string: "http://address.com")!)) ], requestBodies: [ "five": .init(content: [:]) diff --git a/Tests/OpenAPIKitTests/Content/ContentTests.swift b/Tests/OpenAPIKitTests/Content/ContentTests.swift index e1cdca2ef..d20e2dd97 100644 --- a/Tests/OpenAPIKitTests/Content/ContentTests.swift +++ b/Tests/OpenAPIKitTests/Content/ContentTests.swift @@ -33,32 +33,32 @@ final class ContentTests: XCTestCase { let withExamples = OpenAPI.Content( schema: .init(.string), examples: [ - "hello": .example(.init(value: .init("world"))), - "bbbb": .example(.init(value: .b("pick me"))), - "aaaa": .example(.init(value: .a(URL(string: "http://google.com")!))) + "hello": .example(serializedValue: "world"), + "bbbb": .example(dataValue: .init(["hi": "hello"])), + "aaaa": .example(externalValue: URL(string: "http://google.com")!) ] ) XCTAssertNotNil(withExamples.examples) - // we expect the example to be the first example where ordering - // is the order in which the examples are given: - XCTAssertEqual(withExamples.example?.value as? String, "world") - XCTAssertEqual(withExamples.examples?["hello"]?.exampleValue, .init(value: .init("world"))) + // we expect the example (singular) to be the first example with a data value + // where ordering is the order in which the examples are given: + XCTAssertEqual(withExamples.example?.value as? [String: String], ["hi": "hello"]) + XCTAssertEqual(withExamples.examples?["hello"]?.exampleValue, .init(serializedValue: "world")) XCTAssertEqual(withExamples.conditionalWarnings.count, 0) let withExamples2 = OpenAPI.Content( itemSchema: .string, examples: [ - "hello": .example(.init(value: .init("world"))), - "bbbb": .example(.init(value: .b("pick me"))), - "aaaa": .example(.init(value: .a(URL(string: "http://google.com")!))) + "hello": .example(serializedValue: "world"), + "bbbb": .example(dataValue: .init(["hi": "hello"])), + "aaaa": .example(externalValue: URL(string: "http://google.com")!) ] ) XCTAssertEqual(withExamples2.itemSchema, .string) XCTAssertNotNil(withExamples2.examples) - // we expect the example to be the first example where ordering - // is the order in which the examples are given: - XCTAssertEqual(withExamples2.example?.value as? String, "world") - XCTAssertEqual(withExamples2.examples?["hello"]?.exampleValue, .init(value: .init("world"))) + // we expect the example (singular) to be the first example with a data value + // where ordering is the order in which the examples are given: + XCTAssertEqual(withExamples.example?.value as? [String: String], ["hi": "hello"]) + XCTAssertEqual(withExamples.examples?["hello"]?.exampleValue, .init(serializedValue: "world")) XCTAssertEqual(withExamples2.conditionalWarnings.count, 1) let t4 = OpenAPI.Content( @@ -412,7 +412,7 @@ extension ContentTests { func test_examplesAndSchemaContent_encode() { let content = OpenAPI.Content(schema: .init(.object(properties: ["hello": .string])), - examples: ["hello": .b(OpenAPI.Example(value: .init(.init([ "hello": "world" ]))))]) + examples: ["hello": .b(OpenAPI.Example(dataValue: .init([ "hello": "world" ])))]) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) assertJSONEquivalent( @@ -421,7 +421,7 @@ extension ContentTests { { "examples" : { "hello" : { - "value" : { + "dataValue" : { "hello" : "world" } } @@ -448,7 +448,7 @@ extension ContentTests { { "examples" : { "hello": { - "value": { + "dataValue": { "hello" : "world" } } @@ -471,7 +471,7 @@ extension ContentTests { XCTAssertEqual(content.schema, .init(.object(properties: ["hello": .string]))) XCTAssertEqual(content.example?.value as? [String: String], [ "hello": "world" ]) - XCTAssertEqual(content.examples?["hello"]?.exampleValue?.value?.codableValue?.value as? [String: String], [ "hello": "world" ]) + XCTAssertEqual(content.examples?["hello"]?.exampleValue?.value?.value?.value as? [String: String], [ "hello": "world" ]) } func test_decodeFailureForBothExampleAndExamples() { diff --git a/Tests/OpenAPIKitTests/Content/DereferencedContentTests.swift b/Tests/OpenAPIKitTests/Content/DereferencedContentTests.swift index 07f6f0a8e..7ae97e8b1 100644 --- a/Tests/OpenAPIKitTests/Content/DereferencedContentTests.swift +++ b/Tests/OpenAPIKitTests/Content/DereferencedContentTests.swift @@ -17,7 +17,7 @@ final class DereferencedContentTests: XCTestCase { func test_oneExampleReferenced() throws { let components = OpenAPI.Components.direct( - examples: ["test": .init(value: .init("hello world"))] + examples: ["test": .init(legacyValue: .init("hello world"))] ) let t1 = try OpenAPI.Content( schema: .string, @@ -26,15 +26,15 @@ final class DereferencedContentTests: XCTestCase { XCTAssertEqual(t1.example, "hello world") XCTAssertEqual( t1.examples, - ["test": .init(value: .init("hello world"), vendorExtensions: ["x-component-name": "test"])] + ["test": .init(legacyValue: .init("hello world"), vendorExtensions: ["x-component-name": "test"])] ) } func test_multipleExamplesReferenced() throws { let components = OpenAPI.Components.direct( examples: [ - "test1": .init(value: .init("hello world")), - "test2": .init(value: .a(URL(string: "http://website.com")!)) + "test1": .init(legacyValue: .init("hello world")), + "test2": .init(legacyValue: .a(URL(string: "http://website.com")!)) ] ) let t1 = try OpenAPI.Content( @@ -48,8 +48,8 @@ final class DereferencedContentTests: XCTestCase { XCTAssertEqual( t1.examples, [ - "test1": .init(value: .init("hello world"), vendorExtensions: ["x-component-name": "test1"]), - "test2": .init(value: .init(URL(string: "http://website.com")!), vendorExtensions: ["x-component-name": "test2"]) + "test1": .init(legacyValue: .init("hello world"), vendorExtensions: ["x-component-name": "test1"]), + "test2": .init(legacyValue: .init(URL(string: "http://website.com")!), vendorExtensions: ["x-component-name": "test2"]) ] ) } diff --git a/Tests/OpenAPIKitTests/ExampleTests.swift b/Tests/OpenAPIKitTests/ExampleTests.swift index 1550a249c..2b23f9687 100644 --- a/Tests/OpenAPIKitTests/ExampleTests.swift +++ b/Tests/OpenAPIKitTests/ExampleTests.swift @@ -14,31 +14,47 @@ final class ExampleTests: XCTestCase { let full1 = OpenAPI.Example( summary: "hello", description: "world", - value: .init(URL(string: "https://google.com")!), + legacyValue: .b(.init(URL(string: "https://google.com")!)), vendorExtensions: ["hello": "world"] ) XCTAssertEqual(full1.summary, "hello") XCTAssertEqual(full1.description, "world") - XCTAssertEqual(full1.value, .init(URL(string: "https://google.com")!)) + XCTAssertEqual(full1.value?.value, .init(URL(string: "https://google.com")!)) + XCTAssertEqual(full1.legacyValue, .init(URL(string: "https://google.com")!)) + XCTAssertEqual(full1.dataOrLegacyValue, .init(URL(string: "https://google.com")!)) XCTAssertEqual(full1.vendorExtensions["hello"]?.value as? String, "world") let full2 = OpenAPI.Example( summary: "hello", description: "world", - value: .init("hello"), + dataValue: .init("hello"), vendorExtensions: ["hello": "world"] ) + let full3 = OpenAPI.Example( + summary: "hello", + description: "world", + externalValue: URL(string: "https://google.com")!, + vendorExtensions: ["hello": "world"] + ) + + XCTAssertEqual(full3.summary, "hello") + XCTAssertEqual(full3.description, "world") + XCTAssertEqual(full3.externalValue, URL(string: "https://google.com")!) + XCTAssertEqual(full3.vendorExtensions["hello"]?.value as? String, "world") + XCTAssertEqual(full2.summary, "hello") XCTAssertEqual(full2.description, "world") - XCTAssertEqual(full2.value, .init("hello")) + XCTAssertEqual(full2.value?.value, .init("hello")) + XCTAssertEqual(full2.dataValue, .init("hello")) + XCTAssertEqual(full2.dataOrLegacyValue, .init("hello")) XCTAssertEqual(full2.vendorExtensions["hello"]?.value as? String, "world") - let small = OpenAPI.Example(value: .init("hello")) + let small = OpenAPI.Example(serializedValue: "hello") XCTAssertNil(small.summary) XCTAssertNil(small.description) - XCTAssertEqual(small.value, .init("hello")) + XCTAssertEqual(small.serializedValue, "hello") XCTAssertEqual(small.vendorExtensions, [:]) let noValue = OpenAPI.Example() @@ -46,6 +62,9 @@ final class ExampleTests: XCTestCase { XCTAssertNil(noValue.description) XCTAssertNil(noValue.value) XCTAssertEqual(noValue.vendorExtensions, [:]) + + let _ = OpenAPI.Example(legacyValue: .b(.init(["hi": "hello"]))) + let _ = OpenAPI.Example(legacyValue: .b("hello")) } func test_locallyDereferenceable() throws { @@ -53,7 +72,7 @@ final class ExampleTests: XCTestCase { let full1 = OpenAPI.Example( summary: "hello", description: "world", - value: .init(URL(string: "https://google.com")!), + dataValue: .init(URL(string: "https://google.com")!), vendorExtensions: ["hello": "world"] ) XCTAssertEqual(try full1.dereferenced(in: .noComponents), full1) @@ -61,12 +80,12 @@ final class ExampleTests: XCTestCase { let full2 = OpenAPI.Example( summary: "hello", description: "world", - value: .init("hello"), + serializedValue: "hello", vendorExtensions: ["hello": "world"] ) XCTAssertEqual(try full2.dereferenced(in: .noComponents), full2) - let small = OpenAPI.Example(value: .init("hello")) + let small = OpenAPI.Example(legacyValue: .init("hello")) XCTAssertEqual(try small.dereferenced(in: .noComponents), small) let noValue = OpenAPI.Example() @@ -79,7 +98,7 @@ extension ExampleTests { func test_summaryAndExternalExample_encode() throws { let example = OpenAPI.Example( summary: "hello", - value: .init(URL(string: "https://google.com")!) + legacyValue: .init(URL(string: "https://google.com")!) ) let encodedExample = try orderUnstableTestStringFromEncoding(of: example) @@ -106,14 +125,14 @@ extension ExampleTests { let example = try orderUnstableDecode(OpenAPI.Example.self, from: exampleData) XCTAssertEqual(example, OpenAPI.Example(summary: "hello", - value: .init(URL(string: "https://google.com")!))) - XCTAssertEqual(example.value?.urlValue, URL(string: "https://google.com")!) + legacyValue: .init(URL(string: "https://google.com")!))) + XCTAssertEqual(example.externalValue, URL(string: "https://google.com")!) } func test_descriptionAndInternalExample_encode() throws { let example = OpenAPI.Example( description: "hello", - value: .init("world") + serializedValue: "world" ) let encodedExample = try orderUnstableTestStringFromEncoding(of: example) @@ -122,7 +141,7 @@ extension ExampleTests { """ { "description" : "hello", - "value" : "world" + "serializedValue" : "world" } """ ) @@ -133,18 +152,18 @@ extension ExampleTests { """ { "description" : "hello", - "value" : "world" + "serializedValue" : "world" } """.data(using: .utf8)! let example = try orderUnstableDecode(OpenAPI.Example.self, from: exampleData) XCTAssertEqual(example, OpenAPI.Example(description: "hello", - value: .init("world"))) + serializedValue: "world")) } func test_vendorExtensionAndInternalExample_encode() throws { - let example = OpenAPI.Example(value: .init("world"), + let example = OpenAPI.Example(dataValue: .init("world"), vendorExtensions: ["x-hello": 10]) let encodedExample = try orderUnstableTestStringFromEncoding(of: example) @@ -152,7 +171,7 @@ extension ExampleTests { encodedExample, """ { - "value" : "world", + "dataValue" : "world", "x-hello" : 10 } """ @@ -163,26 +182,59 @@ extension ExampleTests { let exampleData = """ { - "value" : "world", + "dataValue" : "world", "x-hello" : 10 } """.data(using: .utf8)! let example = try! orderUnstableDecode(OpenAPI.Example.self, from: exampleData) - XCTAssertEqual(example, OpenAPI.Example(value: .init("world"), + XCTAssertEqual(example, OpenAPI.Example(dataValue: .init("world"), vendorExtensions: ["x-hello": 10])) } + func test_internalLegacyExample_encode() { + let example = OpenAPI.Example(legacyValue: .b(.init(["hi": "world"]))) + let encodedExample = try! orderUnstableTestStringFromEncoding(of: example) + + assertJSONEquivalent( + encodedExample, + """ + { + "value" : { + "hi" : "world" + } + } + """ + ) + } + + func test_internalLegacyExample_decode() throws { + let exampleData = + """ + { + "value" : { + "hi" : "world" + } + } + """.data(using: .utf8)! + + let example = try orderUnstableDecode(OpenAPI.Example.self, from: exampleData) + + XCTAssertEqual(example, OpenAPI.Example(legacyValue: .b(.init(["hi": "world"])))) + } + func test_internalExample_encode() { - let example = OpenAPI.Example(value: .init("world")) + let example = OpenAPI.Example(dataValue: .init(["hi": "world"])) let encodedExample = try! orderUnstableTestStringFromEncoding(of: example) assertJSONEquivalent( encodedExample, """ { - "value" : "world" + "dataValue" : { + "hi" : "world" + } } """ ) @@ -192,17 +244,19 @@ extension ExampleTests { let exampleData = """ { - "value" : "world" + "dataValue" : { + "hi" : "world" + } } """.data(using: .utf8)! let example = try orderUnstableDecode(OpenAPI.Example.self, from: exampleData) - XCTAssertEqual(example, OpenAPI.Example(value: .init("world"))) + XCTAssertEqual(example, OpenAPI.Example(dataValue: .init(["hi": "world"]))) } func test_externalExample_encode() throws { - let example = OpenAPI.Example(value: .init(URL(string: "https://google.com")!)) + let example = OpenAPI.Example(externalValue: URL(string: "https://google.com")!) let encodedExample = try orderUnstableTestStringFromEncoding(of: example) assertJSONEquivalent( @@ -225,7 +279,7 @@ extension ExampleTests { let example = try orderUnstableDecode(OpenAPI.Example.self, from: exampleData) - XCTAssertEqual(example, OpenAPI.Example(value: .init(URL(string: "https://google.com")!))) + XCTAssertEqual(example, OpenAPI.Example(externalValue: URL(string: "https://google.com")!)) } func test_noExample_encode() throws { diff --git a/Tests/OpenAPIKitTests/OpenAPIReferenceTests.swift b/Tests/OpenAPIKitTests/OpenAPIReferenceTests.swift index 63110fe53..7e00d6995 100644 --- a/Tests/OpenAPIKitTests/OpenAPIReferenceTests.swift +++ b/Tests/OpenAPIKitTests/OpenAPIReferenceTests.swift @@ -87,7 +87,7 @@ final class OpenAPIReferenceTests: XCTestCase { "hello": .path(name: "name", content: [:], description: "description") ], examples: [ - "hello": .init(summary: "summary", description: "description", value: .b("")) + "hello": .init(summary: "summary", description: "description", legacyValue: .b("")) ], requestBodies: [ "hello": .init(description: "description", content: [:]) diff --git a/Tests/OpenAPIKitTests/Parameter/DereferencedSchemaContextTests.swift b/Tests/OpenAPIKitTests/Parameter/DereferencedSchemaContextTests.swift index f7e07a8c0..a2708e27d 100644 --- a/Tests/OpenAPIKitTests/Parameter/DereferencedSchemaContextTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/DereferencedSchemaContextTests.swift @@ -23,7 +23,7 @@ final class DereferencedSchemaContextTests: XCTestCase { func test_oneExampleReferenced() throws { let components = OpenAPI.Components.direct( - examples: ["test": .init(value: .init("hello world"))] + examples: ["test": .init(legacyValue: .init("hello world"))] ) let t1 = try OpenAPI.Parameter.SchemaContext( .string, @@ -33,15 +33,15 @@ final class DereferencedSchemaContextTests: XCTestCase { XCTAssertEqual(t1.example, "hello world") XCTAssertEqual( t1.examples, - ["test": .init(value: .init("hello world"), vendorExtensions: ["x-component-name": "test"])] + ["test": .init(legacyValue: .init("hello world"), vendorExtensions: ["x-component-name": "test"])] ) } func test_multipleExamplesReferenced() throws { let components = OpenAPI.Components.direct( examples: [ - "test1": .init(value: .init("hello world")), - "test2": .init(value: .a(URL(string: "http://website.com")!)) + "test1": .init(legacyValue: .init("hello world")), + "test2": .init(legacyValue: .a(URL(string: "http://website.com")!)) ] ) let t1 = try OpenAPI.Parameter.SchemaContext( @@ -56,8 +56,8 @@ final class DereferencedSchemaContextTests: XCTestCase { XCTAssertEqual( t1.examples, [ - "test1": .init(value: .init("hello world"), vendorExtensions: ["x-component-name": "test1"]), - "test2": .init(value: .init(URL(string: "http://website.com")!), vendorExtensions: ["x-component-name": "test2"]) + "test1": .init(legacyValue: .init("hello world"), vendorExtensions: ["x-component-name": "test1"]), + "test2": .init(legacyValue: .init(URL(string: "http://website.com")!), vendorExtensions: ["x-component-name": "test2"]) ] ) } diff --git a/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift b/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift index 0ac00d6f0..b095bd1d6 100644 --- a/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/ParameterSchemaTests.swift @@ -100,8 +100,8 @@ final class ParameterSchemaTests: XCTestCase { .string, style: .deepObject, examples: [ - "one": .example(value: .init("hello")), - "two": .example(value: .init("world")) + "one": .example(.init(legacyValue: .init("hello"))), + "two": .example(.init(legacyValue: .init("world"))) ] ) @@ -112,7 +112,7 @@ final class ParameterSchemaTests: XCTestCase { XCTAssertNotNil(t7.example) XCTAssertEqual(t7.example?.value as? String, "hello") XCTAssertNotNil(t7.examples) - XCTAssertEqual(t7.examples?["two"]?.exampleValue?.value?.codableValue?.value as? String, "world") + XCTAssertEqual(t7.examples?["two"]?.exampleValue?.value?.value?.value as? String, "world") // straight to schema override explode multiple examples let t8 = Schema( @@ -120,8 +120,8 @@ final class ParameterSchemaTests: XCTestCase { style: .deepObject, explode: true, examples: [ - "one": .example(value: .init("hello")), - "two": .example(value: .init("world")) + "one": .example(serializedValue: "hello"), + "two": .example(dataValue: "world") ] ) @@ -130,16 +130,16 @@ final class ParameterSchemaTests: XCTestCase { XCTAssertTrue(t8.explode) XCTAssertFalse(t8.allowReserved) XCTAssertNotNil(t8.example) - XCTAssertEqual(t8.example?.value as? String, "hello") + XCTAssertEqual(t8.example?.value as? String, "world") XCTAssertNotNil(t8.examples) - XCTAssertEqual(t8.examples?["two"]?.exampleValue?.value?.codableValue?.value as? String, "world") + XCTAssertEqual(t8.examples?["two"]?.exampleValue?.value?.value?.value as? String, "world") // schema reference multiple examples let t9 = Schema( schemaReference: .external(URL(string: "hello.yml")!), style: .deepObject, examples: [ - "one": .example(value: .init("hello")), + "one": .example(.init(legacyValue: .init("hello"))), "two": .reference(.external(URL(string: "world.yml")!)) ] ) @@ -159,7 +159,7 @@ final class ParameterSchemaTests: XCTestCase { style: .deepObject, explode: true, examples: [ - "one": .example(value: .init("hello")), + "one": .example(dataValue: .init("hello")), "two": .reference(.external(URL(string: "world.yml")!)) ] ) @@ -322,7 +322,7 @@ extension ParameterSchemaTests { .string, style: .default(for: .path), examples: [ - "one": .example(value: .init("hello")) + "one": .example(dataValue: .init("hello")) ] ) @@ -336,7 +336,7 @@ extension ParameterSchemaTests { "schema" : { "examples" : { "one" : { - "value" : "hello" + "dataValue" : "hello" } }, "schema" : { @@ -356,7 +356,7 @@ extension ParameterSchemaTests { "schema" : { "examples" : { "one" : { - "value" : "hello" + "dataValue" : "hello" } }, "schema" : { @@ -374,7 +374,7 @@ extension ParameterSchemaTests { .string, style: .default(for: .path), examples: [ - "one": .example(value: .init("hello")) + "one": .example(dataValue: .init("hello")) ] ) ) diff --git a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift index e2eb0c66d..fcff9dcbf 100644 --- a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift @@ -723,7 +723,7 @@ extension ParameterTests { style: .default(for: .header), allowReserved: true, examples: [ - "test": .example(value: .init(URL(string: "http://website.com")!)) + "test": .example(externalValue: URL(string: "http://website.com")!) ] ) ) @@ -785,7 +785,7 @@ extension ParameterTests { style: .default(for: .header), allowReserved: true, examples: [ - "test": .example(value: .init(URL(string: "http://website.com")!)) + "test": .example(externalValue: URL(string: "http://website.com")!) ] ) ) diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index 72c446a63..6eae74f86 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -854,7 +854,7 @@ final class BuiltinValidationTests: XCTestCase { "parameter1": .init(name: "test", context: .header(schema: .string)) ], examples: [ - "example1": .init(value: .b("hello")) + "example1": .init(legacyValue: .b("hello")) ], requestBodies: [ "request1": .init(content: [.json: .content(.init(schema: .object))]) diff --git a/documentation/migration_guides/v5_migration_guide.md b/documentation/migration_guides/v5_migration_guide.md index 8c2ced69d..bffd78ecc 100644 --- a/documentation/migration_guides/v5_migration_guide.md +++ b/documentation/migration_guides/v5_migration_guide.md @@ -350,6 +350,75 @@ case. Existing code that switches on that property will need to be updated to match on `oauth2(flows: OAuthFlows, metadataUrl: URL?)` now. +### Example Object (`OpenAPI.Example`) +There are no breaking changes to the `OpenAPIKit30` module in this section. + +OAS 3.2.0 introduced new fields along with restrictions around what combinations +of those fields can be used together. This warranted a restructuring of the +OpenAPIKit types to represent all new and existing restrictions together. + +The old `OpenAPI.Example` `value` property was an `Either` to represent either an +inline value or an external value. The new `value` property is of the +`OpenAPI.Example.Value` type. + +If you accessed the `codableValue` of the `OpenAPI.Example` `value` property +before, now you access the `legacyValue` or `dataOrLegacyValue` of the `OpenAPI.Example`: +```swift +// BEFORE +let codableValue = example.value?.codableValue + +// AFTER +let codableValue = example.legacyValue + +// AFTER (a bit more future-proof) +let codableValue = example.dataOrLegacyValue + +// AFTER (if you've migrated to only using `dataValue`) +let codableValue = example.dataValue +``` + +If you accessed the `urlValue` of the `OpenAPI.Example` `value` property +before, now you access the `externalValue` of the `openAPI.Example`: +```swift +// BEFORE +let urlValue = example.value?.urlValue + +// AFTER +let urlValue = example.externalValue +``` + +#### Initializers +The `Example.init(summary:description:value:vendorExtensions:)` initializer has +been deprecated. Your code should either use the new +`Example.init(summary:description:legacyValue:vendorExtensions:)` initializer +(direct replacement) or switch to one of two new initializers that adopts the +OAS 3.2.0 recommendations for semantics dependening on whether the example value +in question represents the structure of the example, the serialized form of +the example, or an external reference to the example: +```swift +// BEFORE (structural example) +let example = OpenAPI.Example(value: .b(.init(["hi": "hello"]))) + +// AFTER (structural example) +let example = OpenAPI.Example(dataValue: .init(["hi": "hello"])) + +// BEFORE (serialized example, xml) +let example = OpenAPI.Example(value: .b("hello")) + +// AFTER (serialized example, xml) +let example = OpenAPI.Example(serialzedValue: "hello") + +// BEFORE (external value) +let example = OpenAPI.Example( + value: .a(URL(string: "https://example.com/example.json")!) +) + +// AFTER (external value) +let example = OpenAPI.Example( + externalValue: URL(string: "https://example.com/example.json")! +) +``` + ### Errors Some error messages have been tweaked in small ways. If you match on the string descriptions of any OpenAPIKit errors, you may need to update the