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/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/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index 35790e8f2..bf930fd79 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 @@ -97,6 +98,10 @@ public protocol LocallyDereferenceable { following references: Set, dereferencedFromComponentNamed name: String? ) throws -> DereferencedSelf + + 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 07cbc0da6..c50c82976 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -268,4 +268,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 20fb7c45d..b60533ec0 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -74,4 +74,12 @@ extension OpenAPI.Content: LocallyDereferenceable { ) throws -> DereferencedContent { return try DereferencedContent(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Content where Context : ExternalLoaderContext { + var content = self + + // 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 fdd0b1bbc..14fd45248 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -55,4 +55,13 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { ) throws -> DereferencedContentEncoding { return try DereferencedContentEncoding(self, resolvingIn: components, following: references) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Content.Encoding where Context : ExternalLoaderContext { + var contentEncoding = self + + // TODO: need to externally dereference the headers here. +#warning("need to externally dereference the headers here.") + + return self + } } diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index d9c293429..7a0e91798 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 75b0d0669..4fe7b6c17 100644 --- a/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift +++ b/Sources/OpenAPIKit/Either/Either+LocallyDereferenceable.swift @@ -21,4 +21,13 @@ extension Either: LocallyDereferenceable where A: LocallyDereferenceable, B: Loc return try value._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil) } } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { + switch self { + case .a(let a): + return .a(try a.externallyDereferenced(with: &loader)) + case .b(let b): + return .b(try b.externallyDereferenced(with: &loader)) + } + } } diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index 457edcc76..02b59577f 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -25,7 +25,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, @@ -206,6 +206,10 @@ extension OpenAPI.Example: LocallyDereferenceable { vendorExtensions: vendorExtensions ) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Example where Context : ExternalLoaderContext { + return self + } } extension OpenAPI.Example: Validatable {} 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/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 5f8eee40b..80967ccc0 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -81,4 +81,10 @@ extension OpenAPI.Header: LocallyDereferenceable { ) throws -> DereferencedHeader { return try DereferencedHeader(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Header where Context : ExternalLoaderContext { + // 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 9d89efd83..806eedfd5 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -511,7 +511,7 @@ extension OpenAPI.Reference: Decodable { } // MARK: - LocallyDereferenceable -extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable { +extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable & Decodable & Equatable { /// Look up the component this reference points to and then /// dereference it. /// @@ -535,9 +535,23 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere .lookup(self) ._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { + switch self { + case .internal(let ref): + let value = try loader.components.lookup(self) + .externallyDereferenced(with: &loader) + let key = try OpenAPI.ComponentKey.forceInit(rawValue: ref.name) + loader.components[keyPath: ReferenceType.openAPIComponentsKeyPath][key] = + value + return self + case .external(let url): + return try loader.store(type: ReferenceType.self, from: url).jsonReference + } + } } -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. /// @@ -562,6 +576,10 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally .lookup(self) ._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> Self where Context : ExternalLoaderContext { + return .init(try jsonReference.externallyDereferenced(with: &loader)) + } } extension OpenAPI.Reference: Validatable where ReferenceType: Validatable {} 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/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index da2a06050..6f457e48e 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -123,4 +123,10 @@ extension OpenAPI.Operation: LocallyDereferenceable { ) throws -> DereferencedOperation { return try DereferencedOperation(self, resolvingIn: components, following: references) } + + 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..46bbd8ba4 --- /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 { + 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) + newDict[key] = newRef + } + return newDict + } +} diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 97fb607b7..e54680b84 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -81,4 +81,13 @@ extension OpenAPI.Parameter: LocallyDereferenceable { ) throws -> DereferencedParameter { return try DereferencedParameter(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } + + public func externallyDereferenced(with loader: inout ExternalLoader) 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 9801a401a..499b53185 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -68,4 +68,10 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { ) throws -> DereferencedSchemaContext { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } + + 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/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 7b711b1b1..b6f8817a5 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 @@ -136,4 +137,22 @@ extension OpenAPI.PathItem: LocallyDereferenceable { ) throws -> DereferencedPathItem { return try DereferencedPathItem(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } + + 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(with: &loader) + ) + } + + 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 6e54e0c40..ef715500a 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -60,4 +60,10 @@ extension OpenAPI.Request: LocallyDereferenceable { ) throws -> DereferencedRequest { return try DereferencedRequest(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } + + 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 76e883179..20cc3ff6a 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -75,4 +75,10 @@ extension OpenAPI.Response: LocallyDereferenceable { ) throws -> DereferencedResponse { return try DereferencedResponse(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } + + public func externallyDereferenced(with loader: inout ExternalLoader) throws -> OpenAPI.Response where Context : ExternalLoaderContext { + // TODO: externally dereference the headers and content +#warning("externally dereference the headers and content") + return self + } } diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 509df1c09..2674b038c 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -527,4 +527,10 @@ extension JSONSchema: LocallyDereferenceable { public func dereferenced() -> DereferencedJSONSchema? { return try? dereferenced(in: .noComponents) } + + 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/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 { diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 2a33e1acd..f6af60867 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -272,4 +272,8 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { } return ret } + + public func externallyDereferenced(with loader: inout ExternalLoader) 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/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index c68703e8d..2a8236c34 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -1171,3 +1171,130 @@ 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) + + 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 { + // 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": { + "x-source-url": "file:\/\/.\/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 db97d6b4a..0a39a6b13 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -432,3 +432,59 @@ extension PathItemTests { ) } } + +// MARK: External Dereferencing Tests +extension PathItemTests { + struct MockLoad: ExternalLoaderContext { + func nextComponentKey(type: T.Type, at url: URL, given components: OpenAPI.Components) -> OpenAPI.ComponentKey { + "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("------") + let context = MockLoad() + var loader = ExternalLoader(components: components, context: context) + let x = try pathItem.externallyDereferenced(with: &loader) + print(x.parameters.debugDescription) + print("=======") + print(loader.components.parameters) + } +} diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index eb683ccee..7383e6b84 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -755,7 +755,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) 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