diff --git a/README.md b/README.md index 365c92c1a..2d8fa2bd9 100644 --- a/README.md +++ b/README.md @@ -594,7 +594,7 @@ 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`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/externalloader) -protocol. You provide a `load` function and a `componentKey` function, both of +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: @@ -605,14 +605,13 @@ struct ExampleLoader: ExternalLoader { static func load(_ url: URL) async throws -> (T, [Message]) where T : Decodable { // load data from file, perhaps. we will just mock that up for the example: - let data = try await mockData(componentKey(type: T.self, at: url)) + let data = try await mockData(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 example - shows + // while unnecessary, a loader may likely want to attach some extra info + // to keep track of where a reference was loaded from. This example shows // the strategy of using vendor extensions. if var extendable = decoded as? VendorExtendable { extendable.vendorExtensions["x-source-url"] = AnyCodable(url) @@ -623,7 +622,7 @@ struct ExampleLoader: ExternalLoader { return (finished, []) } - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { + static func componentKey(for object: T, 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: diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift index a63a7f2a0..364d68852 100644 --- a/Sources/OpenAPIKit/ExternalLoader.swift +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -45,7 +45,7 @@ public protocol ExternalLoader: _ExternalLoaderMetatype where Message: Sendable /// 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: URL) throws -> OpenAPI.ComponentKey + static func componentKey(for object: T, at url: URL) throws -> OpenAPI.ComponentKey } public protocol ExternallyDereferenceable { diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index d67d8e413..37193776e 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -626,8 +626,8 @@ extension JSONReference: ExternallyDereferenceable where ReferenceType: External case .internal(let ref): return (.internal(ref), .init(), []) case .external(let url): - let componentKey = try loader.componentKey(type: ReferenceType.self, at: url) let (component, messages): (ReferenceType, [Loader.Message]) = try await loader.load(url) + let componentKey = try loader.componentKey(for: component, at: url) var components = OpenAPI.Components() switch ReferenceType.openAPIComponentsKeyPath { case .a(let directPath): diff --git a/Sources/OpenAPIKit30/ExternalLoader.swift b/Sources/OpenAPIKit30/ExternalLoader.swift index a63a7f2a0..364d68852 100644 --- a/Sources/OpenAPIKit30/ExternalLoader.swift +++ b/Sources/OpenAPIKit30/ExternalLoader.swift @@ -45,7 +45,7 @@ public protocol ExternalLoader: _ExternalLoaderMetatype where Message: Sendable /// 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: URL) throws -> OpenAPI.ComponentKey + static func componentKey(for object: T, at url: URL) throws -> OpenAPI.ComponentKey } public protocol ExternallyDereferenceable { diff --git a/Sources/OpenAPIKit30/JSONReference.swift b/Sources/OpenAPIKit30/JSONReference.swift index 14fd382e4..bbf7563ca 100644 --- a/Sources/OpenAPIKit30/JSONReference.swift +++ b/Sources/OpenAPIKit30/JSONReference.swift @@ -391,8 +391,8 @@ extension JSONReference: ExternallyDereferenceable where ReferenceType: External case .internal(let ref): return (.internal(ref), .init(), []) case .external(let url): - let componentKey = try loader.componentKey(type: ReferenceType.self, at: url) let (component, messages): (ReferenceType, [Loader.Message]) = try await loader.load(url) + let componentKey = try loader.componentKey(for: component, at: url) var components = OpenAPI.Components() components[keyPath: ReferenceType.openAPIComponentsKeyPath][componentKey] = component return (try components.reference(named: componentKey.rawValue, ofType: ReferenceType.self), components, messages) diff --git a/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift index 8950cd03f..2a318391d 100644 --- a/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift @@ -24,7 +24,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { static func load(_ url: URL) async throws -> (T, [Message]) 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 data = try await mockData(url) // We use the YAML decoder purely for order-stability. let decoded = try YAMLDecoder().decode(T.self, from: data) @@ -41,7 +41,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { return (finished, [url.absoluteString]) } - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit30.OpenAPI.ComponentKey { + static func componentKey(for object: T, at url: URL) throws -> OpenAPIKit30.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() @@ -51,12 +51,12 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } /// Mock up some data, just for the example. - static func mockData(_ key: OpenAPIKit30.OpenAPI.ComponentKey) async throws -> Data { - return try XCTUnwrap(files[key.rawValue]) + static func mockData(_ url: URL) async throws -> Data { + return try XCTUnwrap(files[url.absoluteString]) } static let files: [String: Data] = [ - "params_name_json": """ + "file://./params/name.json": """ { "name": "name", "description": "a lonely parameter", @@ -67,7 +67,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "schemas_string_param_json": """ + "file://./schemas/string_param.json": """ { "oneOf": [ { "type": "string" }, @@ -75,12 +75,20 @@ final class ExternalDereferencingDocumentTests: XCTestCase { ] } """, - "schemas_basic_object_json": """ + "file://./schemas/string_param.json#": """ + { + "oneOf": [ + { "type": "string" }, + { "$ref": "file://./schemas/basic_object.json" } + ] + } + """, + "file://./schemas/basic_object.json": """ { "type": "object" } """, - "paths_webhook_json": """ + "file://./paths/webhook.json": """ { "summary": "just a webhook", "get": { @@ -95,7 +103,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "requests_webhook_json": """ + "file://./requests/webhook.json": """ { "content": { "application/json": { @@ -128,7 +136,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "responses_webhook_json": """ + "file://./responses/webhook.json": """ { "description": "webhook response", "content": { @@ -154,14 +162,14 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "headers_webhook_json": """ + "file://./headers/webhook.json": """ { "schema": { "$ref": "file://./schemas/string_param.json" } } """, - "headers_webhook2_json": """ + "file://./headers/webhook2.json": """ { "content": { "application/json": { @@ -172,19 +180,19 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "examples_good_json": """ + "file://./examples/good.json": """ { "value": "{\\"body\\": \\"request me\\"}" } """, - "callbacks_one_json": """ + "file://./callbacks/one.json": """ { "https://callback.site.com/callback": { "summary": "just a callback" } } """, - "paths_callback_json": """ + "file://./paths/callback.json": """ { "summary": "just a callback", "get": { @@ -208,7 +216,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "links_first_json": """ + "file://./links/first.json": """ { "operationId": "helloOp" } diff --git a/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift b/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift index 13e23598b..b1f0da7ad 100644 --- a/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift @@ -349,7 +349,7 @@ extension JSONReferenceTests { return (JSONSchema.string as! T, [url.absoluteString]) } - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + static func componentKey(for object: T, at url: URL) throws -> OpenAPI.ComponentKey { return try .forceInit(rawValue: url.absoluteString .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "#", with: "_") diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift index aaced8fc7..a787c334d 100644 --- a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -24,7 +24,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { static func load(_ url: URL) async throws -> (T, [Message]) 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 data = try await mockData(url) // We use the YAML decoder purely for order-stability. let decoded = try YAMLDecoder().decode(T.self, from: data) @@ -41,7 +41,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { return (finished, [url.absoluteString]) } - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey { + static func componentKey(for object: T, 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() @@ -51,12 +51,16 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } /// 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 func mockData(_ url: URL) async throws -> Data { + return try XCTUnwrap(files[url.absoluteString]) } + + + + static let files: [String: Data] = [ - "params_name_json": """ + "file://./params/name.json": """ { "name": "name", "description": "a lonely parameter", @@ -67,7 +71,15 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "schemas_string_param_json": """ + "file://./schemas/string_param.json": """ + { + "oneOf": [ + { "type": "string" }, + { "$ref": "file://./schemas/basic_object.json" } + ] + } + """, + "file://./schemas/string_param.json#": """ { "oneOf": [ { "type": "string" }, @@ -75,7 +87,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { ] } """, - "schemas_basic_object_json": """ + "file://./schemas/basic_object.json": """ { "type": "object", "patternProperties": { @@ -85,12 +97,12 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "schemas_pattern_property_json": """ + "file://./schemas/pattern_property.json": """ { "type": "string" } """, - "requests_hello_json": """ + "file://./requests/hello.json": """ { "content": { "application/json": { @@ -118,14 +130,14 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "headers_hello_json": """ + "file://./headers/hello.json": """ { "schema": { "$ref": "file://./schemas/string_param.json" } } """, - "paths_webhook_json": """ + "file://./paths/webhook.json": """ { "summary": "just a webhook", "get": { @@ -140,7 +152,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "requests_webhook_json": """ + "file://./requests/webhook.json": """ { "content": { "application/json": { @@ -173,7 +185,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "responses_webhook_json": """ + "file://./responses/webhook.json": """ { "description": "webhook response", "content": { @@ -199,14 +211,14 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "headers_webhook_json": """ + "file://./headers/webhook.json": """ { "schema": { "$ref": "file://./schemas/string_param.json" } } """, - "headers_webhook2_json": """ + "file://./headers/webhook2.json": """ { "content": { "application/json": { @@ -217,19 +229,19 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "examples_good_json": """ + "file://./examples/good.json": """ { "value": "{\\"body\\": \\"request me\\"}" } """, - "callbacks_one_json": """ + "file://./callbacks/one.json": """ { "https://callback.site.com/callback": { "$ref": "file://./paths/callback.json" } } """, - "paths_callback_json": """ + "file://./paths/callback.json": """ { "summary": "just a callback", "get": { @@ -253,7 +265,7 @@ final class ExternalDereferencingDocumentTests: XCTestCase { } } """, - "links_first_json": """ + "file://./links/first.json": """ { "operationId": "helloOp" } diff --git a/Tests/OpenAPIKitTests/JSONReferenceTests.swift b/Tests/OpenAPIKitTests/JSONReferenceTests.swift index 2974dd04b..c5d17148f 100644 --- a/Tests/OpenAPIKitTests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKitTests/JSONReferenceTests.swift @@ -462,7 +462,7 @@ extension JSONReferenceTests { return (JSONSchema.string as! T, [url.absoluteString]) } - static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + static func componentKey(for object: T, at url: URL) throws -> OpenAPI.ComponentKey { return try .forceInit(rawValue: url.absoluteString .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "#", with: "_") diff --git a/documentation/migration_guides/v7_migration_guide.md b/documentation/migration_guides/v7_migration_guide.md index 86bf40003..eac7553a4 100644 --- a/documentation/migration_guides/v7_migration_guide.md +++ b/documentation/migration_guides/v7_migration_guide.md @@ -41,3 +41,19 @@ See the [README](https://github.com/mattpolzin/OpenAPIKit/blob/main/README.md#supporting-openapi-30x-documents) for more on how to covert documents so that you can write code against 3.1/3.2 documents but still support reading 3.0 documents. + +### External Loading +The `componentKey()` function required to conform to the `ExternalLoader` +protocol has changed from taking the type of thing being loaded as its first +argument to instead taking the object that has been loaded. This gives the +`componentKey()` function more information if it is needed. It's likely a very +small change to any current implementations of external loading. + +Before: +```swift +static func componentKey(type: T.Type, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey +``` +After: +```swift +static func componentKey(for object: T, at url: URL) throws -> OpenAPIKit.OpenAPI.ComponentKey +```