From 29cea479aab73bc154764eeb7f661d615d59dbc2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 18 Mar 2024 20:04:25 -0500 Subject: [PATCH 1/5] experimenting --- Sources/OpenAPIKit/JSONDynamicReference.swift | 126 ++++++++++++++++++ Sources/OpenAPIKit/JSONReference.swift | 5 + 2 files changed, 131 insertions(+) create mode 100644 Sources/OpenAPIKit/JSONDynamicReference.swift diff --git a/Sources/OpenAPIKit/JSONDynamicReference.swift b/Sources/OpenAPIKit/JSONDynamicReference.swift new file mode 100644 index 000000000..b012a0387 --- /dev/null +++ b/Sources/OpenAPIKit/JSONDynamicReference.swift @@ -0,0 +1,126 @@ +// +// JSONDynamicReference.swift +// +// +// Created by Mathew Polzin. +// + +import OpenAPIKitCore + +@dynamicMemberLookup +public struct JSONDynamicReference: Equatable, Hashable { + public let jsonReference: JSONReference + + public init( + _ reference: JSONReference + ) { + self.jsonReference = reference + } + + public subscript(dynamicMember path: KeyPath, T>) -> T { + return jsonReference[keyPath: path] + } + + /// Reference a component of type `ReferenceType` in the + /// Components Object. + /// + /// Example: + /// + /// JSONDynamicReference.component(named: "greetings") + /// // encoded string: "#/components/schemas/greetings" + /// // Swift: `document.components.schemas["greetings"]` + public static func component(named name: String) -> Self { + return .init(.internal(.component(name: name)), summary: summary, description: description) + } + + /// Reference a path internal to this file but not within the Components Object + /// This is likely not what you are looking for. It is advisable to store reusable components + /// in the Components Object. + /// + /// - Important: The path does not contain a leading '#'. Start with the root '/'. + public static func `internal`(path: JSONReference.Path, summary: String? = nil, description: String? = nil) -> Self { + return .init(.internal(.path(path)), summary: summary, description: description) + } + + /// Reference an external URL. + public static func external(_ url: URL) -> Self { + return .init(.external(url)) + } + + /// `true` for internal references, `false` for + /// external references (i.e. to another file). + public var isInternal: Bool { + return jsonReference.isInternal + } + + /// `true` for external references, `false` for + /// internal references. + public var isExternal: Bool { + return jsonReference.isExternal + } + + /// Get the name of the referenced object. This method returns optional + /// because a reference to an external file might not have any path if the + /// file itself is the referenced component. + public var name: String? { + return jsonReference.name + } + + /// The absolute value of an external reference's + /// URL or the path fragment string for a local + /// reference as defined in [RFC 3986](https://tools.ietf.org/html/rfc3986). + public var absoluteString: String { + return jsonReference.absoluteString + } +} + +extension JSONDynamicReference { + private enum CodingKeys: String, CodingKey { + case dynamicRef = "$dynamicRef" + } +} + +extension JSONDynamicReference: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self.jsonReference { + case .internal(let reference): + try container.encode(reference.rawValue, forKey: .dynamicRef) + case .external(uri: let url): + try container.encode(url.absoluteString, forKey: .dynamicRef) + } + } +} + +extension JSONDynamicReference: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let referenceString = try container.decode(String.self, forKey: .dynamicRef) + + guard referenceString.count > 0 else { + throw DecodingError.dataCorruptedError(forKey: .dynamicRef, in: container, debugDescription: "Expected a reference string, but found an empty string instead.") + } + + if referenceString.first == "#" { + guard let internalReference = InternalReference(rawValue: referenceString) else { + throw InconsistencyError( + subjectName: "JSON Dynamic Reference", + details: "Failed to parse a JSON Dynamic Reference from '\(referenceString)'", + codingPath: container.codingPath + ) + } + self = .internal(internalReference) + } else { + guard let externalReference = URL(string: referenceString) else { + throw InconsistencyError( + subjectName: "JSON Dynamic Reference", + details: "Failed to parse a valid URI for a JSON Dynamic Reference from '\(referenceString)'", + codingPath: container.codingPath + ) + } + self = .external(externalReference) + } + } +} diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 9d89efd83..5b4c65b73 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -393,6 +393,11 @@ public extension JSONReference { description: description ) } + + /// Create a dynamic JSON Reference from the given JSONReference. + var dynamicReference: JSONDynamicReference { + JSONDynamicReference(self) + } } /// `SummaryOverridable` exists to provide a parent protocol to `OpenAPIDescribable` From 1def7b9c0d24ae49a5317af336f7b822b0648812 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 19 Mar 2024 23:16:02 -0500 Subject: [PATCH 2/5] add dynamic references to json schemas --- Sources/OpenAPIKit/JSONDynamicReference.swift | 11 +-- Sources/OpenAPIKit/JSONReference.swift | 2 + .../DereferencedJSONSchema.swift | 13 +++ .../Schema Object/JSONSchema+Combining.swift | 7 +- .../OpenAPIKit/Schema Object/JSONSchema.swift | 83 +++++++++++++++++-- 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/Sources/OpenAPIKit/JSONDynamicReference.swift b/Sources/OpenAPIKit/JSONDynamicReference.swift index b012a0387..416138943 100644 --- a/Sources/OpenAPIKit/JSONDynamicReference.swift +++ b/Sources/OpenAPIKit/JSONDynamicReference.swift @@ -6,6 +6,7 @@ // import OpenAPIKitCore +import Foundation @dynamicMemberLookup public struct JSONDynamicReference: Equatable, Hashable { @@ -30,7 +31,7 @@ public struct JSONDynamicReference: Equatable, Hashable { /// // encoded string: "#/components/schemas/greetings" /// // Swift: `document.components.schemas["greetings"]` public static func component(named name: String) -> Self { - return .init(.internal(.component(name: name)), summary: summary, description: description) + return .init(.internal(.component(name: name))) } /// Reference a path internal to this file but not within the Components Object @@ -39,7 +40,7 @@ public struct JSONDynamicReference: Equatable, Hashable { /// /// - Important: The path does not contain a leading '#'. Start with the root '/'. public static func `internal`(path: JSONReference.Path, summary: String? = nil, description: String? = nil) -> Self { - return .init(.internal(.path(path)), summary: summary, description: description) + return .init(.internal(.path(path))) } /// Reference an external URL. @@ -104,14 +105,14 @@ extension JSONDynamicReference: Decodable { } if referenceString.first == "#" { - guard let internalReference = InternalReference(rawValue: referenceString) else { + guard let internalReference = JSONReference.InternalReference(rawValue: referenceString) else { throw InconsistencyError( subjectName: "JSON Dynamic Reference", details: "Failed to parse a JSON Dynamic Reference from '\(referenceString)'", codingPath: container.codingPath ) } - self = .internal(internalReference) + self = .init(.internal(internalReference)) } else { guard let externalReference = URL(string: referenceString) else { throw InconsistencyError( @@ -120,7 +121,7 @@ extension JSONDynamicReference: Decodable { codingPath: container.codingPath ) } - self = .external(externalReference) + self = .init(.external(externalReference)) } } } diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 5b4c65b73..4937ae2d1 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -393,7 +393,9 @@ public extension JSONReference { description: description ) } +} +public extension JSONReference where ReferenceType == JSONSchema { /// Create a dynamic JSON Reference from the given JSONReference. var dynamicReference: JSONDynamicReference { JSONDynamicReference(self) diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 1ba5ba93a..72eb84ce5 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -27,6 +27,7 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { indirect case one(of: [DereferencedJSONSchema], core: CoreContext) indirect case any(of: [DereferencedJSONSchema], core: CoreContext) indirect case not(DereferencedJSONSchema, core: CoreContext) + case dynamicReference(JSONDynamicReference, CoreContext) /// Schemas without a `type`. case fragment(CoreContext) // This is the "{}" case where not even a type constraint is given. @@ -65,6 +66,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { return .any(of: schemas.map { $0.jsonSchema }, core: coreContext) case .not(let schema, core: let coreContext): return .not(schema.jsonSchema, core: coreContext) + case .dynamicReference(let reference, let coreContext): + return .dynamicReference(reference, coreContext) case .fragment(let context): return .fragment(context) } @@ -96,6 +99,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { return .any(of: schemas, core: core.optionalContext()) case .not(let schema, core: let core): return .not(schema, core: core.optionalContext()) + case .dynamicReference(let reference, let core): + return .dynamicReference(reference, core.optionalContext()) } } @@ -179,6 +184,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { return coreContext.vendorExtensions case .not(_, core: let coreContext): return coreContext.vendorExtensions + case .dynamicReference(_, let coreContext): + return coreContext.vendorExtensions case .fragment(let context): return context.vendorExtensions } @@ -209,6 +216,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { return .any(of: schemas, core: coreContext.with(description: description)) case .not(let schema, core: let coreContext): return .not(schema, core: coreContext.with(description: description)) + case .dynamicReference(let reference, let coreContext): + return .dynamicReference(reference, coreContext.with(description: description)) case .fragment(let context): return .fragment(context.with(description: description)) } @@ -239,6 +248,8 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { return .any(of: schemas, core: coreContext.with(vendorExtensions: vendorExtensions)) case .not(let schema, core: let coreContext): return .not(schema, core: coreContext.with(vendorExtensions: vendorExtensions)) + case .dynamicReference(let reference, let coreContext): + return .dynamicReference(reference, coreContext.with(vendorExtensions: vendorExtensions)) case .fragment(let context): return .fragment(context.with(vendorExtensions: vendorExtensions)) } @@ -520,6 +531,8 @@ extension JSONSchema: LocallyDereferenceable { return .any(of: schemas, core: addComponentNameExtension(to: coreContext)) case .not(let jsonSchema, core: let coreContext): return .not(try jsonSchema._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil), core: addComponentNameExtension(to: coreContext)) + case .dynamicReference(let reference, let coreContext): + return .dynamicReference(reference, addComponentNameExtension(to: coreContext)) case .fragment(let context): return .fragment(addComponentNameExtension(to: context)) } diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift index a8f48b3a5..d461d5949 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift @@ -173,6 +173,8 @@ internal struct FragmentCombiner { self.combinedFragment = .array(try leftCoreContext.combined(with: rightCoreContext), arrayContext) case (.fragment(let leftCoreContext), .object(let rightCoreContext, let objectContext)): self.combinedFragment = .object(try leftCoreContext.combined(with: rightCoreContext), objectContext) + case (.fragment(let leftCoreContext), .dynamicReference(let reference, let rightCoreContext)): + self.combinedFragment = .dynamicReference(reference, try leftCoreContext.combined(with: rightCoreContext)) case (.boolean(let leftCoreContext), .boolean(let rightCoreContext)): self.combinedFragment = .boolean(try leftCoreContext.combined(with: rightCoreContext)) @@ -208,7 +210,8 @@ internal struct FragmentCombiner { (.string, _), (.array, _), (.object, _), - (.null, _): + (.null, _), + (.dynamicReference, _): throw ( zip(combinedFragment.jsonType, fragment.jsonType).map { JSONSchemaResolutionError(.typeConflict(original: $0, new: $1)) @@ -258,6 +261,8 @@ internal struct FragmentCombiner { jsonSchema = try .any(of: schemas, core: coreContext.validatedContext()) case .one(of: let schemas, core: let coreContext): jsonSchema = try .one(of: schemas, core: coreContext.validatedContext()) + case .dynamicReference(let reference, let coreContext): + jsonSchema = try .dynamicReference(reference, coreContext.validatedContext()) case .not: throw JSONSchemaResolutionError(.unsupported(because: "`.not` is not yet supported for schema simplification")) } diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift index 3c7477d37..a40e9020a 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift @@ -63,6 +63,9 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings { public static func reference(_ reference: JSONReference, _ context: CoreContext) -> Self { .init(schema: .reference(reference, context)) } + public static func dynamicReference(_ reference: JSONDynamicReference, _ context: CoreContext) -> Self { + .init(schema: .dynamicReference(reference, context)) + } /// Schemas without a `type`. public static func fragment(_ core: CoreContext) -> Self { .init(schema: .fragment(core)) @@ -83,6 +86,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings { indirect case any(of: [JSONSchema], core: CoreContext) indirect case not(JSONSchema, core: CoreContext) case reference(JSONReference, CoreContext) + case dynamicReference(JSONDynamicReference, CoreContext) /// Schemas without a `type`. case fragment(CoreContext) // This allows for the "{}" case and also fragments of schemas that will later be combined with `all(of:)`. } @@ -104,7 +108,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings { return .integer(context.format) case .string(let context, _): return .string(context.format) - case .all, .one, .any, .not, .reference, .fragment: + case .all, .one, .any, .not, .reference, .dynamicReference, .fragment: return nil } } @@ -145,7 +149,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings { .any(of: _, core: let context), .not(_, core: let context): return context.format.rawValue - case .reference, .null: + case .reference, .dynamicReference, .null: return nil } } @@ -176,7 +180,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings { .any(of: _, core: let context as JSONSchemaContext), .not(_, core: let context as JSONSchemaContext): return context.discriminator - case .reference: + case .reference, .dynamicReference: return nil } } @@ -322,10 +326,20 @@ extension JSONSchema { } /// Check if a schema is a `.reference`. + /// + /// This returns `false` if the schema is a + /// `.dynamicReference` even though a + /// "dynamic reference" is a "reference". public var isReference: Bool { guard case .reference = value else { return false } return true } + + /// Check if a schema is a `.dynamicReference`. + public var isDynamicReference: Bool { + guard case .reference = value else { return false } + return true + } } // MARK: - Context Accessors @@ -347,7 +361,8 @@ extension JSONSchema { .one(of: _, core: let context as JSONSchemaContext), .any(of: _, core: let context as JSONSchemaContext), .not(_, core: let context as JSONSchemaContext), - .reference(_, let context as JSONSchemaContext): + .reference(_, let context as JSONSchemaContext), + .dynamicReference(_, let context as JSONSchemaContext): return context } } @@ -476,6 +491,8 @@ extension JSONSchema.Schema { return .not(of, core: core.with(vendorExtensions: vendorExtensions)) case .reference(let context, let coreContext): return .reference(context, coreContext.with(vendorExtensions: vendorExtensions)) + case .dynamicReference(let context, let coreContext): + return .dynamicReference(context, coreContext.with(vendorExtensions: vendorExtensions)) case .fragment(let context): return .fragment(context.with(vendorExtensions: vendorExtensions)) } @@ -547,6 +564,11 @@ extension JSONSchema { warnings: warnings, schema: .reference(reference, context.optionalContext()) ) + case .dynamicReference(let reference, let context): + return .init( + warnings: warnings, + schema: .dynamicReference(reference, context.optionalContext()) + ) case .null(let context): return .init( warnings: warnings, @@ -618,6 +640,11 @@ extension JSONSchema { warnings: warnings, schema: .reference(reference, context.requiredContext()) ) + case .dynamicReference(let reference, let context): + return .init( + warnings: warnings, + schema: .dynamicReference(reference, context.requiredContext()) + ) case .null(let context): return .init( warnings: warnings, @@ -684,7 +711,7 @@ extension JSONSchema { warnings: warnings, schema: .not(schema, core: core.nullableContext()) ) - case .reference, .null: + case .reference, .dynamicReference, .null: return self } } @@ -753,6 +780,11 @@ extension JSONSchema { warnings: warnings, schema: .reference(schema, core.with(allowedValues: allowedValues)) ) + case .dynamicReference(let schema, let core): + return .init( + warnings: warnings, + schema: .dynamicReference(schema, core.with(allowedValues: allowedValues)) + ) case .null(let core): return .init( warnings: warnings, @@ -824,6 +856,11 @@ extension JSONSchema { warnings: warnings, schema: .reference(schema, core.with(defaultValue: defaultValue)) ) + case .dynamicReference(let schema, let core): + return .init( + warnings: warnings, + schema: .dynamicReference(schema, core.with(defaultValue: defaultValue)) + ) case .null(let core): return .init( warnings: warnings, @@ -902,6 +939,11 @@ extension JSONSchema { warnings: warnings, schema: .reference(schema, core.with(examples: examples)) ) + case .dynamicReference(let schema, let core): + return .init( + warnings: warnings, + schema: .dynamicReference(schema, core.with(examples: examples)) + ) case .null(let core): return .init( warnings: warnings, @@ -968,7 +1010,7 @@ extension JSONSchema { warnings: warnings, schema: .not(schema, core: core.with(discriminator: discriminator)) ) - case .reference, .null: + case .reference, .dynamicReference, .null: return self } } @@ -1036,6 +1078,11 @@ extension JSONSchema { warnings: warnings, schema: .reference(ref, referenceContext.with(description: description)) ) + case .dynamicReference(let ref, let referenceContext): + return .init( + warnings: warnings, + schema: .dynamicReference(ref, referenceContext.with(description: description)) + ) case .null(let referenceContext): return .init( warnings: warnings, @@ -1757,6 +1804,21 @@ extension JSONSchema { .init(required: required, title: title, description: description, anchor: anchor, dynamicAnchor: dynamicAnchor) ) } + + /// Construct a dynamic reference schema + public static func dynamicReference( + _ reference: JSONDynamicReference, + required: Bool = true, + title: String? = nil, + description: String? = nil, + anchor: String? = nil, + dynamicAnchor: String? = nil + ) -> JSONSchema { + return .dynamicReference( + reference, + .init(required: required, title: title, description: description, anchor: anchor, dynamicAnchor: dynamicAnchor) + ) + } } // MARK: - Describable @@ -1852,6 +1914,10 @@ extension JSONSchema: Encodable { try core.encode(to: encoder) try reference.encode(to: encoder) + case .dynamicReference(let reference, let core): + try core.encode(to: encoder) + try reference.encode(to: encoder) + case .fragment(let context): var container = encoder.singleValueContainer() @@ -1890,6 +1956,11 @@ extension JSONSchema: Decodable { self = .init(warnings: coreContext.warnings, schema: .reference(ref, coreContext)) return } + if let dynamicRef = try? JSONDynamicReference(from: decoder) { + let coreContext = try CoreContext(from: decoder) + self = .init(warnings: coreContext.warnings, schema: .dynamicReference(dynamicRef, coreContext)) + return + } let container = try decoder.container(keyedBy: SubschemaCodingKeys.self) From ed5575103cfd74816f750187c044b4af02d1e5a4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 23 Mar 2024 14:43:28 -0500 Subject: [PATCH 3/5] starting to stub out an example to test by. need defs to really use dynamicAnchors --- .../JSONSchemaDynamicReferenceTests.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift new file mode 100644 index 000000000..48ae0099c --- /dev/null +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift @@ -0,0 +1,25 @@ +// +// JSONSchemaDynamicReferenceTests.swift +// +// +// Created by Mathew Polzin. +// + +import Foundation +import XCTest +import OpenAPIKit + +final class SchemaObjectDynamicReferenceTests: XCTestCase { + func test_tmp() throws { + let testComponents = OpenAPI.Components( + schemas: [ + "T": .all(of: [], dynamicAnchor: "T"), + "genericList": .array( + items: .dynamicReference(.internal("T")) + ), + "intList": .init( + ) + ] + ) + } +} From 7e71ce7422365c3d00ae9225595f8bcd32cf4e3e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 23 Mar 2024 15:14:09 -0500 Subject: [PATCH 4/5] use schema-local defs --- .../JSONSchemaDynamicReferenceTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift index 48ae0099c..18a6108cc 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift @@ -13,11 +13,17 @@ final class SchemaObjectDynamicReferenceTests: XCTestCase { func test_tmp() throws { let testComponents = OpenAPI.Components( schemas: [ - "T": .all(of: [], dynamicAnchor: "T"), "genericList": .array( items: .dynamicReference(.internal("T")) + defs: [ + "T": .all(of: [], dynamicAnchor: "T") + ] ), - "intList": .init( + "intList": .reference( + .component(named: "genericList"), + defs: [ + "T": .string(dynamicAnchor: "T") + ] ) ] ) From c4f9c06ed7248ef46c9a4c64fb6306b6fd2e78b9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 24 Mar 2024 12:45:59 -0500 Subject: [PATCH 5/5] support anchor types of references (as opposed to local paths or remote URIs) --- Sources/OpenAPIKit/JSONDynamicReference.swift | 10 +- Sources/OpenAPIKit/JSONReference.swift | 10 ++ .../JSONSchemaDynamicReferenceTests.swift | 94 ++++++++++++++++++- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/Sources/OpenAPIKit/JSONDynamicReference.swift b/Sources/OpenAPIKit/JSONDynamicReference.swift index 416138943..8373c8665 100644 --- a/Sources/OpenAPIKit/JSONDynamicReference.swift +++ b/Sources/OpenAPIKit/JSONDynamicReference.swift @@ -34,13 +34,11 @@ public struct JSONDynamicReference: Equatable, Hashable { return .init(.internal(.component(name: name))) } - /// Reference a path internal to this file but not within the Components Object - /// This is likely not what you are looking for. It is advisable to store reusable components - /// in the Components Object. + /// Reference a dynamic anchor local to this file. /// - /// - Important: The path does not contain a leading '#'. Start with the root '/'. - public static func `internal`(path: JSONReference.Path, summary: String? = nil, description: String? = nil) -> Self { - return .init(.internal(.path(path))) + /// - Important: The anchor does not contain a leading '#'. + public static func anchor(_ anchor: String) -> Self { + return .init(.internal(.anchor(anchor))) } /// Reference an external URL. diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 4937ae2d1..ab138a23d 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -129,6 +129,8 @@ public enum JSONReference: Equatabl case component(name: String) /// The reference refers to some path outside the Components Object. case path(Path) + /// The reference refers to some anchor anywhere in the local Schema. + case anchor(String) /// Get the name of the referenced object. /// @@ -147,6 +149,8 @@ public enum JSONReference: Equatabl return name case .path(let path): return path.components.last?.stringValue + case .anchor(let name): + return name } } @@ -164,6 +168,10 @@ public enum JSONReference: Equatabl } let fragment = rawValue.dropFirst() guard fragment.starts(with: "/components") else { + guard fragment.first == "/" else { + self = .anchor(String(fragment)) + return + } self = .path(Path(rawValue: String(fragment))) return } @@ -190,6 +198,8 @@ public enum JSONReference: Equatabl return "#/components/\(ReferenceType.openAPIComponentsKey)/\(name)" case .path(let path): return "#\(path.rawValue)" + case .anchor(let name): + return "#\(name)" } } } diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift index 18a6108cc..a61fcf2d0 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaDynamicReferenceTests.swift @@ -14,18 +14,106 @@ final class SchemaObjectDynamicReferenceTests: XCTestCase { let testComponents = OpenAPI.Components( schemas: [ "genericList": .array( - items: .dynamicReference(.internal("T")) + items: .dynamicReference(.anchor("T")), defs: [ - "T": .all(of: [], dynamicAnchor: "T") + "generic-param": .all(of: [], core: .init(dynamicAnchor: "T")) ] ), "intList": .reference( .component(named: "genericList"), defs: [ - "T": .string(dynamicAnchor: "T") + "string-param": .string(dynamicAnchor: "T") ] ) ] ) + + let jsonString = """ + { + "schemas" : { + "genericList" : { + "$defs" : { + "T" : { + "$dynamicAnchor" : "T", + "allOf" : [ + + ] + } + }, + "items" : { + "$dynamicRef" : "#T" + }, + "type" : "array" + }, + "intList" : { + "$defs" : { + "T" : { + "$dynamicAnchor" : "T", + "type" : "string" + } + }, + "$ref" : "#/components/schemas/genericList" + } + } + } + """ + + XCTAssertEqual( + testComponents, + try orderUnstableDecode(OpenAPI.Components.self, from: jsonString.data(using: .utf8)!) + ) + } + + func test_tmp2() throws { + let testComponents = OpenAPI.Components( + schemas: [ + "genericList": .array( + items: .dynamicReference(.anchor("T")), + defs: [ + "generic-param": .all(of: [], core: .init(dynamicAnchor: "T")) + ] + ), + "intList": .reference( + .component(named: "genericList"), + defs: [ + "string-param": .string(dynamicAnchor: "T") + ] + ) + ] + ) + + let testDoc = OpenAPI.Document( + info: .init(title: "test", version: "1.0.0"), + servers: [], + paths: [:], + components: testComponents + ) + + let dereferenced = try testDoc.locallyDereferenced() + + print(dereferenced.components.schemas["intList"]!.defs) + } + + func buildDynamicContext(_ defs: OrderedDictionary) -> [String: JSONSchema] { + var dynamicContext: [String: JSONSchema] = [:] + for (_, def) in defs { + if let anchor = def.dynamicAnchor { + dynamicContext[anchor] = def + } + } + return dynamicContext + } + + func t(_ source: JSONSchema, _ components: OpenAPI.Components) throws -> JSONSchema { + return try tp( + source, + dynamicContext: buildDynamicContext(components.schemas) + ) + } + + func tp(_ source: JSONSchema, dynamicContext: [String: JSONSchema]) throws -> JSONSchema { + var localDynamicContext = buildDynamicContext(source.defs) + + localDynamicContext.merge(dynamicContext, uniquingKeysWith: { local, global in local }) } }