From 6726d580cc4d5fe9c7b3fafab84dc98a137f1563 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 4 Aug 2023 16:38:45 -0500 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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 {