From 6726d580cc4d5fe9c7b3fafab84dc98a137f1563 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 4 Aug 2023 16:38:45 -0500 Subject: [PATCH 01/40] beginning to play with an interface for loading external references. --- .../Components+Locatable.swift | 25 ++++---- .../Content/DereferencedContent.swift | 8 +++ .../Content/DereferencedContentEncoding.swift | 9 +++ .../Either+LocallyDereferenceable.swift | 9 +++ Sources/OpenAPIKit/Example.swift | 4 ++ .../OpenAPIKit/ExternalLoaderContext.swift | 39 +++++++++++++ .../Header/DereferencedHeader.swift | 6 ++ Sources/OpenAPIKit/JSONReference.swift | 22 ++++++- .../Operation/DereferencedOperation.swift | 6 ++ .../Parameter/DereferencedParameter.swift | 9 +++ .../Parameter/DereferencedSchemaContext.swift | 6 ++ Sources/OpenAPIKit/Parameter/Parameter.swift | 3 + .../Path Item/DereferencedPathItem.swift | 19 +++++++ .../Request/DereferencedRequest.swift | 6 ++ .../Response/DereferencedResponse.swift | 6 ++ .../DereferencedJSONSchema.swift | 6 ++ .../OpenAPIKit/Security/SecurityScheme.swift | 4 ++ .../OpenAPIKitCore/Shared/ComponentKey.swift | 23 ++++++++ .../Path Item/PathItemTests.swift | 57 +++++++++++++++++++ .../Validator/BuiltinValidationTests.swift | 2 +- 20 files changed, 255 insertions(+), 14 deletions(-) create mode 100644 Sources/OpenAPIKit/ExternalLoaderContext.swift diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index d7ff05880..c38d2aaa9 100644 --- a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift @@ -6,6 +6,7 @@ // import OpenAPIKitCore +import Foundation /// Anything conforming to ComponentDictionaryLocatable knows /// where to find resources of its type in the Components Dictionary. @@ -15,57 +16,57 @@ public protocol ComponentDictionaryLocatable: SummaryOverridable { /// This can be used to create a JSON path /// like `#/name1/name2/name3` static var openAPIComponentsKey: String { get } - static var openAPIComponentsKeyPath: KeyPath> { get } + static var openAPIComponentsKeyPath: WritableKeyPath> { get } } extension JSONSchema: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "schemas" } - public static var openAPIComponentsKeyPath: KeyPath> { \.schemas } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.schemas } } extension OpenAPI.Response: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "responses" } - public static var openAPIComponentsKeyPath: KeyPath> { \.responses } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.responses } } extension OpenAPI.Callbacks: ComponentDictionaryLocatable & SummaryOverridable { public static var openAPIComponentsKey: String { "callbacks" } - public static var openAPIComponentsKeyPath: KeyPath> { \.callbacks } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.callbacks } } extension OpenAPI.Parameter: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "parameters" } - public static var openAPIComponentsKeyPath: KeyPath> { \.parameters } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.parameters } } extension OpenAPI.Example: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "examples" } - public static var openAPIComponentsKeyPath: KeyPath> { \.examples } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.examples } } extension OpenAPI.Request: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "requestBodies" } - public static var openAPIComponentsKeyPath: KeyPath> { \.requestBodies } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.requestBodies } } extension OpenAPI.Header: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "headers" } - public static var openAPIComponentsKeyPath: KeyPath> { \.headers } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.headers } } extension OpenAPI.SecurityScheme: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "securitySchemes" } - public static var openAPIComponentsKeyPath: KeyPath> { \.securitySchemes } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.securitySchemes } } extension OpenAPI.Link: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "links" } - public static var openAPIComponentsKeyPath: KeyPath> { \.links } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.links } } extension OpenAPI.PathItem: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "pathItems" } - public static var openAPIComponentsKeyPath: KeyPath> { \.pathItems } + public static var openAPIComponentsKeyPath: WritableKeyPath> { \.pathItems } } /// A dereferenceable type can be recursively looked up in @@ -93,6 +94,8 @@ public protocol LocallyDereferenceable { /// All types that provide a `_dereferenced(in:following:)` implementation have a `dereferenced(in:)` /// implementation for free. func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedSelf + + func externallyDereferenced(in context: inout Context) throws -> Self } extension LocallyDereferenceable { diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index d078815d2..0bf73f892 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -63,4 +63,12 @@ extension OpenAPI.Content: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedContent { return try DereferencedContent(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Content where Context : ExternalLoaderContext { + var content = self + + // TOOD: need to locally dereference the schema, examples, and content encoding here. +#warning("need to locally dereference the schema, examples, and content encoding here.") + return content + } } diff --git a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift index 9e7a6a549..8b1aa8b18 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -51,4 +51,13 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedContentEncoding { return try DereferencedContentEncoding(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Content.Encoding where Context : ExternalLoaderContext { + var contentEncoding = self + + // TODO: need to externally dereference the headers here. +#warning("need to externally dereference the headers here.") + + return self + } } diff --git a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift index 4c0225baa..321b09406 100644 --- a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift @@ -17,4 +17,13 @@ extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: Loc return try value._dereferenced(in: components, following: references) } } + + public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + switch self { + case .a(let a): + return .a(try a.externallyDereferenced(in: &context)) + case .b(let b): + return .b(try b.externallyDereferenced(in: &context)) + } + } } diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index fe2dd97e9..f6390d2b8 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -186,6 +186,10 @@ extension OpenAPI.Example: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> OpenAPI.Example { return self } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Example where Context : ExternalLoaderContext { + return self + } } extension OpenAPI.Example: Validatable {} diff --git a/Sources/OpenAPIKit/ExternalLoaderContext.swift b/Sources/OpenAPIKit/ExternalLoaderContext.swift new file mode 100644 index 000000000..bdedc2d5c --- /dev/null +++ b/Sources/OpenAPIKit/ExternalLoaderContext.swift @@ -0,0 +1,39 @@ +// +// ExternalLoaderContext.swift +// +// +// Created by Mathew Polzin on 7/30/2023. +// + +import OpenAPIKitCore +import Foundation + +/// An `ExternalLoaderContext` enables `OpenAPIKit` to load external references +/// without knowing the details of what decoder is being used or how new internal +/// references should be named. +public protocol ExternalLoaderContext { + /// External references are loaded into this Components Object. This allows for + /// loading external references into a single Document but also retaining the + /// identity of those refernces; that is, if three parts of a Document refer to + /// the same external reference, the external object will be loaded into this + /// Components Object and the three locations will still refer to the same + /// object (these are now internal references). + var components: OpenAPI.Components { get set } + + /// Load the given URL and decode it as type T. + static func load(_: URL) throws -> T where T: Decodable + + /// Determine the next Component Key (where to store something in the + /// Components Object) for a new object of the given type that was loaded + /// at the given external URL. + mutating func nextComponentKey(type: T.Type, at: URL) -> OpenAPI.ComponentKey +} + +extension ExternalLoaderContext { + mutating func store(type: T.Type, from url: URL) throws -> OpenAPI.Reference where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable { + let key = nextComponentKey(type: type, at: url) + let value: T = try Self.load(url) + components[keyPath: T.openAPIComponentsKeyPath][key] = try value.externallyDereferenced(in: &self) + return try components.reference(named: key.rawValue, ofType: T.self) + } +} diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 6762e4d52..1cd81fd1a 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -71,4 +71,10 @@ extension OpenAPI.Header: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedHeader { return try DereferencedHeader(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Header where Context : ExternalLoaderContext { + // TODO: externally dereference the schemaOrContent +#warning("externally dereference the schemaOrContent") + return self + } } diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index f1b5b7074..e6a796a68 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -501,7 +501,7 @@ extension OpenAPI.Reference: Decodable { } // MARK: - LocallyDereferenceable -extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable { +extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable { /// Look up the component this reference points to and then /// dereference it. /// @@ -522,9 +522,23 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere .lookup(self) ._dereferenced(in: components, following: newReferences) } + + public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + switch self { + case .internal(let ref): + let value = try context.components.lookup(self) + .externallyDereferenced(in: &context) + let key = try OpenAPI.ComponentKey.forceInit(rawValue: ref.name) + context.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] = + value + return self + case .external(let url): + return try context.store(type: ReferenceType.self, from: url).jsonReference + } + } } -extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable { +extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable { /// Look up the component this reference points to and then /// dereference it. /// @@ -545,6 +559,10 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally .lookup(self) ._dereferenced(in: components, following: newReferences) } + + public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + return .init(try jsonReference.externallyDereferenced(in: &context)) + } } extension OpenAPI.Reference: Validatable where ReferenceType: Validatable {} diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index ed45d7bc3..d02edc1a8 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -110,4 +110,10 @@ extension OpenAPI.Operation: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedOperation { return try DereferencedOperation(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Operation where Context : ExternalLoaderContext { + // TODO: externally dereference security, responses, requestBody, and parameters +#warning("externally dereference security, responses, requestBody, and parameters") + return self + } } diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 954ad52af..22be440c8 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -71,4 +71,13 @@ extension OpenAPI.Parameter: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedParameter { return try DereferencedParameter(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> Self { + var parameter = self + + // TODO: externallyDerefence the schemaOrContent +#warning("need to externally dereference the schemaOrContent here") + + return parameter + } } diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 76f4305a3..63f7dbec4 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -63,4 +63,10 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedSchemaContext { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Parameter.SchemaContext where Context : ExternalLoaderContext { + // TODO: externally dereference schema, examples, and example +#warning("externally dereference schema, examples, and example") + return self + } } diff --git a/Sources/OpenAPIKit/Parameter/Parameter.swift b/Sources/OpenAPIKit/Parameter/Parameter.swift index 608126efb..a65dfcb0c 100644 --- a/Sources/OpenAPIKit/Parameter/Parameter.swift +++ b/Sources/OpenAPIKit/Parameter/Parameter.swift @@ -46,7 +46,10 @@ extension OpenAPI { /// where the values are anything codable. public var vendorExtensions: [String: AnyCodable] + /// Whether or not this parameter is required. See the context + /// which determines whether the parameter is required or not. public var required: Bool { context.required } + /// The location (e.g. "query") of the parameter. /// /// See the `context` property for more details on the diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index 9081e4650..c7172b708 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -6,6 +6,7 @@ // import OpenAPIKitCore +import Foundation /// An `OpenAPI.PathItem` type that guarantees /// its `parameters` and operations are inlined instead of @@ -126,4 +127,22 @@ extension OpenAPI.PathItem: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedPathItem { return try DereferencedPathItem(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> Self { + var pathItem = self + + var newParameters = OpenAPI.Parameter.Array() + for parameterRef in pathItem.parameters { + newParameters.append( + try parameterRef.externallyDereferenced(in: &context) + ) + } + + pathItem.parameters = newParameters + + // TODO: load external references for entire PathItem object + + return pathItem + } } + diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index 6a2c00001..c0116d0c5 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -50,4 +50,10 @@ extension OpenAPI.Request: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedRequest { return try DereferencedRequest(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Request where Context : ExternalLoaderContext { + // TODO: externally dereference the content +#warning("externally dereference the content") + return self + } } diff --git a/Sources/OpenAPIKit/Response/DereferencedResponse.swift b/Sources/OpenAPIKit/Response/DereferencedResponse.swift index ab6749fdf..7c9666d20 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -59,4 +59,10 @@ extension OpenAPI.Response: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedResponse { return try DereferencedResponse(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Response where Context : ExternalLoaderContext { + // TODO: externally dereference the headers and content +#warning("externally dereference the headers and content") + return self + } } diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index a809c48d2..573837245 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -407,4 +407,10 @@ extension JSONSchema: LocallyDereferenceable { public func dereferenced() -> DereferencedJSONSchema? { return try? dereferenced(in: .noComponents) } + + public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + // TODO: externally dereference this schema +#warning("need to externally dereference json schemas") + return self + } } diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 07e88e63f..8d8f09027 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -264,4 +264,8 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { public func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> OpenAPI.SecurityScheme { return self } + + public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.SecurityScheme where Context : ExternalLoaderContext { + return self + } } diff --git a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift index f8ff7f48e..5388e5d1b 100644 --- a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift +++ b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift @@ -31,6 +31,16 @@ extension Shared { self.rawValue = rawValue } + public static func forceInit(rawValue: String?) throws -> ComponentKey { + guard let rawValue = rawValue else { + throw InvalidComponentKey() + } + guard let value = ComponentKey(rawValue: rawValue) else { + throw InvalidComponentKey(Self.problem(with: rawValue), rawValue: rawValue) + } + return value + } + 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.." @@ -66,4 +76,17 @@ extension Shared { try container.encode(rawValue) } } + + public struct InvalidComponentKey: Swift.Error { + public let description: String + + internal init() { + description = "Failed to create a ComponentKey" + } + + internal init(_ message: String?, rawValue: String) { + description = message + ?? "Failed to create a ComponentKey from \(rawValue)" + } + } } diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index 0225bca79..97ccfbb78 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -472,3 +472,60 @@ extension PathItemTests { ) } } + +// MARK: External Dereferencing Tests +extension PathItemTests { + struct MockLoad: ExternalLoaderContext { + var components: OpenAPI.Components + + func nextComponentKey(type: T.Type, at url: URL) -> OpenAPI.ComponentKey { + "hello-world" + } + + static func load(_: URL) throws -> T where T : Decodable { + if let ret = OpenAPI.Request(description: "hello", content: [:]) as? T { + return ret + } + if let ret = OpenAPI.Parameter(name: "other-param", context: .header, schema: .string) as? T { + return ret + } + throw ValidationError(reason: "", at: []) + } + } + + func test_tmp() throws { + let components = OpenAPI.Components( + parameters: [ + "already-internal": + .init(name: "internal-param", context: .query, schema: .string), + ] + ) + let op = OpenAPI.Operation(responses: [:]) + let pathItem = OpenAPI.PathItem( + summary: "summary", + description: "description", + servers: [OpenAPI.Server(url: URL(string: "http://google.com")!)], + parameters: [ + .parameter(name: "hello", context: .query, schema: .string), + .reference(.component(named: "already-internal")), + .reference(.external(URL(string: "https://some-param.com")!)) + ], + get: .init(requestBody: .reference(.external(URL(string: "https://website.com")!)), responses: [:]), + put: op, + post: op, + delete: op, + options: op, + head: op, + patch: op, + trace: op + ) + + print(pathItem.parameters.debugDescription) + print("------") + var context = MockLoad(components: components) + let x = try pathItem.externallyDereferenced(in: &context) + print(x.parameters.debugDescription) + print("=======") + print(context.components.parameters) + } +} diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index 939057b99..0d2a6b8bf 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -749,7 +749,7 @@ final class BuiltinValidationTests: XCTestCase { components: .noComponents ) - // NOTE this is part of default validation + // NOTE these are part of default validation XCTAssertThrowsError(try document.validate()) { error in let error = error as? ValidationErrorCollection XCTAssertEqual(error?.values.count, 8) From b658586b37f36ebd20240f46809f0062a49d6de1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 5 Aug 2023 16:35:42 -0500 Subject: [PATCH 02/40] continuing to tweak the new external loading interface. --- .../Components+Locatable.swift | 2 +- .../Components Object/Components.swift | 30 +++++ .../Content/DereferencedContent.swift | 2 +- .../Content/DereferencedContentEncoding.swift | 2 +- Sources/OpenAPIKit/Document/Document.swift | 9 ++ .../Either+LocallyDereferenceable.swift | 6 +- Sources/OpenAPIKit/Example.swift | 2 +- Sources/OpenAPIKit/ExternalLoader.swift | 58 +++++++++ .../OpenAPIKit/ExternalLoaderContext.swift | 39 ------ .../Header/DereferencedHeader.swift | 2 +- Sources/OpenAPIKit/JSONReference.swift | 14 +-- .../Operation/DereferencedOperation.swift | 2 +- ...eredDictionary+ExternallyDereference.swift | 19 +++ .../Parameter/DereferencedParameter.swift | 2 +- .../Parameter/DereferencedSchemaContext.swift | 2 +- .../Path Item/DereferencedPathItem.swift | 4 +- .../Request/DereferencedRequest.swift | 2 +- .../Response/DereferencedResponse.swift | 2 +- .../DereferencedJSONSchema.swift | 2 +- .../OpenAPIKit/Security/SecurityScheme.swift | 2 +- .../Document/DocumentTests.swift | 119 ++++++++++++++++++ .../Path Item/PathItemTests.swift | 11 +- 22 files changed, 264 insertions(+), 69 deletions(-) create mode 100644 Sources/OpenAPIKit/ExternalLoader.swift delete mode 100644 Sources/OpenAPIKit/ExternalLoaderContext.swift create mode 100644 Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index c38d2aaa9..f270e1d51 100644 --- a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift @@ -95,7 +95,7 @@ public protocol LocallyDereferenceable { /// implementation for free. func _dereferenced(in components: OpenAPI.Components, following references: Set) throws -> DereferencedSelf - func externallyDereferenced(in context: inout Context) throws -> Self + func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self } extension LocallyDereferenceable { diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index b1aae51b7..2c0bb0804 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -260,4 +260,34 @@ extension OpenAPI.Components { } } +extension OpenAPI.Components { + private mutating func externallyDereference(dictionary: OpenAPI.ComponentDictionary, with loader: inout ExternalLoader) throws -> OpenAPI.ComponentDictionary where Context: ExternalLoaderContext, T: LocallyDereferenceable { + var newValues = OpenAPI.ComponentDictionary() + for (key, value) in dictionary { + newValues[key] = try value.externallyDereferenced(with: &loader) + } + return newValues + } + + internal mutating func externallyDereference(in context: Context) throws -> ExternalLoader where Context: ExternalLoaderContext { + var loader = ExternalLoader(components: self, context: context) + + schemas = try externallyDereference(dictionary: schemas, with: &loader) + responses = try externallyDereference(dictionary: responses, with: &loader) + parameters = try externallyDereference(dictionary: parameters, with: &loader) + examples = try externallyDereference(dictionary: examples, with: &loader) + requestBodies = try externallyDereference(dictionary: requestBodies, with: &loader) + headers = try externallyDereference(dictionary: headers, with: &loader) + securitySchemes = try externallyDereference(dictionary: securitySchemes, with: &loader) + + var newCallbacks = OpenAPI.ComponentDictionary() + for (key, value) in callbacks { + newCallbacks[key] = try value.externallyDereferenced(with: &loader) + } + callbacks = newCallbacks + + return loader + } +} + extension OpenAPI.Components: Validatable {} diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index 0bf73f892..6a6eac1f2 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -64,7 +64,7 @@ extension OpenAPI.Content: LocallyDereferenceable { return try DereferencedContent(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Content where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Content where Context : ExternalLoaderContext { var content = self // TOOD: need to locally dereference the schema, examples, and content encoding here. diff --git a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift index 8b1aa8b18..7e04f20b7 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -52,7 +52,7 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { return try DereferencedContentEncoding(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Content.Encoding where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Content.Encoding where Context : ExternalLoaderContext { var contentEncoding = self // TODO: need to externally dereference the headers here. diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index f34729339..d43f6c5aa 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -350,6 +350,15 @@ extension OpenAPI.Document { public func locallyDereferenced() throws -> DereferencedDocument { return try DereferencedDocument(self) } + + public mutating func externallyDereference(in context: Context) throws where Context: ExternalLoaderContext { + var loader: ExternalLoader = try components.externallyDereference(in: context) + + paths = try paths.externallyDereferenced(with: &loader) + webhooks = try webhooks.externallyDereferenced(with: &loader) + + components = loader.components + } } extension OpenAPI { diff --git a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift index 321b09406..6ccc7b9b6 100644 --- a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift @@ -18,12 +18,12 @@ extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: Loc } } - public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { switch self { case .a(let a): - return .a(try a.externallyDereferenced(in: &context)) + return .a(try a.externallyDereferenced(with: &loader)) case .b(let b): - return .b(try b.externallyDereferenced(in: &context)) + return .b(try b.externallyDereferenced(with: &loader)) } } } diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index f6390d2b8..87702ef7d 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -187,7 +187,7 @@ extension OpenAPI.Example: LocallyDereferenceable { return self } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Example where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Example where Context : ExternalLoaderContext { return self } } diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift new file mode 100644 index 000000000..96f57d2f2 --- /dev/null +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -0,0 +1,58 @@ +// +// ExternalLoader.swift +// +// +// Created by Mathew Polzin on 7/30/2023. +// + +import OpenAPIKitCore +import Foundation + +/// An `ExternalLoaderContext` enables `OpenAPIKit` to load external references +/// without knowing the details of what decoder is being used or how new internal +/// references should be named. +public protocol ExternalLoaderContext { + /// Load the given URL and decode it as Type `T`. All Types `T` are `Decodable`, so + /// the only real responsibility of a `load` function is to locate and load the given + /// `URL` and pass its `Data` or `String` (depending on the decoder) to an appropriate + /// `Decoder` for the given file type. + static func load(_: URL) throws -> T where T: Decodable + + /// Determine the next Component Key (where to store something in the + /// Components Object) for a new object of the given type that was loaded + /// at the given external URL. + /// + /// - Important: Ideally, this function returns distinct keys for all different objects + /// but the same key for all equal objects. In practice, this probably means that any + /// time the same type and URL pair are passed in the same `ComponentKey` should be + /// returned. + mutating func nextComponentKey(type: T.Type, at: URL, given components: OpenAPI.Components) throws -> OpenAPI.ComponentKey +} + +public struct ExternalLoader { + public init(components: OpenAPI.Components, context: Context) { + self.components = components + self.context = context + } + + /// External references are loaded into this Components Object. This allows for + /// loading external references into a single Document but also retaining the + /// identity of those refernces; that is, if three parts of a Document refer to + /// the same external reference, the external object will be loaded into this + /// Components Object and the three locations will still refer to the same + /// object (these are now internal references). + /// + /// In the most common use-cases, the starting place for this `components` property + /// should be the existing `Components` for some OpenAPI `Document`. This allows local + /// references to be followed while external references are loaded. + public internal(set) var components: OpenAPI.Components + + internal var context: Context + + internal mutating func store(type: T.Type, from url: URL) throws -> OpenAPI.Reference where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable { + let key = try context.nextComponentKey(type: type, at: url, given: components) + let value: T = try Context.load(url) + components[keyPath: T.openAPIComponentsKeyPath][key] = try value.externallyDereferenced(with: &self) + return try components.reference(named: key.rawValue, ofType: T.self) + } +} diff --git a/Sources/OpenAPIKit/ExternalLoaderContext.swift b/Sources/OpenAPIKit/ExternalLoaderContext.swift deleted file mode 100644 index bdedc2d5c..000000000 --- a/Sources/OpenAPIKit/ExternalLoaderContext.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ExternalLoaderContext.swift -// -// -// Created by Mathew Polzin on 7/30/2023. -// - -import OpenAPIKitCore -import Foundation - -/// An `ExternalLoaderContext` enables `OpenAPIKit` to load external references -/// without knowing the details of what decoder is being used or how new internal -/// references should be named. -public protocol ExternalLoaderContext { - /// External references are loaded into this Components Object. This allows for - /// loading external references into a single Document but also retaining the - /// identity of those refernces; that is, if three parts of a Document refer to - /// the same external reference, the external object will be loaded into this - /// Components Object and the three locations will still refer to the same - /// object (these are now internal references). - var components: OpenAPI.Components { get set } - - /// Load the given URL and decode it as type T. - static func load(_: URL) throws -> T where T: Decodable - - /// Determine the next Component Key (where to store something in the - /// Components Object) for a new object of the given type that was loaded - /// at the given external URL. - mutating func nextComponentKey(type: T.Type, at: URL) -> OpenAPI.ComponentKey -} - -extension ExternalLoaderContext { - mutating func store(type: T.Type, from url: URL) throws -> OpenAPI.Reference where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable { - let key = nextComponentKey(type: type, at: url) - let value: T = try Self.load(url) - components[keyPath: T.openAPIComponentsKeyPath][key] = try value.externallyDereferenced(in: &self) - return try components.reference(named: key.rawValue, ofType: T.self) - } -} diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 1cd81fd1a..fe9c1476b 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -72,7 +72,7 @@ extension OpenAPI.Header: LocallyDereferenceable { return try DereferencedHeader(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Header where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Header where Context : ExternalLoaderContext { // TODO: externally dereference the schemaOrContent #warning("externally dereference the schemaOrContent") return self diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index e6a796a68..297e6bf93 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -523,17 +523,17 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere ._dereferenced(in: components, following: newReferences) } - public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { switch self { case .internal(let ref): - let value = try context.components.lookup(self) - .externallyDereferenced(in: &context) + let value = try loader.components.lookup(self) + .externallyDereferenced(with: &loader) let key = try OpenAPI.ComponentKey.forceInit(rawValue: ref.name) - context.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] = + loader.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] = value return self case .external(let url): - return try context.store(type: ReferenceType.self, from: url).jsonReference + return try loader.store(type: ReferenceType.self, from: url).jsonReference } } } @@ -560,8 +560,8 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally ._dereferenced(in: components, following: newReferences) } - public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { - return .init(try jsonReference.externallyDereferenced(in: &context)) + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { + return .init(try jsonReference.externallyDereferenced(with: &loader)) } } diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index d02edc1a8..56d341009 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -111,7 +111,7 @@ extension OpenAPI.Operation: LocallyDereferenceable { return try DereferencedOperation(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Operation where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Operation where Context : ExternalLoaderContext { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") return self diff --git a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift new file mode 100644 index 000000000..e436c45f9 --- /dev/null +++ b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift @@ -0,0 +1,19 @@ +// +// OrderedDictionary+ExternallyDereference.swift +// OpenAPI +// +// Created by Mathew Polzin on 08/05/2023. +// + +import OpenAPIKitCore + +extension OrderedDictionary where Value: LocallyDereferenceable { + internal func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context: ExternalLoaderContext { + var newDict = Self() + for (key, value) in self { + let newRef = try value.externallyDereferenced(with: &loader) + newDict[key] = newRef + } + return newDict + } +} diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 22be440c8..0ce65a871 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -72,7 +72,7 @@ extension OpenAPI.Parameter: LocallyDereferenceable { return try DereferencedParameter(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> Self { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self { var parameter = self // TODO: externallyDerefence the schemaOrContent diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 63f7dbec4..aa584bf44 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -64,7 +64,7 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Parameter.SchemaContext where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Parameter.SchemaContext where Context : ExternalLoaderContext { // TODO: externally dereference schema, examples, and example #warning("externally dereference schema, examples, and example") return self diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index c7172b708..28a2290d8 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -128,13 +128,13 @@ extension OpenAPI.PathItem: LocallyDereferenceable { return try DereferencedPathItem(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> Self { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self { var pathItem = self var newParameters = OpenAPI.Parameter.Array() for parameterRef in pathItem.parameters { newParameters.append( - try parameterRef.externallyDereferenced(in: &context) + try parameterRef.externallyDereferenced(with: &loader) ) } diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index c0116d0c5..a157d2ac6 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -51,7 +51,7 @@ extension OpenAPI.Request: LocallyDereferenceable { return try DereferencedRequest(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Request where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Request where Context : ExternalLoaderContext { // TODO: externally dereference the content #warning("externally dereference the content") return self diff --git a/Sources/OpenAPIKit/Response/DereferencedResponse.swift b/Sources/OpenAPIKit/Response/DereferencedResponse.swift index 7c9666d20..3eb2e23d9 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -60,7 +60,7 @@ extension OpenAPI.Response: LocallyDereferenceable { return try DereferencedResponse(self, resolvingIn: components, following: references) } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.Response where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Response where Context : ExternalLoaderContext { // TODO: externally dereference the headers and content #warning("externally dereference the headers and content") return self diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 573837245..6fbfe30be 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -408,7 +408,7 @@ extension JSONSchema: LocallyDereferenceable { return try? dereferenced(in: .noComponents) } - public func externallyDereferenced(in context: inout Context) throws -> Self where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { // TODO: externally dereference this schema #warning("need to externally dereference json schemas") return self diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 8d8f09027..7ab4e42d5 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -265,7 +265,7 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { return self } - public func externallyDereferenced(in context: inout Context) throws -> OpenAPI.SecurityScheme where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.SecurityScheme where Context : ExternalLoaderContext { return self } } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 63a80dbcb..0d2fdd7cd 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1077,3 +1077,122 @@ extension DocumentTests { ) } } + +// MARK: - External Dereferencing +extension DocumentTests { + // temporarily test with an example of the new interface + func test_example() throws { + + /// An example of implementing a loader context for loading external references + /// into an OpenAPI document. + struct ExampleLoaderContext: ExternalLoaderContext { + static func load(_ url: URL) throws -> T where T : Decodable { + // load data from file, perhaps. we will just mock that up for the example: + let data = mockParameterData(url) + + return try JSONDecoder().decode(T.self, from: data) + } + + mutating func nextComponentKey(type: T.Type, at url: URL, given components: OpenAPIKit.OpenAPI.Components) throws -> OpenAPIKit.OpenAPI.ComponentKey { + // do anything you want here to determine what key the new component should be stored at. + // for the example, we will just transform the URL into a valid components key: + let urlString = url.pathComponents.dropFirst().joined(separator: "_").replacingOccurrences(of: ".", with: "_") + return try .forceInit(rawValue: urlString) + } + + /// Mock up some data, just for the example. + static func mockParameterData(_ url: URL) -> Data { + return """ + { + "name": "name", + "in": "path", + "schema": { "type": "string" }, + "required": true + } + """.data(using: .utf8)! + } + } + + + var document = OpenAPI.Document( + info: .init(title: "test document", version: "1.0.0"), + servers: [], + paths: [ + "/hello/{name}": .init( + parameters: [ + .reference(.external(URL(string: "file://./params/name.json")!)) + ] + ) + ], + components: .init( + // just to show, no parameters defined within document components : + parameters: [:] + ) + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + // - MARK: Before + print( + String(data: try encoder.encode(document), encoding: .utf8)! + ) + /* + { + "openapi": "3.1.0", + "info": { + "title": "test document", + "version": "1.0.0" + }, + "paths": { + "\/hello\/{name}": { + "parameters": [ + { + "$ref": "file:\/\/.\/params\/name.json" + } + ] + } + } + } + */ + + let context = ExampleLoaderContext() + try document.externallyDereference(in: context) + + // - MARK: After + + print( + String(data: try encoder.encode(document), encoding: .utf8)! + ) + /* + { + "paths": { + "\/hello\/{name}": { + "parameters": [ + { + "$ref": "#\/components\/parameters\/params_name_json" + } + ] + } + }, + "components": { + "parameters": { + "params_name_json": { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "openapi": "3.1.0", + "info": { + "title": "test document", + "version": "1.0.0" + } + } + */ + } +} diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index 97ccfbb78..3f5a09104 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -476,9 +476,7 @@ extension PathItemTests { // MARK: External Dereferencing Tests extension PathItemTests { struct MockLoad: ExternalLoaderContext { - var components: OpenAPI.Components - - func nextComponentKey(type: T.Type, at url: URL) -> OpenAPI.ComponentKey { + func nextComponentKey(type: T.Type, at url: URL, given components: OpenAPI.Components) -> OpenAPI.ComponentKey { "hello-world" } @@ -522,10 +520,11 @@ extension PathItemTests { print(pathItem.parameters.debugDescription) print("------") - var context = MockLoad(components: components) - let x = try pathItem.externallyDereferenced(in: &context) + let context = MockLoad() + var loader = ExternalLoader(components: components, context: context) + let x = try pathItem.externallyDereferenced(with: &loader) print(x.parameters.debugDescription) print("=======") - print(context.components.parameters) + print(loader.components.parameters) } } From 70c4faf1f5e4ec9924e5148eb99873fdd7f6cb0b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 6 Aug 2023 09:39:10 -0500 Subject: [PATCH 03/40] maybe store source URL as vendor extension. --- Sources/OpenAPIKit/CodableVendorExtendable.swift | 2 +- Sources/OpenAPIKit/Example.swift | 2 +- Tests/OpenAPIKitTests/Document/DocumentTests.swift | 12 ++++++++++-- Tests/OpenAPIKitTests/VendorExtendableTests.swift | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/OpenAPIKit/CodableVendorExtendable.swift b/Sources/OpenAPIKit/CodableVendorExtendable.swift index 9cfa2e0e0..1c75c293e 100644 --- a/Sources/OpenAPIKit/CodableVendorExtendable.swift +++ b/Sources/OpenAPIKit/CodableVendorExtendable.swift @@ -18,7 +18,7 @@ public protocol VendorExtendable { /// These should be of the form: /// `[ "x-extensionKey": ]` /// where the values are anything codable. - var vendorExtensions: VendorExtensions { get } + var vendorExtensions: VendorExtensions { get set } } public enum VendorExtensionsConfiguration { diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index 87702ef7d..2821faf9c 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -24,7 +24,7 @@ extension OpenAPI { /// These should be of the form: /// `[ "x-extensionKey": ]` /// where the values are anything codable. - public let vendorExtensions: [String: AnyCodable] + public var vendorExtensions: [String: AnyCodable] public init( summary: String? = nil, diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 0d2fdd7cd..2094eee32 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1090,7 +1090,15 @@ extension DocumentTests { // load data from file, perhaps. we will just mock that up for the example: let data = mockParameterData(url) - return try JSONDecoder().decode(T.self, from: data) + let decoded = try JSONDecoder().decode(T.self, from: data) + let finished: T + if var extendable = decoded as? VendorExtendable { + extendable.vendorExtensions["x-source-url"] = AnyCodable(url) + finished = extendable as! T + } else { + finished = decoded + } + return finished } mutating func nextComponentKey(type: T.Type, at url: URL, given components: OpenAPIKit.OpenAPI.Components) throws -> OpenAPIKit.OpenAPI.ComponentKey { @@ -1160,7 +1168,6 @@ extension DocumentTests { try document.externallyDereference(in: context) // - MARK: After - print( String(data: try encoder.encode(document), encoding: .utf8)! ) @@ -1178,6 +1185,7 @@ extension DocumentTests { "components": { "parameters": { "params_name_json": { + "x-source-url": "file:\/\/.\/params\/name.json", "in": "path", "name": "name", "required": true, diff --git a/Tests/OpenAPIKitTests/VendorExtendableTests.swift b/Tests/OpenAPIKitTests/VendorExtendableTests.swift index 5a49e5714..b42b0c0bc 100644 --- a/Tests/OpenAPIKitTests/VendorExtendableTests.swift +++ b/Tests/OpenAPIKitTests/VendorExtendableTests.swift @@ -145,7 +145,7 @@ private struct TestStruct: Codable, CodableVendorExtendable { } } - public let vendorExtensions: Self.VendorExtensions + public var vendorExtensions: Self.VendorExtensions init(vendorExtensions: Self.VendorExtensions) { self.vendorExtensions = vendorExtensions From fced755dd7d186a9dffb7dbdc0e41a8ee51f73db Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 1 Mar 2024 18:28:47 -0600 Subject: [PATCH 04/40] just get things building again --- Sources/OpenAPIKit/Callbacks.swift | 8 ++++++++ Sources/OpenAPIKit/Link.swift | 8 ++++++++ .../OrderedDictionary+ExternallyDereference.swift | 2 +- Sources/OpenAPIKit/Schema Object/JSONSchema.swift | 7 ++++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 3d1edf793..5d34d42df 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -35,5 +35,13 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { ) throws -> OpenAPI.CallbackURL { self } + + public func externallyDereferenced( + with loader: inout ExternalLoader + ) throws -> Self where Context : ExternalLoaderContext { + // TODO: externally dereference security, responses, requestBody, and parameters +#warning("externally dereference security, responses, requestBody, and parameters") + return self + } } diff --git a/Sources/OpenAPIKit/Link.swift b/Sources/OpenAPIKit/Link.swift index 428e7c280..836c6fc85 100644 --- a/Sources/OpenAPIKit/Link.swift +++ b/Sources/OpenAPIKit/Link.swift @@ -287,6 +287,14 @@ extension OpenAPI.Link: LocallyDereferenceable { vendorExtensions: vendorExtensions ) } + + public func externallyDereferenced( + with loader: inout ExternalLoader + ) throws -> Self where Context : ExternalLoaderContext { + // TODO: externally dereference security, responses, requestBody, and parameters +#warning("externally dereference security, responses, requestBody, and parameters") + return self + } } extension OpenAPI.Link: Validatable {} diff --git a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift index e436c45f9..46bbd8ba4 100644 --- a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift +++ b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift @@ -8,7 +8,7 @@ import OpenAPIKitCore extension OrderedDictionary where Value: LocallyDereferenceable { - internal func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context: ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context: ExternalLoaderContext { var newDict = Self() for (key, value) in self { let newRef = try value.externallyDereferenced(with: &loader) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift index 122e64beb..9cb813541 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift @@ -427,7 +427,12 @@ extension JSONSchema: VendorExtendable { /// `[ "x-extensionKey": ]` /// where the values are anything codable. public var vendorExtensions: VendorExtensions { - coreContext.vendorExtensions + get { + coreContext.vendorExtensions + } + set { + #warning("implement me") + } } public func with(vendorExtensions: [String: AnyCodable]) -> JSONSchema { From 33f4d1fd1ef41dfdec1865462acfcc62a34977e4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 2 Mar 2024 17:28:02 -0600 Subject: [PATCH 05/40] bump macos version to support concurrency. sprinkle some async function handling into external dereferencing calls --- Package.swift | 2 +- Sources/OpenAPIKit/Callbacks.swift | 2 +- .../Components+Locatable.swift | 2 +- .../Components Object/Components.swift | 22 +++++++++---------- Sources/OpenAPIKit/Document/Document.swift | 9 ++++---- .../Either+LocallyDereferenceable.swift | 6 ++--- Sources/OpenAPIKit/ExternalLoader.swift | 8 +++---- Sources/OpenAPIKit/JSONReference.swift | 10 ++++----- Sources/OpenAPIKit/Link.swift | 2 +- .../Operation/DereferencedOperation.swift | 2 +- ...eredDictionary+ExternallyDereference.swift | 4 ++-- .../Parameter/DereferencedParameter.swift | 2 +- .../Parameter/DereferencedSchemaContext.swift | 2 +- .../Path Item/DereferencedPathItem.swift | 4 ++-- .../Request/DereferencedRequest.swift | 2 +- 15 files changed, 40 insertions(+), 39 deletions(-) diff --git a/Package.swift b/Package.swift index bb7163de1..1e8bed458 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "OpenAPIKit", platforms: [ - .macOS(.v10_13), + .macOS(.v10_15), .iOS(.v11) ], products: [ diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 5d34d42df..1347da656 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -38,7 +38,7 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { public func externallyDereferenced( with loader: inout ExternalLoader - ) throws -> Self where Context : ExternalLoaderContext { + ) async throws -> Self where Context : ExternalLoaderContext { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") return self diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index bf930fd79..b323db8ef 100644 --- a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift @@ -101,7 +101,7 @@ public protocol LocallyDereferenceable { func externallyDereferenced( with loader: inout ExternalLoader - ) throws -> Self + ) async throws -> Self } extension LocallyDereferenceable { diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index c50c82976..a8bc3e1a4 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -269,28 +269,28 @@ extension OpenAPI.Components { } extension OpenAPI.Components { - private mutating func externallyDereference(dictionary: OpenAPI.ComponentDictionary, with loader: inout ExternalLoader) throws -> OpenAPI.ComponentDictionary where Context: ExternalLoaderContext, T: LocallyDereferenceable { + private mutating func externallyDereference(dictionary: OpenAPI.ComponentDictionary, with loader: inout ExternalLoader) async throws -> OpenAPI.ComponentDictionary where Context: ExternalLoaderContext, T: LocallyDereferenceable { var newValues = OpenAPI.ComponentDictionary() for (key, value) in dictionary { - newValues[key] = try value.externallyDereferenced(with: &loader) + newValues[key] = try await value.externallyDereferenced(with: &loader) } return newValues } - internal mutating func externallyDereference(in context: Context) throws -> ExternalLoader where Context: ExternalLoaderContext { + internal mutating func externallyDereference(in context: Context) async throws -> ExternalLoader where Context: ExternalLoaderContext { var loader = ExternalLoader(components: self, context: context) - schemas = try externallyDereference(dictionary: schemas, with: &loader) - responses = try externallyDereference(dictionary: responses, with: &loader) - parameters = try externallyDereference(dictionary: parameters, with: &loader) - examples = try externallyDereference(dictionary: examples, with: &loader) - requestBodies = try externallyDereference(dictionary: requestBodies, with: &loader) - headers = try externallyDereference(dictionary: headers, with: &loader) - securitySchemes = try externallyDereference(dictionary: securitySchemes, with: &loader) + schemas = try await externallyDereference(dictionary: schemas, with: &loader) + responses = try await externallyDereference(dictionary: responses, with: &loader) + parameters = try await externallyDereference(dictionary: parameters, with: &loader) + examples = try await externallyDereference(dictionary: examples, with: &loader) + requestBodies = try await externallyDereference(dictionary: requestBodies, with: &loader) + headers = try await externallyDereference(dictionary: headers, with: &loader) + securitySchemes = try await externallyDereference(dictionary: securitySchemes, with: &loader) var newCallbacks = OpenAPI.ComponentDictionary() for (key, value) in callbacks { - newCallbacks[key] = try value.externallyDereferenced(with: &loader) + newCallbacks[key] = try await value.externallyDereferenced(with: &loader) } callbacks = newCallbacks diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index 7a0e91798..cabf8badb 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -351,11 +351,12 @@ extension OpenAPI.Document { return try DereferencedDocument(self) } - public mutating func externallyDereference(in context: Context) throws where Context: ExternalLoaderContext { - var loader: ExternalLoader = try components.externallyDereference(in: context) + public mutating func externallyDereference(in context: Context) async throws where Context: ExternalLoaderContext { + var loader: ExternalLoader = + try await components.externallyDereference(in: context) - paths = try paths.externallyDereferenced(with: &loader) - webhooks = try webhooks.externallyDereferenced(with: &loader) + paths = try await paths.externallyDereferenced(with: &loader) + webhooks = try await webhooks.externallyDereferenced(with: &loader) components = loader.components } diff --git a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift index 4fe7b6c17..ab7658acd 100644 --- a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift @@ -22,12 +22,12 @@ extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: Loc } } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { switch self { case .a(let a): - return .a(try a.externallyDereferenced(with: &loader)) + return .a(try await a.externallyDereferenced(with: &loader)) case .b(let b): - return .b(try b.externallyDereferenced(with: &loader)) + return .b(try await b.externallyDereferenced(with: &loader)) } } } diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift index 96f57d2f2..3d0fef9bf 100644 --- a/Sources/OpenAPIKit/ExternalLoader.swift +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -16,7 +16,7 @@ public protocol ExternalLoaderContext { /// the only real responsibility of a `load` function is to locate and load the given /// `URL` and pass its `Data` or `String` (depending on the decoder) to an appropriate /// `Decoder` for the given file type. - static func load(_: URL) throws -> T where T: Decodable + static func load(_: URL) async throws -> T where T: Decodable /// Determine the next Component Key (where to store something in the /// Components Object) for a new object of the given type that was loaded @@ -49,10 +49,10 @@ public struct ExternalLoader { internal var context: Context - internal mutating func store(type: T.Type, from url: URL) throws -> OpenAPI.Reference where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable { + internal mutating func store(type: T.Type, from url: URL) async throws -> OpenAPI.Reference where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable { let key = try context.nextComponentKey(type: type, at: url, given: components) - let value: T = try Context.load(url) - components[keyPath: T.openAPIComponentsKeyPath][key] = try value.externallyDereferenced(with: &self) + let value: T = try await Context.load(url) + components[keyPath: T.openAPIComponentsKeyPath][key] = try await value.externallyDereferenced(with: &self) return try components.reference(named: key.rawValue, ofType: T.self) } } diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 806eedfd5..96a21ab23 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -536,17 +536,17 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere ._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { switch self { case .internal(let ref): - let value = try loader.components.lookup(self) + let value = try await loader.components.lookup(self) .externallyDereferenced(with: &loader) let key = try OpenAPI.ComponentKey.forceInit(rawValue: ref.name) loader.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] = value return self case .external(let url): - return try loader.store(type: ReferenceType.self, from: url).jsonReference + return try await loader.store(type: ReferenceType.self, from: url).jsonReference } } } @@ -577,8 +577,8 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally ._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { - return .init(try jsonReference.externallyDereferenced(with: &loader)) + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { + return .init(try await jsonReference.externallyDereferenced(with: &loader)) } } diff --git a/Sources/OpenAPIKit/Link.swift b/Sources/OpenAPIKit/Link.swift index 903a09bba..fa30ee5e6 100644 --- a/Sources/OpenAPIKit/Link.swift +++ b/Sources/OpenAPIKit/Link.swift @@ -290,7 +290,7 @@ extension OpenAPI.Link: LocallyDereferenceable { public func externallyDereferenced( with loader: inout ExternalLoader - ) throws -> Self where Context : ExternalLoaderContext { + ) async throws -> Self where Context : ExternalLoaderContext { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") return self diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index 6f457e48e..f1f605a55 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -124,7 +124,7 @@ extension OpenAPI.Operation: LocallyDereferenceable { return try DereferencedOperation(self, resolvingIn: components, following: references) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Operation where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Operation where Context : ExternalLoaderContext { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") return self diff --git a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift index 46bbd8ba4..172eeacf8 100644 --- a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift +++ b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift @@ -8,10 +8,10 @@ import OpenAPIKitCore extension OrderedDictionary where Value: LocallyDereferenceable { - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context: ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context: ExternalLoaderContext { var newDict = Self() for (key, value) in self { - let newRef = try value.externallyDereferenced(with: &loader) + let newRef = try await value.externallyDereferenced(with: &loader) newDict[key] = newRef } return newDict diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index e111d6518..b0ace8a29 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -82,7 +82,7 @@ extension OpenAPI.Parameter: LocallyDereferenceable { return try DereferencedParameter(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self { var parameter = self // TODO: externallyDerefence the schemaOrContent diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 499b53185..1d98f46fa 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -69,7 +69,7 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Parameter.SchemaContext where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Parameter.SchemaContext where Context : ExternalLoaderContext { // TODO: externally dereference schema, examples, and example #warning("externally dereference schema, examples, and example") return self diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index 3da4d3bd3..d146ab419 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -138,13 +138,13 @@ extension OpenAPI.PathItem: LocallyDereferenceable { return try DereferencedPathItem(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self { var pathItem = self var newParameters = OpenAPI.Parameter.Array() for parameterRef in pathItem.parameters { newParameters.append( - try parameterRef.externallyDereferenced(with: &loader) + try await parameterRef.externallyDereferenced(with: &loader) ) } diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index 45a8174eb..6ce54c66b 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -61,7 +61,7 @@ extension OpenAPI.Request: LocallyDereferenceable { return try DereferencedRequest(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Request where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Request where Context : ExternalLoaderContext { // TODO: externally dereference the content #warning("externally dereference the content") return self From 4a479e70c6b6af5cd2ab0cd8e9e64febf5b36dcf Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 2 Mar 2024 17:38:53 -0600 Subject: [PATCH 06/40] update two tests that use the now-async external dereferencing --- Tests/OpenAPIKitTests/Document/DocumentTests.swift | 10 +++++----- Tests/OpenAPIKitTests/Path Item/PathItemTests.swift | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 2a8236c34..92873e2ec 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1175,14 +1175,14 @@ extension DocumentTests { // MARK: - External Dereferencing extension DocumentTests { // temporarily test with an example of the new interface - func test_example() throws { + func test_example() async throws { /// An example of implementing a loader context for loading external references /// into an OpenAPI document. struct ExampleLoaderContext: ExternalLoaderContext { - static func load(_ url: URL) throws -> T where T : Decodable { + static func load(_ url: URL) async throws -> T where T : Decodable { // load data from file, perhaps. we will just mock that up for the example: - let data = mockParameterData(url) + let data = await mockParameterData(url) let decoded = try JSONDecoder().decode(T.self, from: data) let finished: T @@ -1203,7 +1203,7 @@ extension DocumentTests { } /// Mock up some data, just for the example. - static func mockParameterData(_ url: URL) -> Data { + static func mockParameterData(_ url: URL) async -> Data { return """ { "name": "name", @@ -1259,7 +1259,7 @@ extension DocumentTests { */ let context = ExampleLoaderContext() - try document.externallyDereference(in: context) + try await document.externallyDereference(in: context) // - MARK: After print( diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index 0a39a6b13..7918972c4 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -451,7 +451,7 @@ extension PathItemTests { } } - func test_tmp() throws { + func test_tmp() async throws { let components = OpenAPI.Components( parameters: [ "already-internal": @@ -482,7 +482,7 @@ extension PathItemTests { print("------") let context = MockLoad() var loader = ExternalLoader(components: components, context: context) - let x = try pathItem.externallyDereferenced(with: &loader) + let x = try await pathItem.externallyDereferenced(with: &loader) print(x.parameters.debugDescription) print("=======") print(loader.components.parameters) From 0f426e86b4783e362b32f548918427926524896f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 27 Mar 2024 09:56:10 -0500 Subject: [PATCH 07/40] CoreContext vendor extensions are writable, so just pass that along for setter --- Sources/OpenAPIKit/Schema Object/JSONSchema.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift index ae8cf16a6..8d74d8d40 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift @@ -431,7 +431,7 @@ extension JSONSchema: VendorExtendable { coreContext.vendorExtensions } set { - #warning("implement me") + coreContext.vendorExtensions } } From d283a87137ae9be8de0e8ac5fbe7271ccb6c8b8d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 27 Mar 2024 10:02:42 -0500 Subject: [PATCH 08/40] simplify code by using OrderedDictionary helper --- Sources/OpenAPIKit/Components Object/Components.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index a8bc3e1a4..32df7b357 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -288,11 +288,7 @@ extension OpenAPI.Components { headers = try await externallyDereference(dictionary: headers, with: &loader) securitySchemes = try await externallyDereference(dictionary: securitySchemes, with: &loader) - var newCallbacks = OpenAPI.ComponentDictionary() - for (key, value) in callbacks { - newCallbacks[key] = try await value.externallyDereferenced(with: &loader) - } - callbacks = newCallbacks + callbacks = try await callbacks.externallyDereferenced(with: &loader) return loader } From 79a505cfdedd5ced651e37aa777f0acf4204337a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 2 Apr 2024 09:40:49 -0500 Subject: [PATCH 09/40] trying another design to allow for more concurrent file loading --- .../Components+Locatable.swift | 4 --- .../Either+LocallyDereferenceable.swift | 9 ------ Sources/OpenAPIKit/ExternalLoader.swift | 30 ++---------------- ...eredDictionary+ExternallyDereference.swift | 19 ------------ ...Dictionary+ExternallyDereferenceable.swift | 31 +++++++++++++++++++ .../Response/DereferencedResponse.swift | 12 ++++--- .../Components Object/Components.swift | 21 +++++++++++++ .../OrderedDictionary/OrderedDictionary.swift | 13 +++++++- 8 files changed, 75 insertions(+), 64 deletions(-) delete mode 100644 Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift create mode 100644 Sources/OpenAPIKit/OrderedDictionary+ExternallyDereferenceable.swift diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index b323db8ef..c1a56f0f0 100644 --- a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift @@ -98,10 +98,6 @@ public protocol LocallyDereferenceable { following references: Set, dereferencedFromComponentNamed name: String? ) throws -> DereferencedSelf - - func externallyDereferenced( - with loader: inout ExternalLoader - ) async throws -> Self } extension LocallyDereferenceable { diff --git a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift index ab7658acd..75b0d0669 100644 --- a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift @@ -21,13 +21,4 @@ extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: Loc return try value._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil) } } - - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { - switch self { - case .a(let a): - return .a(try await a.externallyDereferenced(with: &loader)) - case .b(let b): - return .b(try await b.externallyDereferenced(with: &loader)) - } - } } diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift index 3d0fef9bf..58723d870 100644 --- a/Sources/OpenAPIKit/ExternalLoader.swift +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -26,33 +26,9 @@ public protocol ExternalLoaderContext { /// but the same key for all equal objects. In practice, this probably means that any /// time the same type and URL pair are passed in the same `ComponentKey` should be /// returned. - mutating func nextComponentKey(type: T.Type, at: URL, given components: OpenAPI.Components) throws -> OpenAPI.ComponentKey + static func componentKey(type: T.Type, at: URL) throws -> OpenAPI.ComponentKey } -public struct ExternalLoader { - public init(components: OpenAPI.Components, context: Context) { - self.components = components - self.context = context - } - - /// External references are loaded into this Components Object. This allows for - /// loading external references into a single Document but also retaining the - /// identity of those refernces; that is, if three parts of a Document refer to - /// the same external reference, the external object will be loaded into this - /// Components Object and the three locations will still refer to the same - /// object (these are now internal references). - /// - /// In the most common use-cases, the starting place for this `components` property - /// should be the existing `Components` for some OpenAPI `Document`. This allows local - /// references to be followed while external references are loaded. - public internal(set) var components: OpenAPI.Components - - internal var context: Context - - internal mutating func store(type: T.Type, from url: URL) async throws -> OpenAPI.Reference where T: ComponentDictionaryLocatable & Equatable & Decodable & LocallyDereferenceable { - let key = try context.nextComponentKey(type: type, at: url, given: components) - let value: T = try await Context.load(url) - components[keyPath: T.openAPIComponentsKeyPath][key] = try await value.externallyDereferenced(with: &self) - return try components.reference(named: key.rawValue, ofType: T.self) - } +public protocol ExternallyDereferenceable { + func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) } diff --git a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift deleted file mode 100644 index 172eeacf8..000000000 --- a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereference.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// OrderedDictionary+ExternallyDereference.swift -// OpenAPI -// -// Created by Mathew Polzin on 08/05/2023. -// - -import OpenAPIKitCore - -extension OrderedDictionary where Value: LocallyDereferenceable { - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context: ExternalLoaderContext { - var newDict = Self() - for (key, value) in self { - let newRef = try await value.externallyDereferenced(with: &loader) - newDict[key] = newRef - } - return newDict - } -} diff --git a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereferenceable.swift new file mode 100644 index 000000000..94718b0a6 --- /dev/null +++ b/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereferenceable.swift @@ -0,0 +1,31 @@ +// +// OrderedDictionary+ExternallyDereferenceable.swift +// OpenAPI +// +// Created by Mathew Polzin on 08/05/2023. +// + +import OpenAPIKitCore + +extension OrderedDictionary where Value: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components).self) { group in + for (key, value) in self { + group.addTask { + let (newRef, components) = try await value.externallyDereferenced(with: loader) + return (key, newRef, components) + } + } + + var newDict = Self() + var newComponents = OpenAPI.Components() + + for try await (key, newRef, components) in group { + newDict[key] = newRef + try newComponents.merge(components) + } + return (newDict, newComponents) + } + } +} diff --git a/Sources/OpenAPIKit/Response/DereferencedResponse.swift b/Sources/OpenAPIKit/Response/DereferencedResponse.swift index a3d3886c5..10c02f05a 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -75,10 +75,14 @@ extension OpenAPI.Response: LocallyDereferenceable { ) throws -> DereferencedResponse { return try DereferencedResponse(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Response where Context : ExternalLoaderContext { - // TODO: externally dereference the headers and content -#warning("externally dereference the headers and content") - return self +extension OpenAPI.Response: ExternallyDereferenceable { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Response where Context : ExternalLoaderContext { + var response = self + response.headers = try await headers?.externallyDereferenced(with: &loader) + response.content = try await content.externallyDereferenced(with: &loader) + response.links = try await links.externallyDereferenced(with: &loader) + return response } } diff --git a/Sources/OpenAPIKit30/Components Object/Components.swift b/Sources/OpenAPIKit30/Components Object/Components.swift index ca03ff146..93bcad2f5 100644 --- a/Sources/OpenAPIKit30/Components Object/Components.swift +++ b/Sources/OpenAPIKit30/Components Object/Components.swift @@ -260,4 +260,25 @@ extension OpenAPI.Components { } } +public extension OpenAPI.Components { + struct ValueCollision: Swift.Error { + let value1: T + let value2: T + } + + mutating func merge(_ components: OpenAPI.Components) throws { + try schemas.merge(components.schemas, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try responses.merge(components.responses, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try parameters.merge(components.parameters, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try examples.merge(components.examples, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try requestBodies.merge(components.requestBodies, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try headers.merge(components.headers, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try securitySchemes.merge(components.securitySchemes, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try links.merge(components.links, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try callbacks.merge(components.callbacks, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try pathItems.merge(components.pathItems, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + try vendorExtensions.merge(components.vendorExtensions, uniquingKeysWith: { (v1, v2) in throw OpenAPI.Components.ValueCollision(value1: v1, value2: v2) }) + } +} + extension OpenAPI.Components: Validatable {} diff --git a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift index 8779f30ec..8b52db0c5 100644 --- a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift @@ -205,6 +205,18 @@ extension OrderedDictionary: Collection { } } +extension OrderedDictionary { + public mutating func merge(_ other: OrderedDictionary, uniquingKeysWith resolve: (Value, Value) throws -> Value) rethrows { + for (key, value) in other { + if let conflict = self[key] { + self[key] = try resolve(conflict, value) + } else { + self[key] = value + } + } + } +} + // MARK: - Iterator extension OrderedDictionary { public struct Iterator: Sequence, IteratorProtocol { @@ -239,7 +251,6 @@ extension OrderedDictionary: Equatable where Value: Equatable { } // MARK: - Codable - public struct AnyCodingKey: CodingKey { public let stringValue: String From 97c7afbddc938bdbb6f50e955127f850de32886d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 15:27:38 -0500 Subject: [PATCH 10/40] external dereference conformance for either --- .../Either+ExternallyDereferenceable.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift diff --git a/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift new file mode 100644 index 000000000..feee7ebb6 --- /dev/null +++ b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift @@ -0,0 +1,20 @@ +// +// Either+ExternallyDereferenceable.swift +// +// +// Created by Mathew Polzin on 2/28/21. +// + +import OpenAPIKitCore + +// MARK: - ExternallyDereferenceable +extension Either: ExternallyDereferenceable where B: ExternallyDereferenceable { + public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { + switch self { + case .a(let a): + return .a(a) + case .b(let b): + return .b(try await b.externallyDereferenced(with: &loader)) + } + } +} From 1b77de4628c41b24ebacc7a66f55b24bb226d411 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 17:20:50 -0500 Subject: [PATCH 11/40] Almost entirely switched to new external dereferencing framework --- Sources/OpenAPIKit/Callbacks.swift | 9 +- .../Components Object/Components.swift | 82 ++++++++++++++----- .../Content/DereferencedContent.swift | 8 +- .../Content/DereferencedContentEncoding.swift | 9 +- Sources/OpenAPIKit/Document/Document.swift | 17 ++-- .../Either+ExternallyDereferenceable.swift | 11 ++- Sources/OpenAPIKit/Example.swift | 6 +- .../Header/DereferencedHeader.swift | 6 +- Sources/OpenAPIKit/JSONReference.swift | 24 +++--- Sources/OpenAPIKit/Link.swift | 8 +- .../Operation/DereferencedOperation.swift | 6 +- .../Parameter/DereferencedParameter.swift | 8 +- .../Parameter/DereferencedSchemaContext.swift | 6 +- .../Path Item/DereferencedPathItem.swift | 15 ++-- .../Request/DereferencedRequest.swift | 6 +- .../Response/DereferencedResponse.swift | 25 ++++-- .../DereferencedJSONSchema.swift | 6 +- .../OpenAPIKit/Security/SecurityScheme.swift | 6 +- 18 files changed, 169 insertions(+), 89 deletions(-) diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 1347da656..2f81e4a17 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -35,13 +35,12 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { ) throws -> OpenAPI.CallbackURL { self } +} - public func externallyDereferenced( - with loader: inout ExternalLoader - ) async throws -> Self where Context : ExternalLoaderContext { +extension OpenAPI.CallbackURL: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") - return self + return (self, .init()) } } - diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index 32df7b357..d08b9e672 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -71,6 +71,28 @@ extension OpenAPI { } } +extension OpenAPI.Components { + public struct ComponentCollision: Swift.Error { + public let componentType: String + public let existingComponent: String + public let newComponent: String + } + + public mutating func merge(_ other: OpenAPI.Components) throws { + try schemas.merge(other.schemas, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "schema", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try responses.merge(other.responses, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "responses", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try parameters.merge(other.parameters, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "parameters", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try examples.merge(other.examples, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "examples", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try requestBodies.merge(other.requestBodies, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "requestBodies", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try headers.merge(other.headers, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "headers", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "securitySchemes", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try links.merge(other.links, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "links", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try callbacks.merge(other.callbacks, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "callbacks", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try pathItems.merge(other.pathItems, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "pathItems", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "vendorExtensions", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + } +} + extension OpenAPI.Components { /// The extension name used to store a Components Object name (the key something is stored under /// within the Components Object). This is used by OpenAPIKit to store the previous Component name @@ -269,28 +291,44 @@ extension OpenAPI.Components { } extension OpenAPI.Components { - private mutating func externallyDereference(dictionary: OpenAPI.ComponentDictionary, with loader: inout ExternalLoader) async throws -> OpenAPI.ComponentDictionary where Context: ExternalLoaderContext, T: LocallyDereferenceable { - var newValues = OpenAPI.ComponentDictionary() - for (key, value) in dictionary { - newValues[key] = try await value.externallyDereferenced(with: &loader) - } - return newValues - } - - internal mutating func externallyDereference(in context: Context) async throws -> ExternalLoader where Context: ExternalLoaderContext { - var loader = ExternalLoader(components: self, context: context) - - schemas = try await externallyDereference(dictionary: schemas, with: &loader) - responses = try await externallyDereference(dictionary: responses, with: &loader) - parameters = try await externallyDereference(dictionary: parameters, with: &loader) - examples = try await externallyDereference(dictionary: examples, with: &loader) - requestBodies = try await externallyDereference(dictionary: requestBodies, with: &loader) - headers = try await externallyDereference(dictionary: headers, with: &loader) - securitySchemes = try await externallyDereference(dictionary: securitySchemes, with: &loader) - - callbacks = try await callbacks.externallyDereferenced(with: &loader) - - return loader + internal mutating func externallyDereference(in context: Context.Type) async throws { + let oldSchemas = schemas + let oldResponses = responses + let oldParameters = parameters + let oldExamples = examples + let oldRequestBodies = requestBodies + let oldHeaders = headers + let oldSecuritySchemes = securitySchemes + + let oldCallbacks = callbacks + + async let (newSchemas, c1) = oldSchemas.externallyDereferenced(with: context) + async let (newResponses, c2) = oldResponses.externallyDereferenced(with: context) + async let (newParameters, c3) = oldParameters.externallyDereferenced(with: context) + async let (newExamples, c4) = oldExamples.externallyDereferenced(with: context) + async let (newRequestBodies, c5) = oldRequestBodies.externallyDereferenced(with: context) + async let (newHeaders, c6) = oldHeaders.externallyDereferenced(with: context) + async let (newSecuritySchemes, c7) = oldSecuritySchemes.externallyDereferenced(with: context) + + async let (newCallbacks, c8) = oldCallbacks.externallyDereferenced(with: context) + + schemas = try await newSchemas + responses = try await newResponses + parameters = try await newParameters + examples = try await newExamples + requestBodies = try await newRequestBodies + headers = try await newHeaders + securitySchemes = try await newSecuritySchemes + + callbacks = try await newCallbacks + + try merge(c1) + try merge(c2) + try merge(c3) + try merge(c4) + try merge(c5) + try merge(c6) + try merge(c7) } } diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index b60533ec0..27cfdc10e 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -74,12 +74,12 @@ extension OpenAPI.Content: LocallyDereferenceable { ) throws -> DereferencedContent { return try DereferencedContent(self, resolvingIn: components, following: references) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Content where Context : ExternalLoaderContext { - var content = self - +extension OpenAPI.Content: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TOOD: need to locally dereference the schema, examples, and content encoding here. #warning("need to locally dereference the schema, examples, and content encoding here.") - return content + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift index 14fd45248..f68c51449 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -55,13 +55,12 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { ) throws -> DereferencedContentEncoding { return try DereferencedContentEncoding(self, resolvingIn: components, following: references) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Content.Encoding where Context : ExternalLoaderContext { - var contentEncoding = self - +extension OpenAPI.Content.Encoding: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: need to externally dereference the headers here. #warning("need to externally dereference the headers here.") - - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index cabf8badb..5f5cf19c8 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -351,14 +351,19 @@ extension OpenAPI.Document { return try DereferencedDocument(self) } - public mutating func externallyDereference(in context: Context) async throws where Context: ExternalLoaderContext { - var loader: ExternalLoader = - try await components.externallyDereference(in: context) + public mutating func externallyDereference(in context: Context.Type) async throws { + let oldPaths = paths + let oldWebhooks = webhooks - paths = try await paths.externallyDereferenced(with: &loader) - webhooks = try await webhooks.externallyDereferenced(with: &loader) + try await components.externallyDereference(in: context) - components = loader.components + async let (newPaths, c1) = oldPaths.externallyDereferenced(with: context) + async let (newWebhooks, c2) = oldWebhooks.externallyDereferenced(with: context) + + paths = try await newPaths + webhooks = try await newWebhooks + try await components.merge(c1) + try await components.merge(c2) } } diff --git a/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift index feee7ebb6..10e56b368 100644 --- a/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift @@ -8,13 +8,16 @@ import OpenAPIKitCore // MARK: - ExternallyDereferenceable -extension Either: ExternallyDereferenceable where B: ExternallyDereferenceable { - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { +extension Either: ExternallyDereferenceable where A: ExternallyDereferenceable, B: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { switch self { case .a(let a): - return .a(a) + let (newA, components) = try await a.externallyDereferenced(with: loader) + return (.a(newA), components) case .b(let b): - return .b(try await b.externallyDereferenced(with: &loader)) + let (newB, components) = try await b.externallyDereferenced(with: loader) + return (.b(newB), components) } } } diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index 4535010e3..8831c6d97 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -206,9 +206,11 @@ extension OpenAPI.Example: LocallyDereferenceable { vendorExtensions: vendorExtensions ) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Example where Context : ExternalLoaderContext { - return self +extension OpenAPI.Example: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 8c8d2311f..d34a7374c 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -81,10 +81,12 @@ extension OpenAPI.Header: LocallyDereferenceable { ) throws -> DereferencedHeader { return try DereferencedHeader(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Header where Context : ExternalLoaderContext { +extension OpenAPI.Header: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference the schemaOrContent #warning("externally dereference the schemaOrContent") - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 96a21ab23..d3f4fb72e 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -535,18 +535,19 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere .lookup(self) ._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { +extension JSONReference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { switch self { case .internal(let ref): - let value = try await loader.components.lookup(self) - .externallyDereferenced(with: &loader) - let key = try OpenAPI.ComponentKey.forceInit(rawValue: ref.name) - loader.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] = - value - return self + return (.internal(ref), .init()) case .external(let url): - return try await loader.store(type: ReferenceType.self, from: url).jsonReference + let componentKey = try loader.componentKey(type: ReferenceType.self, at: url) + let component: ReferenceType = try await loader.load(url) + var components = OpenAPI.Components() + components[keyPath: ReferenceType.openAPIComponentsKeyPath][componentKey] = component + return (try components.reference(named: componentKey.rawValue, ofType: ReferenceType.self).jsonReference, components) } } } @@ -576,9 +577,12 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally .lookup(self) ._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self where Context : ExternalLoaderContext { - return .init(try await jsonReference.externallyDereferenced(with: &loader)) +extension OpenAPI.Reference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + let (newRef, components) = try await jsonReference.externallyDereferenced(with: loader) + return (.init(newRef), components) } } diff --git a/Sources/OpenAPIKit/Link.swift b/Sources/OpenAPIKit/Link.swift index fa30ee5e6..409b1ca4d 100644 --- a/Sources/OpenAPIKit/Link.swift +++ b/Sources/OpenAPIKit/Link.swift @@ -287,13 +287,13 @@ extension OpenAPI.Link: LocallyDereferenceable { vendorExtensions: vendorExtensions ) } +} - public func externallyDereferenced( - with loader: inout ExternalLoader - ) async throws -> Self where Context : ExternalLoaderContext { +extension OpenAPI.Link: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index f1f605a55..e39a3f975 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -123,10 +123,12 @@ extension OpenAPI.Operation: LocallyDereferenceable { ) throws -> DereferencedOperation { return try DereferencedOperation(self, resolvingIn: components, following: references) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Operation where Context : ExternalLoaderContext { +extension OpenAPI.Operation: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference security, responses, requestBody, and parameters #warning("externally dereference security, responses, requestBody, and parameters") - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index b0ace8a29..5168ad22b 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -81,13 +81,13 @@ extension OpenAPI.Parameter: LocallyDereferenceable { ) throws -> DereferencedParameter { return try DereferencedParameter(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self { - var parameter = self +extension OpenAPI.Parameter: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externallyDerefence the schemaOrContent #warning("need to externally dereference the schemaOrContent here") - - return parameter + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 1d98f46fa..81d01ef8f 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -68,10 +68,12 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { ) throws -> DereferencedSchemaContext { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Parameter.SchemaContext where Context : ExternalLoaderContext { +extension OpenAPI.Parameter.SchemaContext: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference schema, examples, and example #warning("externally dereference schema, examples, and example") - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index d146ab419..12608d57a 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -137,22 +137,25 @@ extension OpenAPI.PathItem: LocallyDereferenceable { ) throws -> DereferencedPathItem { return try DereferencedPathItem(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> Self { +extension OpenAPI.PathItem: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { var pathItem = self + // TODO: Make the following into an async task list for parallel execution. var newParameters = OpenAPI.Parameter.Array() + var newComponents = OpenAPI.Components() for parameterRef in pathItem.parameters { - newParameters.append( - try await parameterRef.externallyDereferenced(with: &loader) - ) + let (newParameter, newComponent) = try await parameterRef.externallyDereferenced(with: loader) + newParameters.append(newParameter) + try newComponents.merge(newComponent) } pathItem.parameters = newParameters // TODO: load external references for entire PathItem object - return pathItem + return (pathItem, .init()) } } - diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index 6ce54c66b..97c947fde 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -60,10 +60,12 @@ extension OpenAPI.Request: LocallyDereferenceable { ) throws -> DereferencedRequest { return try DereferencedRequest(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Request where Context : ExternalLoaderContext { +extension OpenAPI.Request: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference the content #warning("externally dereference the content") - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Response/DereferencedResponse.swift b/Sources/OpenAPIKit/Response/DereferencedResponse.swift index 10c02f05a..3a8e7bdb6 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -78,11 +78,26 @@ extension OpenAPI.Response: LocallyDereferenceable { } extension OpenAPI.Response: ExternallyDereferenceable { - public func externallyDereferenced(with loader: inout ExternalLoader) async throws -> OpenAPI.Response where Context : ExternalLoaderContext { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + let oldContent = content + let oldLinks = links + + async let (newContent, c1) = oldContent.externallyDereferenced(with: loader) + async let (newLinks, c2) = oldLinks.externallyDereferenced(with: loader) + var response = self - response.headers = try await headers?.externallyDereferenced(with: &loader) - response.content = try await content.externallyDereferenced(with: &loader) - response.links = try await links.externallyDereferenced(with: &loader) - return response + response.content = try await newContent + response.links = try await newLinks + + var components = try await c1 + try await components.merge(c2) + + if let oldHeaders = headers { + let (newHeaders, c3) = try await oldHeaders.externallyDereferenced(with: loader) + response.headers = newHeaders + try components.merge(c3) + } + + return (response, components) } } diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 523067dc0..8cac1722a 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -533,10 +533,12 @@ extension JSONSchema: LocallyDereferenceable { public func dereferenced() -> DereferencedJSONSchema? { return try? dereferenced(in: .noComponents) } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { +extension JSONSchema: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // TODO: externally dereference this schema #warning("need to externally dereference json schemas") - return self + return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 0edcf03ee..2429466c1 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -272,8 +272,10 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { } return ret } +} - public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.SecurityScheme where Context : ExternalLoaderContext { - return self +extension OpenAPI.SecurityScheme: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + return (self, .init()) } } From 62758d39f4392654109f805872184599c341a774 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 18:21:46 -0500 Subject: [PATCH 12/40] Make path item parameter external dereferencing properly concurrent --- .../Path Item/DereferencedPathItem.swift | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index 12608d57a..6cffe2f84 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -141,21 +141,29 @@ extension OpenAPI.PathItem: LocallyDereferenceable { extension OpenAPI.PathItem: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - var pathItem = self - - // TODO: Make the following into an async task list for parallel execution. - var newParameters = OpenAPI.Parameter.Array() - var newComponents = OpenAPI.Components() - for parameterRef in pathItem.parameters { - let (newParameter, newComponent) = try await parameterRef.externallyDereferenced(with: loader) - newParameters.append(newParameter) - try newComponents.merge(newComponent) + let (newParameters, newComponents) = try await withThrowingTaskGroup(of: (OpenAPI.Parameter.Array.Element, OpenAPI.Components).self) { group in + for elem in parameters { + group.addTask { + return try await elem.externallyDereferenced(with: loader) + } + } + + var newParameters = OpenAPI.Parameter.Array() + var newComponents = OpenAPI.Components() + + for try await (elem, components) in group { + newParameters.append(elem) + try newComponents.merge(components) + } + return (newParameters, newComponents) } + var pathItem = self pathItem.parameters = newParameters // TODO: load external references for entire PathItem object + // also merge components before returning newComponents! - return (pathItem, .init()) + return (pathItem, newComponents) } } From 8d90a6d6cfa28a1f5814cd48abf83814c576760d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 18:22:07 -0500 Subject: [PATCH 13/40] make code compile. seems like a workaround for some compiler bug to me. --- .../Components Object/Components.swift | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index d08b9e672..eec77534f 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -310,7 +310,14 @@ extension OpenAPI.Components { async let (newHeaders, c6) = oldHeaders.externallyDereferenced(with: context) async let (newSecuritySchemes, c7) = oldSecuritySchemes.externallyDereferenced(with: context) - async let (newCallbacks, c8) = oldCallbacks.externallyDereferenced(with: context) +// async let (newCallbacks, c8) = oldCallbacks.externallyDereferenced(with: context) + var c8 = OpenAPI.Components() + var newCallbacks = oldCallbacks + for (key, callback) in oldCallbacks { + let (newCallback, components) = try await callback.externallyDereferenced(with: context) + newCallbacks[key] = newCallback + try c8.merge(components) + } schemas = try await newSchemas responses = try await newResponses @@ -320,15 +327,17 @@ extension OpenAPI.Components { headers = try await newHeaders securitySchemes = try await newSecuritySchemes - callbacks = try await newCallbacks + callbacks = newCallbacks + + try await merge(c1) + try await merge(c2) + try await merge(c3) + try await merge(c4) + try await merge(c5) + try await merge(c6) + try await merge(c7) - try merge(c1) - try merge(c2) - try merge(c3) - try merge(c4) - try merge(c5) - try merge(c6) - try merge(c7) + try merge(c8) } } From 78638155208adc6cf34e3ac9624d9ef193dd2825 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 18:34:38 -0500 Subject: [PATCH 14/40] fix some tests --- Sources/OpenAPIKit/ExternalLoader.swift | 2 +- Tests/OpenAPIKitTests/Document/DocumentTests.swift | 5 ++--- .../OpenAPIKitTests/Path Item/PathItemTests.swift | 14 +++----------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift index 58723d870..f937ac917 100644 --- a/Sources/OpenAPIKit/ExternalLoader.swift +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -26,7 +26,7 @@ public protocol ExternalLoaderContext { /// but the same key for all equal objects. In practice, this probably means that any /// time the same type and URL pair are passed in the same `ComponentKey` should be /// returned. - static func componentKey(type: T.Type, at: URL) throws -> OpenAPI.ComponentKey + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey } public protocol ExternallyDereferenceable { diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 92873e2ec..1a5e0ae15 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1195,7 +1195,7 @@ extension DocumentTests { return finished } - mutating func nextComponentKey(type: T.Type, at url: URL, given components: OpenAPIKit.OpenAPI.Components) throws -> OpenAPIKit.OpenAPI.ComponentKey { + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { // do anything you want here to determine what key the new component should be stored at. // for the example, we will just transform the URL into a valid components key: let urlString = url.pathComponents.dropFirst().joined(separator: "_").replacingOccurrences(of: ".", with: "_") @@ -1258,8 +1258,7 @@ extension DocumentTests { } */ - let context = ExampleLoaderContext() - try await document.externallyDereference(in: context) + try await document.externallyDereference(in: ExampleLoaderContext.self) // - MARK: After print( diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index 7918972c4..2e55b6aec 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -436,7 +436,7 @@ extension PathItemTests { // MARK: External Dereferencing Tests extension PathItemTests { struct MockLoad: ExternalLoaderContext { - func nextComponentKey(type: T.Type, at url: URL, given components: OpenAPI.Components) -> OpenAPI.ComponentKey { + static func componentKey(type: T.Type, at url: URL) -> OpenAPI.ComponentKey { "hello-world" } @@ -452,12 +452,6 @@ extension PathItemTests { } func test_tmp() async throws { - let components = OpenAPI.Components( - parameters: [ - "already-internal": - .init(name: "internal-param", context: .query, schema: .string), - ] - ) let op = OpenAPI.Operation(responses: [:]) let pathItem = OpenAPI.PathItem( summary: "summary", @@ -480,11 +474,9 @@ extension PathItemTests { print(pathItem.parameters.debugDescription) print("------") - let context = MockLoad() - var loader = ExternalLoader(components: components, context: context) - let x = try await pathItem.externallyDereferenced(with: &loader) + let (x, loaderComponents) = try await pathItem.externallyDereferenced(with: MockLoad.self) print(x.parameters.debugDescription) print("=======") - print(loader.components.parameters) + print(loaderComponents.parameters) } } From d2aea20abdb5236e57ce0502899956481852f682 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 19:25:02 -0500 Subject: [PATCH 15/40] Add some external dereferencing convenience around array and optional --- .../Array+ExternallyDereferenceable.swift | 27 +++++++++++++++++++ .../Optional+ExternallyDereferenceable.swift | 13 +++++++++ ...Dictionary+ExternallyDereferenceable.swift | 0 3 files changed, 40 insertions(+) create mode 100644 Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift create mode 100644 Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift rename Sources/OpenAPIKit/{ => Utility}/OrderedDictionary+ExternallyDereferenceable.swift (100%) diff --git a/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift new file mode 100644 index 000000000..59970df82 --- /dev/null +++ b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift @@ -0,0 +1,27 @@ +// +// Array+ExternallyDereferenceable.swift +// + +import OpenAPIKitCore + +extension Array where Element: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + try await withThrowingTaskGroup(of: (Element, OpenAPI.Components).self) { group in + for elem in self { + group.addTask { + return try await elem.externallyDereferenced(with: loader) + } + } + + var newElems = Self() + var newComponents = OpenAPI.Components() + + for try await (elem, components) in group { + newElems.append(elem) + try newComponents.merge(components) + } + return (newElems, newComponents) + } + } +} diff --git a/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift new file mode 100644 index 000000000..671657f86 --- /dev/null +++ b/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift @@ -0,0 +1,13 @@ +// +// Optional+ExternallyDereferenceable.swift +// + +import OpenAPIKitCore + +extension Optional where Wrapped: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + guard let wrapped = self else { return (nil, .init()) } + return try await wrapped.externallyDereferenced(with: loader) + } +} diff --git a/Sources/OpenAPIKit/OrderedDictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift similarity index 100% rename from Sources/OpenAPIKit/OrderedDictionary+ExternallyDereferenceable.swift rename to Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift From e1428901ede2bf58edc09d6a051e8815714f7e57 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 19:25:33 -0500 Subject: [PATCH 16/40] Make Server externally dereferenceable --- Sources/OpenAPIKit/Server.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/OpenAPIKit/Server.swift b/Sources/OpenAPIKit/Server.swift index 2b7d17b2c..ad9bf6094 100644 --- a/Sources/OpenAPIKit/Server.swift +++ b/Sources/OpenAPIKit/Server.swift @@ -259,5 +259,11 @@ extension OpenAPI.Server.Variable { } } +extension OpenAPI.Server: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + return (self, .init()) + } +} + extension OpenAPI.Server: Validatable {} extension OpenAPI.Server.Variable: Validatable {} From 0a6500569e1ee7b58ba011f22cb4200dd07fa9f1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 19:25:49 -0500 Subject: [PATCH 17/40] Finish PathItem external dereferencing implementation --- .../Path Item/DereferencedPathItem.swift | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index 6cffe2f84..6f4727f45 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -141,28 +141,89 @@ extension OpenAPI.PathItem: LocallyDereferenceable { extension OpenAPI.PathItem: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - let (newParameters, newComponents) = try await withThrowingTaskGroup(of: (OpenAPI.Parameter.Array.Element, OpenAPI.Components).self) { group in - for elem in parameters { - group.addTask { - return try await elem.externallyDereferenced(with: loader) - } - } - - var newParameters = OpenAPI.Parameter.Array() - var newComponents = OpenAPI.Components() - - for try await (elem, components) in group { - newParameters.append(elem) - try newComponents.merge(components) - } - return (newParameters, newComponents) - } + let oldParameters = parameters + let oldServers = servers + let oldGet = get + let oldPut = put + let oldPost = post + let oldDelete = delete + let oldOptions = options + let oldHead = head + let oldPatch = patch + let oldTrace = trace + + async let (newParameters, c1) = oldParameters.externallyDereferenced(with: loader) +// async let (newServers, c2) = oldServers.externallyDereferenced(with: loader) +// async let (newGet, c3) = oldGet.externallyDereferenced(with: loader) +// async let (newPut, c4) = oldPut.externallyDereferenced(with: loader) +// async let (newPost, c5) = oldPost.externallyDereferenced(with: loader) +// async let (newDelete, c6) = oldDelete.externallyDereferenced(with: loader) +// async let (newOptions, c7) = oldOptions.externallyDereferenced(with: loader) +// async let (newHead, c8) = oldHead.externallyDereferenced(with: loader) +// async let (newPatch, c9) = oldPatch.externallyDereferenced(with: loader) +// async let (newTrace, c10) = oldTrace.externallyDereferenced(with: loader) var pathItem = self - pathItem.parameters = newParameters + var newComponents = try await c1 + + // ideally we would async let all of the props above and then set them here, + // but for now since there seems to be some sort of compiler bug we will do + // most of them in if lets below + pathItem.parameters = try await newParameters + + if let oldServers { + async let (newServers, c2) = oldServers.externallyDereferenced(with: loader) + pathItem.servers = try await newServers + try await newComponents.merge(c2) + } - // TODO: load external references for entire PathItem object - // also merge components before returning newComponents! + if let oldGet { + async let (newGet, c3) = oldGet.externallyDereferenced(with: loader) + pathItem.get = try await newGet + try await newComponents.merge(c3) + } + + if let oldPut { + async let (newPut, c4) = oldPut.externallyDereferenced(with: loader) + pathItem.put = try await newPut + try await newComponents.merge(c4) + } + + if let oldPost { + async let (newPost, c5) = oldPost.externallyDereferenced(with: loader) + pathItem.post = try await newPost + try await newComponents.merge(c5) + } + + if let oldDelete { + async let (newDelete, c6) = oldDelete.externallyDereferenced(with: loader) + pathItem.delete = try await newDelete + try await newComponents.merge(c6) + } + + if let oldOptions { + async let (newOptions, c7) = oldOptions.externallyDereferenced(with: loader) + pathItem.options = try await newOptions + try await newComponents.merge(c7) + } + + if let oldHead { + async let (newHead, c8) = oldHead.externallyDereferenced(with: loader) + pathItem.head = try await newHead + try await newComponents.merge(c8) + } + + if let oldPatch { + async let (newPatch, c9) = oldPatch.externallyDereferenced(with: loader) + pathItem.patch = try await newPatch + try await newComponents.merge(c9) + } + + if let oldTrace { + async let (newTrace, c10) = oldTrace.externallyDereferenced(with: loader) + pathItem.trace = try await newTrace + try await newComponents.merge(c10) + } return (pathItem, newComponents) } From f5177dc9ae6d2656a0de64cc0e39ca1381276aa1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Apr 2024 19:47:02 -0500 Subject: [PATCH 18/40] a bit more work towards external dereferencing implementations --- .../Content/DereferencedContent.swift | 23 ++++++++++++++++--- .../Content/DereferencedContentEncoding.swift | 22 +++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index 27cfdc10e..926572467 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -78,8 +78,25 @@ extension OpenAPI.Content: LocallyDereferenceable { extension OpenAPI.Content: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TOOD: need to locally dereference the schema, examples, and content encoding here. -#warning("need to locally dereference the schema, examples, and content encoding here.") - return (self, .init()) + let oldSchema = schema +// let oldExamples = examples +// let oldEncoding = encoding + + async let (newSchema, c1) = oldSchema.externallyDereferenced(with: loader) +// async let (newExamples, c2) = oldExamples.externallyDereferenced(with: loader) +// async let (newEncoding, c3) = oldEncoding.externallyDereferenced(with: loader) + + var newContent = self + var newComponents = try await c1 + + newContent.schema = try await newSchema +// newContext.examples = try await newExamples +// newComponents.merge(c2) +// newContext.encoding = try await newEncoding +// newComponents.merge(c3) + + // TOOD: need to locally dereference the examples, and encoding here. +#warning("need to locally dereference the examples, and encoding here.") + return (newContent, newComponents) } } diff --git a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift index f68c51449..6100b01ce 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -59,8 +59,24 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { extension OpenAPI.Content.Encoding: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: need to externally dereference the headers here. -#warning("need to externally dereference the headers here.") - return (self, .init()) + let newHeaders: OpenAPI.Header.Map? + let newComponents: OpenAPI.Components + + if let oldHeaders = headers { + (newHeaders, newComponents) = try await oldHeaders.externallyDereferenced(with: loader) + } else { + newHeaders = nil + newComponents = .init() + } + + let newEncoding = OpenAPI.Content.Encoding( + contentType: contentType, + headers: newHeaders, + style: style, + explode: explode, + allowReserved: allowReserved + ) + + return (newEncoding, newComponents) } } From 2508f854444e6e57a300b5dfdebe19ecacb36eef Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 7 Apr 2024 19:51:32 -0500 Subject: [PATCH 19/40] Add utility for externally dereferencing a regular non-ordered dictionary --- ...Dictionary+ExternallyDereferenceable.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift diff --git a/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift new file mode 100644 index 000000000..75a4cb0b0 --- /dev/null +++ b/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift @@ -0,0 +1,29 @@ +// +// Dictionary+ExternallyDereferenceable.swift +// OpenAPI +// + +import OpenAPIKitCore + +extension Dictionary where Value: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components).self) { group in + for (key, value) in self { + group.addTask { + let (newRef, components) = try await value.externallyDereferenced(with: loader) + return (key, newRef, components) + } + } + + var newDict = Self() + var newComponents = OpenAPI.Components() + + for try await (key, newRef, components) in group { + newDict[key] = newRef + try newComponents.merge(components) + } + return (newDict, newComponents) + } + } +} From 2b90c7c55e6d8ff38bfe9699b472b2c584271997 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 7 Apr 2024 19:51:54 -0500 Subject: [PATCH 20/40] a bit more work towards external dereferencing implementations --- .../Operation/DereferencedOperation.swift | 26 +++++++++++++++--- .../Parameter/DereferencedParameter.swift | 27 ++++++++++++++++--- .../Request/DereferencedRequest.swift | 9 ++++--- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index e39a3f975..4f7ccda18 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -127,8 +127,28 @@ extension OpenAPI.Operation: LocallyDereferenceable { extension OpenAPI.Operation: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference security, responses, requestBody, and parameters -#warning("externally dereference security, responses, requestBody, and parameters") - return (self, .init()) + let oldParameters = parameters + let oldRequestBody = requestBody + let oldResponses = responses + + async let (newParameters, c1) = oldParameters.externallyDereferenced(with: loader) + async let (newRequestBody, c2) = oldRequestBody.externallyDereferenced(with: loader) + async let (newResponses, c3) = oldResponses.externallyDereferenced(with: loader) +// let (newCallbacks, c4) = try await callbacks.externallyDereferenced(with: loader) +// let (newSecurtiy, c5) = try await security.externallyDereferenced(with: loader) +// let (newServers, c6) = try await servers.externallyDereferenced(with: loader) + + var newOperation = self + var newComponents = try await c1 + + newOperation.parameters = try await newParameters + newOperation.requestBody = try await newRequestBody + try await newComponents.merge(c2) + newOperation.responses = try await newResponses + try await newComponents.merge(c3) + + // TODO: externally dereference servers, callbacks, and security +#warning("externally dereference servers, callbacks, and security") + return (newOperation, newComponents) } } diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 5168ad22b..2d3153847 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -85,9 +85,28 @@ extension OpenAPI.Parameter: LocallyDereferenceable { extension OpenAPI.Parameter: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - - // TODO: externallyDerefence the schemaOrContent -#warning("need to externally dereference the schemaOrContent here") - return (self, .init()) + + // if not for a Swift bug, this whole function would just be the + // next line: +// let (newSchemaOrContent, components) = try await schemaOrContent.externallyDereferenced(with: loader) + + let newSchemaOrContent: Either + let newComponents: OpenAPI.Components + + switch schemaOrContent { + case .a(let schemaContext): + let (context, components) = try await schemaContext.externallyDereferenced(with: loader) + newSchemaOrContent = .a(context) + newComponents = components + case .b(let contentMap): + let (map, components) = try await contentMap.externallyDereferenced(with: loader) + newSchemaOrContent = .b(map) + newComponents = components + } + + var newParameter = self + newParameter.schemaOrContent = newSchemaOrContent + + return (newParameter, newComponents) } } diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index 97c947fde..27ee92d0b 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -64,8 +64,11 @@ extension OpenAPI.Request: LocallyDereferenceable { extension OpenAPI.Request: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference the content -#warning("externally dereference the content") - return (self, .init()) + var newRequest = self + + let (newContent, components) = try await content.externallyDereferenced(with: loader) + + newRequest.content = newContent + return (newRequest, components) } } From fdfb40ec7fc9da25c4e6be9686fb27cebe803fd0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 9 Apr 2024 08:39:09 -0500 Subject: [PATCH 21/40] Support external dereferencing for JSONSchema --- .../DereferencedJSONSchema.swift | 98 ++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 8cac1722a..6cfef7a2c 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -537,8 +537,100 @@ extension JSONSchema: LocallyDereferenceable { extension JSONSchema: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference this schema -#warning("need to externally dereference json schemas") - return (self, .init()) + let newSchema: JSONSchema + let newComponents: OpenAPI.Components + + switch value { + case .null(_): + newComponents = .noComponents + newSchema = self + case .boolean(_): + newComponents = .noComponents + newSchema = self + case .number(_, _): + newComponents = .noComponents + newSchema = self + case .integer(_, _): + newComponents = .noComponents + newSchema = self + case .string(_, _): + newComponents = .noComponents + newSchema = self + case .object(let core, let object): + var components = OpenAPI.Components() + + let (newProperties, c1) = try await object.properties.externallyDereferenced(with: loader) + try components.merge(c1) + + let newAdditionalProperties: Either? + if case .b(let schema) = object.additionalProperties { + let (additionalProperties, c2) = try await schema.externallyDereferenced(with: loader) + try components.merge(c2) + newAdditionalProperties = .b(additionalProperties) + } else { + newAdditionalProperties = object.additionalProperties + } + newComponents = components + newSchema = .init( + schema: .object( + core, + .init( + properties: newProperties, + additionalProperties: newAdditionalProperties, + maxProperties: object.maxProperties, + minProperties: object._minProperties + ) + ) + ) + case .array(let core, let array): + let (newItems, components) = try await array.items.externallyDereferenced(with: loader) + newComponents = components + newSchema = .init( + schema: .array( + core, + .init( + items: newItems, + maxItems: array.maxItems, + minItems: array._minItems, + uniqueItems: array._uniqueItems + ) + ) + ) + case .all(let schema, let core): + let (newSubschemas, components) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newSchema = .init( + schema: .all(of: newSubschemas, core: core) + ) + case .one(let schema, let core): + let (newSubschemas, components) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newSchema = .init( + schema: .one(of: newSubschemas, core: core) + ) + case .any(let schema, let core): + let (newSubschemas, components) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newSchema = .init( + schema: .any(of: newSubschemas, core: core) + ) + case .not(let schema, let core): + let (newSubschema, components) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newSchema = .init( + schema: .not(newSubschema, core: core) + ) + case .reference(let reference, let core): + let (newReference, components) = try await reference.externallyDereferenced(with: loader) + newComponents = components + newSchema = .init( + schema: .reference(newReference, core) + ) + case .fragment(_): + newComponents = .noComponents + newSchema = self + } + + return (newSchema, newComponents) } } From 3c1977789809e8b5b37027938d253b0efc5f42f5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 9 Apr 2024 10:43:29 -0500 Subject: [PATCH 22/40] more external dereferencing conformances --- Sources/OpenAPIKit/Callbacks.swift | 9 +++- .../Content/DereferencedContent.swift | 22 +++++---- .../Header/DereferencedHeader.swift | 31 ++++++++++-- Sources/OpenAPIKit/Link.swift | 17 ++++--- .../Operation/DereferencedOperation.swift | 47 ++++++++++++++++++- Sources/OpenAPIKit/Operation/Operation.swift | 2 +- .../Parameter/DereferencedSchemaContext.swift | 19 ++++++-- .../Parameter/ParameterSchemaContext.swift | 12 ++--- 8 files changed, 125 insertions(+), 34 deletions(-) diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 2f81e4a17..407c495f3 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -39,8 +39,13 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { extension OpenAPI.CallbackURL: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference security, responses, requestBody, and parameters -#warning("externally dereference security, responses, requestBody, and parameters") return (self, .init()) } } + +// The following conformance is theoretically unnecessary but the compiler is +// only able to find the conformance if we explicitly declare it here, though +// it is apparently able to determine the conformance is already satisfied here +// at least. +extension OpenAPI.Callbacks: ExternallyDereferenceable { } + diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index 926572467..18d02f624 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -79,24 +79,26 @@ extension OpenAPI.Content: LocallyDereferenceable { extension OpenAPI.Content: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let oldSchema = schema -// let oldExamples = examples -// let oldEncoding = encoding async let (newSchema, c1) = oldSchema.externallyDereferenced(with: loader) -// async let (newExamples, c2) = oldExamples.externallyDereferenced(with: loader) -// async let (newEncoding, c3) = oldEncoding.externallyDereferenced(with: loader) var newContent = self var newComponents = try await c1 newContent.schema = try await newSchema -// newContext.examples = try await newExamples -// newComponents.merge(c2) -// newContext.encoding = try await newEncoding -// newComponents.merge(c3) - // TOOD: need to locally dereference the examples, and encoding here. -#warning("need to locally dereference the examples, and encoding here.") + if let oldExamples = examples { + let (newExamples, c2) = try await oldExamples.externallyDereferenced(with: loader) + newContent.examples = newExamples + try newComponents.merge(c2) + } + + if let oldEncoding = encoding { + let (newEncoding, c3) = try await oldEncoding.externallyDereferenced(with: loader) + newContent.encoding = newEncoding + try newComponents.merge(c3) + } + return (newContent, newComponents) } } diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index d34a7374c..891331af9 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -85,8 +85,33 @@ extension OpenAPI.Header: LocallyDereferenceable { extension OpenAPI.Header: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference the schemaOrContent -#warning("externally dereference the schemaOrContent") - return (self, .init()) + + // if not for a Swift bug, this whole next bit would just be the + // next line: +// let (newSchemaOrContent, components) = try await schemaOrContent.externallyDereferenced(with: loader) + + let newSchemaOrContent: Either + let newComponents: OpenAPI.Components + + switch schemaOrContent { + case .a(let schemaContext): + let (context, components) = try await schemaContext.externallyDereferenced(with: loader) + newSchemaOrContent = .a(context) + newComponents = components + case .b(let contentMap): + let (map, components) = try await contentMap.externallyDereferenced(with: loader) + newSchemaOrContent = .b(map) + newComponents = components + } + + let newHeader = OpenAPI.Header( + schemaOrContent: newSchemaOrContent, + description: description, + required: required, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + + return (newHeader, newComponents) } } diff --git a/Sources/OpenAPIKit/Link.swift b/Sources/OpenAPIKit/Link.swift index 409b1ca4d..d79316bad 100644 --- a/Sources/OpenAPIKit/Link.swift +++ b/Sources/OpenAPIKit/Link.swift @@ -20,18 +20,18 @@ extension OpenAPI { /// The **OpenAPI**` `operationRef` or `operationId` field, depending on whether /// a `URL` of a remote or local Operation Object or a `operationId` (String) of an /// operation defined in the same document is given. - public let operation: Either + public var operation: Either /// A map from parameter names to either runtime expressions that evaluate to values or /// constant values (`AnyCodable`). /// /// See the docuemntation for the [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object) for more details. /// /// Empty dictionaries will be omitted from encoding. - public let parameters: OrderedDictionary> + public var parameters: OrderedDictionary> /// A literal value or expression to use as a request body when calling the target operation. - public let requestBody: Either? + public var requestBody: Either? public var description: String? - public let server: Server? + public var server: Server? /// Dictionary of vendor extensions. /// @@ -291,9 +291,12 @@ extension OpenAPI.Link: LocallyDereferenceable { extension OpenAPI.Link: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference security, responses, requestBody, and parameters -#warning("externally dereference security, responses, requestBody, and parameters") - return (self, .init()) + let (newServer, newComponents) = try await server.externallyDereferenced(with: loader) + + var newLink = self + newLink.server = newServer + + return (newLink, newComponents) } } diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index 4f7ccda18..7e32c7f14 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -147,8 +147,51 @@ extension OpenAPI.Operation: ExternallyDereferenceable { newOperation.responses = try await newResponses try await newComponents.merge(c3) - // TODO: externally dereference servers, callbacks, and security -#warning("externally dereference servers, callbacks, and security") + // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: + var newCallbacks = OpenAPI.CallbacksMap() + for (key, value) in callbacks { + switch value { + case .a(let callbackReference): + let oldJsonReference = callbackReference.jsonReference + + let (newJsonReference, components) = try await oldJsonReference.externallyDereferenced(with: loader) + try newComponents.merge(components) + + let newCallbackReference = OpenAPI.Reference( + newJsonReference, + summary: callbackReference.summary, + description: callbackReference.description + ) + + newCallbacks[key] = .a(newCallbackReference) + case .b(let callback): + let (newCallback, components) = try await callback.externallyDereferenced(with: loader) + newCallbacks[key] = .b(newCallback) + try newComponents.merge(components) + } + } + newOperation.callbacks = newCallbacks + + if let oldServers = servers { + let (newServers, c6) = try await oldServers.externallyDereferenced(with: loader) + newOperation.servers = newServers + try newComponents.merge(c6) + } + + // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: + if let oldRequestBody = requestBody { + let (newRequestBody, c5) = try await oldRequestBody.externallyDereferenced(with: loader) + newOperation.requestBody = newRequestBody + try newComponents.merge(c5) + } + + // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: + if let oldServers = servers { + let (newServers, c6) = try await oldServers.externallyDereferenced(with: loader) + newOperation.servers = newServers + try newComponents.merge(c6) + } + return (newOperation, newComponents) } } diff --git a/Sources/OpenAPIKit/Operation/Operation.swift b/Sources/OpenAPIKit/Operation/Operation.swift index 4efdc5899..c5adb49fa 100644 --- a/Sources/OpenAPIKit/Operation/Operation.swift +++ b/Sources/OpenAPIKit/Operation/Operation.swift @@ -76,7 +76,7 @@ extension OpenAPI { /// The key is a unique identifier for the Callback Object. Each value in the /// map is a Callback Object that describes a request that may be initiated /// by the API provider and the expected responses. - public let callbacks: OpenAPI.CallbacksMap + public var callbacks: OpenAPI.CallbacksMap /// Indicates that the operation is deprecated or not. /// diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 81d01ef8f..00cfaa7af 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -72,8 +72,21 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { extension OpenAPI.Parameter.SchemaContext: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - // TODO: externally dereference schema, examples, and example -#warning("externally dereference schema, examples, and example") - return (self, .init()) + let oldSchema = schema + + async let (newSchema, c1) = oldSchema.externallyDereferenced(with: loader) + + var newSchemaContext = self + var newComponents = try await c1 + + newSchemaContext.schema = try await newSchema + + if let oldExamples = examples { + let (newExamples, c2) = try await oldExamples.externallyDereferenced(with: loader) + newSchemaContext.examples = newExamples + try newComponents.merge(c2) + } + + return (newSchemaContext, newComponents) } } diff --git a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift index 336305738..cd65f89c5 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift @@ -13,13 +13,13 @@ extension OpenAPI.Parameter { /// See [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#parameter-object) /// and [OpenAPI Style Values](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#style-values). public struct SchemaContext: Equatable { - public let style: Style - public let explode: Bool - public let allowReserved: Bool //defaults to false - public let schema: Either, JSONSchema> + public var style: Style + public var explode: Bool + public var allowReserved: Bool //defaults to false + public var schema: Either, JSONSchema> - public let example: AnyCodable? - public let examples: OpenAPI.Example.Map? + public var example: AnyCodable? + public var examples: OpenAPI.Example.Map? public init(_ schema: JSONSchema, style: Style, From 93486b2a61a40d4c63f77d050235080913514b05 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 10 Apr 2024 08:09:38 -0500 Subject: [PATCH 23/40] fix temporary test. fix path operation external dereferencing. remove most over the top workaround code having found a better workaround for compiler bug. --- .../Operation/DereferencedOperation.swift | 37 ++----------------- .../Path Item/PathItemTests.swift | 11 ++++-- 2 files changed, 10 insertions(+), 38 deletions(-) diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index 7e32c7f14..8f7844694 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -134,8 +134,7 @@ extension OpenAPI.Operation: ExternallyDereferenceable { async let (newParameters, c1) = oldParameters.externallyDereferenced(with: loader) async let (newRequestBody, c2) = oldRequestBody.externallyDereferenced(with: loader) async let (newResponses, c3) = oldResponses.externallyDereferenced(with: loader) -// let (newCallbacks, c4) = try await callbacks.externallyDereferenced(with: loader) -// let (newSecurtiy, c5) = try await security.externallyDereferenced(with: loader) + async let (newCallbacks, c4) = callbacks.externallyDereferenced(with: loader) // let (newServers, c6) = try await servers.externallyDereferenced(with: loader) var newOperation = self @@ -146,44 +145,14 @@ extension OpenAPI.Operation: ExternallyDereferenceable { try await newComponents.merge(c2) newOperation.responses = try await newResponses try await newComponents.merge(c3) - - // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: - var newCallbacks = OpenAPI.CallbacksMap() - for (key, value) in callbacks { - switch value { - case .a(let callbackReference): - let oldJsonReference = callbackReference.jsonReference - - let (newJsonReference, components) = try await oldJsonReference.externallyDereferenced(with: loader) - try newComponents.merge(components) - - let newCallbackReference = OpenAPI.Reference( - newJsonReference, - summary: callbackReference.summary, - description: callbackReference.description - ) - - newCallbacks[key] = .a(newCallbackReference) - case .b(let callback): - let (newCallback, components) = try await callback.externallyDereferenced(with: loader) - newCallbacks[key] = .b(newCallback) - try newComponents.merge(components) - } - } - newOperation.callbacks = newCallbacks + newOperation.callbacks = try await newCallbacks + try await newComponents.merge(c4) if let oldServers = servers { let (newServers, c6) = try await oldServers.externallyDereferenced(with: loader) newOperation.servers = newServers try newComponents.merge(c6) } - - // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: - if let oldRequestBody = requestBody { - let (newRequestBody, c5) = try await oldRequestBody.externallyDereferenced(with: loader) - newOperation.requestBody = newRequestBody - try newComponents.merge(c5) - } // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: if let oldServers = servers { diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index 2e55b6aec..ebd4188dc 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -7,6 +7,7 @@ import XCTest import OpenAPIKit +import Foundation final class PathItemTests: XCTestCase { func test_initializePathComponents() { @@ -436,8 +437,10 @@ extension PathItemTests { // MARK: External Dereferencing Tests extension PathItemTests { struct MockLoad: ExternalLoaderContext { - static func componentKey(type: T.Type, at url: URL) -> OpenAPI.ComponentKey { - "hello-world" + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + let urlString = url.pathComponents.dropFirst().joined(separator: "_").replacingOccurrences(of: ".", with: "_") + + return try .forceInit(rawValue: urlString) } static func load(_: URL) throws -> T where T : Decodable { @@ -460,9 +463,9 @@ extension PathItemTests { parameters: [ .parameter(name: "hello", context: .query, schema: .string), .reference(.component(named: "already-internal")), - .reference(.external(URL(string: "https://some-param.com")!)) + .reference(.external(URL(string: "https://some-param.com/param")!)) ], - get: .init(requestBody: .reference(.external(URL(string: "https://website.com")!)), responses: [:]), + get: .init(requestBody: .reference(.external(URL(string: "https://website.com/request")!)), responses: [:]), put: op, post: op, delete: op, From 6d6be196945f651dfa74de6d94603ee14155b0a8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 18 Apr 2024 09:00:46 -0500 Subject: [PATCH 24/40] remove unnecessary change --- Sources/OpenAPIKit/JSONReference.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index d3f4fb72e..259ae77ca 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -511,7 +511,7 @@ extension OpenAPI.Reference: Decodable { } // MARK: - LocallyDereferenceable -extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable { +extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable { /// Look up the component this reference points to and then /// dereference it. /// @@ -552,7 +552,7 @@ extension JSONReference: ExternallyDereferenceable where ReferenceType: External } } -extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable { +extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable { /// Look up the component this reference points to and then /// dereference it. /// From 3fc8750d49af0603bed2fcc987451bf32e171e08 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 18 Apr 2024 10:37:04 -0500 Subject: [PATCH 25/40] rename ExternalLoaderContext to ExternalLoader --- Sources/OpenAPIKit/Callbacks.swift | 2 +- .../OpenAPIKit/Components Object/Components.swift | 2 +- .../OpenAPIKit/Content/DereferencedContent.swift | 2 +- .../Content/DereferencedContentEncoding.swift | 2 +- Sources/OpenAPIKit/Document/Document.swift | 2 +- .../Either/Either+ExternallyDereferenceable.swift | 2 +- Sources/OpenAPIKit/Example.swift | 2 +- Sources/OpenAPIKit/ExternalLoader.swift | 6 +++--- Sources/OpenAPIKit/Header/DereferencedHeader.swift | 2 +- Sources/OpenAPIKit/JSONReference.swift | 4 ++-- Sources/OpenAPIKit/Link.swift | 2 +- .../Operation/DereferencedOperation.swift | 2 +- .../Parameter/DereferencedParameter.swift | 2 +- .../Parameter/DereferencedSchemaContext.swift | 2 +- .../Path Item/DereferencedPathItem.swift | 2 +- .../OpenAPIKit/Request/DereferencedRequest.swift | 2 +- .../OpenAPIKit/Response/DereferencedResponse.swift | 2 +- .../Schema Object/DereferencedJSONSchema.swift | 2 +- Sources/OpenAPIKit/Security/SecurityScheme.swift | 2 +- Sources/OpenAPIKit/Server.swift | 2 +- .../Utility/Array+ExternallyDereferenceable.swift | 2 +- .../Dictionary+ExternallyDereferenceable.swift | 2 +- .../Optional+ExternallyDereferenceable.swift | 2 +- ...deredDictionary+ExternallyDereferenceable.swift | 2 +- Tests/OpenAPIKitTests/Document/DocumentTests.swift | 2 +- Tests/OpenAPIKitTests/JSONReferenceTests.swift | 14 ++++++++++++++ .../OpenAPIKitTests/Path Item/PathItemTests.swift | 2 +- 27 files changed, 43 insertions(+), 29 deletions(-) diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 407c495f3..30baf3e3d 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -38,7 +38,7 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { } extension OpenAPI.CallbackURL: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index eec77534f..0c2798b87 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -291,7 +291,7 @@ extension OpenAPI.Components { } extension OpenAPI.Components { - internal mutating func externallyDereference(in context: Context.Type) async throws { + internal mutating func externallyDereference(in context: Context.Type) async throws { let oldSchemas = schemas let oldResponses = responses let oldParameters = parameters diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index 18d02f624..992eea1a7 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -77,7 +77,7 @@ extension OpenAPI.Content: LocallyDereferenceable { } extension OpenAPI.Content: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let oldSchema = schema async let (newSchema, c1) = oldSchema.externallyDereferenced(with: loader) diff --git a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift index 6100b01ce..f6d43cb5e 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -58,7 +58,7 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { } extension OpenAPI.Content.Encoding: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let newHeaders: OpenAPI.Header.Map? let newComponents: OpenAPI.Components diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index 5f5cf19c8..5807b265f 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -351,7 +351,7 @@ extension OpenAPI.Document { return try DereferencedDocument(self) } - public mutating func externallyDereference(in context: Context.Type) async throws { + public mutating func externallyDereference(in context: Context.Type) async throws { let oldPaths = paths let oldWebhooks = webhooks diff --git a/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift index 10e56b368..5dfe12868 100644 --- a/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore // MARK: - ExternallyDereferenceable extension Either: ExternallyDereferenceable where A: ExternallyDereferenceable, B: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { switch self { case .a(let a): let (newA, components) = try await a.externallyDereferenced(with: loader) diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index 8831c6d97..d61fc8392 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -209,7 +209,7 @@ extension OpenAPI.Example: LocallyDereferenceable { } extension OpenAPI.Example: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { return (self, .init()) } } diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift index f937ac917..2b62c1699 100644 --- a/Sources/OpenAPIKit/ExternalLoader.swift +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -8,10 +8,10 @@ import OpenAPIKitCore import Foundation -/// An `ExternalLoaderContext` enables `OpenAPIKit` to load external references +/// An `ExternalLoader` enables `OpenAPIKit` to load external references /// without knowing the details of what decoder is being used or how new internal /// references should be named. -public protocol ExternalLoaderContext { +public protocol ExternalLoader { /// Load the given URL and decode it as Type `T`. All Types `T` are `Decodable`, so /// the only real responsibility of a `load` function is to locate and load the given /// `URL` and pass its `Data` or `String` (depending on the decoder) to an appropriate @@ -30,5 +30,5 @@ public protocol ExternalLoaderContext { } public protocol ExternallyDereferenceable { - func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) + func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) } diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 891331af9..554a5b266 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -84,7 +84,7 @@ extension OpenAPI.Header: LocallyDereferenceable { } extension OpenAPI.Header: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // if not for a Swift bug, this whole next bit would just be the // next line: diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 259ae77ca..75621e00a 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -538,7 +538,7 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere } extension JSONReference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { switch self { case .internal(let ref): return (.internal(ref), .init()) @@ -580,7 +580,7 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally } extension OpenAPI.Reference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let (newRef, components) = try await jsonReference.externallyDereferenced(with: loader) return (.init(newRef), components) } diff --git a/Sources/OpenAPIKit/Link.swift b/Sources/OpenAPIKit/Link.swift index d79316bad..be416dcff 100644 --- a/Sources/OpenAPIKit/Link.swift +++ b/Sources/OpenAPIKit/Link.swift @@ -290,7 +290,7 @@ extension OpenAPI.Link: LocallyDereferenceable { } extension OpenAPI.Link: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let (newServer, newComponents) = try await server.externallyDereferenced(with: loader) var newLink = self diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index 8f7844694..dfdd14c9d 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -126,7 +126,7 @@ extension OpenAPI.Operation: LocallyDereferenceable { } extension OpenAPI.Operation: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let oldParameters = parameters let oldRequestBody = requestBody let oldResponses = responses diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 2d3153847..b2b6b9604 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -84,7 +84,7 @@ extension OpenAPI.Parameter: LocallyDereferenceable { } extension OpenAPI.Parameter: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { // if not for a Swift bug, this whole function would just be the // next line: diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 00cfaa7af..cea39b921 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -71,7 +71,7 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { } extension OpenAPI.Parameter.SchemaContext: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let oldSchema = schema async let (newSchema, c1) = oldSchema.externallyDereferenced(with: loader) diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index 6f4727f45..fc284b1df 100644 --- a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift @@ -140,7 +140,7 @@ extension OpenAPI.PathItem: LocallyDereferenceable { } extension OpenAPI.PathItem: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let oldParameters = parameters let oldServers = servers let oldGet = get diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index 27ee92d0b..a5dce43bd 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -63,7 +63,7 @@ extension OpenAPI.Request: LocallyDereferenceable { } extension OpenAPI.Request: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { var newRequest = self let (newContent, components) = try await content.externallyDereferenced(with: loader) diff --git a/Sources/OpenAPIKit/Response/DereferencedResponse.swift b/Sources/OpenAPIKit/Response/DereferencedResponse.swift index 3a8e7bdb6..363e33448 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -78,7 +78,7 @@ extension OpenAPI.Response: LocallyDereferenceable { } extension OpenAPI.Response: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let oldContent = content let oldLinks = links diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 6cfef7a2c..faeccde28 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -536,7 +536,7 @@ extension JSONSchema: LocallyDereferenceable { } extension JSONSchema: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { let newSchema: JSONSchema let newComponents: OpenAPI.Components diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 2429466c1..a304bccfd 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -275,7 +275,7 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { } extension OpenAPI.SecurityScheme: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Server.swift b/Sources/OpenAPIKit/Server.swift index ad9bf6094..4bea3607f 100644 --- a/Sources/OpenAPIKit/Server.swift +++ b/Sources/OpenAPIKit/Server.swift @@ -260,7 +260,7 @@ extension OpenAPI.Server.Variable { } extension OpenAPI.Server: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { return (self, .init()) } } diff --git a/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift index 59970df82..bb570c7b8 100644 --- a/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift @@ -6,7 +6,7 @@ import OpenAPIKitCore extension Array where Element: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { try await withThrowingTaskGroup(of: (Element, OpenAPI.Components).self) { group in for elem in self { group.addTask { diff --git a/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift index 75a4cb0b0..2ba0a8fcb 100644 --- a/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift @@ -7,7 +7,7 @@ import OpenAPIKitCore extension Dictionary where Value: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components).self) { group in for (key, value) in self { group.addTask { diff --git a/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift index 671657f86..adf2b1816 100644 --- a/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift @@ -6,7 +6,7 @@ import OpenAPIKitCore extension Optional where Wrapped: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { guard let wrapped = self else { return (nil, .init()) } return try await wrapped.externallyDereferenced(with: loader) } diff --git a/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift index 94718b0a6..037cdabc0 100644 --- a/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift @@ -9,7 +9,7 @@ import OpenAPIKitCore extension OrderedDictionary where Value: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { + public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components).self) { group in for (key, value) in self { group.addTask { diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 1a5e0ae15..a2d02698b 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1179,7 +1179,7 @@ extension DocumentTests { /// An example of implementing a loader context for loading external references /// into an OpenAPI document. - struct ExampleLoaderContext: ExternalLoaderContext { + struct ExampleLoaderContext: ExternalLoader { static func load(_ url: URL) async throws -> T where T : Decodable { // load data from file, perhaps. we will just mock that up for the example: let data = await mockParameterData(url) diff --git a/Tests/OpenAPIKitTests/JSONReferenceTests.swift b/Tests/OpenAPIKitTests/JSONReferenceTests.swift index 53c7a8b64..13b385f5c 100644 --- a/Tests/OpenAPIKitTests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKitTests/JSONReferenceTests.swift @@ -377,9 +377,23 @@ extension JSONReferenceTests { } } +// MARK: - External Dereferencing +extension JSONReferenceTests { + +} + // MARK: - Test Types extension JSONReferenceTests { struct ReferenceWrapper: Codable, Equatable { let reference: JSONReference } + + struct SchemaLoader: ExternalLoader { + static func load(_ url: URL) -> T where T: Decodable { + return JSONSchema.string as! T + } + + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + } + } } diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index ebd4188dc..62ddd9a83 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -436,7 +436,7 @@ extension PathItemTests { // MARK: External Dereferencing Tests extension PathItemTests { - struct MockLoad: ExternalLoaderContext { + struct MockLoad: ExternalLoader { static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { let urlString = url.pathComponents.dropFirst().joined(separator: "_").replacingOccurrences(of: ".", with: "_") From 47af3484da9f04948f9c10cf0ff206c5e096bbcd Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 18 Apr 2024 11:01:12 -0500 Subject: [PATCH 26/40] fill in some actual external dereferencing tests for json references --- .../OpenAPIKitTests/JSONReferenceTests.swift | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIKitTests/JSONReferenceTests.swift b/Tests/OpenAPIKitTests/JSONReferenceTests.swift index 13b385f5c..c5e74f18f 100644 --- a/Tests/OpenAPIKitTests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKitTests/JSONReferenceTests.swift @@ -379,7 +379,32 @@ extension JSONReferenceTests { // MARK: - External Dereferencing extension JSONReferenceTests { - + func test_externalDerefNoFragment() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json")!) + + let (newReference, components) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json")) + XCTAssertEqual(components, .init(schemas: ["__schema_json": .string])) + } + + func test_externalDerefFragment() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json#/test")!) + + let (newReference, components) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json__test")) + XCTAssertEqual(components, .init(schemas: ["__schema_json__test": .string])) + } + + func test_externalDerefExternalComponents() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json#/components/schemas/test")!) + + let (newReference, components) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json__components_schemas_test")) + XCTAssertEqual(components, .init(schemas: ["__schema_json__components_schemas_test": .string])) + } } // MARK: - Test Types @@ -394,6 +419,10 @@ extension JSONReferenceTests { } static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + return try .forceInit(rawValue: url.absoluteString + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "#", with: "_") + .replacingOccurrences(of: ".", with: "_")) } } } From f11d2cd1f6fab0fc257efff5c59548f1e46c6fb9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 21 Apr 2024 13:51:34 -0500 Subject: [PATCH 27/40] start building a Document test for external dereferencing. fix component collisions to not occur for identical components --- .../Components Object/Components.swift | 29 +++++++----- Sources/OpenAPIKit/Parameter/Parameter.swift | 2 + .../Document/DocumentTests.swift | 46 ++++++++++++++----- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index 0c2798b87..0cc169a99 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -78,18 +78,25 @@ extension OpenAPI.Components { public let newComponent: String } + private func detectCollision(type: String) throws -> (_ old: T, _ new: T) throws -> T { + return { old, new in + if(old == new) { return old } + throw ComponentCollision(componentType: type, existingComponent: String(describing:old), newComponent: String(describing:new)) + } + } + public mutating func merge(_ other: OpenAPI.Components) throws { - try schemas.merge(other.schemas, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "schema", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try responses.merge(other.responses, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "responses", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try parameters.merge(other.parameters, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "parameters", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try examples.merge(other.examples, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "examples", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try requestBodies.merge(other.requestBodies, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "requestBodies", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try headers.merge(other.headers, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "headers", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "securitySchemes", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try links.merge(other.links, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "links", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try callbacks.merge(other.callbacks, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "callbacks", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try pathItems.merge(other.pathItems, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "pathItems", existingComponent: String(describing: a), newComponent: String(describing: b)) }) - try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: { a, b in throw ComponentCollision(componentType: "vendorExtensions", existingComponent: String(describing: a), newComponent: String(describing: b)) }) + try schemas.merge(other.schemas, uniquingKeysWith: detectCollision(type: "schema")) + try responses.merge(other.responses, uniquingKeysWith: detectCollision(type: "responses")) + try parameters.merge(other.parameters, uniquingKeysWith: detectCollision(type: "parameters")) + try examples.merge(other.examples, uniquingKeysWith: detectCollision(type: "examples")) + try requestBodies.merge(other.requestBodies, uniquingKeysWith: detectCollision(type: "requestBodies")) + try headers.merge(other.headers, uniquingKeysWith: detectCollision(type: "headers")) + try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: detectCollision(type: "securitySchemes")) + try links.merge(other.links, uniquingKeysWith: detectCollision(type: "links")) + try callbacks.merge(other.callbacks, uniquingKeysWith: detectCollision(type: "callbacks")) + try pathItems.merge(other.pathItems, uniquingKeysWith: detectCollision(type: "pathItems")) + try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: detectCollision(type: "vendorExtensions")) } } diff --git a/Sources/OpenAPIKit/Parameter/Parameter.swift b/Sources/OpenAPIKit/Parameter/Parameter.swift index a65dfcb0c..9c4cf5611 100644 --- a/Sources/OpenAPIKit/Parameter/Parameter.swift +++ b/Sources/OpenAPIKit/Parameter/Parameter.swift @@ -21,6 +21,8 @@ extension OpenAPI { /// parameters in the given location. public var context: Context public var description: String? + /// Whether or not the parameter is deprecated. Defaults to false + /// if unspecified and only gets encoded if true. public var deprecated: Bool // default is false /// OpenAPI Spec "content" or "schema" properties. diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index a2d02698b..e28ec4ea6 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1179,13 +1179,16 @@ extension DocumentTests { /// An example of implementing a loader context for loading external references /// into an OpenAPI document. - struct ExampleLoaderContext: ExternalLoader { + struct ExampleLoader: ExternalLoader { static func load(_ url: URL) async throws -> T where T : Decodable { - // load data from file, perhaps. we will just mock that up for the example: - let data = await mockParameterData(url) + // load data from file, perhaps. we will just mock that up for the test: + let data = try await mockData(componentKey(type: T.self, at: url)) let decoded = try JSONDecoder().decode(T.self, from: data) let finished: T + // while unnecessary, a loader may likely want to attatch some extra info + // to keep track of where a reference was loaded from. This test makes sure + // the following strategy of using vendor extensions works. if var extendable = decoded as? VendorExtendable { extendable.vendorExtensions["x-source-url"] = AnyCodable(url) finished = extendable as! T @@ -1198,24 +1201,38 @@ extension DocumentTests { static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { // do anything you want here to determine what key the new component should be stored at. // for the example, we will just transform the URL into a valid components key: - let urlString = url.pathComponents.dropFirst().joined(separator: "_").replacingOccurrences(of: ".", with: "_") + let urlString = url.pathComponents.dropFirst() + .joined(separator: "_") + .replacingOccurrences(of: ".", with: "_") return try .forceInit(rawValue: urlString) } /// Mock up some data, just for the example. - static func mockParameterData(_ url: URL) async -> Data { - return """ + static func mockData(_ key: OpenAPIKit.OpenAPI.ComponentKey) async throws -> Data { + print("looking up \(key.rawValue)") + return try XCTUnwrap(files[key.rawValue]) + } + + static let files: [String: Data] = [ + "params_name_json": """ { "name": "name", + "description": "a lonely parameter", "in": "path", - "schema": { "type": "string" }, - "required": true + "required": true, + "schema": { + "$ref": "file://./schemas/name_param.json#" + } } - """.data(using: .utf8)! - } + """, + "schemas_name_param_json": """ + { + "type": "string" + } + """ + ].mapValues { $0.data(using: .utf8)! } } - var document = OpenAPI.Document( info: .init(title: "test document", version: "1.0.0"), servers: [], @@ -1224,6 +1241,11 @@ extension DocumentTests { parameters: [ .reference(.external(URL(string: "file://./params/name.json")!)) ] + ), + "/goodbye/{name}": .init( + parameters: [ + .reference(.external(URL(string: "file://./params/name.json")!)) + ] ) ], components: .init( @@ -1258,7 +1280,7 @@ extension DocumentTests { } */ - try await document.externallyDereference(in: ExampleLoaderContext.self) + try await document.externallyDereference(in: ExampleLoader.self) // - MARK: After print( From e17bfd84eca9cce02a9c5033652f58fe4178ecde Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 21 Apr 2024 14:17:24 -0500 Subject: [PATCH 28/40] Add depth to external dereferencing of the whole document --- Sources/OpenAPIKit/Document/Document.swift | 6 +- .../Document/DocumentTests.swift | 88 +++++++++---------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index 5807b265f..c8eaa0afa 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -351,7 +351,9 @@ extension OpenAPI.Document { return try DereferencedDocument(self) } - public mutating func externallyDereference(in context: Context.Type) async throws { + public mutating func externallyDereference(in context: Context.Type, depth: Int = 1) async throws { + guard depth > 0 else { return } + let oldPaths = paths let oldWebhooks = webhooks @@ -364,6 +366,8 @@ extension OpenAPI.Document { webhooks = try await newWebhooks try await components.merge(c1) try await components.merge(c2) + + try await externallyDereference(in: context, depth: depth - 1) } } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index e28ec4ea6..6f1ba6761 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1233,7 +1233,7 @@ extension DocumentTests { ].mapValues { $0.data(using: .utf8)! } } - var document = OpenAPI.Document( + let document = OpenAPI.Document( info: .init(title: "test document", version: "1.0.0"), servers: [], paths: [ @@ -1254,66 +1254,66 @@ extension DocumentTests { ) ) - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + var docCopy1 = document + try await docCopy1.externallyDereference(in: ExampleLoader.self) + try await docCopy1.externallyDereference(in: ExampleLoader.self) + + var docCopy2 = document + try await docCopy2.externallyDereference(in: ExampleLoader.self, depth: 2) + + XCTAssertEqual(String(describing: docCopy1), String(describing: docCopy2)) - // - MARK: Before + // - MARK: After + print( + String(data: try encoder.encode(docCopy1), encoding: .utf8)! + ) print( - String(data: try encoder.encode(document), encoding: .utf8)! + String(data: try encoder.encode(docCopy2), encoding: .utf8)! ) /* { - "openapi": "3.1.0", - "info": { - "title": "test document", - "version": "1.0.0" + "info" : { + "version" : "1.0.0", + "title" : "test document" }, - "paths": { - "\/hello\/{name}": { - "parameters": [ + "openapi" : "3.1.0", + "paths" : { + "\/goodbye\/{name}" : { + "parameters" : [ { - "$ref": "file:\/\/.\/params\/name.json" + "$ref" : "#\/components\/parameters\/params_name_json" } ] - } - } - } - */ - - try await document.externallyDereference(in: ExampleLoader.self) - - // - MARK: After - print( - String(data: try encoder.encode(document), encoding: .utf8)! - ) - /* - { - "paths": { - "\/hello\/{name}": { - "parameters": [ + }, + "\/hello\/{name}" : { + "parameters" : [ { - "$ref": "#\/components\/parameters\/params_name_json" + "$ref" : "#\/components\/parameters\/params_name_json" } ] } }, - "components": { - "parameters": { - "params_name_json": { - "x-source-url": "file:\/\/.\/params\/name.json", - "in": "path", - "name": "name", - "required": true, - "schema": { - "type": "string" + "components" : { + "parameters" : { + "params_name_json" : { + "description" : "a lonely parameter", + "in" : "path", + "name" : "name", + "x-source-url" : "file:\/\/.\/params\/name.json", + "required" : true, + "schema" : { + "$ref" : "#\/components\/schemas\/schemas_name_param_json" } } + }, + "schemas" : { + "schemas_name_param_json" : { + "type" : "string" + } } - }, - "openapi": "3.1.0", - "info": { - "title": "test document", - "version": "1.0.0" } } */ From b032ded5dc1a204eeb44986f76598a0d40ccb41f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 21 Apr 2024 15:16:43 -0500 Subject: [PATCH 29/40] support depth less naively. allow full depth --- .../Components Object/Components.swift | 53 +++++++++++++++---- Sources/OpenAPIKit/Document/Document.swift | 22 ++++++-- .../Document/DocumentTests.swift | 6 ++- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index 0cc169a99..b9193e6dd 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -298,7 +298,12 @@ extension OpenAPI.Components { } extension OpenAPI.Components { - internal mutating func externallyDereference(in context: Context.Type) async throws { + internal mutating func externallyDereference(in context: Context.Type, depth: ExternalDereferenceDepth = .iterations(1)) async throws { + if case let .iterations(number) = depth, + number <= 0 { + return + } + let oldSchemas = schemas let oldResponses = responses let oldParameters = parameters @@ -336,15 +341,43 @@ extension OpenAPI.Components { callbacks = newCallbacks - try await merge(c1) - try await merge(c2) - try await merge(c3) - try await merge(c4) - try await merge(c5) - try await merge(c6) - try await merge(c7) - - try merge(c8) + let c1Resolved = try await c1 + let c2Resolved = try await c2 + let c3Resolved = try await c3 + let c4Resolved = try await c4 + let c5Resolved = try await c5 + let c6Resolved = try await c6 + let c7Resolved = try await c7 + let c8Resolved = c8 + + let noNewComponents = + c1Resolved.isEmpty + && c2Resolved.isEmpty + && c3Resolved.isEmpty + && c4Resolved.isEmpty + && c5Resolved.isEmpty + && c6Resolved.isEmpty + && c7Resolved.isEmpty + && c8Resolved.isEmpty + + if noNewComponents { return } + + try merge(c1Resolved) + try merge(c2Resolved) + try merge(c3Resolved) + try merge(c4Resolved) + try merge(c5Resolved) + try merge(c6Resolved) + try merge(c7Resolved) + + try merge(c8Resolved) + + switch depth { + case .iterations(let number): + try await externallyDereference(in: context, depth: .iterations(number - 1)) + case .full: + try await externallyDereference(in: context, depth: .full) + } } } diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index c8eaa0afa..75a703363 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -324,6 +324,17 @@ extension OpenAPI.Document { } } +public enum ExternalDereferenceDepth { + case iterations(Int) + case full +} + +extension ExternalDereferenceDepth: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .iterations(value) + } +} + extension OpenAPI.Document { /// Create a locally-dereferenced OpenAPI /// Document. @@ -351,14 +362,15 @@ extension OpenAPI.Document { return try DereferencedDocument(self) } - public mutating func externallyDereference(in context: Context.Type, depth: Int = 1) async throws { - guard depth > 0 else { return } + public mutating func externallyDereference(in context: Context.Type, depth: ExternalDereferenceDepth = .iterations(1)) async throws { + if case let .iterations(number) = depth, + number <= 0 { + return + } let oldPaths = paths let oldWebhooks = webhooks - try await components.externallyDereference(in: context) - async let (newPaths, c1) = oldPaths.externallyDereferenced(with: context) async let (newWebhooks, c2) = oldWebhooks.externallyDereferenced(with: context) @@ -367,7 +379,7 @@ extension OpenAPI.Document { try await components.merge(c1) try await components.merge(c2) - try await externallyDereference(in: context, depth: depth - 1) + try await components.externallyDereference(in: context, depth: depth) } } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 6f1ba6761..7a57f642c 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1264,7 +1264,11 @@ extension DocumentTests { var docCopy2 = document try await docCopy2.externallyDereference(in: ExampleLoader.self, depth: 2) - XCTAssertEqual(String(describing: docCopy1), String(describing: docCopy2)) + var docCopy3 = document + try await docCopy3.externallyDereference(in: ExampleLoader.self, depth: .full) + + XCTAssertEqual(docCopy1, docCopy2) + XCTAssertEqual(docCopy2, docCopy3) // - MARK: After print( From b80657eec68f0aa9d03af59302dc52d732ccd7b4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 21 Apr 2024 15:47:54 -0500 Subject: [PATCH 30/40] fix issue with async ordered dictionary code not retaining original order. --- ...deredDictionary+ExternallyDereferenceable.swift | 3 +++ .../OrderedDictionary/OrderedDictionary.swift | 14 ++++++++++++++ Tests/OpenAPIKitTests/Document/DocumentTests.swift | 3 --- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift index 037cdabc0..283aab817 100644 --- a/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift @@ -25,6 +25,9 @@ extension OrderedDictionary where Value: ExternallyDereferenceable { newDict[key] = newRef try newComponents.merge(components) } + // things may come in out of order because of concurrency + // so we reorder after completing all entries. + try newDict.applyOrder(self) return (newDict, newComponents) } } diff --git a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift index 8b52db0c5..7ed7bd91a 100644 --- a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift @@ -159,6 +159,20 @@ public struct OrderedDictionary: HasWarnings where Key: Hashable { } return ret } + + struct KeysDontMatch : Swift.Error {} + + /// Given two ordered dictionaries with the exact same keys, + /// apply the ordering of one to the other. This will throw if + /// the dictionary keys are not the same. + public mutating func applyOrder(_ other: Self) throws { + guard other.orderedKeys.count == orderedKeys.count, + other.orderedKeys.allSatisfy({ orderedKeys.contains($0) }) else { + throw KeysDontMatch() + } + + orderedKeys = other.orderedKeys + } } // MARK: - Dictionary Literal diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 7a57f642c..479ac8ae0 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1274,9 +1274,6 @@ extension DocumentTests { print( String(data: try encoder.encode(docCopy1), encoding: .utf8)! ) - print( - String(data: try encoder.encode(docCopy2), encoding: .utf8)! - ) /* { "info" : { From 6ce5f4f18fbc344ff1aa24f4f426fe9f91b9e000 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 22 Apr 2024 09:54:40 -0500 Subject: [PATCH 31/40] continuing to fill out document external dereferencing test. fix bug with array ordering. fix bug with missing path items components --- .../Components Object/Components.swift | 9 +- .../Array+ExternallyDereferenceable.swift | 17 +- .../Document/DocumentTests.swift | 146 ++++++++++++------ 3 files changed, 111 insertions(+), 61 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index b9193e6dd..0b17f0b56 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -311,8 +311,8 @@ extension OpenAPI.Components { let oldRequestBodies = requestBodies let oldHeaders = headers let oldSecuritySchemes = securitySchemes - let oldCallbacks = callbacks + let oldPathItems = pathItems async let (newSchemas, c1) = oldSchemas.externallyDereferenced(with: context) async let (newResponses, c2) = oldResponses.externallyDereferenced(with: context) @@ -321,6 +321,7 @@ extension OpenAPI.Components { async let (newRequestBodies, c5) = oldRequestBodies.externallyDereferenced(with: context) async let (newHeaders, c6) = oldHeaders.externallyDereferenced(with: context) async let (newSecuritySchemes, c7) = oldSecuritySchemes.externallyDereferenced(with: context) + async let (newPathItems, c9) = oldPathItems.externallyDereferenced(with: context) // async let (newCallbacks, c8) = oldCallbacks.externallyDereferenced(with: context) var c8 = OpenAPI.Components() @@ -338,8 +339,8 @@ extension OpenAPI.Components { requestBodies = try await newRequestBodies headers = try await newHeaders securitySchemes = try await newSecuritySchemes - callbacks = newCallbacks + pathItems = try await newPathItems let c1Resolved = try await c1 let c2Resolved = try await c2 @@ -349,6 +350,7 @@ extension OpenAPI.Components { let c6Resolved = try await c6 let c7Resolved = try await c7 let c8Resolved = c8 + let c9Resolved = try await c9 let noNewComponents = c1Resolved.isEmpty @@ -359,6 +361,7 @@ extension OpenAPI.Components { && c6Resolved.isEmpty && c7Resolved.isEmpty && c8Resolved.isEmpty + && c9Resolved.isEmpty if noNewComponents { return } @@ -369,8 +372,8 @@ extension OpenAPI.Components { try merge(c5Resolved) try merge(c6Resolved) try merge(c7Resolved) - try merge(c8Resolved) + try merge(c9Resolved) switch depth { case .iterations(let number): diff --git a/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift index bb570c7b8..1435c2c95 100644 --- a/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift @@ -7,21 +7,24 @@ import OpenAPIKitCore extension Array where Element: ExternallyDereferenceable { public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - try await withThrowingTaskGroup(of: (Element, OpenAPI.Components).self) { group in - for elem in self { + try await withThrowingTaskGroup(of: (Int, (Element, OpenAPI.Components)).self) { group in + for (idx, elem) in zip(self.indices, self) { group.addTask { - return try await elem.externallyDereferenced(with: loader) + return try await (idx, elem.externallyDereferenced(with: loader)) } } - var newElems = Self() + var newElems = Array<(Int, Element)>() var newComponents = OpenAPI.Components() - for try await (elem, components) in group { - newElems.append(elem) + for try await (idx, (elem, components)) in group { + newElems.append((idx, elem)) try newComponents.merge(components) } - return (newElems, newComponents) + // things may come in out of order because of concurrency + // so we reorder after completing all entries. + newElems.sort { left, right in left.0 < right.0 } + return (newElems.map { $0.1 }, newComponents) } } } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 479ac8ae0..4f0e60040 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1209,7 +1209,6 @@ extension DocumentTests { /// Mock up some data, just for the example. static func mockData(_ key: OpenAPIKit.OpenAPI.ComponentKey) async throws -> Data { - print("looking up \(key.rawValue)") return try XCTUnwrap(files[key.rawValue]) } @@ -1229,6 +1228,80 @@ extension DocumentTests { { "type": "string" } + """, + "paths_webhook_json": """ + { + "summary": "just a webhook", + "get": { + "requestBody": { + "$ref": "file://./requests/webhook.json" + }, + "responses": { + "200": { + "$ref": "file://./responses/webhook.json" + } + } + } + } + """, + "requests_webhook_json": """ + { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + } + }, + "examples": { + "good": { + "$ref": "file://./examples/good.json" + } + } + } + } + } + """, + "responses_webhook_json": """ + { + "description": "webhook response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "length": { + "type": "integer", + "minimum": 0 + } + } + } + } + }, + "headers": { + "X-Hello": { + "$ref": "file://./headers/webhook.json" + } + } + } + """, + "headers_webhook_json": """ + { + "schema": { + "$ref": "file://./schemas/name_param.json" + } + } + """, + "examples_good_json": """ + { + "value": "{\\"body\\": \\"request me\\"}" + } """ ].mapValues { $0.data(using: .utf8)! } } @@ -1246,9 +1319,16 @@ extension DocumentTests { parameters: [ .reference(.external(URL(string: "file://./params/name.json")!)) ] - ) + ), + "/webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) ], + webhooks: [ + "webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) + ], components: .init( + schemas: [ + "name_param": .reference(.external(URL(string: "file://./schemas/name_param.json")!)) + ], // just to show, no parameters defined within document components : parameters: [:] ) @@ -1260,63 +1340,27 @@ extension DocumentTests { var docCopy1 = document try await docCopy1.externallyDereference(in: ExampleLoader.self) try await docCopy1.externallyDereference(in: ExampleLoader.self) + try await docCopy1.externallyDereference(in: ExampleLoader.self) var docCopy2 = document - try await docCopy2.externallyDereference(in: ExampleLoader.self, depth: 2) + try await docCopy2.externallyDereference(in: ExampleLoader.self, depth: 3) var docCopy3 = document try await docCopy3.externallyDereference(in: ExampleLoader.self, depth: .full) - XCTAssertEqual(docCopy1, docCopy2) +// XCTAssertEqual(docCopy1, docCopy2) XCTAssertEqual(docCopy2, docCopy3) + XCTAssertEqual(String(describing: docCopy2), String(describing: docCopy3)) // - MARK: After - print( - String(data: try encoder.encode(docCopy1), encoding: .utf8)! - ) - /* - { - "info" : { - "version" : "1.0.0", - "title" : "test document" - }, - "openapi" : "3.1.0", - "paths" : { - "\/goodbye\/{name}" : { - "parameters" : [ - { - "$ref" : "#\/components\/parameters\/params_name_json" - } - ] - }, - "\/hello\/{name}" : { - "parameters" : [ - { - "$ref" : "#\/components\/parameters\/params_name_json" - } - ] - } - }, - "components" : { - "parameters" : { - "params_name_json" : { - "description" : "a lonely parameter", - "in" : "path", - "name" : "name", - "x-source-url" : "file:\/\/.\/params\/name.json", - "required" : true, - "schema" : { - "$ref" : "#\/components\/schemas\/schemas_name_param_json" - } - } - }, - "schemas" : { - "schemas_name_param_json" : { - "type" : "string" - } - } - } - } - */ +// print( +// String(data: try encoder.encode(docCopy1), encoding: .utf8)! +// ) +// print( +// String(data: try encoder.encode(docCopy2), encoding: .utf8)! +// ) +// print( +// String(data: try encoder.encode(docCopy3), encoding: .utf8)! +// ) } } From 773df590c7d500fa6b6bae923b64f63e652e0544 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 22 Apr 2024 10:36:37 -0500 Subject: [PATCH 32/40] work on fixing ordering bugs in code and test --- .../Components Object/Components.swift | 14 ++ .../OrderedDictionary/OrderedDictionary.swift | 10 + .../OpenAPIKitCore/Shared/ComponentKey.swift | 6 + .../Document/DocumentTests.swift | 193 --------------- .../ExternalDereferencingDocumentTests.swift | 224 ++++++++++++++++++ 5 files changed, 254 insertions(+), 193 deletions(-) create mode 100644 Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index 0b17f0b56..bbafe0cdc 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -98,6 +98,20 @@ extension OpenAPI.Components { try pathItems.merge(other.pathItems, uniquingKeysWith: detectCollision(type: "pathItems")) try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: detectCollision(type: "vendorExtensions")) } + + /// Sort the components within each type by the component key. + public mutating func sort() { + schemas.sortKeys() + responses.sortKeys() + parameters.sortKeys() + examples.sortKeys() + requestBodies.sortKeys() + headers.sortKeys() + securitySchemes.sortKeys() + links.sortKeys() + callbacks.sortKeys() + pathItems.sortKeys() + } } extension OpenAPI.Components { diff --git a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift index 7ed7bd91a..8a9cd91a8 100644 --- a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift @@ -173,6 +173,16 @@ public struct OrderedDictionary: HasWarnings where Key: Hashable { orderedKeys = other.orderedKeys } + + public mutating func sortKeys(by sort: (Key, Key) throws -> Bool) rethrows { + try orderedKeys.sort(by: sort) + } +} + +extension OrderedDictionary where Key: Comparable { + public mutating func sortKeys() { + orderedKeys.sort() + } } // MARK: - Dictionary Literal diff --git a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift index 5388e5d1b..251ad343a 100644 --- a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift +++ b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift @@ -90,3 +90,9 @@ extension Shared { } } } + +extension Shared.ComponentKey: Comparable { + public static func < (lhs: Shared.ComponentKey, rhs: Shared.ComponentKey) -> Bool { + lhs.rawValue < rhs.rawValue + } +} diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 4f0e60040..c68703e8d 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1171,196 +1171,3 @@ extension DocumentTests { ) } } - -// MARK: - External Dereferencing -extension DocumentTests { - // temporarily test with an example of the new interface - func test_example() async throws { - - /// An example of implementing a loader context for loading external references - /// into an OpenAPI document. - struct ExampleLoader: ExternalLoader { - static func load(_ url: URL) async throws -> T where T : Decodable { - // load data from file, perhaps. we will just mock that up for the test: - let data = try await mockData(componentKey(type: T.self, at: url)) - - let decoded = try JSONDecoder().decode(T.self, from: data) - let finished: T - // while unnecessary, a loader may likely want to attatch some extra info - // to keep track of where a reference was loaded from. This test makes sure - // the following strategy of using vendor extensions works. - if var extendable = decoded as? VendorExtendable { - extendable.vendorExtensions["x-source-url"] = AnyCodable(url) - finished = extendable as! T - } else { - finished = decoded - } - return finished - } - - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { - // do anything you want here to determine what key the new component should be stored at. - // for the example, we will just transform the URL into a valid components key: - let urlString = url.pathComponents.dropFirst() - .joined(separator: "_") - .replacingOccurrences(of: ".", with: "_") - return try .forceInit(rawValue: urlString) - } - - /// Mock up some data, just for the example. - static func mockData(_ key: OpenAPIKit.OpenAPI.ComponentKey) async throws -> Data { - return try XCTUnwrap(files[key.rawValue]) - } - - static let files: [String: Data] = [ - "params_name_json": """ - { - "name": "name", - "description": "a lonely parameter", - "in": "path", - "required": true, - "schema": { - "$ref": "file://./schemas/name_param.json#" - } - } - """, - "schemas_name_param_json": """ - { - "type": "string" - } - """, - "paths_webhook_json": """ - { - "summary": "just a webhook", - "get": { - "requestBody": { - "$ref": "file://./requests/webhook.json" - }, - "responses": { - "200": { - "$ref": "file://./responses/webhook.json" - } - } - } - } - """, - "requests_webhook_json": """ - { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "body": { - "type": "string" - } - } - }, - "examples": { - "good": { - "$ref": "file://./examples/good.json" - } - } - } - } - } - """, - "responses_webhook_json": """ - { - "description": "webhook response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "body": { - "type": "string" - }, - "length": { - "type": "integer", - "minimum": 0 - } - } - } - } - }, - "headers": { - "X-Hello": { - "$ref": "file://./headers/webhook.json" - } - } - } - """, - "headers_webhook_json": """ - { - "schema": { - "$ref": "file://./schemas/name_param.json" - } - } - """, - "examples_good_json": """ - { - "value": "{\\"body\\": \\"request me\\"}" - } - """ - ].mapValues { $0.data(using: .utf8)! } - } - - let document = OpenAPI.Document( - info: .init(title: "test document", version: "1.0.0"), - servers: [], - paths: [ - "/hello/{name}": .init( - parameters: [ - .reference(.external(URL(string: "file://./params/name.json")!)) - ] - ), - "/goodbye/{name}": .init( - parameters: [ - .reference(.external(URL(string: "file://./params/name.json")!)) - ] - ), - "/webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) - ], - webhooks: [ - "webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) - ], - components: .init( - schemas: [ - "name_param": .reference(.external(URL(string: "file://./schemas/name_param.json")!)) - ], - // just to show, no parameters defined within document components : - parameters: [:] - ) - ) - - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - - var docCopy1 = document - try await docCopy1.externallyDereference(in: ExampleLoader.self) - try await docCopy1.externallyDereference(in: ExampleLoader.self) - try await docCopy1.externallyDereference(in: ExampleLoader.self) - - var docCopy2 = document - try await docCopy2.externallyDereference(in: ExampleLoader.self, depth: 3) - - var docCopy3 = document - try await docCopy3.externallyDereference(in: ExampleLoader.self, depth: .full) - -// XCTAssertEqual(docCopy1, docCopy2) - XCTAssertEqual(docCopy2, docCopy3) - XCTAssertEqual(String(describing: docCopy2), String(describing: docCopy3)) - - // - MARK: After -// print( -// String(data: try encoder.encode(docCopy1), encoding: .utf8)! -// ) -// print( -// String(data: try encoder.encode(docCopy2), encoding: .utf8)! -// ) -// print( -// String(data: try encoder.encode(docCopy3), encoding: .utf8)! -// ) - } -} diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift new file mode 100644 index 000000000..5747f2d05 --- /dev/null +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -0,0 +1,224 @@ +// +// ExternalDereferencingDocumentTests.swift +// + +import Foundation +import Yams +import OpenAPIKit +import XCTest + +final class ExternalDereferencingDocumentTests: XCTestCase { + // temporarily test with an example of the new interface + func test_example() async throws { + + /// An example of implementing a loader context for loading external references + /// into an OpenAPI document. + struct ExampleLoader: ExternalLoader { + static func load(_ url: URL) async throws -> T where T : Decodable { + // load data from file, perhaps. we will just mock that up for the test: + let data = try await mockData(componentKey(type: T.self, at: url)) + + // We use the YAML decoder purely for order-stability. + let decoded = try YAMLDecoder().decode(T.self, from: data) + let finished: T + // while unnecessary, a loader may likely want to attatch some extra info + // to keep track of where a reference was loaded from. This test makes sure + // the following strategy of using vendor extensions works. + if var extendable = decoded as? VendorExtendable { + extendable.vendorExtensions["x-source-url"] = AnyCodable(url) + finished = extendable as! T + } else { + finished = decoded + } + return finished + } + + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { + // do anything you want here to determine what key the new component should be stored at. + // for the example, we will just transform the URL into a valid components key: + let urlString = url.pathComponents.dropFirst() + .joined(separator: "_") + .replacingOccurrences(of: ".", with: "_") + return try .forceInit(rawValue: urlString) + } + + /// Mock up some data, just for the example. + static func mockData(_ key: OpenAPIKit.OpenAPI.ComponentKey) async throws -> Data { + return try XCTUnwrap(files[key.rawValue]) + } + + static let files: [String: Data] = [ + "params_name_json": """ + { + "name": "name", + "description": "a lonely parameter", + "in": "path", + "required": true, + "schema": { + "$ref": "file://./schemas/name_param.json#" + } + } + """, + "schemas_name_param_json": """ + { + "type": "string" + } + """, + "paths_webhook_json": """ + { + "summary": "just a webhook", + "get": { + "requestBody": { + "$ref": "file://./requests/webhook.json" + }, + "responses": { + "200": { + "$ref": "file://./responses/webhook.json" + } + } + } + } + """, + "requests_webhook_json": """ + { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + } + }, + "examples": { + "good": { + "$ref": "file://./examples/good.json" + } + } + } + } + } + """, + "responses_webhook_json": """ + { + "description": "webhook response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "length": { + "type": "integer", + "minimum": 0 + } + } + } + } + }, + "headers": { + "X-Hello": { + "$ref": "file://./headers/webhook.json" + } + } + } + """, + "headers_webhook_json": """ + { + "schema": { + "$ref": "file://./schemas/name_param.json" + } + } + """, + "examples_good_json": """ + { + "value": "{\\"body\\": \\"request me\\"}" + } + """ + ].mapValues { $0.data(using: .utf8)! } + } + + let document = OpenAPI.Document( + info: .init(title: "test document", version: "1.0.0"), + servers: [], + paths: [ + "/hello/{name}": .init( + parameters: [ + .reference(.external(URL(string: "file://./params/name.json")!)) + ] + ), + "/goodbye/{name}": .init( + parameters: [ + .reference(.external(URL(string: "file://./params/name.json")!)) + ] + ), + "/webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) + ], + webhooks: [ + "webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) + ], + components: .init( + schemas: [ + "name_param": .reference(.external(URL(string: "file://./schemas/name_param.json")!)) + ], + // just to show, no parameters defined within document components : + parameters: [:] + ) + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + var docCopy1 = document + try await docCopy1.externallyDereference(in: ExampleLoader.self) + try await docCopy1.externallyDereference(in: ExampleLoader.self) + try await docCopy1.externallyDereference(in: ExampleLoader.self) + docCopy1.components.sort() + + var docCopy2 = document + try await docCopy2.externallyDereference(in: ExampleLoader.self, depth: 3) + docCopy2.components.sort() + + var docCopy3 = document + try await docCopy3.externallyDereference(in: ExampleLoader.self, depth: .full) + docCopy3.components.sort() + +// print("-----") +// print(docCopy2 == docCopy3) +// print(docCopy2.components == docCopy3.components) +// print("+++++") +// print(docCopy2.components.schemas == docCopy3.components.schemas) +// print(docCopy2.components.responses == docCopy3.components.responses) +// print(docCopy2.components.parameters == docCopy3.components.parameters) +// print(docCopy2.components.examples == docCopy3.components.examples) +// print(docCopy2.components.requestBodies == docCopy3.components.requestBodies) +// print(docCopy2.components.headers == docCopy3.components.headers) +// print(docCopy2.components.securitySchemes == docCopy3.components.securitySchemes) +// print(docCopy2.components.links == docCopy3.components.links) +// print(docCopy2.components.callbacks == docCopy3.components.callbacks) +// print(docCopy2.components.pathItems == docCopy3.components.pathItems) +// print("=====") +// print(docCopy2.components.responses) +// print("&&&&&") +// print(docCopy3.components.responses) +// print("$$$$$") + + XCTAssertEqual(docCopy1, docCopy2) + XCTAssertEqual(docCopy2, docCopy3) +// XCTAssertEqual(String(describing: docCopy2), String(describing: docCopy3)) + + // - MARK: After +// print( +// String(data: try encoder.encode(docCopy1), encoding: .utf8)! +// ) +// print( +// String(data: try encoder.encode(docCopy2), encoding: .utf8)! +// ) +// print( +// String(data: try encoder.encode(docCopy3), encoding: .utf8)! +// ) + } +} From d092f487ae3b6cad5123604f89d6e25437095d10 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 22 Apr 2024 23:06:58 -0500 Subject: [PATCH 33/40] Add external reference to a Callbacks object. --- .../ExternalDereferencingDocumentTests.swift | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift index 5747f2d05..214c05a04 100644 --- a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -137,6 +137,32 @@ final class ExternalDereferencingDocumentTests: XCTestCase { { "value": "{\\"body\\": \\"request me\\"}" } + """, + "callbacks_one_json": """ + { + "https://callback.site.com/callback": { + "$ref": "file://./paths/callback.json" + } + } + """, + "paths_callback_json": """ + { + "summary": "just a callback", + "get": { + "responses": { + "200": { + "description": "callback response", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + } """ ].mapValues { $0.data(using: .utf8)! } } @@ -148,7 +174,13 @@ final class ExternalDereferencingDocumentTests: XCTestCase { "/hello/{name}": .init( parameters: [ .reference(.external(URL(string: "file://./params/name.json")!)) - ] + ], + get: .init( + responses: [:], + callbacks: [ + "callback1": .reference(.external(URL(string: "file://./callbacks/one.json")!)) + ] + ) ), "/goodbye/{name}": .init( parameters: [ From e5c44b4e72abe06cb279de5a905cbd63f74e88a7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 22 Apr 2024 23:15:58 -0500 Subject: [PATCH 34/40] Add links external reference --- .../Document/ExternalDereferencingDocumentTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift index 214c05a04..5bbee8fcb 100644 --- a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -158,11 +158,21 @@ final class ExternalDereferencingDocumentTests: XCTestCase { "type": "object" } } + }, + "links": { + "link1": { + "$ref": "file://./links/first.json" + } } } } } } + """, + "links_first_json": """ + { + "operationId": "helloOp" + } """ ].mapValues { $0.data(using: .utf8)! } } @@ -176,6 +186,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { .reference(.external(URL(string: "file://./params/name.json")!)) ], get: .init( + operationId: "helloOp", responses: [:], callbacks: [ "callback1": .reference(.external(URL(string: "file://./callbacks/one.json")!)) From bd97176ccbcaddfdf72f7cdf4e0f1016ebe7d3e9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Apr 2024 09:42:51 -0500 Subject: [PATCH 35/40] Add an external dereferencing case that proves the basic collision check doesn't work. remove collision check. --- .../Components Object/Components.swift | 12 +++++++++-- .../ExternalDereferencingDocumentTests.swift | 20 +++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index bbafe0cdc..d3d13cbdc 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -80,8 +80,16 @@ extension OpenAPI.Components { private func detectCollision(type: String) throws -> (_ old: T, _ new: T) throws -> T { return { old, new in - if(old == new) { return old } - throw ComponentCollision(componentType: type, existingComponent: String(describing:old), newComponent: String(describing:new)) + // theoretically we can detect collisions here, but we would need to compare + // for equality up-to but not including the difference between an external and + // internal reference which is not supported yet. +// if(old == new) { return old } +// throw ComponentCollision(componentType: type, existingComponent: String(describing:old), newComponent: String(describing:new)) + + // Given we aren't ensuring there are no collisions, the old version is going to be + // the one more likely to have been _further_ dereferenced than the new record, so + // we keep that version. + return old } } diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift index 5bbee8fcb..1ffc7e2ed 100644 --- a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -55,13 +55,21 @@ final class ExternalDereferencingDocumentTests: XCTestCase { "in": "path", "required": true, "schema": { - "$ref": "file://./schemas/name_param.json#" + "$ref": "file://./schemas/string_param.json#" } } """, - "schemas_name_param_json": """ + "schemas_string_param_json": """ { - "type": "string" + "oneOf": [ + { "type": "string" }, + { "$ref": "file://./schemas/basic_object.json" } + ] + } + """, + "schemas_basic_object_json": """ + { + "type": "object" } """, "paths_webhook_json": """ @@ -87,7 +95,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { "type": "object", "properties": { "body": { - "type": "string" + "$ref": "file://./schemas/string_param.json" } } }, @@ -129,7 +137,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { "headers_webhook_json": """ { "schema": { - "$ref": "file://./schemas/name_param.json" + "$ref": "file://./schemas/string_param.json" } } """, @@ -205,7 +213,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { ], components: .init( schemas: [ - "name_param": .reference(.external(URL(string: "file://./schemas/name_param.json")!)) + "name_param": .reference(.external(URL(string: "file://./schemas/string_param.json")!)) ], // just to show, no parameters defined within document components : parameters: [:] From 412ec1c2408c7cbfe412875b3967ea16351570ba Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Apr 2024 09:55:52 -0500 Subject: [PATCH 36/40] remove unused external dereferencing code --- Sources/OpenAPIKit/Callbacks.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 30baf3e3d..7a671bdeb 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -37,12 +37,6 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { } } -extension OpenAPI.CallbackURL: ExternallyDereferenceable { - public func externallyDereferenced(with loader: Context.Type) async throws -> (Self, OpenAPI.Components) { - return (self, .init()) - } -} - // The following conformance is theoretically unnecessary but the compiler is // only able to find the conformance if we explicitly declare it here, though // it is apparently able to determine the conformance is already satisfied here From 45b8df9f9161f159d99ea891ca17c5956e12987a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Apr 2024 10:00:54 -0500 Subject: [PATCH 37/40] hit the encoding header external dereference codepath --- .../Document/ExternalDereferencingDocumentTests.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift index 1ffc7e2ed..b063d048a 100644 --- a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -103,6 +103,15 @@ final class ExternalDereferencingDocumentTests: XCTestCase { "good": { "$ref": "file://./examples/good.json" } + }, + "encoding": { + "enc1": { + "headers": { + "head1": { + "$ref": "file://./headers/webhook.json" + } + } + } } } } From 76d27d63b683d8768e01e6704084ff37d0bc9f50 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Apr 2024 10:10:53 -0500 Subject: [PATCH 38/40] undo a workaround that is no longer needed --- .../OpenAPIKit/Components Object/Components.swift | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index d3d13cbdc..a92862d76 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -343,17 +343,9 @@ extension OpenAPI.Components { async let (newRequestBodies, c5) = oldRequestBodies.externallyDereferenced(with: context) async let (newHeaders, c6) = oldHeaders.externallyDereferenced(with: context) async let (newSecuritySchemes, c7) = oldSecuritySchemes.externallyDereferenced(with: context) + async let (newCallbacks, c8) = oldCallbacks.externallyDereferenced(with: context) async let (newPathItems, c9) = oldPathItems.externallyDereferenced(with: context) -// async let (newCallbacks, c8) = oldCallbacks.externallyDereferenced(with: context) - var c8 = OpenAPI.Components() - var newCallbacks = oldCallbacks - for (key, callback) in oldCallbacks { - let (newCallback, components) = try await callback.externallyDereferenced(with: context) - newCallbacks[key] = newCallback - try c8.merge(components) - } - schemas = try await newSchemas responses = try await newResponses parameters = try await newParameters @@ -361,7 +353,7 @@ extension OpenAPI.Components { requestBodies = try await newRequestBodies headers = try await newHeaders securitySchemes = try await newSecuritySchemes - callbacks = newCallbacks + callbacks = try await newCallbacks pathItems = try await newPathItems let c1Resolved = try await c1 @@ -371,7 +363,7 @@ extension OpenAPI.Components { let c5Resolved = try await c5 let c6Resolved = try await c6 let c7Resolved = try await c7 - let c8Resolved = c8 + let c8Resolved = try await c8 let c9Resolved = try await c9 let noNewComponents = From 9ed957042fa781db9d81ea4936014f75693c2ff1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Apr 2024 10:29:29 -0500 Subject: [PATCH 39/40] clean up some temporary test code --- .../ExternalDereferencingDocumentTests.swift | 32 ------------ .../Path Item/PathItemTests.swift | 50 ------------------- 2 files changed, 82 deletions(-) diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift index b063d048a..914e575cc 100644 --- a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -246,39 +246,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { try await docCopy3.externallyDereference(in: ExampleLoader.self, depth: .full) docCopy3.components.sort() -// print("-----") -// print(docCopy2 == docCopy3) -// print(docCopy2.components == docCopy3.components) -// print("+++++") -// print(docCopy2.components.schemas == docCopy3.components.schemas) -// print(docCopy2.components.responses == docCopy3.components.responses) -// print(docCopy2.components.parameters == docCopy3.components.parameters) -// print(docCopy2.components.examples == docCopy3.components.examples) -// print(docCopy2.components.requestBodies == docCopy3.components.requestBodies) -// print(docCopy2.components.headers == docCopy3.components.headers) -// print(docCopy2.components.securitySchemes == docCopy3.components.securitySchemes) -// print(docCopy2.components.links == docCopy3.components.links) -// print(docCopy2.components.callbacks == docCopy3.components.callbacks) -// print(docCopy2.components.pathItems == docCopy3.components.pathItems) -// print("=====") -// print(docCopy2.components.responses) -// print("&&&&&") -// print(docCopy3.components.responses) -// print("$$$$$") - XCTAssertEqual(docCopy1, docCopy2) XCTAssertEqual(docCopy2, docCopy3) -// XCTAssertEqual(String(describing: docCopy2), String(describing: docCopy3)) - - // - MARK: After -// print( -// String(data: try encoder.encode(docCopy1), encoding: .utf8)! -// ) -// print( -// String(data: try encoder.encode(docCopy2), encoding: .utf8)! -// ) -// print( -// String(data: try encoder.encode(docCopy3), encoding: .utf8)! -// ) } } diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index 62ddd9a83..22c9e2601 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -433,53 +433,3 @@ extension PathItemTests { ) } } - -// MARK: External Dereferencing Tests -extension PathItemTests { - struct MockLoad: ExternalLoader { - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { - let urlString = url.pathComponents.dropFirst().joined(separator: "_").replacingOccurrences(of: ".", with: "_") - - return try .forceInit(rawValue: urlString) - } - - static func load(_: URL) throws -> T where T : Decodable { - if let ret = OpenAPI.Request(description: "hello", content: [:]) as? T { - return ret - } - if let ret = OpenAPI.Parameter(name: "other-param", context: .header, schema: .string) as? T { - return ret - } - throw ValidationError(reason: "", at: []) - } - } - - func test_tmp() async throws { - let op = OpenAPI.Operation(responses: [:]) - let pathItem = OpenAPI.PathItem( - summary: "summary", - description: "description", - servers: [OpenAPI.Server(url: URL(string: "http://google.com")!)], - parameters: [ - .parameter(name: "hello", context: .query, schema: .string), - .reference(.component(named: "already-internal")), - .reference(.external(URL(string: "https://some-param.com/param")!)) - ], - get: .init(requestBody: .reference(.external(URL(string: "https://website.com/request")!)), responses: [:]), - put: op, - post: op, - delete: op, - options: op, - head: op, - patch: op, - trace: op - ) - - print(pathItem.parameters.debugDescription) - print("------") - let (x, loaderComponents) = try await pathItem.externallyDereferenced(with: MockLoad.self) - print(x.parameters.debugDescription) - print("=======") - print(loaderComponents.parameters) - } -} From 728218ea50f17615e20827fe0cfa662d7437825f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Apr 2024 10:50:16 -0500 Subject: [PATCH 40/40] update the docs to talk about external dereferencing --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 07e427694..5f0eccb65 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ You can create an external reference with `JSONReference.external(URL)`. Interna You can check whether a given `JSONReference` exists in the Components Object with `document.components.contains()`. You can access a referenced object in the Components Object with `document.components[reference]`. -You can create references from the Components Object with `document.components.reference(named:ofType:)`. This method will throw an error if the given component does not exist in the ComponentsObject. +References can be created from the Components Object with `document.components.reference(named:ofType:)`. This method will throw an error if the given component does not exist in the ComponentsObject. You can use `document.components.lookup()` or the `Components` type's `subscript` to turn an `Either` containing either a reference or a component into an optional value of that component's type (having either pulled it out of the `Either` or looked it up in the Components Object). The `lookup()` method throws when it can't find an item whereas `subscript` returns `nil`. @@ -284,7 +284,7 @@ let document = OpenAPI.Document( ``` #### Specification Extensions -Many OpenAPIKit types support [Specification Extensions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#specification-extensions). As described in the OpenAPI Specification, these extensions must be objects that are keyed with the prefix "x-". For example, a property named "specialProperty" on the root OpenAPI Object (`OpenAPI.Document`) is invalid but the property "x-specialProperty" is a valid specification extension. +Many OpenAPIKit types support [Specification Extensions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#specification-extensions). As described in the OpenAPI Specification, these extensions must be objects that are keyed with the prefix "x-". For example, a property named "specialProperty" on the root OpenAPI Object (`OpenAPI.Document`) is invalid but the property "x-specialProperty" is a valid specification extension. You can get or set specification extensions via the `vendorExtensions` property on any object that supports this feature. The keys are `Strings` beginning with the aforementioned "x-" prefix and the values are `AnyCodable`. If you set an extension without using the "x-" prefix, the prefix will be added upon encoding. @@ -323,11 +323,51 @@ try encodeEqual(URL(string: "https://website.com"), AnyCodable(URL(string: "http ``` ### Dereferencing & Resolving +#### External References +This is currently only available for OAS 3.1 documents (supported by the `OpenAPIKit` module (as opposed to the `OpenAPIKit30` moudle). External dereferencing does not resolve any local (internal) references, it just loads external references into the Document. It does this by storing any loaded externally referenced objects in the Components Object and transforming the reference being resolved from an external reference to an internal one. That way, you can always run internal dereferencing as a second step if you want a fully dereferenced document, but if you simply wanted to load additional referenced files then you can stop after external dereferencing. + +OpenAPIKit leaves it to you to decide how to load external files and where to store the results in the Components Object. It does this by requiring that you provide an implementation of the `ExternalLoader` protocol. You provide a `load` function and a `componentKey` function, both of which accept as input the `URL` to load. A simple mock example implementation from the OpenAPIKit tests will go a long way to showing how the `ExternalLoader` can be set up: + +```swift +struct ExampleLoader: ExternalLoader { + static func load(_ url: URL) async throws -> T where T : Decodable { + // load data from file, perhaps. we will just mock that up for the test: + let data = try await mockData(componentKey(type: T.self, at: url)) + + // We use the YAML decoder mostly for order-stability in this case but it is + // also nice that it will handle both YAML and JSON data. + let decoded = try YAMLDecoder().decode(T.self, from: data) + let finished: T + // while unnecessary, a loader may likely want to attatch some extra info + // to keep track of where a reference was loaded from. + if var extendable = decoded as? VendorExtendable { + extendable.vendorExtensions["x-source-url"] = AnyCodable(url) + finished = extendable as! T + } else { + finished = decoded + } + return finished + } + + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { + // do anything you want here to determine what key the new component should be stored at. + // for the example, we will just transform the URL path into a valid components key: + let urlString = url.pathComponents + .joined(separator: "_") + .replacingOccurrences(of: ".", with: "_") + return try .forceInit(rawValue: urlString) + } +} +``` + +Once you have an `ExternalLoader`, you can call an `OpenAPI.Document`'s `externallyDereference()` method to externally dereference it. You get to choose whether to only load references to a certain depth or to fully resolve references until you run out of them; any given referenced document may itself contain references and these references may point back to things loaded into the Document previously so dereferencing is done recursively up to a given depth (or until fully dereferenced if you use the `.full` depth). + +#### Internal References In addition to looking something up in the `Components` object, you can entirely derefererence many OpenAPIKit types. A dereferenced type has had all of its references looked up (and all of its properties' references, all the way down). You use a value's `dereferenced(in:)` method to fully dereference it. -You can even dereference the whole document with the `OpenAPI.Document` `locallyDereferenced()` method. As the name implies, you can only derefence whole documents that are contained within one file (which is another way of saying that all references are "local"). Specifically, all references must be located within the document's Components Object. +You can even dereference the whole document with the `OpenAPI.Document` `locallyDereferenced()` method. As the name implies, you can only derefence whole documents that are contained within one file (which is another way of saying that all references are "local"). Specifically, all references must be located within the document's Components Object. External dereferencing is done as a separeate step, but you can first dereference externally and then dereference internally if you'd like to perform both. Unlike what happens when you lookup an individual component using the `lookup()` method on `Components`, dereferencing a whole `OpenAPI.Document` will result in type-level changes that guarantee all references are removed. `OpenAPI.Document`'s `locallyDereferenced()` method returns a `DereferencedDocument` which exposes `DereferencedPathItem`s which have `DereferencedParameter`s and `DereferencedOperation`s and so on.