diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index aa220ae00..eee4443c7 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -729,12 +729,12 @@ extension JSONSchema: ExternallyDereferenceable { newSchema = .init( schema: .reference(newReference, core) ) - case .dynamicReference: - // TODO: external dereferencing of `$dynamicRef` is not implemented; - // deferred alongside local dynamic-scope resolution (see #359). - newComponents = .noComponents - newSchema = self - newMessages = [] + case .dynamicReference(let dynamicRef, let core): + // Delegate to the wrapped JSONReference's external deref (same path as $ref). + let (newReference, components, messages) = try await dynamicRef.jsonReference.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init(schema: .dynamicReference(JSONDynamicReference(newReference), core)) case .fragment(_): newComponents = .noComponents newSchema = self diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift index e0d1631da..e7231b43b 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift @@ -439,3 +439,36 @@ final class JSONSchemaDynamicReferenceTests: XCTestCase { } } } + +#if ExternalLoading +extension JSONSchemaDynamicReferenceTests { + func test_externalDeref_dynamicReference_external() async throws { + // An external `$dynamicRef` is dereferenced through its underlying + // `JSONReference` -- same path as `$ref`: fetch + convert to an + // internal component reference. + let schema = JSONSchema.dynamicReference( + JSONDynamicReference(.external(.init(string: "./schema.json")!)) + ) + + let (newSchema, components, messages) = try await schema.externallyDereferenced(with: JSONReferenceTests.SchemaLoader.self) + + XCTAssertTrue(newSchema.isDynamicReference) + XCTAssertEqual(newSchema.dynamicReference?.name, "__schema_json") + XCTAssertEqual(components, .init(schemas: ["__schema_json": .string])) + XCTAssertEqual(messages, ["./schema.json"]) + } + + func test_externalDeref_dynamicReference_internal_noop() async throws { + // An internal `$dynamicRef` (anchor) is not external; external + // dereferencing leaves it unchanged. + let schema = JSONSchema.dynamicReference(.anchor("node")) + + let (newSchema, components, messages) = try await schema.externallyDereferenced(with: JSONReferenceTests.SchemaLoader.self) + + XCTAssertTrue(newSchema.isDynamicReference) + XCTAssertEqual(newSchema.dynamicReference?.absoluteString, "#node") + XCTAssertTrue(components.schemas.isEmpty) + XCTAssertEqual(messages, []) + } +} +#endif diff --git a/documentation/migration_guides/v7_migration_guide.md b/documentation/migration_guides/v7_migration_guide.md index 4402109cd..d7de8cebe 100644 --- a/documentation/migration_guides/v7_migration_guide.md +++ b/documentation/migration_guides/v7_migration_guide.md @@ -22,6 +22,14 @@ encodes/decodes the `$dynamicRef` keyword. Schemas whose only attribute is `locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve `$dynamicRef` against the dynamic scope. +### External dereferencing resolves `$dynamicRef` + +`externallyDereferenced(with:)` (under the `ExternalLoading` trait) resolves an +external `$dynamicRef` the same way it resolves an external `$ref`: fetch via +the `ExternalLoader` and rewrite to an internal component. The result stays +`.dynamicReference` (still serializes as `$dynamicRef`); cross-document +dynamic-scope resolution is out of scope. + ### `$ref` with a plain fragment now round-trips verbatim As part of anchor support, `JSONReference.InternalReference` now parses a `$ref`