diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 81b5994ae..cfae74263 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -5,7 +5,7 @@ on: [pull_request] jobs: codecov: container: - image: swift:5.8 + image: swift:5.10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 7ed549dc4..be25e892d 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -3,25 +3,33 @@ name: Documentation on: push: branches: - - main + - release/4_0 + # ^ for now, we only want to use the v4.0 release branch. jobs: build: runs-on: ubuntu-latest + container: + image: swift:5.10 steps: - uses: actions/checkout@v4 - # TODO: replace the documentation generator with something that is still maintained. + - name: Build Docs + run: | + mkdir -p ./gh-pages + swift package --allow-writing-to-directory ./gh-pages/docs \ + generate-documentation --include-extended-types \ + --disable-indexing \ + --output-path ./gh-pages/docs \ + --transform-for-static-hosting \ + --hosting-base-path OpenAPIKit \ + --target OpenAPIKit + - name: Install rsync + run: | + apt-get update && apt-get install -y rsync + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@v4.6.8 + with: + folder: gh-pages + branch: gh-pages -# - name: Generate Documentation -# uses: SwiftDocOrg/swift-doc@master -# with: -# inputs: Sources -# module-name: OpenAPIKit -# output: Documentation -# - name: Upload Documentation to Wiki -# uses: SwiftDocOrg/github-wiki-publish-action@v1 -# with: -# path: Documentation -# env: -# GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee27e6c57..9752fc667 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,44 +13,6 @@ jobs: fail-fast: false matrix: image: - - swift:5.2-focal - - swift:5.2-centos8 - - swift:5.3-focal - - swift:5.3-centos8 - # see below for 5.4, 5.5, etc. - container: ${{ matrix.image }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Run tests - run: swift test --enable-test-discovery - # 5.4 is separate because there was a bug in the compiler that caused - # us to need to run swift tets with the -sil-verify-none flag. - linux-5_4: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - image: - - swift:5.4-focal - - swift:5.4-centos8 - container: ${{ matrix.image }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Run tests (without test discovery flag) - run: swift test -Xswiftc -Xfrontend -Xswiftc -sil-verify-none - linux-5_5-plus: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - image: - - swift:5.5-focal - - swift:5.5-centos8 - - swift:5.6-focal - - swift:5.7-focal - - swift:5.7-jammy - swift:5.8-focal - swift:5.8-jammy - swift:5.9-focal @@ -67,7 +29,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Run tests - run: swift test + run: swift test -Xswiftc -strict-concurrency=complete osx: strategy: fail-fast: false @@ -84,4 +46,4 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Run tests - run: swift test --enable-test-discovery + run: swift test -Xswiftc -strict-concurrency=complete diff --git a/Package.resolved b/Package.resolved index 3c56c5b7e..95608ba82 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,32 @@ { - "object": { - "pins": [ - { - "package": "Yams", - "repositoryURL": "https://github.com/jpsim/Yams.git", - "state": { - "branch": null, - "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", - "version": "4.0.6" - } + "pins" : [ + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", + "version" : "1.4.3" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "2688707e563b44d7d87c29ba6c5ca04ce86ae58b", + "version" : "5.3.0" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 9da86ea46..218350b9c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.1 +// swift-tools-version: 5.8 import PackageDescription let package = Package( name: "OpenAPIKit", platforms: [ - .macOS(.v10_10), + .macOS(.v10_15), .iOS(.v11) ], products: [ @@ -20,12 +20,14 @@ let package = Package( targets: ["OpenAPIKitCompat"]), ], dependencies: [ - .package(url: "https://github.com/jpsim/Yams.git", "4.0.0"..<"6.0.0") // just for tests + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/jpsim/Yams.git", "5.1.0"..<"6.0.0") // just for tests ], targets: [ .target( name: "OpenAPIKitCore", - dependencies: []), + dependencies: [], + exclude: ["AnyCodable/README.md"]), .testTarget( name: "OpenAPIKitCoreTests", dependencies: ["OpenAPIKitCore"]), diff --git a/README.md b/README.md index 16ffa8797..cb47a8f01 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ -[![sswg:sandbox|94x20](https://img.shields.io/badge/sswg-sandbox-lightgrey.svg)](https://github.com/swift-server/sswg/blob/master/process/incubation.md#sandbox-level) [![Swift 5.1+](http://img.shields.io/badge/Swift-5.1+-blue.svg)](https://swift.org) +[![sswg:sandbox|94x20](https://img.shields.io/badge/sswg-sandbox-lightgrey.svg)](https://github.com/swift-server/sswg/blob/master/process/incubation.md#sandbox-level) [![Swift 5.8+](http://img.shields.io/badge/Swift-5.8+-blue.svg)](https://swift.org) [![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) ![Tests](https://github.com/mattpolzin/OpenAPIKit/workflows/Tests/badge.svg) # OpenAPIKit -A library containing Swift types that encode to- and decode from [OpenAPI 3.0.x](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md) and [OpenAPI 3.1.x](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md) Documents and their components. +A library containing Swift types that encode to- and decode from [OpenAPI 3.0.x](https://spec.openapis.org/oas/v3.0.4.html) and [OpenAPI 3.1.x](https://spec.openapis.org/oas/v3.1.1.html) Documents and their components. -The single most confusing thing you will grapple with out of the gate is explained by the following grid of what OpenAPIKit versions support which OpenAPI specification versions. +OpenAPIKit follows semantic versioning despite the fact that the OpenAPI specificaiton does not. The following chart shows which OpenAPI specification versions and key features are supported by which OpenAPIKit versions. -| OpenAPIKit | OpenAPI v3.0 | OpenAPI v3.1 | -|-------------|---------------|--------------| -| v2.x | ✅ | ❌ | -| v3.x | ✅ | ✅ | +| OpenAPIKit | Swift | OpenAPI v3.0 | OpenAPI v3.1 | External Dereferencing | +|------------|-------|--------------|--------------|------------------------| +| v2.x | 5.1+ | ✅ | | | +| v3.x | 5.1+ | ✅ | ✅ | | +| v4.x | 5.8+ | ✅ | ✅ | ✅ | - [Usage](#usage) - [Migration](#migration) - [1.x to 2.x](#1.x-to-2.x) - [2.x to 3.x](#2.x-to-3.x) + - [3.x to 4.x](#3.x-to-4.x) - [Decoding OpenAPI Documents](#decoding-openapi-documents) - [Decoding Errors](#decoding-errors) - [Encoding OpenAPI Documents](#encoding-openapi-documents) @@ -73,6 +75,9 @@ import OpenAPIKit It is recommended that you build your project against the `OpenAPIKit` module and only use `OpenAPIKit30` to support reading OpenAPI 3.0.x documents in and then [converting them](#supporting-openapi-30x-documents) to OpenAPI 3.1.x documents. The situation not supported yet by this strategy is where you need to write out an OpenAPI 3.0.x document (as opposed to 3.1.x). That is a planned feature but it has not yet been implemented. If your use-case benefits from reading in an OpenAPI 3.0.x document and also writing out an OpenAPI 3.0.x document then you can operate entirely against the `OpenAPIKit30` module. +#### 3.x to 4.x +If you are migrating from OpenAPIKit 3.x to OpenAPIKit 4.x, check out the [v4 migration guide](./documentation/v4_migration_guide.md). + ### Decoding OpenAPI Documents Most documentation will focus on what it looks like to work with the `OpenAPIKit` module and OpenAPI 3.1.x documents. If you need to support OpenAPI 3.0.x documents, take a look at the section on [supporting OpenAPI 3.0.x documents](#supporting-openapi-30x-documents) before you get too deep into this library's docs. @@ -144,7 +149,7 @@ let newDoc: OpenAPIKit.OpenAPI.Document oldDoc = try? JSONDecoder().decode(OpenAPI.Document.self, from: someFileData) -newDoc = oldDoc?.convert(to: .v3_1_0) ?? +newDoc = oldDoc?.convert(to: .v3_1_1) ?? (try! JSONDecoder().decode(OpenAPI.Document.self, from: someFileData)) // ^ Here we simply fall-back to 3.1.x if loading as 3.0.x failed. You could do a more // graceful job of this by determining up front which version to attempt to load or by @@ -160,7 +165,7 @@ If retaining order is important for your use-case, I recommend the [**Yams**](ht The Foundation JSON encoding and decoding will be the most stable and battle-tested option with Yams as a pretty well established and stable option as well. FineJSON is lesser used (to my knowledge) but I have had success with it in the past. ### OpenAPI Document structure -The types used by this library largely mirror the object definitions found in the OpenAPI specification [version 3.1.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md) (`OpenAPIKit` module) and [version 3.0.3](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md) (`OpenAPIKit30` module). The [Project Status](#project-status) lists each object defined by the spec and the name of the respective type in this library. The project status page currently focuses on OpenAPI 3.1.x but for the purposes of determining what things are named and what is supported you can mostly infer the status of the OpenAPI 3.0.x support as well. +The types used by this library largely mirror the object definitions found in the OpenAPI specification [version 3.1.1](https://spec.openapis.org/oas/v3.1.1.html) (`OpenAPIKit` module) and [version 3.0.4](https://spec.openapis.org/oas/v3.0.4.html) (`OpenAPIKit30` module). The [Project Status](#project-status) lists each object defined by the spec and the name of the respective type in this library. The project status page currently focuses on OpenAPI 3.1.x but for the purposes of determining what things are named and what is supported you can mostly infer the status of the OpenAPI 3.0.x support as well. #### Document Root At the root there is an `OpenAPI.Document`. In addition to some information that applies to the entire API, the document contains `OpenAPI.Components` (essentially a dictionary of reusable components that can be referenced with `JSONReferences` and `OpenAPI.References`) and an `OpenAPI.PathItem.Map` (a dictionary of routes your API defines). @@ -172,7 +177,7 @@ Each route is an entry in the document's `OpenAPI.PathItem.Map`. The keys of thi Each endpoint on a route is defined by an `OpenAPI.Operation`. Among other things, this operation can specify the parameters (path, query, header, etc.), request body, and response bodies/codes supported by the given endpoint. #### Request/Response Bodies -Request and response bodies can be defined in great detail using OpenAPI's derivative of the JSON Schema specification. This library uses the `JSONSchema` type for such schema definitions. +Request and response bodies can be defined in great detail using OpenAPI's derivative of the JSON Schema specification. This library uses the [`JSONSchema`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/jsonschema) type for such schema definitions. #### Schemas **Fundamental types** are specified as `JSONSchema.integer`, `JSONSchema.string`, `JSONSchema.boolean`, etc. @@ -209,18 +214,18 @@ JSONSchema.object( Take a look at the [OpenAPIKit Schema Object](./documentation/schema_object.md) documentation for more information. #### OpenAPI References -The `OpenAPI.Reference` type represents the OpenAPI specification's reference support that is essentially just JSON Reference specification compliant but with the ability to override summaries and descriptions at the reference site where appropriate. +The [`OpenAPI.Reference`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/openapi/reference) type represents the OpenAPI specification's reference support that is essentially just JSON Reference specification compliant but with the ability to override summaries and descriptions at the reference site where appropriate. For details on the underlying reference support, see the next section on the `JSONReference` type. #### JSON References -The `JSONReference` type allows you to work with OpenAPIDocuments that store some of their information in the shared Components Object dictionary or even external files. Only documents where all references point to the Components Object can be dereferenced currently, but you can encode and decode all references. +The [`JSONReference`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/jsonreference) type allows you to work with OpenAPIDocuments that store some of their information in the shared Components Object dictionary or even external files. Only documents where all references point to the Components Object can be dereferenced currently, but you can encode and decode all references. You can create an external reference with `JSONReference.external(URL)`. Internal references usually refer to an object in the Components Object dictionary and are constructed with `JSONReference.component(named:)`. If you need to refer to something in the current file but not in the Components Object, you can use `JSONReference.internal(path:)`. You can check whether a given `JSONReference` exists in the Components Object with `document.components.contains()`. You can access a referenced object in the Components Object with `document.components[reference]`. -You can create references from the Components Object with `document.components.reference(named:ofType:)`. This method will throw an error if the given component does not exist in the ComponentsObject. +References can be created from the Components Object with `document.components.reference(named:ofType:)`. This method will throw an error if the given component does not exist in the ComponentsObject. You can use `document.components.lookup()` or the `Components` type's `subscript` to turn an `Either` containing either a reference or a component into an optional value of that component's type (having either pulled it out of the `Either` or looked it up in the Components Object). The `lookup()` method throws when it can't find an item whereas `subscript` returns `nil`. @@ -237,7 +242,7 @@ Note that this looks a component up in the Components Object but it does not tra #### Security Requirements In the OpenAPI specifcation, a security requirement (like can be found on the root Document or on Operations) is a dictionary where each key is the name of a security scheme found in the Components Object and each value is an array of applicable scopes (which is of course only a non-empty array when the security scheme type is one for which "scopes" are relevant). -OpenAPIKit defines the `SecurityRequirement` typealias as a dictionary with `JSONReference` keys; These references point to the Components Object and provide a slightly stronger contract than the String values required by the OpenAPI specification. Naturally, these are encoded to JSON/YAML as String values rather than JSON References to maintain compliance with the OpenAPI Specification. +OpenAPIKit defines the [`SecurityRequirement`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/openapi/securityrequirement) typealias as a dictionary with `JSONReference` keys; These references point to the Components Object and provide a slightly stronger contract than the String values required by the OpenAPI specification. Naturally, these are encoded to JSON/YAML as String values rather than JSON References to maintain compliance with the OpenAPI Specification. To give an example, let's say you want to describe OAuth 2.0 authentication via the implicit flow. First, define a Security Scheme: ```swift @@ -284,9 +289,22 @@ let document = OpenAPI.Document( ``` #### Specification Extensions -Many OpenAPIKit types support [Specification Extensions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#specification-extensions). As described in the OpenAPI Specification, these extensions must be objects that are keyed with the prefix "x-". For example, a property named "specialProperty" on the root OpenAPI Object (`OpenAPI.Document`) is invalid but the property "x-specialProperty" is a valid specification extension. +Many OpenAPIKit types support [Specification Extensions](https://spec.openapis.org/oas/v3.1.1.html#specification-extensions). As described in the OpenAPI Specification, these extensions must be objects that are keyed with the prefix "x-". For example, a property named "specialProperty" on the root OpenAPI Object (`OpenAPI.Document`) is invalid but the property "x-specialProperty" is a valid specification extension. + +You can get or set specification extensions via the [`vendorExtensions`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/vendorextendable/vendorextensions-swift.property) property on any object that supports this feature. The keys are `Strings` beginning with the aforementioned "x-" prefix and the values are `AnyCodable`. If you set an extension without using the "x-" prefix, the prefix will be added upon encoding. + +If you wish to disable decoding/encoding of vendor extensions for performance reasons, you can configure the Encoder and Decoder using their `userInfo`: +```swift +let userInfo = [VendorExtensionsConfiguration.enabledKey: false] +let encoder = JSONEncoder() +encoder.userInfo = userInfo + +let decoder = JSONDecoder() +decoder.userInfo = userInfo +``` -You can get or set specification extensions via the `vendorExtensions` property on any object that supports this feature. The keys are `Strings` beginning with the aforementioned "x-" prefix and the values are `AnyCodable`. If you set an extension without using the "x-" prefix, the prefix will be added upon encoding. +#### AnyCodable +OpenAPIKit uses the `AnyCodable` type for vendor extensions and constructing examples for JSON Schemas. OpenAPIKit's `AnyCodable` type is an adaptation of the Flight School library that can be found [here](https://github.com/Flight-School/AnyCodable). `AnyCodable` can be constructed from literals or explicitly. The following are all valid. @@ -300,12 +318,75 @@ document.vendorExtensions["x-specialProperty4"] = ["hello": "world"] document.vendorExtensions["x-specialProperty5"] = AnyCodable("hello world") ``` +It is important to note that `AnyCodable` wraps Swift types in a way that keeps track of the Swift type used to construct it as much as possible, but if you encode an `AnyCodable` and then decode that result, the decoded value may not always be the same as the pre-encoded value started out. This is because many Swift types will encode to "stringy" values and then decode as simply `String` values. There are two ways to cope with this: + 1. When adding stringy values to structures that will be passed to `AnyCodable`, you can explicitly turn them into `String`s. For example, you can use `URL(...).absoluteString` both to specify you want the absolute value of the URL encoded and also to turn it into a `String` up front. + 2. When comparing `AnyCodable` values that have passed through a full encode/decode cycle, you can compare the `description` of the two `AnyCodable` values. This stringy result is _more likely_ to compare equivalently. + +Keep in mind, this issue only occurs when you are comparing value `a` and value `b` for equality given that `b` is `a` after being encoded and then subsequently decoded. + +The other sure-fire way to handle this (if you need encode-decode equality, not just equivalence) is to make sure you run both values being compared through encoding. For example, you might use the following function which doesn't even care if the input is `AnyCodable` or not: +```swift +func encodedEqual(_ a: A, _ b: B) throws -> Bool { + let a = try JSONEncoder().encode(a) + let b = try JSONEncoder().encode(b) + return a == b +} +``` +For example, the result of the following is `true`: +```swift +try encodeEqual(URL(string: "https://website.com"), AnyCodable(URL(string: "https://website.com"))) +``` + ### Dereferencing & Resolving -In addition to looking something up in the `Components` object, you can entirely derefererence many OpenAPIKit types. A dereferenced type has had all of its references looked up (and all of its properties' references, all the way down). +#### External References +External dereferencing does not resolve any local (internal) references, it just loads external references into the Document. It does this by storing any loaded externally referenced objects in the Components Object and transforming the reference being resolved from an external reference to an internal one. That way, you can always run internal dereferencing as a second step if you want a fully dereferenced document, but if you simply wanted to load additional referenced files then you can stop after external dereferencing. + +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 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: + +```swift +struct ExampleLoader: ExternalLoader { + typealias Message = Void + + 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)) + + // 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 test makes sure + // the following strategy of using vendor extensions works. + if var extendable = decoded as? VendorExtendable { + extendable.vendorExtensions["x-source-url"] = AnyCodable(url) + finished = extendable as! T + } else { + finished = decoded + } + return (finished, []) + } + + static func componentKey(type: T.Type, 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: + let urlString = url.pathComponents + .joined(separator: "_") + .replacingOccurrences(of: ".", with: "_") + return try .forceInit(rawValue: urlString) + } +} +``` + +Once you have an `ExternalLoader`, you can call an `OpenAPI.Document`'s `externallyDereference()` method to externally dereference it. You get to choose whether to only load references to a certain depth or to fully resolve references until you run out of them; any given referenced document may itself contain references and these references may point back to things loaded into the Document previously so dereferencing is done recursively up to a given depth (or until fully dereferenced if you use the `.full` depth). + +If you have some information that you want to pass back to yourself from the `load()` function, you can specify any type you want as the `Message` type and return any number of messages from each `load()` function execution. These messages could be warnings, additional information about the files that each newly loaded Component came from, etc. If you want to tie some information about file loading to new Components in your messages, you can use the `componentKey()` function to get the key the new Component will be found under once external dereferencing is complete. + +#### Internal References +In addition to looking something up in the [`Components`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/openapi/components) object, you can entirely derefererence many OpenAPIKit types. A dereferenced type has had all of its references looked up (and all of its properties' references, all the way down). -You use a value's `dereferenced(in:)` method to fully dereference it. +You use a value's [`dereferenced(in:)`](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit/locallydereferenceable) method to fully dereference it. -You can even dereference the whole document with the `OpenAPI.Document` `locallyDereferenced()` method. As the name implies, you can only derefence whole documents that are contained within one file (which is another way of saying that all references are "local"). Specifically, all references must be located within the document's Components Object. +You can even dereference the whole document with the `OpenAPI.Document` `locallyDereferenced()` method. As the name implies, you can only derefence whole documents that are contained within one file (which is another way of saying that all references are "local"). Specifically, all references must be located within the document's Components Object. External dereferencing is done as a separeate step, but you can first dereference externally and then dereference internally if you'd like to perform both. Unlike what happens when you lookup an individual component using the `lookup()` method on `Components`, dereferencing a whole `OpenAPI.Document` will result in type-level changes that guarantee all references are removed. `OpenAPI.Document`'s `locallyDereferenced()` method returns a `DereferencedDocument` which exposes `DereferencedPathItem`s which have `DereferencedParameter`s and `DereferencedOperation`s and so on. @@ -402,4 +483,4 @@ Please see [Security](./SECURITY.md) for information on how to report vulnerabil **Please do not report security vulnerabilities via GitHub issues.** ## Specification Coverage & Type Reference -For a full list of OpenAPI Specification types annotated with whether OpenAPIKit supports them and relevant translations to OpenAPIKit types, see the [Specification Coverage](./documentation/specification_coverage.md) documentation. For detailed information on the OpenAPIKit types, see the [full type documentation](https://github.com/mattpolzin/OpenAPIKit/wiki). +For a full list of OpenAPI Specification types annotated with whether OpenAPIKit supports them and relevant translations to OpenAPIKit types, see the [Specification Coverage](./documentation/specification_coverage.md) documentation. For detailed information on the OpenAPIKit types, see the [full type documentation](https://mattpolzin.github.io/OpenAPIKit/documentation/openapikit). diff --git a/Sources/OpenAPIKit/Callbacks.swift b/Sources/OpenAPIKit/Callbacks.swift index 3d1edf793..2d0019ac4 100644 --- a/Sources/OpenAPIKit/Callbacks.swift +++ b/Sources/OpenAPIKit/Callbacks.swift @@ -13,7 +13,7 @@ extension OpenAPI { /// A map from runtime expressions to path items to be used as /// callbacks for the API. The OpenAPI Spec "Callback Object." /// - /// See [OpenAPI Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#callback-object). + /// See [OpenAPI Callback Object](https://spec.openapis.org/oas/v3.1.1.html#callback-object). /// public typealias Callbacks = OrderedDictionary, PathItem>> @@ -37,3 +37,9 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { } } +// The following conformance is theoretically unnecessary but the compiler is +// only able to find the conformance if we explicitly declare it here, though +// it is apparently able to determine the conformance is already satisfied here +// at least. +extension OpenAPI.Callbacks: ExternallyDereferenceable { } + diff --git a/Sources/OpenAPIKit/CodableVendorExtendable.swift b/Sources/OpenAPIKit/CodableVendorExtendable.swift index 9cfa2e0e0..dc3b0b67b 100644 --- a/Sources/OpenAPIKit/CodableVendorExtendable.swift +++ b/Sources/OpenAPIKit/CodableVendorExtendable.swift @@ -18,11 +18,24 @@ 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 } } +/// OpenAPIKit supports some additional Encoder/Decoder configuration above and beyond +/// what the Encoder or Decoder support out of box. +/// +/// To _disable_ encoding or decoding of Vendor Extensions (by default these are _enabled), +/// set `userInfo[VendorExtensionsConfiguration.enabledKey] = false` for your encoder or decoder. public enum VendorExtensionsConfiguration { - public static var isEnabled = true + public static let enabledKey: CodingUserInfoKey = .init(rawValue: "vendor-extensions-enabled")! + + static func isEnabled(for decoder: Decoder) -> Bool { + decoder.userInfo[enabledKey] as? Bool ?? true + } + + static func isEnabled(for encoder: Encoder) -> Bool { + encoder.userInfo[enabledKey] as? Bool ?? true + } } internal protocol ExtendableCodingKey: CodingKey, Equatable { @@ -75,7 +88,7 @@ internal enum VendorExtensionDecodingError: Swift.Error, CustomStringConvertible extension CodableVendorExtendable { internal static func extensions(from decoder: Decoder) throws -> VendorExtensions { - guard VendorExtensionsConfiguration.isEnabled else { + guard VendorExtensionsConfiguration.isEnabled(for: decoder) else { return [:] } @@ -85,7 +98,7 @@ extension CodableVendorExtendable { throw VendorExtensionDecodingError.selfIsArrayNotDict } - guard let decodedAny = decoded as? [String: Any] else { + guard let decodedAny = decoded as? [String: any Sendable] else { throw VendorExtensionDecodingError.foundNonStringKeys } @@ -109,9 +122,6 @@ extension CodableVendorExtendable { } internal func encodeExtensions(to container: inout T) throws where T.Key == Self.CodingKeys { - guard VendorExtensionsConfiguration.isEnabled else { - return - } for (key, value) in vendorExtensions { let xKey = key.starts(with: "x-") ? key : "x-\(key)" try container.encode(value, forKey: .extendedKey(for: xKey)) diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index 35790e8f2..c1a56f0f0 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 diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index 07cbc0da6..de8556e83 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -11,11 +11,11 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Components Object". /// - /// See [OpenAPI Components Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#components-object). + /// See [OpenAPI Components Object](https://spec.openapis.org/oas/v3.1.1.html#components-object). /// /// This is a place to put reusable components to /// be referenced from other parts of the spec. - public struct Components: Equatable, CodableVendorExtendable { + public struct Components: Equatable, CodableVendorExtendable, Sendable { public var schemas: ComponentDictionary public var responses: ComponentDictionary @@ -71,6 +71,57 @@ extension OpenAPI { } } +extension OpenAPI.Components { + public struct ComponentCollision: Swift.Error { + public let componentType: String + public let existingComponent: String + public let newComponent: String + } + + private func detectCollision(type: String) throws -> (_ old: T, _ new: T) throws -> T { + return { old, new in + // theoretically we can detect collisions here, but we would need to compare + // for equality up-to but not including the difference between an external and + // internal reference which is not supported yet. +// if(old == new) { return old } +// throw ComponentCollision(componentType: type, existingComponent: String(describing:old), newComponent: String(describing:new)) + + // Given we aren't ensuring there are no collisions, the old version is going to be + // the one more likely to have been _further_ dereferenced than the new record, so + // we keep that version. + return old + } + } + + public mutating func merge(_ other: OpenAPI.Components) throws { + try schemas.merge(other.schemas, uniquingKeysWith: detectCollision(type: "schema")) + try responses.merge(other.responses, uniquingKeysWith: detectCollision(type: "responses")) + try parameters.merge(other.parameters, uniquingKeysWith: detectCollision(type: "parameters")) + try examples.merge(other.examples, uniquingKeysWith: detectCollision(type: "examples")) + try requestBodies.merge(other.requestBodies, uniquingKeysWith: detectCollision(type: "requestBodies")) + try headers.merge(other.headers, uniquingKeysWith: detectCollision(type: "headers")) + try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: detectCollision(type: "securitySchemes")) + try links.merge(other.links, uniquingKeysWith: detectCollision(type: "links")) + try callbacks.merge(other.callbacks, uniquingKeysWith: detectCollision(type: "callbacks")) + try pathItems.merge(other.pathItems, uniquingKeysWith: detectCollision(type: "pathItems")) + try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: detectCollision(type: "vendorExtensions")) + } + + /// Sort the components within each type by the component key. + public mutating func sort() { + schemas.sortKeys() + responses.sortKeys() + parameters.sortKeys() + examples.sortKeys() + requestBodies.sortKeys() + headers.sortKeys() + securitySchemes.sortKeys() + links.sortKeys() + callbacks.sortKeys() + pathItems.sortKeys() + } +} + extension OpenAPI.Components { /// The extension name used to store a Components Object name (the key something is stored under /// within the Components Object). This is used by OpenAPIKit to store the previous Component name @@ -129,7 +180,9 @@ extension OpenAPI.Components: Encodable { try container.encode(pathItems, forKey: .pathItems) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -268,4 +321,104 @@ extension OpenAPI.Components { } } +extension OpenAPI.Components { + internal mutating func externallyDereference(with loader: Loader.Type, depth: ExternalDereferenceDepth = .iterations(1), context: [Loader.Message] = []) async throws -> [Loader.Message] { + if case let .iterations(number) = depth, + number <= 0 { + return context + } + + // NOTE: The links and callbacks related code commented out below pushes Swift 5.8 and 5.9 + // over the edge and you get exit code 137 crashes in CI. + // Swift 5.10 handles it fine. + + let oldSchemas = schemas + let oldResponses = responses + let oldParameters = parameters + let oldExamples = examples + let oldRequestBodies = requestBodies + let oldHeaders = headers + let oldSecuritySchemes = securitySchemes + let oldLinks = links + let oldCallbacks = callbacks + let oldPathItems = pathItems + + async let (newSchemas, c1, m1) = oldSchemas.externallyDereferenced(with: loader) + async let (newResponses, c2, m2) = oldResponses.externallyDereferenced(with: loader) + async let (newParameters, c3, m3) = oldParameters.externallyDereferenced(with: loader) + async let (newExamples, c4, m4) = oldExamples.externallyDereferenced(with: loader) + async let (newRequestBodies, c5, m5) = oldRequestBodies.externallyDereferenced(with: loader) + async let (newHeaders, c6, m6) = oldHeaders.externallyDereferenced(with: loader) + async let (newSecuritySchemes, c7, m7) = oldSecuritySchemes.externallyDereferenced(with: loader) +// async let (newLinks, c8, m8) = oldLinks.externallyDereferenced(with: loader) +// async let (newCallbacks, c9, m9) = oldCallbacks.externallyDereferenced(with: loader) + async let (newPathItems, c10, m10) = oldPathItems.externallyDereferenced(with: loader) + + schemas = try await newSchemas + responses = try await newResponses + parameters = try await newParameters + examples = try await newExamples + requestBodies = try await newRequestBodies + headers = try await newHeaders + securitySchemes = try await newSecuritySchemes +// links = try await newLinks +// callbacks = try await newCallbacks + pathItems = try await newPathItems + + let c1Resolved = try await c1 + let c2Resolved = try await c2 + let c3Resolved = try await c3 + let c4Resolved = try await c4 + let c5Resolved = try await c5 + let c6Resolved = try await c6 + let c7Resolved = try await c7 +// let c8Resolved = try await c8 +// let c9Resolved = try await c9 + let c10Resolved = try await c10 + + // For Swift 5.10+ we can delete the following links and callbacks code and uncomment the + // preferred code above. + let (newLinks, c8, m8) = try await oldLinks.externallyDereferenced(with: loader) + links = newLinks + let c8Resolved = c8 + let (newCallbacks, c9, m9) = try await oldCallbacks.externallyDereferenced(with: loader) + callbacks = newCallbacks + let c9Resolved = c9 + + let noNewComponents = + c1Resolved.isEmpty + && c2Resolved.isEmpty + && c3Resolved.isEmpty + && c4Resolved.isEmpty + && c5Resolved.isEmpty + && c6Resolved.isEmpty + && c7Resolved.isEmpty + && c8Resolved.isEmpty + && c9Resolved.isEmpty + && c10Resolved.isEmpty + + let newMessages = try await context + m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10 + + if noNewComponents { return newMessages } + + try merge(c1Resolved) + try merge(c2Resolved) + try merge(c3Resolved) + try merge(c4Resolved) + try merge(c5Resolved) + try merge(c6Resolved) + try merge(c7Resolved) + try merge(c8Resolved) + try merge(c9Resolved) + try merge(c10Resolved) + + switch depth { + case .iterations(let number): + return try await externallyDereference(with: loader, depth: .iterations(number - 1), context: newMessages) + case .full: + return try await externallyDereference(with: loader, depth: .full, context: newMessages) + } + } +} + extension OpenAPI.Components: Validatable {} diff --git a/Sources/OpenAPIKit/Content/Content.swift b/Sources/OpenAPIKit/Content/Content.swift index e782c9818..928c90ac2 100644 --- a/Sources/OpenAPIKit/Content/Content.swift +++ b/Sources/OpenAPIKit/Content/Content.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Media Type Object" /// - /// See [OpenAPI Media Type Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#media-type-object). - public struct Content: Equatable, CodableVendorExtendable { + /// See [OpenAPI Media Type Object](https://spec.openapis.org/oas/v3.1.1.html#media-type-object). + public struct Content: Equatable, CodableVendorExtendable, Sendable { public var schema: Either, JSONSchema>? public var example: AnyCodable? public var examples: Example.Map? @@ -161,7 +161,9 @@ extension OpenAPI.Content: Encodable { try container.encodeIfPresent(encoding, forKey: .encoding) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Content/ContentEncoding.swift b/Sources/OpenAPIKit/Content/ContentEncoding.swift index 20814859a..a446fab7d 100644 --- a/Sources/OpenAPIKit/Content/ContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/ContentEncoding.swift @@ -10,58 +10,37 @@ import OpenAPIKitCore extension OpenAPI.Content { /// OpenAPI Spec "Encoding Object" /// - /// See [OpenAPI Encoding Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#encoding-object). - public struct Encoding: Equatable { + /// See [OpenAPI Encoding Object](https://spec.openapis.org/oas/v3.1.1.html#encoding-object). + public struct Encoding: Equatable, Sendable { public typealias Style = OpenAPI.Parameter.SchemaContext.Style - /// If an encoding object only contains 1 content type, it will be populated here. - /// Two or more content types will result in a null value here but the `contentTypes` - /// (plural) property will contain all content types specified. - /// - /// The singular `contentType` property is only provided for backwards compatibility and - /// using the plural `contentTypes` property should be preferred. - @available(*, deprecated, message: "use contentTypes instead") - public var contentType: OpenAPI.ContentType? { - guard let contentType = contentTypes.first, - contentTypes.count == 1 else { - return nil - } - return contentType - } - public let contentTypes: [OpenAPI.ContentType] public let headers: OpenAPI.Header.Map? public let style: Style public let explode: Bool public let allowReserved: Bool - /// The singular `contentType` argument is only provided for backwards compatibility and - /// using the plural `contentTypes` argument should be preferred. public init( - contentType: OpenAPI.ContentType? = nil, contentTypes: [OpenAPI.ContentType] = [], headers: OpenAPI.Header.Map? = nil, style: Style = Self.defaultStyle, allowReserved: Bool = false ) { - self.contentTypes = contentTypes + [contentType].compactMap { $0 } + self.contentTypes = contentTypes self.headers = headers self.style = style self.explode = style.defaultExplode self.allowReserved = allowReserved } - /// The singular `contentType` argument is only provided for backwards compatibility and - /// using the plural `contentTypes` argument should be preferred. public init( - contentType: OpenAPI.ContentType? = nil, contentTypes: [OpenAPI.ContentType] = [], headers: OpenAPI.Header.Map? = nil, style: Style = Self.defaultStyle, explode: Bool, allowReserved: Bool = false ) { - self.contentTypes = contentTypes + [contentType].compactMap { $0 } + self.contentTypes = contentTypes self.headers = headers self.style = style self.explode = explode @@ -104,7 +83,7 @@ extension OpenAPI.Content.Encoding: Decodable { let container = try decoder.container(keyedBy: CodingKeys.self) let contentTypesString = try container.decodeIfPresent(String.self, forKey: .contentType) - if let contentTypesString = contentTypesString { + if let contentTypesString { contentTypes = contentTypesString .split(separator: ",") .compactMap { string in diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index 20fb7c45d..5350b55ee 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -75,3 +75,33 @@ extension OpenAPI.Content: LocallyDereferenceable { return try DereferencedContent(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Content: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldSchema = schema + + async let (newSchema, c1, m1) = oldSchema.externallyDereferenced(with: loader) + + var newContent = self + var newComponents = try await c1 + var newMessages = try await m1 + + newContent.schema = try await newSchema + + if let oldExamples = examples { + let (newExamples, c2, m2) = try await oldExamples.externallyDereferenced(with: loader) + newContent.examples = newExamples + try newComponents.merge(c2) + newMessages += m2 + } + + if let oldEncoding = encoding { + let (newEncoding, c3, m3) = try await oldEncoding.externallyDereferenced(with: loader) + newContent.encoding = newEncoding + try newComponents.merge(c3) + newMessages += m3 + } + + return (newContent, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift index fdd0b1bbc..b715e7ac7 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContentEncoding.swift @@ -56,3 +56,29 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { return try DereferencedContentEncoding(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Content.Encoding: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let newHeaders: OpenAPI.Header.Map? + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + if let oldHeaders = headers { + (newHeaders, newComponents, newMessages) = try await oldHeaders.externallyDereferenced(with: loader) + } else { + newHeaders = nil + newComponents = .init() + newMessages = [] + } + + let newEncoding = OpenAPI.Content.Encoding( + contentTypes: contentTypes, + headers: newHeaders, + style: style, + explode: explode, + allowReserved: allowReserved + ) + + return (newEncoding, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Document/DereferencedDocument.swift b/Sources/OpenAPIKit/Document/DereferencedDocument.swift index 7906923fd..eac8afc80 100644 --- a/Sources/OpenAPIKit/Document/DereferencedDocument.swift +++ b/Sources/OpenAPIKit/Document/DereferencedDocument.swift @@ -105,7 +105,7 @@ extension DereferencedDocument { /// each path, traversed in the order the paths appear in /// the document. /// - /// See [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object) in the specifcation. + /// See [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) in the specifcation. /// public var allOperationIds: [String] { return paths.values diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index 8264509c0..8e5ee58df 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore extension OpenAPI { /// The root of an OpenAPI 3.1 document. /// - /// See [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md). + /// See [OpenAPI Specification](https://spec.openapis.org/oas/v3.1.1.html). /// /// An OpenAPI Document can say a _lot_ about the API it describes. /// A read-through of the specification is highly recommended because @@ -100,7 +100,7 @@ extension OpenAPI { /// /// Closely related to the callbacks feature, this section describes requests initiated other than by an API call, for example by an out of band registration. /// The key name is a unique string to refer to each webhook, while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider and the expected responses - /// See [OpenAPI Webhook Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#fixed-fields) + /// See [OpenAPI Webhook Object](https://spec.openapis.org/oas/v3.1.1.html#fixed-fields) public var webhooks: OrderedDictionary, OpenAPI.PathItem>> /// A declaration of which security mechanisms can be used across the API. @@ -142,7 +142,7 @@ extension OpenAPI { public var vendorExtensions: [String: AnyCodable] public init( - openAPIVersion: Version = .v3_1_0, + openAPIVersion: Version = .v3_1_1, info: Info, servers: [Server], paths: PathItem.Map, @@ -230,7 +230,7 @@ extension OpenAPI.Document { /// each path, traversed in the order the paths appear in /// the document. /// - /// See [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object) in the specifcation. + /// See [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) in the specifcation. /// public var allOperationIds: [String] { return (paths.values + webhooks.values) @@ -325,6 +325,17 @@ extension OpenAPI.Document { } } +public enum ExternalDereferenceDepth { + case iterations(Int) + case full +} + +extension ExternalDereferenceDepth: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .iterations(value) + } +} + extension OpenAPI.Document { /// Create a locally-dereferenced OpenAPI /// Document. @@ -351,6 +362,46 @@ extension OpenAPI.Document { public func locallyDereferenced() throws -> DereferencedDocument { return try DereferencedDocument(self) } + + /// Load all remote references into the document. A remote reference is one + /// that points to another file rather than a location within the + /// same file. + /// + /// This function will load remote references into the Components object + /// and replace the remote reference with a local reference to that component. + /// No local references are modified or resolved by this function. You can + /// call `locallyDereferenced()` on the externally dereferenced document if + /// you want to also remove local references by inlining all of them. + /// + /// Externally dereferencing a document requires that you provide both a + /// function that produces a `OpenAPI.ComponentKey` for any given remote + /// file URI and also a function that loads and decodes the data found in + /// that remote file. The latter is less work than it may sound like because + /// the function is told what Decodable thing it wants, so you really just + /// need to decide what decoder to use and provide the file data to that + /// decoder. See `ExternalLoader` documentation for details. + @discardableResult + public mutating func externallyDereference(with loader: Loader.Type, depth: ExternalDereferenceDepth = .iterations(1), context: [Loader.Message] = []) async throws -> [Loader.Message] { + if case let .iterations(number) = depth, + number <= 0 { + return context + } + + let oldPaths = paths + let oldWebhooks = webhooks + + async let (newPaths, c1, m1) = oldPaths.externallyDereferenced(with: loader) + async let (newWebhooks, c2, m2) = oldWebhooks.externallyDereferenced(with: loader) + + paths = try await newPaths + webhooks = try await newWebhooks + try await components.merge(c1) + try await components.merge(c2) + + let m3 = try await components.externallyDereference(with: loader, depth: depth, context: context) + + return try await context + m1 + m2 + m3 + } } extension OpenAPI { @@ -363,7 +414,7 @@ extension OpenAPI { /// Multiple entries in this dictionary indicate all schemes named are /// required on the same request. /// - /// See [OpenAPI Security Requirement Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-requirement-object). + /// See [OpenAPI Security Requirement Object](https://spec.openapis.org/oas/v3.1.1.html#security-requirement-object). public typealias SecurityRequirement = [JSONReference: [String]] } @@ -373,9 +424,49 @@ extension OpenAPI.Document { /// OpenAPIKit only explicitly supports versions that can be found in /// this enum. Other versions may or may not be decodable by /// OpenAPIKit to a certain extent. - public enum Version: String, Codable { - case v3_1_0 = "3.1.0" - case v3_1_1 = "3.1.1" + /// + ///**IMPORTANT**: Although the `v3_1_x` case supports arbitrary + /// patch versions, only _known_ patch versions are decodable. That is, if the OpenAPI + /// specification releases a new patch version, OpenAPIKit will see a patch version release + /// explicitly supports decoding documents of that new patch version before said version will + /// succesfully decode as the `v3_1_x` case. + public enum Version: RawRepresentable, Equatable, Codable { + case v3_1_0 + case v3_1_1 + case v3_1_x(x: Int) + + public init?(rawValue: String) { + switch rawValue { + case "3.1.0": self = .v3_1_0 + case "3.1.1": self = .v3_1_1 + default: + let components = rawValue.split(separator: ".") + guard components.count == 3 else { + return nil + } + guard components[0] == "3", components[1] == "1" else { + return nil + } + guard let patchVersion = Int(components[2], radix: 10) else { + return nil + } + // to support newer versions released in the future without a breaking + // change to the enumeration, bump the upper limit here to e.g. 2 or 3 + // or 6: + guard patchVersion > 1 && patchVersion <= 1 else { + return nil + } + self = .v3_1_x(x: patchVersion) + } + } + + public var rawValue: String { + switch self { + case .v3_1_0: return "3.1.0" + case .v3_1_1: return "3.1.1" + case .v3_1_x(x: let x): return "3.1.\(x)" + } + } } } @@ -406,7 +497,9 @@ extension OpenAPI.Document: Encodable { try container.encode(paths, forKey: .paths) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } if !components.isEmpty { try container.encode(components, forKey: .components) diff --git a/Sources/OpenAPIKit/Document/DocumentInfo.swift b/Sources/OpenAPIKit/Document/DocumentInfo.swift index 13a74f1ee..a31af810f 100644 --- a/Sources/OpenAPIKit/Document/DocumentInfo.swift +++ b/Sources/OpenAPIKit/Document/DocumentInfo.swift @@ -11,7 +11,7 @@ import Foundation extension OpenAPI.Document { /// OpenAPI Spec "Info Object" /// - /// See [OpenAPI Info Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#info-object). + /// See [OpenAPI Info Object](https://spec.openapis.org/oas/v3.1.1.html#info-object). public struct Info: Equatable, CodableVendorExtendable { public var title: String public var summary: String? @@ -50,7 +50,7 @@ extension OpenAPI.Document { /// OpenAPI Spec "Contact Object" /// - /// See [OpenAPI Contact Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#contact-object). + /// See [OpenAPI Contact Object](https://spec.openapis.org/oas/v3.1.1.html#contact-object). public struct Contact: Equatable, CodableVendorExtendable { public let name: String? public let url: URL? @@ -78,7 +78,7 @@ extension OpenAPI.Document { /// OpenAPI Spec "License Object" /// - /// See [OpenAPI License Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#license-object). + /// See [OpenAPI License Object](https://spec.openapis.org/oas/v3.1.1.html#license-object). public struct License: Equatable, CodableVendorExtendable { public let name: String public let identifier: Identifier? @@ -191,7 +191,9 @@ extension OpenAPI.Document.Info.License: Encodable { } } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -269,7 +271,9 @@ extension OpenAPI.Document.Info.Contact: Encodable { try container.encodeIfPresent(url?.absoluteString, forKey: .url) try container.encodeIfPresent(email, forKey: .email) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -345,7 +349,9 @@ extension OpenAPI.Document.Info: Encodable { try container.encodeIfPresent(license, forKey: .license) try container.encode(version, forKey: .version) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift new file mode 100644 index 000000000..62c919355 --- /dev/null +++ b/Sources/OpenAPIKit/Either/Either+ExternallyDereferenceable.swift @@ -0,0 +1,23 @@ +// +// Either+ExternallyDereferenceable.swift +// +// +// Created by Mathew Polzin on 2/28/21. +// + +import OpenAPIKitCore + +// MARK: - ExternallyDereferenceable +extension Either: ExternallyDereferenceable where A: ExternallyDereferenceable, B: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + switch self { + case .a(let a): + let (newA, components, messages) = try await a.externallyDereferenced(with: loader) + return (.a(newA), components, messages) + case .b(let b): + let (newB, components, messages) = try await b.externallyDereferenced(with: loader) + return (.b(newB), components, messages) + } + } +} diff --git a/Sources/OpenAPIKit/Encoding and Decoding Errors/DocumentDecodingError.swift b/Sources/OpenAPIKit/Encoding and Decoding Errors/DocumentDecodingError.swift index a585f7d93..6786b6034 100644 --- a/Sources/OpenAPIKit/Encoding and Decoding Errors/DocumentDecodingError.swift +++ b/Sources/OpenAPIKit/Encoding and Decoding Errors/DocumentDecodingError.swift @@ -12,7 +12,7 @@ extension OpenAPI.Error.Decoding { public let context: Context public let codingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case path(Path) case inconsistency(InconsistencyError) case other(Swift.DecodingError) diff --git a/Sources/OpenAPIKit/Encoding and Decoding Errors/OperationDecodingError.swift b/Sources/OpenAPIKit/Encoding and Decoding Errors/OperationDecodingError.swift index 354b16730..c62a3720a 100644 --- a/Sources/OpenAPIKit/Encoding and Decoding Errors/OperationDecodingError.swift +++ b/Sources/OpenAPIKit/Encoding and Decoding Errors/OperationDecodingError.swift @@ -13,7 +13,7 @@ extension OpenAPI.Error.Decoding { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case request(Request) case response(Response) case inconsistency(InconsistencyError) diff --git a/Sources/OpenAPIKit/Encoding and Decoding Errors/PathDecodingError.swift b/Sources/OpenAPIKit/Encoding and Decoding Errors/PathDecodingError.swift index 4b6e09b50..445604621 100644 --- a/Sources/OpenAPIKit/Encoding and Decoding Errors/PathDecodingError.swift +++ b/Sources/OpenAPIKit/Encoding and Decoding Errors/PathDecodingError.swift @@ -13,7 +13,7 @@ extension OpenAPI.Error.Decoding { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case endpoint(Operation) case inconsistency(InconsistencyError) case other(Swift.DecodingError) diff --git a/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift b/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift index 7d918a3f6..0089935d5 100644 --- a/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift +++ b/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift @@ -13,7 +13,7 @@ extension OpenAPI.Error.Decoding { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case inconsistency(InconsistencyError) case other(Swift.DecodingError) case neither(EitherDecodeNoTypesMatchedError) diff --git a/Sources/OpenAPIKit/Example.swift b/Sources/OpenAPIKit/Example.swift index 457edcc76..d55528f93 100644 --- a/Sources/OpenAPIKit/Example.swift +++ b/Sources/OpenAPIKit/Example.swift @@ -11,8 +11,8 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Example Object" /// - /// See [OpenAPI Example Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#example-object). - public struct Example: Equatable, CodableVendorExtendable { + /// See [OpenAPI Example Object](https://spec.openapis.org/oas/v3.1.1.html#example-object). + public struct Example: Equatable, CodableVendorExtendable, Sendable { public let summary: String? public let description: String? @@ -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, @@ -106,7 +106,9 @@ extension OpenAPI.Example: Encodable { break } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -195,7 +197,7 @@ extension OpenAPI.Example: LocallyDereferenceable { dereferencedFromComponentNamed name: String? ) throws -> OpenAPI.Example{ var vendorExtensions = self.vendorExtensions - if let name = name { + if let name { vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -208,4 +210,10 @@ extension OpenAPI.Example: LocallyDereferenceable { } } +extension OpenAPI.Example: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} + extension OpenAPI.Example: Validatable {} diff --git a/Sources/OpenAPIKit/ExternalDocumentation.swift b/Sources/OpenAPIKit/ExternalDocumentation.swift index 6fb8a7761..0582b39d9 100644 --- a/Sources/OpenAPIKit/ExternalDocumentation.swift +++ b/Sources/OpenAPIKit/ExternalDocumentation.swift @@ -11,8 +11,8 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "External Documentation Object" /// - /// See [OpenAPI External Documentation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#external-documentation-object). - public struct ExternalDocumentation: Equatable, CodableVendorExtendable { + /// See [OpenAPI External Documentation Object](https://spec.openapis.org/oas/v3.1.1.html#external-documentation-object). + public struct ExternalDocumentation: Equatable, CodableVendorExtendable, Sendable { public var description: String? public var url: URL @@ -57,7 +57,9 @@ extension OpenAPI.ExternalDocumentation: Encodable { try container.encodeIfPresent(description, forKey: .description) try container.encode(url.absoluteString, forKey: .url) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/ExternalLoader.swift b/Sources/OpenAPIKit/ExternalLoader.swift new file mode 100644 index 000000000..ba67073de --- /dev/null +++ b/Sources/OpenAPIKit/ExternalLoader.swift @@ -0,0 +1,40 @@ +// +// ExternalLoader.swift +// +// +// Created by Mathew Polzin on 7/30/2023. +// + +import OpenAPIKitCore +import Foundation + +/// An `ExternalLoader` 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 ExternalLoader where Message: Sendable { + /// This can be anything that an implementor of this protocol wants to pass back from + /// the `load()` function and have available after all external loading has been done. + /// + /// A trivial type if no Messages are needed would be Void. + associatedtype Message + + /// 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) async throws -> (T, [Message]) 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. + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey +} + +public protocol ExternallyDereferenceable { + func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) +} diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 5f8eee40b..40509234a 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -57,7 +57,7 @@ public struct DereferencedHeader: Equatable { } var header = header - if let name = name { + if let name { header.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -82,3 +82,39 @@ extension OpenAPI.Header: LocallyDereferenceable { return try DereferencedHeader(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Header: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + + // if not for a Swift bug, this whole next bit would just be the + // next line: +// let (newSchemaOrContent, components) = try await schemaOrContent.externallyDereferenced(with: loader) + + let newSchemaOrContent: Either + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + switch schemaOrContent { + case .a(let schemaContext): + let (context, components, messages) = try await schemaContext.externallyDereferenced(with: loader) + newSchemaOrContent = .a(context) + newComponents = components + newMessages = messages + case .b(let contentMap): + let (map, components, messages) = try await contentMap.externallyDereferenced(with: loader) + newSchemaOrContent = .b(map) + newComponents = components + newMessages = messages + } + + let newHeader = OpenAPI.Header( + schemaOrContent: newSchemaOrContent, + description: description, + required: required, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + + return (newHeader, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Header/Header.swift b/Sources/OpenAPIKit/Header/Header.swift index 309901af9..98765de53 100644 --- a/Sources/OpenAPIKit/Header/Header.swift +++ b/Sources/OpenAPIKit/Header/Header.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Header Object" /// - /// See [OpenAPI Header Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#header-object). - public struct Header: Equatable, CodableVendorExtendable { + /// See [OpenAPI Header Object](https://spec.openapis.org/oas/v3.1.1.html#header-object). + public struct Header: Equatable, CodableVendorExtendable, Sendable { public typealias SchemaContext = Parameter.SchemaContext public let description: String? @@ -289,7 +289,9 @@ extension OpenAPI.Header: Encodable { try container.encode(deprecated, forKey: .deprecated) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index ad9f9649a..3177fb2d3 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -39,7 +39,7 @@ import Foundation /// Components Object will be validated when you call `validate()` on an /// `OpenAPI.Document`. /// -public enum JSONReference: Equatable, Hashable, _OpenAPIReference { +public enum JSONReference: Equatable, Hashable, _OpenAPIReference, Sendable { /// The reference is internal to the file. case `internal`(InternalReference) /// The reference refers to another file. @@ -124,7 +124,7 @@ public enum JSONReference: Equatabl /// `JSONReference`. /// /// This reference must start with "#". - public enum InternalReference: LosslessStringConvertible, RawRepresentable, Equatable, Hashable { + public enum InternalReference: LosslessStringConvertible, RawRepresentable, Equatable, Hashable, Sendable { /// The reference refers to a component (i.e. `#/components/...`). case component(name: String) /// The reference refers to some path outside the Components Object. @@ -202,7 +202,7 @@ public enum JSONReference: Equatabl /// /// This path does _not_ start with "#". It starts with a forward slash. By contrast, an /// `InternalReference` starts with "#" and is followed by the start of a `Path`. - public struct Path: ExpressibleByArrayLiteral, ExpressibleByStringLiteral, LosslessStringConvertible, RawRepresentable, Equatable, Hashable { + public struct Path: ExpressibleByArrayLiteral, ExpressibleByStringLiteral, LosslessStringConvertible, RawRepresentable, Equatable, Hashable, Sendable { /// The Path's components. In the `rawValue`, these components are joined /// with forward slashes '/' per the JSON Reference specification. @@ -312,7 +312,7 @@ extension OpenAPI { /// Per the specification, these summary and description overrides are irrelevant /// if the referenced component does not support the given attribute. @dynamicMemberLookup - public struct Reference: Equatable, Hashable, _OpenAPIReference { + public struct Reference: Equatable, Hashable, _OpenAPIReference, Sendable { public let jsonReference: JSONReference public let summary: String? public let description: String? @@ -469,19 +469,19 @@ extension JSONReference: Decodable { } self = .internal(internalReference) } else { - let externalReferenceCandidate: URL? + let externalReference: URL? #if canImport(FoundationEssentials) - externalReferenceCandidate = URL(string: referenceString, encodingInvalidCharacters: false) + externalReference = URL(string: referenceString, encodingInvalidCharacters: false) #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - externalReferenceCandidate = URL(string: referenceString, encodingInvalidCharacters: false) + externalReference = URL(string: referenceString, encodingInvalidCharacters: false) } else { - externalReferenceCandidate = URL(string: referenceString) + externalReference = URL(string: referenceString) } #else - externalReferenceCandidate = URL(string: referenceString) + externalReference = URL(string: referenceString) #endif - guard let externalReference = externalReferenceCandidate else { + guard let externalReference else { throw InconsistencyError( subjectName: "JSON Reference", details: "Failed to parse a valid URI for a JSON Reference from '\(referenceString)'", @@ -549,6 +549,21 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere } } +extension JSONReference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + switch self { + 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) + var components = OpenAPI.Components() + components[keyPath: ReferenceType.openAPIComponentsKeyPath][componentKey] = component + return (try components.reference(named: componentKey.rawValue, ofType: ReferenceType.self).jsonReference, components, messages) + } + } +} + extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: LocallyDereferenceable { /// Look up the component this reference points to and then /// dereference it. @@ -576,4 +591,11 @@ extension OpenAPI.Reference: LocallyDereferenceable where ReferenceType: Locally } } +extension OpenAPI.Reference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let (newRef, components, messages) = try await jsonReference.externallyDereferenced(with: loader) + return (.init(newRef), components, messages) + } +} + extension OpenAPI.Reference: Validatable where ReferenceType: Validatable {} diff --git a/Sources/OpenAPIKit/Link.swift b/Sources/OpenAPIKit/Link.swift index 428e7c280..61ceb08b6 100644 --- a/Sources/OpenAPIKit/Link.swift +++ b/Sources/OpenAPIKit/Link.swift @@ -15,23 +15,23 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Link Object" /// - /// See [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object). - public struct Link: Equatable, CodableVendorExtendable { + /// See [OpenAPI Link Object](https://spec.openapis.org/oas/v3.1.1.html#link-object). + public struct Link: Equatable, CodableVendorExtendable, Sendable { /// The **OpenAPI**` `operationRef` or `operationId` field, depending on whether /// a `URL` of a remote or local Operation Object or a `operationId` (String) of an /// operation defined in the same document is given. - public let operation: Either + public var operation: Either /// A map from parameter names to either runtime expressions that evaluate to values or /// constant values (`AnyCodable`). /// - /// See the docuemntation for the [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object) for more details. + /// See the docuemntation for the [OpenAPI Link Object](https://spec.openapis.org/oas/v3.1.1.html#link-object) for more details. /// /// Empty dictionaries will be omitted from encoding. - public let parameters: OrderedDictionary> + public var parameters: OrderedDictionary> /// A literal value or expression to use as a request body when calling the target operation. - public let requestBody: Either? + public var requestBody: Either? public var description: String? - public let server: Server? + public var server: Server? /// Dictionary of vendor extensions. /// @@ -174,7 +174,9 @@ extension OpenAPI.Link: Encodable { try container.encodeIfPresent(description, forKey: .description) try container.encodeIfPresent(server, forKey: .server) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -274,7 +276,7 @@ extension OpenAPI.Link: LocallyDereferenceable { dereferencedFromComponentNamed name: String? ) throws -> OpenAPI.Link { var vendorExtensions = self.vendorExtensions - if let name = name { + if let name { vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -289,4 +291,15 @@ extension OpenAPI.Link: LocallyDereferenceable { } } +extension OpenAPI.Link: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let (newServer, newComponents, newMessages) = try await server.externallyDereferenced(with: loader) + + var newLink = self + newLink.server = newServer + + return (newLink, newComponents, newMessages) + } +} + extension OpenAPI.Link: Validatable {} diff --git a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift index 9beaf77f9..3a4347a19 100644 --- a/Sources/OpenAPIKit/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit/Operation/DereferencedOperation.swift @@ -124,3 +124,42 @@ extension OpenAPI.Operation: LocallyDereferenceable { return try DereferencedOperation(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Operation: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldParameters = parameters + let oldRequestBody = requestBody + let oldResponses = responses + + async let (newParameters, c1, m1) = oldParameters.externallyDereferenced(with: loader) + async let (newRequestBody, c2, m2) = oldRequestBody.externallyDereferenced(with: loader) + async let (newResponses, c3, m3) = oldResponses.externallyDereferenced(with: loader) + async let (newCallbacks, c4, m4) = callbacks.externallyDereferenced(with: loader) +// let (newServers, c5, m5) = try await servers.externallyDereferenced(with: loader) + + var newOperation = self + var newComponents = try await c1 + var newMessages = try await m1 + + newOperation.parameters = try await newParameters + newOperation.requestBody = try await newRequestBody + try await newComponents.merge(c2) + try await newMessages += m2 + newOperation.responses = try await newResponses + try await newComponents.merge(c3) + try await newMessages += m3 + newOperation.callbacks = try await newCallbacks + try await newComponents.merge(c4) + try await newMessages += m4 + + // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: + if let oldServers = servers { + let (newServers, c5, m5) = try await oldServers.externallyDereferenced(with: loader) + newOperation.servers = newServers + try newComponents.merge(c5) + newMessages += m5 + } + + return (newOperation, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Operation/Operation.swift b/Sources/OpenAPIKit/Operation/Operation.swift index 73f517d95..cf9629059 100644 --- a/Sources/OpenAPIKit/Operation/Operation.swift +++ b/Sources/OpenAPIKit/Operation/Operation.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Operation Object" /// - /// See [OpenAPI Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object). - public struct Operation: Equatable, CodableVendorExtendable { + /// See [OpenAPI Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object). + public struct Operation: Equatable, CodableVendorExtendable, Sendable { public var tags: [String]? public var summary: String? public var description: String? @@ -76,7 +76,7 @@ extension OpenAPI { /// The key is a unique identifier for the Callback Object. Each value in the /// map is a Callback Object that describes a request that may be initiated /// by the API provider and the expected responses. - public let callbacks: OpenAPI.CallbacksMap + public var callbacks: OpenAPI.CallbacksMap /// Indicates that the operation is deprecated or not. /// @@ -291,7 +291,9 @@ extension OpenAPI.Operation: Encodable { try container.encodeIfPresent(servers, forKey: .servers) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 97fb607b7..4b5002ca3 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -59,7 +59,7 @@ public struct DereferencedParameter: Equatable { } var parameter = parameter - if let name = name { + if let name { parameter.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -82,3 +82,34 @@ extension OpenAPI.Parameter: LocallyDereferenceable { return try DereferencedParameter(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Parameter: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + + // if not for a Swift bug, this whole function would just be the + // next line: +// let (newSchemaOrContent, components) = try await schemaOrContent.externallyDereferenced(with: loader) + + let newSchemaOrContent: Either + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + switch schemaOrContent { + case .a(let schemaContext): + let (context, components, messages) = try await schemaContext.externallyDereferenced(with: loader) + newSchemaOrContent = .a(context) + newComponents = components + newMessages = messages + case .b(let contentMap): + let (map, components, messages) = try await contentMap.externallyDereferenced(with: loader) + newSchemaOrContent = .b(map) + newComponents = components + newMessages = messages + } + + var newParameter = self + newParameter.schemaOrContent = newSchemaOrContent + + return (newParameter, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift index 9801a401a..a101c1653 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedSchemaContext.swift @@ -69,3 +69,26 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Parameter.SchemaContext: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldSchema = schema + + async let (newSchema, c1, m1) = oldSchema.externallyDereferenced(with: loader) + + var newSchemaContext = self + var newComponents = try await c1 + var newMessages = try await m1 + + newSchemaContext.schema = try await newSchema + + if let oldExamples = examples { + let (newExamples, c2, m2) = try await oldExamples.externallyDereferenced(with: loader) + newSchemaContext.examples = newExamples + try newComponents.merge(c2) + newMessages += m2 + } + + return (newSchemaContext, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Parameter/Parameter.swift b/Sources/OpenAPIKit/Parameter/Parameter.swift index 608126efb..fc8f150a3 100644 --- a/Sources/OpenAPIKit/Parameter/Parameter.swift +++ b/Sources/OpenAPIKit/Parameter/Parameter.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Parameter Object" /// - /// See [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#parameter-object). - public struct Parameter: Equatable, CodableVendorExtendable { + /// See [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object). + public struct Parameter: Equatable, CodableVendorExtendable, Sendable { public var name: String /// OpenAPI Spec "in" property determines the `Context`. @@ -21,6 +21,8 @@ extension OpenAPI { /// parameters in the given location. public var context: Context public var description: String? + /// Whether or not the parameter is deprecated. Defaults to false + /// if unspecified and only gets encoded if true. public var deprecated: Bool // default is false /// OpenAPI Spec "content" or "schema" properties. @@ -46,7 +48,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 @@ -157,7 +162,7 @@ extension OpenAPI.Parameter { /// containing exactly the things that differentiate /// one parameter from another, per the specification. /// - /// See [Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#parameter-object). + /// See [Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object). internal struct ParameterIdentity: Hashable { let name: String let location: Context.Location @@ -269,7 +274,9 @@ extension OpenAPI.Parameter: Encodable { try container.encode(deprecated, forKey: .deprecated) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Parameter/ParameterContext.swift b/Sources/OpenAPIKit/Parameter/ParameterContext.swift index 27ae9c2c0..d017b7c67 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterContext.swift @@ -10,13 +10,13 @@ import OpenAPIKitCore extension OpenAPI.Parameter { /// OpenAPI Spec "Parameter Object" location-specific configuration. /// - /// See [OpenAPI Parameter Locations](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#parameter-locations). + /// See [OpenAPI Parameter Locations](https://spec.openapis.org/oas/v3.1.1.html#parameter-locations). /// /// Query, Header, and Cookie parameters are /// all optional by default unless you pass /// `required: true` to the context construction. /// Path parameters are always required. - public enum Context: Equatable { + public enum Context: Equatable, Sendable { case query(required: Bool, allowEmptyValue: Bool) case header(required: Bool) case path diff --git a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift index 336305738..f8828da2e 100644 --- a/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit/Parameter/ParameterSchemaContext.swift @@ -10,16 +10,16 @@ import OpenAPIKitCore extension OpenAPI.Parameter { /// OpenAPI Spec "Parameter Object" schema and style configuration. /// - /// See [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#parameter-object) - /// and [OpenAPI Style Values](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#style-values). - public struct SchemaContext: Equatable { - public let style: Style - public let explode: Bool - public let allowReserved: Bool //defaults to false - public let schema: Either, JSONSchema> + /// See [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object) + /// and [OpenAPI Style Values](https://spec.openapis.org/oas/v3.1.1.html#style-values). + public struct SchemaContext: Equatable, Sendable { + public var style: Style + public var explode: Bool + public var allowReserved: Bool //defaults to false + public var schema: Either, JSONSchema> - public let example: AnyCodable? - public let examples: OpenAPI.Example.Map? + public var example: AnyCodable? + public var examples: OpenAPI.Example.Map? public init(_ schema: JSONSchema, style: Style, @@ -132,7 +132,7 @@ extension OpenAPI.Parameter.SchemaContext.Style { /// per the OpenAPI Specification. /// /// See the `style` fixed field under - /// [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#parameter-object). + /// [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object). public static func `default`(for location: OpenAPI.Parameter.Context) -> Self { switch location { case .query: diff --git a/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift index 7b711b1b1..d9f25538f 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 @@ -65,7 +66,7 @@ public struct DereferencedPathItem: Equatable { self.trace = try pathItem.trace.map { try DereferencedOperation($0, resolvingIn: components, following: references) } var pathItem = pathItem - if let name = name { + if let name { pathItem.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -137,3 +138,73 @@ extension OpenAPI.PathItem: LocallyDereferenceable { return try DereferencedPathItem(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.PathItem: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldParameters = parameters + let oldServers = servers + let oldGet = get + let oldPut = put + let oldPost = post + let oldDelete = delete + let oldOptions = options + let oldHead = head + let oldPatch = patch + let oldTrace = trace + + async let (newParameters, c1, m1) = oldParameters.externallyDereferenced(with: loader) +// async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader) + async let (newGet, c3, m3) = oldGet.externallyDereferenced(with: loader) + async let (newPut, c4, m4) = oldPut.externallyDereferenced(with: loader) + async let (newPost, c5, m5) = oldPost.externallyDereferenced(with: loader) + async let (newDelete, c6, m6) = oldDelete.externallyDereferenced(with: loader) + async let (newOptions, c7, m7) = oldOptions.externallyDereferenced(with: loader) + async let (newHead, c8, m8) = oldHead.externallyDereferenced(with: loader) + async let (newPatch, c9, m9) = oldPatch.externallyDereferenced(with: loader) + async let (newTrace, c10, m10) = oldTrace.externallyDereferenced(with: loader) + + var pathItem = self + var newComponents = try await c1 + var newMessages = try await m1 + + // ideally we would async let all of the props above and then set them here, + // but for now since there seems to be some sort of compiler bug we will do + // newServers in an if let below + pathItem.parameters = try await newParameters + pathItem.get = try await newGet + pathItem.put = try await newPut + pathItem.post = try await newPost + pathItem.delete = try await newDelete + pathItem.options = try await newOptions + pathItem.head = try await newHead + pathItem.patch = try await newPatch + pathItem.trace = try await newTrace + + try await newComponents.merge(c3) + try await newComponents.merge(c4) + try await newComponents.merge(c5) + try await newComponents.merge(c6) + try await newComponents.merge(c7) + try await newComponents.merge(c8) + try await newComponents.merge(c9) + try await newComponents.merge(c10) + + try await newMessages += m3 + try await newMessages += m4 + try await newMessages += m5 + try await newMessages += m6 + try await newMessages += m7 + try await newMessages += m8 + try await newMessages += m9 + try await newMessages += m10 + + if let oldServers { + async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader) + pathItem.servers = try await newServers + try await newComponents.merge(c2) + try await newMessages += m2 + } + + return (pathItem, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Path Item/PathItem.swift b/Sources/OpenAPIKit/Path Item/PathItem.swift index 71f213c35..6db744f78 100644 --- a/Sources/OpenAPIKit/Path Item/PathItem.swift +++ b/Sources/OpenAPIKit/Path Item/PathItem.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Path Item Object" /// - /// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#path-item-object). + /// See [OpenAPI Path Item Object](https://spec.openapis.org/oas/v3.1.1.html#path-item-object). /// /// In addition to parameters that apply to all endpoints under the current path, /// this type offers access to each possible endpoint operation under properties @@ -21,7 +21,7 @@ extension OpenAPI { /// /// You can access an array of equatable `HttpMethod`/`Operation` paris with the /// `endpoints` property. - public struct PathItem: Equatable, CodableVendorExtendable { + public struct PathItem: Equatable, CodableVendorExtendable, Sendable { public var summary: String? public var description: String? public var servers: [OpenAPI.Server]? @@ -236,24 +236,6 @@ extension OpenAPI.PathItem : OpenAPISummarizable { // MARK: - Codable -extension OpenAPI.Path: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(rawValue) - } -} - -extension OpenAPI.Path: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let rawValue = try container.decode(String.self) - - self.init(rawValue: rawValue) - } -} - extension OpenAPI.PathItem: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -275,7 +257,9 @@ extension OpenAPI.PathItem: Encodable { try container.encodeIfPresent(patch, forKey: .patch) try container.encodeIfPresent(trace, forKey: .trace) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Request/DereferencedRequest.swift b/Sources/OpenAPIKit/Request/DereferencedRequest.swift index 6e54e0c40..36d802767 100644 --- a/Sources/OpenAPIKit/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit/Request/DereferencedRequest.swift @@ -38,7 +38,7 @@ public struct DereferencedRequest: Equatable { } var request = request - if let name = name { + if let name { request.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -61,3 +61,14 @@ extension OpenAPI.Request: LocallyDereferenceable { return try DereferencedRequest(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Request: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + var newRequest = self + + let (newContent, components, messages) = try await content.externallyDereferenced(with: loader) + + newRequest.content = newContent + return (newRequest, components, messages) + } +} diff --git a/Sources/OpenAPIKit/Request/Request.swift b/Sources/OpenAPIKit/Request/Request.swift index 9d6df8d4e..58e1e01e3 100644 --- a/Sources/OpenAPIKit/Request/Request.swift +++ b/Sources/OpenAPIKit/Request/Request.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Request Body Object" /// - /// See [OpenAPI Request Body Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#request-body-object). - public struct Request: Equatable, CodableVendorExtendable { + /// See [OpenAPI Request Body Object](https://spec.openapis.org/oas/v3.1.1.html#request-body-object). + public struct Request: Equatable, CodableVendorExtendable, Sendable { public var description: String? public var content: Content.Map public var required: Bool @@ -107,7 +107,9 @@ extension OpenAPI.Request: Encodable { try container.encode(required, forKey: .required) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Response/DereferencedResponse.swift b/Sources/OpenAPIKit/Response/DereferencedResponse.swift index 76e883179..702da8765 100644 --- a/Sources/OpenAPIKit/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit/Response/DereferencedResponse.swift @@ -51,7 +51,7 @@ public struct DereferencedResponse: Equatable { } var response = response - if let name = name { + if let name { response.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -76,3 +76,33 @@ extension OpenAPI.Response: LocallyDereferenceable { return try DereferencedResponse(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Response: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldContent = content + let oldLinks = links + let oldHeaders = headers + + async let (newContent, c1, m1) = oldContent.externallyDereferenced(with: loader) + async let (newLinks, c2, m2) = oldLinks.externallyDereferenced(with: loader) +// async let (newHeaders, c3, m3) = oldHeaders.externallyDereferenced(with: loader) + + var response = self + var messages = try await m1 + response.content = try await newContent + response.links = try await newLinks + + var components = try await c1 + try await components.merge(c2) + try await messages += m2 + + if let oldHeaders { + let (newHeaders, c3, m3) = try await oldHeaders.externallyDereferenced(with: loader) + response.headers = newHeaders + try components.merge(c3) + messages += m3 + } + + return (response, components, messages) + } +} diff --git a/Sources/OpenAPIKit/Response/Response.swift b/Sources/OpenAPIKit/Response/Response.swift index 351d487bf..d8b3f3000 100644 --- a/Sources/OpenAPIKit/Response/Response.swift +++ b/Sources/OpenAPIKit/Response/Response.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Response Object" /// - /// See [OpenAPI Response Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#response-object). - public struct Response: Equatable, CodableVendorExtendable { + /// See [OpenAPI Response Object](https://spec.openapis.org/oas/v3.1.1.html#response-object). + public struct Response: Equatable, CodableVendorExtendable, Sendable { public var description: String public var headers: Header.Map? /// An empty Content map will be omitted from encoding. @@ -168,7 +168,9 @@ extension OpenAPI.Response: Encodable { try container.encode(links, forKey: .links) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -197,30 +199,4 @@ extension OpenAPI.Response: Decodable { } } -extension OpenAPI.Response.StatusCode: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(self.rawValue) - } -} - -extension OpenAPI.Response.StatusCode: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let strVal = try container.decode(String.self) - let val = OpenAPI.Response.StatusCode(rawValue: strVal) - - guard let value = val else { - throw InconsistencyError( - subjectName: "status code", - details: "Expected the status code to be either an Int, a range like '1XX', or 'default' but found \(strVal) instead", - codingPath: decoder.codingPath - ) - } - - self = value - } -} - extension OpenAPI.Response: Validatable {} diff --git a/Sources/OpenAPIKit/RuntimeExpression.swift b/Sources/OpenAPIKit/RuntimeExpression.swift index ac168bbef..19e77c076 100644 --- a/Sources/OpenAPIKit/RuntimeExpression.swift +++ b/Sources/OpenAPIKit/RuntimeExpression.swift @@ -10,9 +10,9 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Runtime Expression" /// - /// See [OpenAPI Runtime Expression[(https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#runtime-expressions). + /// See [OpenAPI Runtime Expression[(https://spec.openapis.org/oas/v3.1.1.html#runtime-expressions). /// - public enum RuntimeExpression: RawRepresentable, Equatable { + public enum RuntimeExpression: RawRepresentable, Equatable, Sendable { case url case method case statusCode @@ -74,7 +74,7 @@ extension OpenAPI { return nil } - public enum Source: RawRepresentable, Equatable { + public enum Source: RawRepresentable, Equatable, Sendable { /// A reference to one of the header parameters. case header(name: String) /// A reference to one of the query parameters. diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 1ba5ba93a..d1a86b33d 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore /// A `JSONSchema` type that guarantees none of its /// nodes are references. @dynamicMemberLookup -public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { +public enum DereferencedJSONSchema: Equatable, JSONSchemaContext, Sendable { public typealias CoreContext = JSONSchema.CoreContext public typealias NumericContext = JSONSchema.NumericContext public typealias IntegerContext = JSONSchema.IntegerContext @@ -265,7 +265,7 @@ extension DereferencedJSONSchema { } /// The context that only applies to `.array` schemas. - public struct ArrayContext: Equatable { + public struct ArrayContext: Equatable, Sendable { /// A JSON Type Node that describes /// the type of each element in the array. public let items: DereferencedJSONSchema? @@ -333,7 +333,7 @@ extension DereferencedJSONSchema { } /// The context that only applies to `.object` schemas. - public struct ObjectContext: Equatable { + public struct ObjectContext: Equatable, Sendable { public let maxProperties: Int? let _minProperties: Int? public let properties: OrderedDictionary @@ -463,7 +463,7 @@ extension JSONSchema: LocallyDereferenceable { ) throws -> DereferencedJSONSchema { func addComponentNameExtension(to context: CoreContext) -> CoreContext { var extensions = context.vendorExtensions - if let name = name { + if let name { extensions[OpenAPI.Components.componentNameExtension] = .init(name) } return context.with(vendorExtensions: extensions) @@ -485,7 +485,7 @@ extension JSONSchema: LocallyDereferenceable { // TODO: consider which other core context properties to override here as with description ^ var extensions = dereferenced.vendorExtensions - if let name = name { + if let name { extensions[OpenAPI.Components.componentNameExtension] = .init(name) } dereferenced = dereferenced.with(vendorExtensions: vendorExtensions) @@ -534,3 +534,120 @@ extension JSONSchema: LocallyDereferenceable { return try? dereferenced(in: .noComponents) } } + +extension JSONSchema: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let newSchema: JSONSchema + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + switch value { + case .null(_): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .boolean(_): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .number(_, _): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .integer(_, _): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .string(_, _): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .object(let core, let object): + var components = OpenAPI.Components() + var messages = [Loader.Message]() + + let (newProperties, c1, m1) = try await object.properties.externallyDereferenced(with: loader) + try components.merge(c1) + messages += m1 + + let newAdditionalProperties: Either? + if case .b(let schema) = object.additionalProperties { + let (additionalProperties, c2, m2) = try await schema.externallyDereferenced(with: loader) + try components.merge(c2) + messages += m2 + newAdditionalProperties = .b(additionalProperties) + } else { + newAdditionalProperties = object.additionalProperties + } + newComponents = components + newMessages = messages + newSchema = .init( + schema: .object( + core, + .init( + properties: newProperties, + additionalProperties: newAdditionalProperties, + maxProperties: object.maxProperties, + minProperties: object._minProperties + ) + ) + ) + case .array(let core, let array): + let (newItems, components, messages) = try await array.items.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .array( + core, + .init( + items: newItems, + maxItems: array.maxItems, + minItems: array._minItems, + uniqueItems: array._uniqueItems + ) + ) + ) + case .all(let schema, let core): + let (newSubschemas, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .all(of: newSubschemas, core: core) + ) + case .one(let schema, let core): + let (newSubschemas, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .one(of: newSubschemas, core: core) + ) + case .any(let schema, let core): + let (newSubschemas, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .any(of: newSubschemas, core: core) + ) + case .not(let schema, let core): + let (newSubschema, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .not(newSubschema, core: core) + ) + case .reference(let reference, let core): + let (newReference, components, messages) = try await reference.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .reference(newReference, core) + ) + case .fragment(_): + newComponents = .noComponents + newSchema = self + newMessages = [] + } + + return (newSchema, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift index a8f48b3a5..cac3517a3 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift @@ -62,7 +62,7 @@ public func ~=(lhs: JSONSchemaResolutionError, rhs: JSONSchemaResolutionError) - /// I expect this to be an area where I may want to make fixes and add /// errors without breaknig changes, so this annoying workaround for /// the absense of a "non-frozen" enum is a must. -internal enum _JSONSchemaResolutionError: CustomStringConvertible, Equatable { +internal enum _JSONSchemaResolutionError: CustomStringConvertible, Equatable, Sendable { case unsupported(because: String) case typeConflict(original: JSONType, new: JSONType) case formatConflict(original: String, new: String) @@ -608,7 +608,7 @@ extension JSONSchema.CoreContext { extension JSONSchema.IntegerContext { internal func validatedContext() throws -> JSONSchema.IntegerContext { let validatedMinimum: Bound? - if let minimum = minimum { + if let minimum { guard minimum.value >= 0 else { throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(minimum.value) cannot be below 0")) } @@ -633,7 +633,7 @@ extension JSONSchema.IntegerContext { extension JSONSchema.NumericContext { internal func validatedContext() throws -> JSONSchema.NumericContext { let validatedMinimum: Bound? - if let minimum = minimum { + if let minimum { guard minimum.value >= 0 else { throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(minimum.value) cannot be below 0")) } diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift index bbc6d7ca8..5e3d4a6ab 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift @@ -9,11 +9,11 @@ import OpenAPIKitCore /// OpenAPI "Schema Object" /// -/// See [OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#schema-object). -public struct JSONSchema: JSONSchemaContext, HasWarnings { +/// See [OpenAPI Schema Object](https://spec.openapis.org/oas/v3.1.1.html#schema-object). +public struct JSONSchema: JSONSchemaContext, HasWarnings, Sendable { public let warnings: [OpenAPI.Warning] - public let value: Schema + public var value: Schema internal init(warnings: [OpenAPI.Warning], schema: Schema) { self.warnings = warnings @@ -68,7 +68,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings { .init(schema: .fragment(core)) } - public enum Schema: Equatable { + public enum Schema: Equatable, Sendable { /// The null type, which replaces the functionality of the `nullable` property from /// previous versions of the OpenAPI specification. case null(CoreContext) @@ -470,7 +470,12 @@ extension JSONSchema: VendorExtendable { /// `[ "x-extensionKey": ]` /// where the values are anything codable. public var vendorExtensions: VendorExtensions { - coreContext.vendorExtensions + get { + coreContext.vendorExtensions + } + set(extensions) { + self.value = value.with(vendorExtensions: extensions) + } } public func with(vendorExtensions: [String: AnyCodable]) -> JSONSchema { @@ -1925,7 +1930,7 @@ extension JSONSchema: Encodable { // Ad-hoc vendor extension encoding because keys are done differently for // JSONSchema - guard VendorExtensionsConfiguration.isEnabled else { + guard VendorExtensionsConfiguration.isEnabled(for: encoder) else { return } var container = encoder.container(keyedBy: VendorExtensionKeys.self) @@ -2056,7 +2061,7 @@ extension JSONSchema: Decodable { // TODO: support multiple types instead of just grabbing the first one (see TODO immediately above as well) let typeHint = typeHints.first - if let typeHint = typeHint { + if let typeHint { let keysFromElsewhere = keysFrom.filter({ $0 != typeHint.group }) if !keysFromElsewhere.isEmpty { _warnings.append( @@ -2135,7 +2140,7 @@ extension JSONSchema: Decodable { // Ad-hoc vendor extension support since JSONSchema does coding keys differently. let extensions: [String: AnyCodable] - guard VendorExtensionsConfiguration.isEnabled else { + guard VendorExtensionsConfiguration.isEnabled(for: decoder) else { self.value = value return } @@ -2146,7 +2151,7 @@ extension JSONSchema: Decodable { throw VendorExtensionDecodingError.selfIsArrayNotDict } - guard let decodedAny = decoded as? [String: Any] else { + guard let decodedAny = decoded as? [String: any Sendable] else { throw VendorExtensionDecodingError.foundNonStringKeys } diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index 37be04003..fed5f4556 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -12,7 +12,7 @@ import OpenAPIKitCore /// A schema context stores information about a schema. /// All schemas can have the contextual information in /// this protocol. -public protocol JSONSchemaContext { +public protocol JSONSchemaContext: Sendable { /// The format of the schema as a string value. /// /// This can be set even when a schema type has @@ -60,7 +60,7 @@ public protocol JSONSchemaContext { /// be placed on a parent object (one level up from an `allOf`, `anyOf`, /// or `oneOf`) as a way to reduce redundancy. /// - /// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#discriminator-object). + /// See [OpenAPI Discriminator Object](https://spec.openapis.org/oas/v3.1.1.html#discriminator-object). var discriminator: OpenAPI.Discriminator? { get } /// Get the external docs, if specified. If unspecified, returns `nil`. @@ -135,22 +135,6 @@ public protocol JSONSchemaContext { var vendorExtensions: [String: AnyCodable] { get } } -extension JSONSchemaContext { - - // TODO: Remove the default implementations of the following in v4 of OpenAPIKit. - // They are only here to make their addition non-breaking. - - // Default implementation to make addition of this new property which is only - // supposed to be set internally a non-breaking addition. - public var inferred: Bool { false } - - // Default implementation to make addition non-breaking - public var anchor: String? { nil } - - // Default implementation to make addition non-breaking - public var dynamicAnchor: String? { nil } -} - extension JSONSchema { /// The context that applies to all schemas. public struct CoreContext: JSONSchemaContext, HasWarnings { @@ -590,8 +574,8 @@ extension JSONSchema { /// `IntegerContext` _can_ be asked for the /// `NumericContext` that would describe it via its /// `numericContext` property. - public struct NumericContext: Equatable { - public struct Bound: Equatable { + public struct NumericContext: Equatable, Sendable { + public struct Bound: Equatable, Sendable { public let value: Double public let exclusive: Bool @@ -626,8 +610,8 @@ extension JSONSchema { } /// The context that only applies to `.integer` schemas. - public struct IntegerContext: Equatable { - public struct Bound: Equatable { + public struct IntegerContext: Equatable, Sendable { + public struct Bound: Equatable, Sendable { public let value: Int public let exclusive: Bool @@ -712,7 +696,7 @@ extension JSONSchema { } /// The context that only applies to `.array` schemas. - public struct ArrayContext: Equatable { + public struct ArrayContext: Equatable, Sendable { /// A JSON Type Node that describes /// the type of each element in the array. public let items: JSONSchema? @@ -745,7 +729,7 @@ extension JSONSchema { } /// The context that only applies to `.object` schemas. - public struct ObjectContext: Equatable { + public struct ObjectContext: Equatable, Sendable { /// The maximum number of properties the object /// is allowed to have. public let maxProperties: Int? @@ -812,7 +796,7 @@ extension JSONSchema { } /// The context that only applies to `.string` schemas. - public struct StringContext: Equatable { + public struct StringContext: Equatable, Sendable { public let maxLength: Int? let _minLength: Int? @@ -849,7 +833,7 @@ extension JSONSchema { extension OpenAPI { /// An encoding, as specified in RFC 2045, part 6.1 and RFC 4648. - public enum ContentEncoding: String, Codable { + public enum ContentEncoding: String, Codable, Sendable { case _7bit = "7bit" case _8bit = "8bit" case binary @@ -1051,7 +1035,7 @@ extension JSONSchema.CoreContext: Decodable { .underlyingError( InconsistencyError( subjectName: "OpenAPI Schema", - details: "Found 'nullable' property. This property is not supported by OpenAPI v3.1.0. OpenAPIKit has translated it into 'type: [\"null\", ...]'.", + details: "Found 'nullable' property. This property is not supported by OpenAPI v3.1.x. OpenAPIKit has translated it into 'type: [\"null\", ...]'.", codingPath: container.codingPath ) ) diff --git a/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift b/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift index edb593d96..1dc4771de 100644 --- a/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift +++ b/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift @@ -18,7 +18,7 @@ public protocol SwiftTyped { /// The raw types supported by JSON Schema. /// -/// These are the OpenAPI [data types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#data-types) +/// These are the OpenAPI [data types](https://spec.openapis.org/oas/v3.1.1.html#data-types) /// and additionally the `object` and `array` /// "compound" data types. /// - boolean @@ -27,7 +27,7 @@ public protocol SwiftTyped { /// - number /// - integer /// - string -public enum JSONType: String, Codable { +public enum JSONType: String, Codable, Sendable { case null = "null" case boolean = "boolean" case object = "object" @@ -54,8 +54,8 @@ public enum JSONType: String, Codable { /// /// You can also find information on types and /// formats in the OpenAPI Specification's -/// section on [data types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#data-types). -public enum JSONTypeFormat: Equatable { +/// section on [data types](https://spec.openapis.org/oas/v3.1.1.html#data-types). +public enum JSONTypeFormat: Equatable, Sendable { case null case boolean(BooleanFormat) case object(ObjectFormat) @@ -113,9 +113,9 @@ public enum JSONTypeFormat: Equatable { /// adheres to the [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14) /// specification for a "date-time." /// -/// See "formats" under the OpenAPI [data type](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#data-types) +/// See "formats" under the OpenAPI [data type](https://spec.openapis.org/oas/v3.1.1.html#data-types) /// documentation. -public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, Validatable where RawValue == String { +public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, Validatable, Sendable where RawValue == String { static var unspecified: Self { get } var jsonType: JSONType { get } diff --git a/Sources/OpenAPIKit/Security/DereferencedSecurityRequirement.swift b/Sources/OpenAPIKit/Security/DereferencedSecurityRequirement.swift index 1645310a9..1894eae61 100644 --- a/Sources/OpenAPIKit/Security/DereferencedSecurityRequirement.swift +++ b/Sources/OpenAPIKit/Security/DereferencedSecurityRequirement.swift @@ -63,7 +63,7 @@ public struct DereferencedSecurityRequirement: Equatable { /// not require a specified scope. For other security scheme types, /// the array MUST be empty. /// - /// See [Security Requirement Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-requirement-object) for more. + /// See [Security Requirement Object](https://spec.openapis.org/oas/v3.1.1.html#security-requirement-object) for more. public let requiredScopes: [String] } } diff --git a/Sources/OpenAPIKit/Security/SecurityScheme.swift b/Sources/OpenAPIKit/Security/SecurityScheme.swift index 2a33e1acd..eec43ef35 100644 --- a/Sources/OpenAPIKit/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit/Security/SecurityScheme.swift @@ -11,8 +11,8 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Security Scheme Object" /// - /// See [OpenAPI Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-scheme-object). - public struct SecurityScheme: Equatable, CodableVendorExtendable { + /// See [OpenAPI Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object). + public struct SecurityScheme: Equatable, CodableVendorExtendable, Sendable { public var type: SecurityType public var description: String? @@ -53,7 +53,7 @@ extension OpenAPI { return .init(type: .mutualTLS, description: description) } - public enum SecurityType: Equatable { + public enum SecurityType: Equatable, Sendable { case apiKey(name: String, location: Location) case http(scheme: String, bearerFormat: String?) case oauth2(flows: OAuthFlows) @@ -125,7 +125,9 @@ extension OpenAPI.SecurityScheme: Encodable { try container.encode(SecurityType.Name.mutualTLS, forKey: .type) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -267,9 +269,15 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { dereferencedFromComponentNamed name: String? ) throws -> OpenAPI.SecurityScheme { var ret = self - if let name = name { + if let name { ret.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } return ret } } + +extension OpenAPI.SecurityScheme: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} diff --git a/Sources/OpenAPIKit/Server.swift b/Sources/OpenAPIKit/Server.swift index 2b7d17b2c..5ca7f810a 100644 --- a/Sources/OpenAPIKit/Server.swift +++ b/Sources/OpenAPIKit/Server.swift @@ -11,9 +11,9 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Server Object" /// - /// See [OpenAPI Server Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#server-object). + /// See [OpenAPI Server Object](https://spec.openapis.org/oas/v3.1.1.html#server-object). /// - public struct Server: Equatable, CodableVendorExtendable { + public struct Server: Equatable, CodableVendorExtendable, Sendable { /// OpenAPI Server URLs can have variable placeholders in them. /// The `urlTemplate` can be asked for a well-formed Foundation /// `URL` if all variables in it have been replaced by constant values. @@ -63,9 +63,9 @@ extension OpenAPI { extension OpenAPI.Server { /// OpenAPI Spec "Server Variable Object" /// - /// See [OpenAPI Server Variable Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#server-variable-object). + /// See [OpenAPI Server Variable Object](https://spec.openapis.org/oas/v3.1.1.html#server-variable-object). /// - public struct Variable: Equatable, CodableVendorExtendable { + public struct Variable: Equatable, CodableVendorExtendable, Sendable { public var `enum`: [String]? public var `default`: String public var description: String? @@ -118,7 +118,9 @@ extension OpenAPI.Server: Encodable { try container.encode(variables, forKey: .variables) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -194,7 +196,9 @@ extension OpenAPI.Server.Variable: Encodable { try container.encodeIfPresent(description, forKey: .description) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -259,5 +263,11 @@ extension OpenAPI.Server.Variable { } } +extension OpenAPI.Server: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} + extension OpenAPI.Server: Validatable {} extension OpenAPI.Server.Variable: Validatable {} diff --git a/Sources/OpenAPIKit/Tag.swift b/Sources/OpenAPIKit/Tag.swift index 73ff53a64..4cc411b6d 100644 --- a/Sources/OpenAPIKit/Tag.swift +++ b/Sources/OpenAPIKit/Tag.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Tag Object" /// - /// See [OpenAPI Tag Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#tag-object). + /// See [OpenAPI Tag Object](https://spec.openapis.org/oas/v3.1.1.html#tag-object). public struct Tag: Equatable, CodableVendorExtendable { public let name: String public let description: String? @@ -69,7 +69,9 @@ extension OpenAPI.Tag: Encodable { try container.encodeIfPresent(externalDocs, forKey: .externalDocs) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift new file mode 100644 index 000000000..ca1f3dd54 --- /dev/null +++ b/Sources/OpenAPIKit/Utility/Array+ExternallyDereferenceable.swift @@ -0,0 +1,32 @@ +// +// Array+ExternallyDereferenceable.swift +// + +import OpenAPIKitCore + +extension Array where Element: ExternallyDereferenceable & Sendable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + try await withThrowingTaskGroup(of: (Int, (Element, OpenAPI.Components, [Loader.Message])).self) { group in + for (idx, elem) in zip(self.indices, self) { + group.addTask { + return try await (idx, elem.externallyDereferenced(with: loader)) + } + } + + var newElems = Array<(Int, Element)>() + var newComponents = OpenAPI.Components() + var newMessages = [Loader.Message]() + + for try await (idx, (elem, components, messages)) in group { + newElems.append((idx, elem)) + try newComponents.merge(components) + newMessages += messages + } + // things may come in out of order because of concurrency + // so we reorder after completing all entries. + newElems.sort { left, right in left.0 < right.0 } + return (newElems.map { $0.1 }, newComponents, newMessages) + } + } +} diff --git a/Sources/OpenAPIKit/Utility/Container+DecodeURLAsString.swift b/Sources/OpenAPIKit/Utility/Container+DecodeURLAsString.swift index ae5fd9fb5..d750d99de 100644 --- a/Sources/OpenAPIKit/Utility/Container+DecodeURLAsString.swift +++ b/Sources/OpenAPIKit/Utility/Container+DecodeURLAsString.swift @@ -11,19 +11,19 @@ import Foundation extension KeyedDecodingContainerProtocol { internal func decodeURLAsString(forKey key: Self.Key) throws -> URL { let string = try decode(String.self, forKey: key) - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: string, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: string, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: string, encodingInvalidCharacters: false) + url = URL(string: string, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: string) + url = URL(string: string) } -#else - urlCandidate = URL(string: string) -#endif - guard let url = urlCandidate else { + #else + url = URL(string: string) + #endif + guard let url else { throw InconsistencyError( subjectName: key.stringValue, details: "If specified, must be a valid URL", @@ -38,19 +38,19 @@ extension KeyedDecodingContainerProtocol { return nil } - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: string, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: string, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: string, encodingInvalidCharacters: false) + url = URL(string: string, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: string) + url = URL(string: string) } -#else - urlCandidate = URL(string: string) + #else + url = URL(string: string) #endif - guard let url = urlCandidate else { + guard let url else { throw InconsistencyError( subjectName: key.stringValue, details: "If specified, must be a valid URL", diff --git a/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift new file mode 100644 index 000000000..0bc061a0f --- /dev/null +++ b/Sources/OpenAPIKit/Utility/Dictionary+ExternallyDereferenceable.swift @@ -0,0 +1,31 @@ +// +// Dictionary+ExternallyDereferenceable.swift +// OpenAPI +// + +import OpenAPIKitCore + +extension Dictionary where Key: Sendable, Value: ExternallyDereferenceable & Sendable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components, [Loader.Message]).self) { group in + for (key, value) in self { + group.addTask { + let (newRef, components, messages) = try await value.externallyDereferenced(with: loader) + return (key, newRef, components, messages) + } + } + + var newDict = Self() + var newComponents = OpenAPI.Components() + var newMessage = [Loader.Message]() + + for try await (key, newRef, components, messages) in group { + newDict[key] = newRef + try newComponents.merge(components) + newMessage += messages + } + return (newDict, newComponents, newMessage) + } + } +} diff --git a/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift new file mode 100644 index 000000000..87c7a0649 --- /dev/null +++ b/Sources/OpenAPIKit/Utility/Optional+ExternallyDereferenceable.swift @@ -0,0 +1,13 @@ +// +// Optional+ExternallyDereferenceable.swift +// + +import OpenAPIKitCore + +extension Optional where Wrapped: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + guard let wrapped = self else { return (nil, .init(), []) } + return try await wrapped.externallyDereferenced(with: loader) + } +} diff --git a/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift new file mode 100644 index 000000000..66e78ab4f --- /dev/null +++ b/Sources/OpenAPIKit/Utility/OrderedDictionary+ExternallyDereferenceable.swift @@ -0,0 +1,36 @@ +// +// OrderedDictionary+ExternallyDereferenceable.swift +// OpenAPI +// +// Created by Mathew Polzin on 08/05/2023. +// + +import OpenAPIKitCore + +extension OrderedDictionary where Key: Sendable, Value: ExternallyDereferenceable & Sendable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components, [Loader.Message]).self) { group in + for (key, value) in self { + group.addTask { + let (newRef, components, messages) = try await value.externallyDereferenced(with: loader) + return (key, newRef, components, messages) + } + } + + var newDict = Self() + var newComponents = OpenAPI.Components() + var newMessages = [Loader.Message]() + + for try await (key, newRef, components, messages) in group { + newDict[key] = newRef + try newComponents.merge(components) + newMessages += messages + } + // things may come in out of order because of concurrency + // so we reorder after completing all entries. + try newDict.applyOrder(self) + return (newDict, newComponents, newMessages) + } + } +} diff --git a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift index 55af772fb..bc3354790 100644 --- a/Sources/OpenAPIKit/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit/Validator/Validation+Builtins.swift @@ -14,7 +14,7 @@ extension Validation { /// `PathItem.Map`. /// /// The OpenAPI Specification does not require that the document - /// contain any paths for [security reasons](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-filtering) + /// contain any paths for [security reasons](https://spec.openapis.org/oas/v3.1.1.html#security-filtering) /// or even because it only contains webhooks, but authors may still /// want to protect against an empty `PathItem.Map` in some cases. /// @@ -30,7 +30,7 @@ extension Validation { /// one operation. /// /// The OpenAPI Specification does not require that path items - /// contain any operations for [security reasons](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#security-filtering) + /// contain any operations for [security reasons](https://spec.openapis.org/oas/v3.1.1.html#security-filtering) /// but documentation that is public in nature might only ever have /// a `PathItem` with no operations in error. /// @@ -183,7 +183,7 @@ extension Validation { /// Validate that the OpenAPI Document's `Tags` all have unique names. /// /// The OpenAPI Specification requires that tag names on the Document - /// [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#openapi-object). + /// [are unique](https://spec.openapis.org/oas/v3.1.1.html#openapi-object). /// /// - Important: This is included in validation by default. public static var documentTagNamesAreUnique: Validation { @@ -203,7 +203,7 @@ extension Validation { /// A Path Item Parameter's identity is defined as the pairing of its `name` and /// `location`. /// - /// The OpenAPI Specification requires that these parameters [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#path-item-object). + /// The OpenAPI Specification requires that these parameters [are unique](https://spec.openapis.org/oas/v3.1.1.html#path-item-object). /// /// - Important: This is included in validation by default. /// @@ -221,7 +221,7 @@ extension Validation { /// An Operation's Parameter's identity is defined as the pairing of its `name` and /// `location`. /// - /// The OpenAPI Specification requires that these parameters [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object). + /// The OpenAPI Specification requires that these parameters [are unique](https://spec.openapis.org/oas/v3.1.1.html#operation-object). /// /// - Important: This is included in validation by default. /// @@ -235,7 +235,7 @@ extension Validation { /// Validate that all OpenAPI Operation Ids are unique across the whole Document. /// - /// The OpenAPI Specification requires that Operation Ids [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object). + /// The OpenAPI Specification requires that Operation Ids [are unique](https://spec.openapis.org/oas/v3.1.1.html#operation-object). /// /// - Important: This validation does not assert that all path references are valid and found in the /// components for the document. It skips over missing path items. @@ -444,16 +444,6 @@ extension Validation { } ) } - - /// Validate that `enum` must not be empty in the document's - /// Server Variable. - /// - /// - Important: This is included in validation by default. - /// - @available(*, deprecated, renamed: "serverVariableEnumIsValid") - public static var serverVarialbeEnumIsValid: Validation { - return serverVariableEnumIsValid - } /// Validate that `default` must exist in the enum values in the document's /// Server Variable, if such values (enum) are defined. @@ -469,16 +459,6 @@ extension Validation { } ) } - - /// Validate that `default` must exist in the enum values in the document's - /// Server Variable, if such values (enum) are defined. - /// - /// - Important: This is included in validation by default. - /// - @available(*, deprecated, renamed: "serverVariableDefaultExistsInEnum") - public static var serverVarialbeDefaultExistsInEnum : Validation { - return serverVariableDefaultExistsInEnum - } } /// Used by both the Path Item parameter check and the diff --git a/Sources/OpenAPIKit/XML.swift b/Sources/OpenAPIKit/XML.swift index 4793be230..b49d30b0b 100644 --- a/Sources/OpenAPIKit/XML.swift +++ b/Sources/OpenAPIKit/XML.swift @@ -11,7 +11,7 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "XML Object" /// - /// See [OpenAPI XML Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#xml-object). + /// See [OpenAPI XML Object](https://spec.openapis.org/oas/v3.1.1.html#xml-object). public struct XML: Equatable { public let name: String? public let namespace: URL? diff --git a/Sources/OpenAPIKit30/Callbacks.swift b/Sources/OpenAPIKit30/Callbacks.swift index f0504fa25..ada29dc5f 100644 --- a/Sources/OpenAPIKit30/Callbacks.swift +++ b/Sources/OpenAPIKit30/Callbacks.swift @@ -12,7 +12,7 @@ extension OpenAPI { /// A map from runtime expressions to path items to be used as /// callbacks for the API. /// - /// See [OpenAPI Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#callback-object). + /// See [OpenAPI Callback Object](https://spec.openapis.org/oas/v3.0.4.html#callback-object). /// public typealias Callbacks = OrderedDictionary @@ -35,3 +35,9 @@ extension OpenAPI.CallbackURL: LocallyDereferenceable { self } } + +// The following conformance is theoretically unnecessary but the compiler is +// only able to find the conformance if we explicitly declare it here, though +// it is apparently able to determine the conformance is already satisfied here +// at least. +extension OpenAPI.Callbacks: ExternallyDereferenceable { } diff --git a/Sources/OpenAPIKit30/CodableVendorExtendable.swift b/Sources/OpenAPIKit30/CodableVendorExtendable.swift index 9cfa2e0e0..dc3b0b67b 100644 --- a/Sources/OpenAPIKit30/CodableVendorExtendable.swift +++ b/Sources/OpenAPIKit30/CodableVendorExtendable.swift @@ -18,11 +18,24 @@ 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 } } +/// OpenAPIKit supports some additional Encoder/Decoder configuration above and beyond +/// what the Encoder or Decoder support out of box. +/// +/// To _disable_ encoding or decoding of Vendor Extensions (by default these are _enabled), +/// set `userInfo[VendorExtensionsConfiguration.enabledKey] = false` for your encoder or decoder. public enum VendorExtensionsConfiguration { - public static var isEnabled = true + public static let enabledKey: CodingUserInfoKey = .init(rawValue: "vendor-extensions-enabled")! + + static func isEnabled(for decoder: Decoder) -> Bool { + decoder.userInfo[enabledKey] as? Bool ?? true + } + + static func isEnabled(for encoder: Encoder) -> Bool { + encoder.userInfo[enabledKey] as? Bool ?? true + } } internal protocol ExtendableCodingKey: CodingKey, Equatable { @@ -75,7 +88,7 @@ internal enum VendorExtensionDecodingError: Swift.Error, CustomStringConvertible extension CodableVendorExtendable { internal static func extensions(from decoder: Decoder) throws -> VendorExtensions { - guard VendorExtensionsConfiguration.isEnabled else { + guard VendorExtensionsConfiguration.isEnabled(for: decoder) else { return [:] } @@ -85,7 +98,7 @@ extension CodableVendorExtendable { throw VendorExtensionDecodingError.selfIsArrayNotDict } - guard let decodedAny = decoded as? [String: Any] else { + guard let decodedAny = decoded as? [String: any Sendable] else { throw VendorExtensionDecodingError.foundNonStringKeys } @@ -109,9 +122,6 @@ extension CodableVendorExtendable { } internal func encodeExtensions(to container: inout T) throws where T.Key == Self.CodingKeys { - guard VendorExtensionsConfiguration.isEnabled else { - return - } for (key, value) in vendorExtensions { let xKey = key.starts(with: "x-") ? key : "x-\(key)" try container.encode(value, forKey: .extendedKey(for: xKey)) diff --git a/Sources/OpenAPIKit30/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit30/Components Object/Components+Locatable.swift index ed2172e2b..cd6fcfd91 100644 --- a/Sources/OpenAPIKit30/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit30/Components Object/Components+Locatable.swift @@ -15,59 +15,59 @@ public protocol ComponentDictionaryLocatable { /// 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 { 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 } } // Until OpenAPI 3.1, path items cannot actually be stored in the Components Object. This is here to facilitate path item // references, albeit in a less than ideal way. 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 diff --git a/Sources/OpenAPIKit30/Components Object/Components.swift b/Sources/OpenAPIKit30/Components Object/Components.swift index ca03ff146..929807d65 100644 --- a/Sources/OpenAPIKit30/Components Object/Components.swift +++ b/Sources/OpenAPIKit30/Components Object/Components.swift @@ -11,11 +11,11 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Components Object". /// - /// See [OpenAPI Components Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#components-object). + /// See [OpenAPI Components Object](https://spec.openapis.org/oas/v3.0.4.html#components-object). /// /// This is a place to put reusable components to /// be referenced from other parts of the spec. - public struct Components: Equatable, CodableVendorExtendable { + public struct Components: Equatable, CodableVendorExtendable, Sendable { public var schemas: ComponentDictionary public var responses: ComponentDictionary @@ -71,6 +71,57 @@ extension OpenAPI { } } +extension OpenAPI.Components { + public struct ComponentCollision: Swift.Error { + public let componentType: String + public let existingComponent: String + public let newComponent: String + } + + private func detectCollision(type: String) throws -> (_ old: T, _ new: T) throws -> T { + return { old, new in + // theoretically we can detect collisions here, but we would need to compare + // for equality up-to but not including the difference between an external and + // internal reference which is not supported yet. +// if(old == new) { return old } +// throw ComponentCollision(componentType: type, existingComponent: String(describing:old), newComponent: String(describing:new)) + + // Given we aren't ensuring there are no collisions, the old version is going to be + // the one more likely to have been _further_ dereferenced than the new record, so + // we keep that version. + return old + } + } + + public mutating func merge(_ other: OpenAPI.Components) throws { + try schemas.merge(other.schemas, uniquingKeysWith: detectCollision(type: "schema")) + try responses.merge(other.responses, uniquingKeysWith: detectCollision(type: "responses")) + try parameters.merge(other.parameters, uniquingKeysWith: detectCollision(type: "parameters")) + try examples.merge(other.examples, uniquingKeysWith: detectCollision(type: "examples")) + try requestBodies.merge(other.requestBodies, uniquingKeysWith: detectCollision(type: "requestBodies")) + try headers.merge(other.headers, uniquingKeysWith: detectCollision(type: "headers")) + try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: detectCollision(type: "securitySchemes")) + try links.merge(other.links, uniquingKeysWith: detectCollision(type: "links")) + try callbacks.merge(other.callbacks, uniquingKeysWith: detectCollision(type: "callbacks")) + try pathItems.merge(other.pathItems, uniquingKeysWith: detectCollision(type: "pathItems")) + try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: detectCollision(type: "vendorExtensions")) + } + + /// Sort the components within each type by the component key. + public mutating func sort() { + schemas.sortKeys() + responses.sortKeys() + parameters.sortKeys() + examples.sortKeys() + requestBodies.sortKeys() + headers.sortKeys() + securitySchemes.sortKeys() + links.sortKeys() + callbacks.sortKeys() + pathItems.sortKeys() + } +} + extension OpenAPI.Components { /// The extension name used to store a Components Object name (the key something is stored under /// within the Components Object). This is used by OpenAPIKit to store the previous Component name @@ -125,7 +176,9 @@ extension OpenAPI.Components: Encodable { try container.encode(callbacks, forKey: .callbacks) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -260,4 +313,104 @@ extension OpenAPI.Components { } } +extension OpenAPI.Components { + internal mutating func externallyDereference(with loader: Loader.Type, depth: ExternalDereferenceDepth = .iterations(1), context: [Loader.Message] = []) async throws -> [Loader.Message] { + if case let .iterations(number) = depth, + number <= 0 { + return context + } + + // NOTE: The links and callbacks related code commented out below pushes Swift 5.8 and 5.9 + // over the edge and you get exit code 137 crashes in CI. + // Swift 5.10 handles it fine. + + let oldSchemas = schemas + let oldResponses = responses + let oldParameters = parameters + let oldExamples = examples + let oldRequestBodies = requestBodies + let oldHeaders = headers + let oldSecuritySchemes = securitySchemes + let oldLinks = links + let oldCallbacks = callbacks + let oldPathItems = pathItems + + async let (newSchemas, c1, m1) = oldSchemas.externallyDereferenced(with: loader) + async let (newResponses, c2, m2) = oldResponses.externallyDereferenced(with: loader) + async let (newParameters, c3, m3) = oldParameters.externallyDereferenced(with: loader) + async let (newExamples, c4, m4) = oldExamples.externallyDereferenced(with: loader) + async let (newRequestBodies, c5, m5) = oldRequestBodies.externallyDereferenced(with: loader) + async let (newHeaders, c6, m6) = oldHeaders.externallyDereferenced(with: loader) + async let (newSecuritySchemes, c7, m7) = oldSecuritySchemes.externallyDereferenced(with: loader) +// async let (newLinks, c8, m8) = oldLinks.externallyDereferenced(with: loader) +// async let (newCallbacks, c9, m9) = oldCallbacks.externallyDereferenced(with: loader) + async let (newPathItems, c10, m10) = oldPathItems.externallyDereferenced(with: loader) + + schemas = try await newSchemas + responses = try await newResponses + parameters = try await newParameters + examples = try await newExamples + requestBodies = try await newRequestBodies + headers = try await newHeaders + securitySchemes = try await newSecuritySchemes +// links = try await newLinks +// callbacks = try await newCallbacks + pathItems = try await newPathItems + + let c1Resolved = try await c1 + let c2Resolved = try await c2 + let c3Resolved = try await c3 + let c4Resolved = try await c4 + let c5Resolved = try await c5 + let c6Resolved = try await c6 + let c7Resolved = try await c7 +// let c8Resolved = try await c8 +// let c9Resolved = try await c9 + let c10Resolved = try await c10 + + // For Swift 5.10+ we can delete the following links and callbacks code and uncomment the + // preferred code above. + let (newLinks, c8, m8) = try await oldLinks.externallyDereferenced(with: loader) + links = newLinks + let c8Resolved = c8 + let (newCallbacks, c9, m9) = try await oldCallbacks.externallyDereferenced(with: loader) + callbacks = newCallbacks + let c9Resolved = c9 + + let noNewComponents = + c1Resolved.isEmpty + && c2Resolved.isEmpty + && c3Resolved.isEmpty + && c4Resolved.isEmpty + && c5Resolved.isEmpty + && c6Resolved.isEmpty + && c7Resolved.isEmpty + && c8Resolved.isEmpty + && c9Resolved.isEmpty + && c10Resolved.isEmpty + + let newMessages = try await context + m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10 + + if noNewComponents { return newMessages } + + try merge(c1Resolved) + try merge(c2Resolved) + try merge(c3Resolved) + try merge(c4Resolved) + try merge(c5Resolved) + try merge(c6Resolved) + try merge(c7Resolved) + try merge(c8Resolved) + try merge(c9Resolved) + try merge(c10Resolved) + + switch depth { + case .iterations(let number): + return try await externallyDereference(with: loader, depth: .iterations(number - 1), context: newMessages) + case .full: + return try await externallyDereference(with: loader, depth: .full, context: newMessages) + } + } +} + extension OpenAPI.Components: Validatable {} diff --git a/Sources/OpenAPIKit30/Content/Content.swift b/Sources/OpenAPIKit30/Content/Content.swift index f2dc83f2e..8ade49c91 100644 --- a/Sources/OpenAPIKit30/Content/Content.swift +++ b/Sources/OpenAPIKit30/Content/Content.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Media Type Object" /// - /// See [OpenAPI Media Type Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#media-type-object). - public struct Content: Equatable, CodableVendorExtendable { + /// See [OpenAPI Media Type Object](https://spec.openapis.org/oas/v3.0.4.html#media-type-object). + public struct Content: Equatable, CodableVendorExtendable, Sendable { public var schema: Either, JSONSchema>? public var example: AnyCodable? public var examples: Example.Map? @@ -161,7 +161,9 @@ extension OpenAPI.Content: Encodable { try container.encodeIfPresent(encoding, forKey: .encoding) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Content/ContentEncoding.swift b/Sources/OpenAPIKit30/Content/ContentEncoding.swift index 6e61195c4..656e43e7f 100644 --- a/Sources/OpenAPIKit30/Content/ContentEncoding.swift +++ b/Sources/OpenAPIKit30/Content/ContentEncoding.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI.Content { /// OpenAPI Spec "Encoding Object" /// - /// See [OpenAPI Encoding Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#encoding-object). - public struct Encoding: Equatable { + /// See [OpenAPI Encoding Object](https://spec.openapis.org/oas/v3.0.4.html#encoding-object). + public struct Encoding: Equatable, Sendable { public typealias Style = OpenAPI.Parameter.SchemaContext.Style public let contentType: OpenAPI.ContentType? diff --git a/Sources/OpenAPIKit30/Content/DereferencedContent.swift b/Sources/OpenAPIKit30/Content/DereferencedContent.swift index e9ee1fef8..ebfa10f07 100644 --- a/Sources/OpenAPIKit30/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit30/Content/DereferencedContent.swift @@ -75,3 +75,33 @@ extension OpenAPI.Content: LocallyDereferenceable { return try DereferencedContent(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Content: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldSchema = schema + + async let (newSchema, c1, m1) = oldSchema.externallyDereferenced(with: loader) + + var newContent = self + var newComponents = try await c1 + var newMessages = try await m1 + + newContent.schema = try await newSchema + + if let oldExamples = examples { + let (newExamples, c2, m2) = try await oldExamples.externallyDereferenced(with: loader) + newContent.examples = newExamples + try newComponents.merge(c2) + newMessages += m2 + } + + if let oldEncoding = encoding { + let (newEncoding, c3, m3) = try await oldEncoding.externallyDereferenced(with: loader) + newContent.encoding = newEncoding + try newComponents.merge(c3) + newMessages += m3 + } + + return (newContent, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Content/DereferencedContentEncoding.swift b/Sources/OpenAPIKit30/Content/DereferencedContentEncoding.swift index fdd0b1bbc..aaa9a1fd5 100644 --- a/Sources/OpenAPIKit30/Content/DereferencedContentEncoding.swift +++ b/Sources/OpenAPIKit30/Content/DereferencedContentEncoding.swift @@ -56,3 +56,29 @@ extension OpenAPI.Content.Encoding: LocallyDereferenceable { return try DereferencedContentEncoding(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Content.Encoding: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let newHeaders: OpenAPI.Header.Map? + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + if let oldHeaders = headers { + (newHeaders, newComponents, newMessages) = try await oldHeaders.externallyDereferenced(with: loader) + } else { + newHeaders = nil + newComponents = .init() + newMessages = [] + } + + let newEncoding = OpenAPI.Content.Encoding( + contentType: contentType, + headers: newHeaders, + style: style, + explode: explode, + allowReserved: allowReserved + ) + + return (newEncoding, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Document/DereferencedDocument.swift b/Sources/OpenAPIKit30/Document/DereferencedDocument.swift index 4a1008f4b..fed984635 100644 --- a/Sources/OpenAPIKit30/Document/DereferencedDocument.swift +++ b/Sources/OpenAPIKit30/Document/DereferencedDocument.swift @@ -100,7 +100,7 @@ extension DereferencedDocument { /// each path, traversed in the order the paths appear in /// the document. /// - /// See [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#operation-object) in the specifcation. + /// See [Operation Object](https://spec.openapis.org/oas/v3.0.4.html#operation-object) in the specifcation. /// public var allOperationIds: [String] { return paths.values diff --git a/Sources/OpenAPIKit30/Document/Document.swift b/Sources/OpenAPIKit30/Document/Document.swift index 8f2b06158..c4bc3a127 100644 --- a/Sources/OpenAPIKit30/Document/Document.swift +++ b/Sources/OpenAPIKit30/Document/Document.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore extension OpenAPI { /// The root of an OpenAPI 3.0 document. /// - /// See [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md). + /// See [OpenAPI Specification](https://spec.openapis.org/oas/v3.0.4.html). /// /// An OpenAPI Document can say a _lot_ about the API it describes. /// A read-through of the specification is highly recommended because @@ -135,7 +135,7 @@ extension OpenAPI { public var vendorExtensions: [String: AnyCodable] public init( - openAPIVersion: Version = .v3_0_0, + openAPIVersion: Version = .v3_0_4, info: Info, servers: [Server], paths: PathItem.Map, @@ -213,7 +213,7 @@ extension OpenAPI.Document { /// each path, traversed in the order the paths appear in /// the document. /// - /// See [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#operation-object) in the specifcation. + /// See [Operation Object](https://spec.openapis.org/oas/v3.0.4.html#operation-object) in the specifcation. /// public var allOperationIds: [String] { return paths.values @@ -308,10 +308,26 @@ extension OpenAPI.Document { } } +public enum ExternalDereferenceDepth { + case iterations(Int) + case full +} + +extension ExternalDereferenceDepth: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .iterations(value) + } +} + extension OpenAPI.Document { /// Create a locally-dereferenced OpenAPI /// Document. /// + /// This function assumes all references are + /// local to the same file. If you want to resolve + /// remote references as well, call `externallyDereference()` + /// first and then locally dereference the result. + /// /// A dereferenced document contains no /// `JSONReferences`. All components have been /// inlined. @@ -334,6 +350,42 @@ extension OpenAPI.Document { public func locallyDereferenced() throws -> DereferencedDocument { return try DereferencedDocument(self) } + + /// Load all remote references into the document. A remote reference is one + /// that points to another file rather than a location within the + /// same file. + /// + /// This function will load remote references into the Components object + /// and replace the remote reference with a local reference to that component. + /// No local references are modified or resolved by this function. You can + /// call `locallyDereferenced()` on the externally dereferenced document if + /// you want to also remove local references by inlining all of them. + /// + /// Externally dereferencing a document requires that you provide both a + /// function that produces a `OpenAPI.ComponentKey` for any given remote + /// file URI and also a function that loads and decodes the data found in + /// that remote file. The latter is less work than it may sound like because + /// the function is told what Decodable thing it wants, so you really just + /// need to decide what decoder to use and provide the file data to that + /// decoder. See `ExternalLoader` documentation for details. + @discardableResult + public mutating func externallyDereference(with loader: Loader.Type, depth: ExternalDereferenceDepth = .iterations(1), context: [Loader.Message] = []) async throws -> [Loader.Message] { + if case let .iterations(number) = depth, + number <= 0 { + return context + } + + let oldPaths = paths + + async let (newPaths, c1, m1) = oldPaths.externallyDereferenced(with: loader) + + paths = try await newPaths + try await components.merge(c1) + + let m2 = try await components.externallyDereference(with: loader, depth: depth) + + return try await context + m1 + m2 + } } extension OpenAPI { @@ -346,7 +398,7 @@ extension OpenAPI { /// Multiple entries in this dictionary indicate all schemes named are /// required on the same request. /// - /// See [OpenAPI Security Requirement Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#security-requirement-object). + /// See [OpenAPI Security Requirement Object](https://spec.openapis.org/oas/v3.0.4.html#security-requirement-object). public typealias SecurityRequirement = [JSONReference: [String]] } @@ -356,12 +408,58 @@ extension OpenAPI.Document { /// OpenAPIKit only explicitly supports versions that can be found in /// this enum. Other versions may or may not be decodable by /// OpenAPIKit to a certain extent. - public enum Version: String, Codable { - case v3_0_0 = "3.0.0" - case v3_0_1 = "3.0.1" - case v3_0_2 = "3.0.2" - case v3_0_3 = "3.0.3" - case v3_0_4 = "3.0.4" + /// + ///**IMPORTANT**: Although the `v3_0_x` case supports arbitrary + /// patch versions, only _known_ patch versions are decodable. That is, if the OpenAPI + /// specification releases a new patch version, OpenAPIKit will see a patch version release + /// explicitly supports decoding documents of that new patch version before said version will + /// succesfully decode as the `v3_0_x` case. + public enum Version: RawRepresentable, Equatable, Codable { + case v3_0_0 + case v3_0_1 + case v3_0_2 + case v3_0_3 + case v3_0_4 + case v3_0_x(x: Int) + + public init?(rawValue: String) { + switch rawValue { + case "3.0.0": self = .v3_0_0 + case "3.0.1": self = .v3_0_1 + case "3.0.2": self = .v3_0_2 + case "3.0.3": self = .v3_0_3 + case "3.0.4": self = .v3_0_4 + default: + let components = rawValue.split(separator: ".") + guard components.count == 3 else { + return nil + } + guard components[0] == "3", components[1] == "0" else { + return nil + } + guard let patchVersion = Int(components[2], radix: 10) else { + return nil + } + // to support newer versions released in the future without a breaking + // change to the enumeration, bump the upper limit here to e.g. 5 or 6 + // or 9: + guard patchVersion > 4 && patchVersion <= 4 else { + return nil + } + self = .v3_0_x(x: patchVersion) + } + } + + public var rawValue: String { + switch self { + case .v3_0_0: return "3.0.0" + case .v3_0_1: return "3.0.1" + case .v3_0_2: return "3.0.2" + case .v3_0_3: return "3.0.3" + case .v3_0_4: return "3.0.4" + case .v3_0_x(x: let x): return "3.0.\(x)" + } + } } } @@ -390,7 +488,9 @@ extension OpenAPI.Document: Encodable { try container.encode(paths, forKey: .paths) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } if !components.isEmpty { try container.encode(components, forKey: .components) diff --git a/Sources/OpenAPIKit30/Document/DocumentInfo.swift b/Sources/OpenAPIKit30/Document/DocumentInfo.swift index 7eda90a3c..c5d64a4ff 100644 --- a/Sources/OpenAPIKit30/Document/DocumentInfo.swift +++ b/Sources/OpenAPIKit30/Document/DocumentInfo.swift @@ -11,7 +11,7 @@ import Foundation extension OpenAPI.Document { /// OpenAPI Spec "Info Object" /// - /// See [OpenAPI Info Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#info-object). + /// See [OpenAPI Info Object](https://spec.openapis.org/oas/v3.0.4.html#info-object). public struct Info: Equatable, CodableVendorExtendable { public let title: String public let description: String? @@ -47,7 +47,7 @@ extension OpenAPI.Document { /// OpenAPI Spec "Contact Object" /// - /// See [OpenAPI Contact Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#contact-object). + /// See [OpenAPI Contact Object](https://spec.openapis.org/oas/v3.0.4.html#contact-object). public struct Contact: Equatable, CodableVendorExtendable { public let name: String? public let url: URL? @@ -75,7 +75,7 @@ extension OpenAPI.Document { /// OpenAPI Spec "License Object" /// - /// See [OpenAPI License Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#license-object). + /// See [OpenAPI License Object](https://spec.openapis.org/oas/v3.0.4.html#license-object). public struct License: Equatable, CodableVendorExtendable { public let name: String public let url: URL? @@ -128,7 +128,9 @@ extension OpenAPI.Document.Info.License: Encodable { try container.encode(name, forKey: .name) try container.encodeIfPresent(url?.absoluteString, forKey: .url) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -194,7 +196,9 @@ extension OpenAPI.Document.Info.Contact: Encodable { try container.encodeIfPresent(url?.absoluteString, forKey: .url) try container.encodeIfPresent(email, forKey: .email) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -269,7 +273,9 @@ extension OpenAPI.Document.Info: Encodable { try container.encodeIfPresent(license, forKey: .license) try container.encode(version, forKey: .version) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Either/Either+ExternallyDereferenceable.swift b/Sources/OpenAPIKit30/Either/Either+ExternallyDereferenceable.swift new file mode 100644 index 000000000..62c919355 --- /dev/null +++ b/Sources/OpenAPIKit30/Either/Either+ExternallyDereferenceable.swift @@ -0,0 +1,23 @@ +// +// Either+ExternallyDereferenceable.swift +// +// +// Created by Mathew Polzin on 2/28/21. +// + +import OpenAPIKitCore + +// MARK: - ExternallyDereferenceable +extension Either: ExternallyDereferenceable where A: ExternallyDereferenceable, B: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + switch self { + case .a(let a): + let (newA, components, messages) = try await a.externallyDereferenced(with: loader) + return (.a(newA), components, messages) + case .b(let b): + let (newB, components, messages) = try await b.externallyDereferenced(with: loader) + return (.b(newB), components, messages) + } + } +} diff --git a/Sources/OpenAPIKit30/Encoding and Decoding Errors/DocumentDecodingError.swift b/Sources/OpenAPIKit30/Encoding and Decoding Errors/DocumentDecodingError.swift index a585f7d93..6786b6034 100644 --- a/Sources/OpenAPIKit30/Encoding and Decoding Errors/DocumentDecodingError.swift +++ b/Sources/OpenAPIKit30/Encoding and Decoding Errors/DocumentDecodingError.swift @@ -12,7 +12,7 @@ extension OpenAPI.Error.Decoding { public let context: Context public let codingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case path(Path) case inconsistency(InconsistencyError) case other(Swift.DecodingError) diff --git a/Sources/OpenAPIKit30/Encoding and Decoding Errors/OperationDecodingError.swift b/Sources/OpenAPIKit30/Encoding and Decoding Errors/OperationDecodingError.swift index 354b16730..c62a3720a 100644 --- a/Sources/OpenAPIKit30/Encoding and Decoding Errors/OperationDecodingError.swift +++ b/Sources/OpenAPIKit30/Encoding and Decoding Errors/OperationDecodingError.swift @@ -13,7 +13,7 @@ extension OpenAPI.Error.Decoding { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case request(Request) case response(Response) case inconsistency(InconsistencyError) diff --git a/Sources/OpenAPIKit30/Encoding and Decoding Errors/PathDecodingError.swift b/Sources/OpenAPIKit30/Encoding and Decoding Errors/PathDecodingError.swift index c2809d9be..af865d31e 100644 --- a/Sources/OpenAPIKit30/Encoding and Decoding Errors/PathDecodingError.swift +++ b/Sources/OpenAPIKit30/Encoding and Decoding Errors/PathDecodingError.swift @@ -13,7 +13,7 @@ extension OpenAPI.Error.Decoding { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case endpoint(Operation) case inconsistency(InconsistencyError) case other(Swift.DecodingError) diff --git a/Sources/OpenAPIKit30/Encoding and Decoding Errors/ResponseDecodingError.swift b/Sources/OpenAPIKit30/Encoding and Decoding Errors/ResponseDecodingError.swift index 7d918a3f6..0089935d5 100644 --- a/Sources/OpenAPIKit30/Encoding and Decoding Errors/ResponseDecodingError.swift +++ b/Sources/OpenAPIKit30/Encoding and Decoding Errors/ResponseDecodingError.swift @@ -13,7 +13,7 @@ extension OpenAPI.Error.Decoding { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case inconsistency(InconsistencyError) case other(Swift.DecodingError) case neither(EitherDecodeNoTypesMatchedError) diff --git a/Sources/OpenAPIKit30/Example.swift b/Sources/OpenAPIKit30/Example.swift index 013f5b213..b4e564ebd 100644 --- a/Sources/OpenAPIKit30/Example.swift +++ b/Sources/OpenAPIKit30/Example.swift @@ -11,8 +11,8 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Example Object" /// - /// See [OpenAPI Example Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#example-object). - public struct Example: Equatable, CodableVendorExtendable { + /// See [OpenAPI Example Object](https://spec.openapis.org/oas/v3.0.4.html#example-object). + public struct Example: Equatable, CodableVendorExtendable, Sendable { public let summary: String? public let description: String? @@ -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, @@ -82,7 +82,9 @@ extension OpenAPI.Example: Encodable { break } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -171,7 +173,7 @@ extension OpenAPI.Example: LocallyDereferenceable { dereferencedFromComponentNamed name: String? ) throws -> OpenAPI.Example{ var vendorExtensions = self.vendorExtensions - if let name = name { + if let name { vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -184,4 +186,10 @@ extension OpenAPI.Example: LocallyDereferenceable { } } +extension OpenAPI.Example: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} + extension OpenAPI.Example: Validatable {} diff --git a/Sources/OpenAPIKit30/ExternalDocumentation.swift b/Sources/OpenAPIKit30/ExternalDocumentation.swift index 84a90a273..a79ae106a 100644 --- a/Sources/OpenAPIKit30/ExternalDocumentation.swift +++ b/Sources/OpenAPIKit30/ExternalDocumentation.swift @@ -11,8 +11,8 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "External Documentation Object" /// - /// See [OpenAPI External Documentation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#external-documentation-object). - public struct ExternalDocumentation: Equatable, CodableVendorExtendable { + /// See [OpenAPI External Documentation Object](https://spec.openapis.org/oas/v3.0.4.html#external-documentation-object). + public struct ExternalDocumentation: Equatable, CodableVendorExtendable, Sendable { public var description: String? public var url: URL @@ -44,7 +44,9 @@ extension OpenAPI.ExternalDocumentation: Encodable { try container.encodeIfPresent(description, forKey: .description) try container.encode(url.absoluteString, forKey: .url) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/ExternalLoader.swift b/Sources/OpenAPIKit30/ExternalLoader.swift new file mode 100644 index 000000000..ba67073de --- /dev/null +++ b/Sources/OpenAPIKit30/ExternalLoader.swift @@ -0,0 +1,40 @@ +// +// ExternalLoader.swift +// +// +// Created by Mathew Polzin on 7/30/2023. +// + +import OpenAPIKitCore +import Foundation + +/// An `ExternalLoader` 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 ExternalLoader where Message: Sendable { + /// This can be anything that an implementor of this protocol wants to pass back from + /// the `load()` function and have available after all external loading has been done. + /// + /// A trivial type if no Messages are needed would be Void. + associatedtype Message + + /// 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) async throws -> (T, [Message]) 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. + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey +} + +public protocol ExternallyDereferenceable { + func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) +} diff --git a/Sources/OpenAPIKit30/Header/DereferencedHeader.swift b/Sources/OpenAPIKit30/Header/DereferencedHeader.swift index 5f8eee40b..40509234a 100644 --- a/Sources/OpenAPIKit30/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit30/Header/DereferencedHeader.swift @@ -57,7 +57,7 @@ public struct DereferencedHeader: Equatable { } var header = header - if let name = name { + if let name { header.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -82,3 +82,39 @@ extension OpenAPI.Header: LocallyDereferenceable { return try DereferencedHeader(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Header: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + + // if not for a Swift bug, this whole next bit would just be the + // next line: +// let (newSchemaOrContent, components) = try await schemaOrContent.externallyDereferenced(with: loader) + + let newSchemaOrContent: Either + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + switch schemaOrContent { + case .a(let schemaContext): + let (context, components, messages) = try await schemaContext.externallyDereferenced(with: loader) + newSchemaOrContent = .a(context) + newComponents = components + newMessages = messages + case .b(let contentMap): + let (map, components, messages) = try await contentMap.externallyDereferenced(with: loader) + newSchemaOrContent = .b(map) + newComponents = components + newMessages = messages + } + + let newHeader = OpenAPI.Header( + schemaOrContent: newSchemaOrContent, + description: description, + required: required, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + + return (newHeader, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Header/Header.swift b/Sources/OpenAPIKit30/Header/Header.swift index 718ed935b..219a80ce4 100644 --- a/Sources/OpenAPIKit30/Header/Header.swift +++ b/Sources/OpenAPIKit30/Header/Header.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Header Object" /// - /// See [OpenAPI Header Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#header-object). - public struct Header: Equatable, CodableVendorExtendable { + /// See [OpenAPI Header Object](https://spec.openapis.org/oas/v3.0.4.html#header-object). + public struct Header: Equatable, CodableVendorExtendable, Sendable { public typealias SchemaContext = Parameter.SchemaContext public let description: String? @@ -275,7 +275,9 @@ extension OpenAPI.Header: Encodable { try container.encode(deprecated, forKey: .deprecated) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/JSONReference.swift b/Sources/OpenAPIKit30/JSONReference.swift index 812ee788d..8057bf6e9 100644 --- a/Sources/OpenAPIKit30/JSONReference.swift +++ b/Sources/OpenAPIKit30/JSONReference.swift @@ -39,7 +39,7 @@ import Foundation /// Components Object will be validated when you call `validate()` on an /// `OpenAPI.Document`. /// -public enum JSONReference: Equatable, Hashable, _OpenAPIReference { +public enum JSONReference: Equatable, Hashable, _OpenAPIReference, Sendable { /// The reference is internal to the file. case `internal`(InternalReference) /// The reference refers to another file. @@ -112,7 +112,7 @@ public enum JSONReference: Equatabl /// `JSONReference`. /// /// This reference must start with "#". - public enum InternalReference: LosslessStringConvertible, RawRepresentable, Equatable, Hashable { + public enum InternalReference: LosslessStringConvertible, RawRepresentable, Equatable, Hashable, Sendable { /// The reference refers to a component (i.e. `#/components/...`). case component(name: String) /// The reference refers to some path outside the Components Object. @@ -190,7 +190,7 @@ public enum JSONReference: Equatabl /// /// This path does _not_ start with "#". It starts with a forward slash. By contrast, an /// `InternalReference` starts with "#" and is followed by the start of a `Path`. - public struct Path: ExpressibleByArrayLiteral, ExpressibleByStringLiteral, LosslessStringConvertible, RawRepresentable, Equatable, Hashable { + public struct Path: ExpressibleByArrayLiteral, ExpressibleByStringLiteral, LosslessStringConvertible, RawRepresentable, Equatable, Hashable, Sendable { /// The Path's components. In the `rawValue`, these components are joined /// with forward slashes '/' per the JSON Reference specification. @@ -333,19 +333,19 @@ extension JSONReference: Decodable { } self = .internal(internalReference) } else { - let externalReferenceCandidate: URL? -#if canImport(FoundationEssentials) - externalReferenceCandidate = URL(string: referenceString, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let externalReference: URL? + #if canImport(FoundationEssentials) + externalReference = URL(string: referenceString, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - externalReferenceCandidate = URL(string: referenceString, encodingInvalidCharacters: false) + externalReference = URL(string: referenceString, encodingInvalidCharacters: false) } else { - externalReferenceCandidate = URL(string: referenceString) + externalReference = URL(string: referenceString) } -#else - externalReferenceCandidate = URL(string: referenceString) -#endif - guard let externalReference = externalReferenceCandidate else { + #else + externalReference = URL(string: referenceString) + #endif + guard let externalReference else { throw InconsistencyError( subjectName: "JSON Reference", details: "Failed to parse a valid URI for a JSON Reference from '\(referenceString)'", @@ -385,4 +385,20 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere } } +// MARK: - ExternallyDereferenceable +extension JSONReference: ExternallyDereferenceable where ReferenceType: ExternallyDereferenceable & Decodable & Equatable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + switch self { + 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) + var components = OpenAPI.Components() + components[keyPath: ReferenceType.openAPIComponentsKeyPath][componentKey] = component + return (try components.reference(named: componentKey.rawValue, ofType: ReferenceType.self), components, messages) + } + } +} + extension JSONReference: Validatable where ReferenceType: Validatable {} diff --git a/Sources/OpenAPIKit30/Link.swift b/Sources/OpenAPIKit30/Link.swift index 21c56ca7c..4b6d46d2a 100644 --- a/Sources/OpenAPIKit30/Link.swift +++ b/Sources/OpenAPIKit30/Link.swift @@ -15,23 +15,23 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Link Object" /// - /// See [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object). - public struct Link: Equatable, CodableVendorExtendable { + /// See [OpenAPI Link Object](https://spec.openapis.org/oas/v3.1.1.html#link-object). + public struct Link: Equatable, CodableVendorExtendable, Sendable { /// The **OpenAPI**` `operationRef` or `operationId` field, depending on whether /// a `URL` of a remote or local Operation Object or a `operationId` (String) of an /// operation defined in the same document is given. - public let operation: Either + public var operation: Either /// A map from parameter names to either runtime expressions that evaluate to values or /// constant values (`AnyCodable`). /// - /// See the docuemntation for the [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#link-object) for more details. + /// See the docuemntation for the [OpenAPI Link Object](https://spec.openapis.org/oas/v3.1.1.html#link-object) for more details. /// /// Empty dictionaries will be omitted from encoding. - public let parameters: OrderedDictionary> + public var parameters: OrderedDictionary> /// A literal value or expression to use as a request body when calling the target operation. - public let requestBody: Either? + public var requestBody: Either? public var description: String? - public let server: Server? + public var server: Server? /// Dictionary of vendor extensions. /// @@ -163,7 +163,9 @@ extension OpenAPI.Link: Encodable { try container.encodeIfPresent(description, forKey: .description) try container.encodeIfPresent(server, forKey: .server) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -263,7 +265,7 @@ extension OpenAPI.Link: LocallyDereferenceable { dereferencedFromComponentNamed name: String? ) throws -> OpenAPI.Link { var vendorExtensions = self.vendorExtensions - if let name = name { + if let name { vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -278,4 +280,15 @@ extension OpenAPI.Link: LocallyDereferenceable { } } +extension OpenAPI.Link: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let (newServer, newComponents, newMessages) = try await server.externallyDereferenced(with: loader) + + var newLink = self + newLink.server = newServer + + return (newLink, newComponents, newMessages) + } +} + extension OpenAPI.Link: Validatable {} diff --git a/Sources/OpenAPIKit30/Operation/DereferencedOperation.swift b/Sources/OpenAPIKit30/Operation/DereferencedOperation.swift index 4ecde92c5..a9daeb843 100644 --- a/Sources/OpenAPIKit30/Operation/DereferencedOperation.swift +++ b/Sources/OpenAPIKit30/Operation/DereferencedOperation.swift @@ -124,3 +124,44 @@ extension OpenAPI.Operation: LocallyDereferenceable { return try DereferencedOperation(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Operation: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldParameters = parameters + let oldRequestBody = requestBody + let oldResponses = responses + let oldCallbacks = callbacks + let oldServers = servers + + async let (newParameters, c1, m1) = oldParameters.externallyDereferenced(with: loader) + async let (newRequestBody, c2, m2) = oldRequestBody.externallyDereferenced(with: loader) + async let (newResponses, c3, m3) = oldResponses.externallyDereferenced(with: loader) + async let (newCallbacks, c4, m4) = oldCallbacks.externallyDereferenced(with: loader) +// let (newServers, c5, m5) = try await oldServers.externallyDereferenced(with: loader) + + var newOperation = self + var newComponents = try await c1 + var newMessages = try await m1 + + newOperation.parameters = try await newParameters + newOperation.requestBody = try await newRequestBody + try await newComponents.merge(c2) + try await newMessages += m2 + newOperation.responses = try await newResponses + try await newComponents.merge(c3) + try await newMessages += m3 + newOperation.callbacks = try await newCallbacks + try await newComponents.merge(c4) + try await newMessages += m4 + + // should not be necessary but current Swift compiler can't figure out conformance of ExternallyDereferenceable: + if let oldServers { + let (newServers, c5, m5) = try await oldServers.externallyDereferenced(with: loader) + newOperation.servers = newServers + try newComponents.merge(c5) + newMessages += m5 + } + + return (newOperation, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Operation/Operation.swift b/Sources/OpenAPIKit30/Operation/Operation.swift index 2c0311fd6..8d97ced60 100644 --- a/Sources/OpenAPIKit30/Operation/Operation.swift +++ b/Sources/OpenAPIKit30/Operation/Operation.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Operation Object" /// - /// See [OpenAPI Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#operation-object). - public struct Operation: Equatable, CodableVendorExtendable { + /// See [OpenAPI Operation Object](https://spec.openapis.org/oas/v3.0.4.html#operation-object). + public struct Operation: Equatable, CodableVendorExtendable, Sendable { public var tags: [String]? public var summary: String? public var description: String? @@ -76,7 +76,7 @@ extension OpenAPI { /// The key is a unique identifier for the Callback Object. Each value in the /// map is a Callback Object that describes a request that may be initiated /// by the API provider and the expected responses. - public let callbacks: OpenAPI.CallbacksMap + public var callbacks: OpenAPI.CallbacksMap /// Indicates that the operation is deprecated or not. /// @@ -271,7 +271,9 @@ extension OpenAPI.Operation: Encodable { try container.encodeIfPresent(servers, forKey: .servers) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit30/Parameter/DereferencedParameter.swift index 97fb607b7..acb30c8d2 100644 --- a/Sources/OpenAPIKit30/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit30/Parameter/DereferencedParameter.swift @@ -59,7 +59,7 @@ public struct DereferencedParameter: Equatable { } var parameter = parameter - if let name = name { + if let name { parameter.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -82,3 +82,34 @@ extension OpenAPI.Parameter: LocallyDereferenceable { return try DereferencedParameter(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Parameter: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + + // if not for a Swift bug, this whole function would just be the + // next line: +// let (newSchemaOrContent, components, messages) = try await schemaOrContent.externallyDereferenced(with: loader) + + let newSchemaOrContent: Either + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + switch schemaOrContent { + case .a(let schemaContext): + let (context, components, messages) = try await schemaContext.externallyDereferenced(with: loader) + newSchemaOrContent = .a(context) + newComponents = components + newMessages = messages + case .b(let contentMap): + let (map, components, messages) = try await contentMap.externallyDereferenced(with: loader) + newSchemaOrContent = .b(map) + newComponents = components + newMessages = messages + } + + var newParameter = self + newParameter.schemaOrContent = newSchemaOrContent + + return (newParameter, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Parameter/DereferencedSchemaContext.swift b/Sources/OpenAPIKit30/Parameter/DereferencedSchemaContext.swift index 299b728cd..9a035f1e7 100644 --- a/Sources/OpenAPIKit30/Parameter/DereferencedSchemaContext.swift +++ b/Sources/OpenAPIKit30/Parameter/DereferencedSchemaContext.swift @@ -68,3 +68,26 @@ extension OpenAPI.Parameter.SchemaContext: LocallyDereferenceable { return try DereferencedSchemaContext(self, resolvingIn: components, following: references) } } + +extension OpenAPI.Parameter.SchemaContext: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldSchema = schema + + async let (newSchema, c1, m1) = oldSchema.externallyDereferenced(with: loader) + + var newSchemaContext = self + var newComponents = try await c1 + var newMessages = try await m1 + + newSchemaContext.schema = try await newSchema + + if let oldExamples = examples { + let (newExamples, c2, m2) = try await oldExamples.externallyDereferenced(with: loader) + newSchemaContext.examples = newExamples + try newComponents.merge(c2) + newMessages += m2 + } + + return (newSchemaContext, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Parameter/Parameter.swift b/Sources/OpenAPIKit30/Parameter/Parameter.swift index 045cdb13e..d69e0cb1e 100644 --- a/Sources/OpenAPIKit30/Parameter/Parameter.swift +++ b/Sources/OpenAPIKit30/Parameter/Parameter.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Parameter Object" /// - /// See [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). - public struct Parameter: Equatable, CodableVendorExtendable { + /// See [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.0.4.html#parameter-object). + public struct Parameter: Equatable, CodableVendorExtendable, Sendable { public var name: String /// OpenAPI Spec "in" property determines the `Context`. @@ -157,7 +157,7 @@ extension OpenAPI.Parameter { /// containing exactly the things that differentiate /// one parameter from another, per the specification. /// - /// See [Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). + /// See [Parameter Object](https://spec.openapis.org/oas/v3.0.4.html#parameter-object). internal struct ParameterIdentity: Hashable { let name: String let location: Context.Location @@ -258,7 +258,9 @@ extension OpenAPI.Parameter: Encodable { try container.encode(deprecated, forKey: .deprecated) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Parameter/ParameterContext.swift b/Sources/OpenAPIKit30/Parameter/ParameterContext.swift index abb134563..3bfb1428a 100644 --- a/Sources/OpenAPIKit30/Parameter/ParameterContext.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterContext.swift @@ -10,13 +10,13 @@ import OpenAPIKitCore extension OpenAPI.Parameter { /// OpenAPI Spec "Parameter Object" location-specific configuration. /// - /// See [OpenAPI Parameter Locations](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-locations). + /// See [OpenAPI Parameter Locations](https://spec.openapis.org/oas/v3.0.4.html#parameter-locations). /// /// Query, Header, and Cookie parameters are /// all optional by default unless you pass /// `required: true` to the context construction. /// Path parameters are always required. - public enum Context: Equatable { + public enum Context: Equatable, Sendable { case query(required: Bool, allowEmptyValue: Bool) case header(required: Bool) case path diff --git a/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift index e44df7313..7dddf716d 100644 --- a/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift +++ b/Sources/OpenAPIKit30/Parameter/ParameterSchemaContext.swift @@ -10,16 +10,16 @@ import OpenAPIKitCore extension OpenAPI.Parameter { /// OpenAPI Spec "Parameter Object" schema and style configuration. /// - /// See [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object) - /// and [OpenAPI Style Values](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#style-values). - public struct SchemaContext: Equatable { - public let style: Style - public let explode: Bool - public let allowReserved: Bool //defaults to false - public let schema: Either, JSONSchema> + /// See [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.0.4.html#parameter-object) + /// and [OpenAPI Style Values](https://spec.openapis.org/oas/v3.0.4.html#style-values). + public struct SchemaContext: Equatable, Sendable { + public var style: Style + public var explode: Bool + public var allowReserved: Bool //defaults to false + public var schema: Either, JSONSchema> - public let example: AnyCodable? - public let examples: OpenAPI.Example.Map? + public var example: AnyCodable? + public var examples: OpenAPI.Example.Map? public init(_ schema: JSONSchema, style: Style, @@ -133,7 +133,7 @@ extension OpenAPI.Parameter.SchemaContext.Style { /// per the OpenAPI Specification. /// /// See the `style` fixed field under - /// [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameter-object). + /// [OpenAPI Parameter Object](https://spec.openapis.org/oas/v3.0.4.html#parameter-object). public static func `default`(for location: OpenAPI.Parameter.Context) -> Self { switch location { case .query: diff --git a/Sources/OpenAPIKit30/Path Item/DereferencedPathItem.swift b/Sources/OpenAPIKit30/Path Item/DereferencedPathItem.swift index 7b711b1b1..542b8aa54 100644 --- a/Sources/OpenAPIKit30/Path Item/DereferencedPathItem.swift +++ b/Sources/OpenAPIKit30/Path Item/DereferencedPathItem.swift @@ -65,7 +65,7 @@ public struct DereferencedPathItem: Equatable { self.trace = try pathItem.trace.map { try DereferencedOperation($0, resolvingIn: components, following: references) } var pathItem = pathItem - if let name = name { + if let name { pathItem.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -137,3 +137,73 @@ extension OpenAPI.PathItem: LocallyDereferenceable { return try DereferencedPathItem(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.PathItem: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldParameters = parameters + let oldServers = servers + let oldGet = get + let oldPut = put + let oldPost = post + let oldDelete = delete + let oldOptions = options + let oldHead = head + let oldPatch = patch + let oldTrace = trace + + async let (newParameters, c1, m1) = oldParameters.externallyDereferenced(with: loader) +// async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader) + async let (newGet, c3, m3) = oldGet.externallyDereferenced(with: loader) + async let (newPut, c4, m4) = oldPut.externallyDereferenced(with: loader) + async let (newPost, c5, m5) = oldPost.externallyDereferenced(with: loader) + async let (newDelete, c6, m6) = oldDelete.externallyDereferenced(with: loader) + async let (newOptions, c7, m7) = oldOptions.externallyDereferenced(with: loader) + async let (newHead, c8, m8) = oldHead.externallyDereferenced(with: loader) + async let (newPatch, c9, m9) = oldPatch.externallyDereferenced(with: loader) + async let (newTrace, c10, m10) = oldTrace.externallyDereferenced(with: loader) + + var pathItem = self + var newComponents = try await c1 + var newMessages = try await m1 + + // ideally we would async let all of the props above and then set them here, + // but for now since there seems to be some sort of compiler bug we will do + // newServers in an if let below + pathItem.parameters = try await newParameters + pathItem.get = try await newGet + pathItem.put = try await newPut + pathItem.post = try await newPost + pathItem.delete = try await newDelete + pathItem.options = try await newOptions + pathItem.head = try await newHead + pathItem.patch = try await newPatch + pathItem.trace = try await newTrace + + try await newComponents.merge(c3) + try await newComponents.merge(c4) + try await newComponents.merge(c5) + try await newComponents.merge(c6) + try await newComponents.merge(c7) + try await newComponents.merge(c8) + try await newComponents.merge(c9) + try await newComponents.merge(c10) + + try await newMessages += m3 + try await newMessages += m4 + try await newMessages += m5 + try await newMessages += m6 + try await newMessages += m7 + try await newMessages += m8 + try await newMessages += m9 + try await newMessages += m10 + + if let oldServers { + async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader) + pathItem.servers = try await newServers + try await newComponents.merge(c2) + try await newMessages += m2 + } + + return (pathItem, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Path Item/PathItem.swift b/Sources/OpenAPIKit30/Path Item/PathItem.swift index 5829c6534..518818192 100644 --- a/Sources/OpenAPIKit30/Path Item/PathItem.swift +++ b/Sources/OpenAPIKit30/Path Item/PathItem.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Path Item Object" /// - /// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object). + /// See [OpenAPI Path Item Object](https://spec.openapis.org/oas/v3.0.4.html#path-item-object). /// /// In addition to parameters that apply to all endpoints under the current path, /// this type offers access to each possible endpoint operation under properties @@ -21,7 +21,7 @@ extension OpenAPI { /// /// You can access an array of equatable `HttpMethod`/`Operation` paris with the /// `endpoints` property. - public struct PathItem: Equatable, CodableVendorExtendable { + public struct PathItem: Equatable, CodableVendorExtendable, Sendable { public var summary: String? public var description: String? public var servers: [OpenAPI.Server]? @@ -218,24 +218,6 @@ extension OpenAPI.PathItem { // MARK: - Codable -extension OpenAPI.Path: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(rawValue) - } -} - -extension OpenAPI.Path: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let rawValue = try container.decode(String.self) - - self.init(rawValue: rawValue) - } -} - extension OpenAPI.PathItem: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -257,7 +239,9 @@ extension OpenAPI.PathItem: Encodable { try container.encodeIfPresent(patch, forKey: .patch) try container.encodeIfPresent(trace, forKey: .trace) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Request/DereferencedRequest.swift b/Sources/OpenAPIKit30/Request/DereferencedRequest.swift index 18d37c70d..6c7fd3189 100644 --- a/Sources/OpenAPIKit30/Request/DereferencedRequest.swift +++ b/Sources/OpenAPIKit30/Request/DereferencedRequest.swift @@ -38,7 +38,7 @@ public struct DereferencedRequest: Equatable { } var request = request - if let name = name { + if let name { request.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -61,3 +61,14 @@ extension OpenAPI.Request: LocallyDereferenceable { return try DereferencedRequest(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Request: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + var newRequest = self + + let (newContent, components, messages) = try await content.externallyDereferenced(with: loader) + + newRequest.content = newContent + return (newRequest, components, messages) + } +} diff --git a/Sources/OpenAPIKit30/Request/Request.swift b/Sources/OpenAPIKit30/Request/Request.swift index 0692215a4..c0c9b5e20 100644 --- a/Sources/OpenAPIKit30/Request/Request.swift +++ b/Sources/OpenAPIKit30/Request/Request.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Request Body Object" /// - /// See [OpenAPI Request Body Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#request-body-object). - public struct Request: Equatable, CodableVendorExtendable { + /// See [OpenAPI Request Body Object](https://spec.openapis.org/oas/v3.0.4.html#request-body-object). + public struct Request: Equatable, CodableVendorExtendable, Sendable { public var description: String? public var content: Content.Map public var required: Bool @@ -97,7 +97,9 @@ extension OpenAPI.Request: Encodable { try container.encode(required, forKey: .required) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Response/DereferencedResponse.swift b/Sources/OpenAPIKit30/Response/DereferencedResponse.swift index 76e883179..ced58ae20 100644 --- a/Sources/OpenAPIKit30/Response/DereferencedResponse.swift +++ b/Sources/OpenAPIKit30/Response/DereferencedResponse.swift @@ -51,7 +51,7 @@ public struct DereferencedResponse: Equatable { } var response = response - if let name = name { + if let name { response.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } @@ -76,3 +76,34 @@ extension OpenAPI.Response: LocallyDereferenceable { return try DereferencedResponse(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } + +extension OpenAPI.Response: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let oldContent = content + let oldLinks = links + let oldHeaders = headers + + async let (newContent, c1, m1) = oldContent.externallyDereferenced(with: loader) + async let (newLinks, c2, m2) = oldLinks.externallyDereferenced(with: loader) +// async let (newHeaders, c3, m3) = oldHeaders.externallyDereferenced(with: loader) + + var response = self + response.content = try await newContent + response.links = try await newLinks + + var components = try await c1 + try await components.merge(c2) + + var messages = try await m1 + try await messages += m2 + + if let oldHeaders { + let (newHeaders, c3, m3) = try await oldHeaders.externallyDereferenced(with: loader) + response.headers = newHeaders + try components.merge(c3) + messages += m3 + } + + return (response, components, messages) + } +} diff --git a/Sources/OpenAPIKit30/Response/Response.swift b/Sources/OpenAPIKit30/Response/Response.swift index 56e057ddc..3af8733d5 100644 --- a/Sources/OpenAPIKit30/Response/Response.swift +++ b/Sources/OpenAPIKit30/Response/Response.swift @@ -10,8 +10,8 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Response Object" /// - /// See [OpenAPI Response Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#response-object). - public struct Response: Equatable, CodableVendorExtendable { + /// See [OpenAPI Response Object](https://spec.openapis.org/oas/v3.0.4.html#response-object). + public struct Response: Equatable, CodableVendorExtendable, Sendable { public var description: String public var headers: Header.Map? /// An empty Content map will be omitted from encoding. @@ -158,7 +158,9 @@ extension OpenAPI.Response: Encodable { try container.encode(links, forKey: .links) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -187,30 +189,4 @@ extension OpenAPI.Response: Decodable { } } -extension OpenAPI.Response.StatusCode: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(self.rawValue) - } -} - -extension OpenAPI.Response.StatusCode: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let strVal = try container.decode(String.self) - let val = OpenAPI.Response.StatusCode(rawValue: strVal) - - guard let value = val else { - throw InconsistencyError( - subjectName: "status code", - details: "Expected the status code to be either an Int, a range like '1XX', or 'default' but found \(strVal) instead", - codingPath: decoder.codingPath - ) - } - - self = value - } -} - extension OpenAPI.Response: Validatable {} diff --git a/Sources/OpenAPIKit30/RuntimeExpression.swift b/Sources/OpenAPIKit30/RuntimeExpression.swift index 960e01ffc..7e6c1a79e 100644 --- a/Sources/OpenAPIKit30/RuntimeExpression.swift +++ b/Sources/OpenAPIKit30/RuntimeExpression.swift @@ -10,9 +10,9 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Runtime Expression" /// - /// See [OpenAPI Runtime Expression[(https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#runtime-expressions). + /// See [OpenAPI Runtime Expression[(https://spec.openapis.org/oas/v3.0.4.html#runtime-expressions). /// - public enum RuntimeExpression: RawRepresentable, Equatable { + public enum RuntimeExpression: RawRepresentable, Equatable, Sendable { case url case method case statusCode @@ -74,7 +74,7 @@ extension OpenAPI { return nil } - public enum Source: RawRepresentable, Equatable { + public enum Source: RawRepresentable, Equatable, Sendable { /// A reference to one of the header parameters. case header(name: String) /// A reference to one of the query parameters. diff --git a/Sources/OpenAPIKit30/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit30/Schema Object/DereferencedJSONSchema.swift index 07af9bfb1..496ec328e 100644 --- a/Sources/OpenAPIKit30/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit30/Schema Object/DereferencedJSONSchema.swift @@ -162,7 +162,7 @@ extension DereferencedJSONSchema { } /// The context that only applies to `.array` schemas. - public struct ArrayContext: Equatable { + public struct ArrayContext: Equatable, Sendable { /// A JSON Type Node that describes /// the type of each element in the array. public let items: DereferencedJSONSchema? @@ -230,7 +230,7 @@ extension DereferencedJSONSchema { } /// The context that only applies to `.object` schemas. - public struct ObjectContext: Equatable { + public struct ObjectContext: Equatable, Sendable { public let maxProperties: Int? let _minProperties: Int? public let properties: OrderedDictionary @@ -408,3 +408,123 @@ extension JSONSchema: LocallyDereferenceable { return try? dereferenced(in: .noComponents) } } + +extension JSONSchema: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + let newSchema: JSONSchema + let newComponents: OpenAPI.Components + let newMessages: [Loader.Message] + + switch value { + case .boolean(_): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .number(_, _): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .integer(_, _): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .string(_, _): + newComponents = .noComponents + newSchema = self + newMessages = [] + case .object(let core, let object): + var components = OpenAPI.Components() + var messages = [Loader.Message]() + + let (newProperties, c1, m1) = try await object.properties.externallyDereferenced(with: loader) + try components.merge(c1) + messages += m1 + + let newAdditionalProperties: Either? + if case .b(let schema) = object.additionalProperties { + let (additionalProperties, c2, m2) = try await schema.externallyDereferenced(with: loader) + try components.merge(c2) + messages += m2 + newAdditionalProperties = .b(additionalProperties) + } else { + newAdditionalProperties = object.additionalProperties + } + newComponents = components + newMessages = messages + newSchema = .init( + schema: .object( + core, + .init( + properties: newProperties, + additionalProperties: newAdditionalProperties, + maxProperties: object.maxProperties, + minProperties: object._minProperties + ) + ), + vendorExtensions: vendorExtensions + ) + case .array(let core, let array): + let (newItems, components, messages) = try await array.items.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .array( + core, + .init( + items: newItems, + maxItems: array.maxItems, + minItems: array._minItems, + uniqueItems: array._uniqueItems + ) + ), + vendorExtensions: vendorExtensions + ) + case .all(let schema, let core): + let (newSubschemas, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .all(of: newSubschemas, core: core), + vendorExtensions: vendorExtensions + ) + case .one(let schema, let core): + let (newSubschemas, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .one(of: newSubschemas, core: core), + vendorExtensions: vendorExtensions + ) + case .any(let schema, let core): + let (newSubschemas, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .any(of: newSubschemas, core: core), + vendorExtensions: vendorExtensions + ) + case .not(let schema, let core): + let (newSubschema, components, messages) = try await schema.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .not(newSubschema, core: core), + vendorExtensions: vendorExtensions + ) + case .reference(let reference, let core): + let (newReference, components, messages) = try await reference.externallyDereferenced(with: loader) + newComponents = components + newMessages = messages + newSchema = .init( + schema: .reference(newReference, core), + vendorExtensions: vendorExtensions + ) + case .fragment(_): + newComponents = .noComponents + newSchema = self + newMessages = [] + } + + return (newSchema, newComponents, newMessages) + } +} diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift index 79adb97c1..dd213437a 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift @@ -62,7 +62,7 @@ public func ~=(lhs: JSONSchemaResolutionError, rhs: JSONSchemaResolutionError) - /// I expect this to be an area where I may want to make fixes and add /// errors without breaknig changes, so this annoying workaround for /// the absense of a "non-frozen" enum is a must. -internal enum _JSONSchemaResolutionError: CustomStringConvertible, Equatable { +internal enum _JSONSchemaResolutionError: CustomStringConvertible, Equatable, Sendable { case unsupported(because: String) case typeConflict(original: JSONType, new: JSONType) case formatConflict(original: String, new: String) @@ -578,7 +578,7 @@ extension JSONSchema.CoreContext { extension JSONSchema.IntegerContext { internal func validatedContext() throws -> JSONSchema.IntegerContext { let validatedMinimum: Bound? - if let minimum = minimum { + if let minimum { guard minimum.value >= 0 else { throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(minimum.value) cannot be below 0")) } @@ -603,7 +603,7 @@ extension JSONSchema.IntegerContext { extension JSONSchema.NumericContext { internal func validatedContext() throws -> JSONSchema.NumericContext { let validatedMinimum: Bound? - if let minimum = minimum { + if let minimum { guard minimum.value >= 0 else { throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(minimum.value) cannot be below 0")) } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift index 514bf3659..d53a086ad 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchema.swift @@ -9,12 +9,12 @@ import OpenAPIKitCore /// OpenAPI "Schema Object" /// -/// See [OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schema-object). -public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable { +/// See [OpenAPI Schema Object](https://spec.openapis.org/oas/v3.0.4.html#schema-object). +public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable, Sendable { public let warnings: [OpenAPI.Warning] public let value: Schema - public let vendorExtensions: [String: AnyCodable] + public var vendorExtensions: [String: AnyCodable] internal init(warnings: [OpenAPI.Warning], schema: Schema, vendorExtensions: [String: AnyCodable]) { self.warnings = warnings @@ -74,7 +74,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable { .init(schema: .fragment(core)) } - public enum Schema: Equatable { + public enum Schema: Equatable, Sendable { case boolean(CoreContext) case number(CoreContext, NumericContext) case integer(CoreContext, IntegerContext) @@ -264,7 +264,8 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable { extension JSONSchema: Equatable { public static func == (lhs: JSONSchema, rhs: JSONSchema) -> Bool { - lhs.value == rhs.value + lhs.value == rhs.value && + lhs.vendorExtensions == rhs.vendorExtensions } } @@ -1718,7 +1719,7 @@ extension JSONSchema: Encodable { // Ad-hoc vendor extension encoding because keys are done differently for // JSONSchema - guard VendorExtensionsConfiguration.isEnabled else { + guard VendorExtensionsConfiguration.isEnabled(for: encoder) else { return } var container = encoder.container(keyedBy: VendorExtensionKeys.self) @@ -1831,7 +1832,7 @@ extension JSONSchema: Decodable { ) } - if let typeHint = typeHint { + if let typeHint { let keysFromElsewhere = keysFrom.filter({ $0 != typeHint.group }) if !keysFromElsewhere.isEmpty { _warnings.append( @@ -1889,7 +1890,7 @@ extension JSONSchema: Decodable { self.warnings = _warnings // Ad-hoc vendor extension support since JSONSchema does coding keys differently. - guard VendorExtensionsConfiguration.isEnabled else { + guard VendorExtensionsConfiguration.isEnabled(for: decoder) else { self.vendorExtensions = [:] return } @@ -1900,7 +1901,7 @@ extension JSONSchema: Decodable { throw VendorExtensionDecodingError.selfIsArrayNotDict } - guard let decodedAny = decoded as? [String: Any] else { + guard let decodedAny = decoded as? [String: any Sendable] else { throw VendorExtensionDecodingError.foundNonStringKeys } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift index 07a12b1f9..c41485aea 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift @@ -12,7 +12,7 @@ import OpenAPIKitCore /// A schema context stores information about a schema. /// All schemas can have the contextual information in /// this protocol. -public protocol JSONSchemaContext { +public protocol JSONSchemaContext: Sendable { /// The format of the schema as a string value. /// /// This can be set even when a schema type has @@ -60,7 +60,7 @@ public protocol JSONSchemaContext { /// be placed on a parent object (one level up from an `allOf`, `anyOf`, /// or `oneOf`) as a way to reduce redundancy. /// - /// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). + /// See [OpenAPI Discriminator Object](https://spec.openapis.org/oas/v3.0.4.html#discriminator-object). var discriminator: OpenAPI.Discriminator? { get } /// Get the external docs, if specified. If unspecified, returns `nil`. @@ -403,8 +403,8 @@ extension JSONSchema { /// `IntegerContext` _can_ be asked for the /// `NumericContext` that would describe it via its /// `numericContext` property. - public struct NumericContext: Equatable { - public struct Bound: Equatable { + public struct NumericContext: Equatable, Sendable { + public struct Bound: Equatable, Sendable { public let value: Double public let exclusive: Bool @@ -439,8 +439,8 @@ extension JSONSchema { } /// The context that only applies to `.integer` schemas. - public struct IntegerContext: Equatable { - public struct Bound: Equatable { + public struct IntegerContext: Equatable, Sendable { + public struct Bound: Equatable, Sendable { public let value: Int public let exclusive: Bool @@ -525,7 +525,7 @@ extension JSONSchema { } /// The context that only applies to `.array` schemas. - public struct ArrayContext: Equatable { + public struct ArrayContext: Equatable, Sendable { /// A JSON Type Node that describes /// the type of each element in the array. public let items: JSONSchema? @@ -558,7 +558,7 @@ extension JSONSchema { } /// The context that only applies to `.object` schemas. - public struct ObjectContext: Equatable { + public struct ObjectContext: Equatable, Sendable { /// The maximum number of properties the object /// is allowed to have. public let maxProperties: Int? @@ -625,7 +625,7 @@ extension JSONSchema { } /// The context that only applies to `.string` schemas. - public struct StringContext: Equatable { + public struct StringContext: Equatable, Sendable { public let maxLength: Int? let _minLength: Int? @@ -653,7 +653,7 @@ extension JSONSchema { } /// The context that only applies to `.reference` schemas. - public struct ReferenceContext: Equatable { + public struct ReferenceContext: Equatable, Sendable { public let required: Bool public init(required: Bool = true) { diff --git a/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift b/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift index 1077135ac..e01b3e9f3 100644 --- a/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift +++ b/Sources/OpenAPIKit30/Schema Object/TypesAndFormats.swift @@ -18,7 +18,7 @@ public protocol SwiftTyped { /// The raw types supported by JSON Schema. /// -/// These are the OpenAPI [data types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#data-types) +/// These are the OpenAPI [data types](https://spec.openapis.org/oas/v3.0.4.html#data-types) /// and additionally the `object` and `array` /// "compound" data types. /// - boolean @@ -27,7 +27,7 @@ public protocol SwiftTyped { /// - number /// - integer /// - string -public enum JSONType: String, Codable { +public enum JSONType: String, Codable, Sendable { case boolean = "boolean" case object = "object" case array = "array" @@ -53,8 +53,8 @@ public enum JSONType: String, Codable { /// /// You can also find information on types and /// formats in the OpenAPI Specification's -/// section on [data types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#data-types). -public enum JSONTypeFormat: Equatable { +/// section on [data types](https://spec.openapis.org/oas/v3.0.4.html#data-types). +public enum JSONTypeFormat: Equatable, Sendable { case boolean(BooleanFormat) case object(ObjectFormat) case array(ArrayFormat) @@ -107,9 +107,9 @@ public enum JSONTypeFormat: Equatable { /// adheres to the [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14) /// specification for a "date-time." /// -/// See "formats" under the OpenAPI [data type](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#data-types) +/// See "formats" under the OpenAPI [data type](https://spec.openapis.org/oas/v3.0.4.html#data-types) /// documentation. -public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, Validatable where RawValue == String { +public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable, RawRepresentable, Validatable, Sendable where RawValue == String { static var unspecified: Self { get } var jsonType: JSONType { get } diff --git a/Sources/OpenAPIKit30/Security/DereferencedSecurityRequirement.swift b/Sources/OpenAPIKit30/Security/DereferencedSecurityRequirement.swift index 7d46b2fe6..20fd0ead3 100644 --- a/Sources/OpenAPIKit30/Security/DereferencedSecurityRequirement.swift +++ b/Sources/OpenAPIKit30/Security/DereferencedSecurityRequirement.swift @@ -63,7 +63,7 @@ public struct DereferencedSecurityRequirement: Equatable { /// not require a specified scope. For other security scheme types, /// the array MUST be empty. /// - /// See [Security Requirement Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#security-requirement-object) for more. + /// See [Security Requirement Object](https://spec.openapis.org/oas/v3.0.4.html#security-requirement-object) for more. public let requiredScopes: [String] } } diff --git a/Sources/OpenAPIKit30/Security/SecurityScheme.swift b/Sources/OpenAPIKit30/Security/SecurityScheme.swift index 516471f21..86c76e019 100644 --- a/Sources/OpenAPIKit30/Security/SecurityScheme.swift +++ b/Sources/OpenAPIKit30/Security/SecurityScheme.swift @@ -11,8 +11,8 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Security Scheme Object" /// - /// See [OpenAPI Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#security-scheme-object). - public struct SecurityScheme: Equatable, CodableVendorExtendable { + /// See [OpenAPI Security Scheme Object](https://spec.openapis.org/oas/v3.0.4.html#security-scheme-object). + public struct SecurityScheme: Equatable, CodableVendorExtendable, Sendable { public var type: SecurityType public var description: String? @@ -49,7 +49,7 @@ extension OpenAPI { return .init(type: .openIdConnect(openIdConnectUrl: url), description: description) } - public enum SecurityType: Equatable { + public enum SecurityType: Equatable, Sendable { case apiKey(name: String, location: Location) case http(scheme: String, bearerFormat: String?) case oauth2(flows: OAuthFlows) @@ -104,7 +104,9 @@ extension OpenAPI.SecurityScheme: Encodable { try container.encode(flows, forKey: .flows) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -244,11 +246,17 @@ extension OpenAPI.SecurityScheme: LocallyDereferenceable { dereferencedFromComponentNamed name: String? ) throws -> OpenAPI.SecurityScheme { var ret = self - if let name = name { + if let name { ret.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) } return ret } } +extension OpenAPI.SecurityScheme: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} + extension OpenAPI.SecurityScheme.SecurityType.Name: Validatable {} diff --git a/Sources/OpenAPIKit30/Server.swift b/Sources/OpenAPIKit30/Server.swift index f0073f827..8d1ed69d8 100644 --- a/Sources/OpenAPIKit30/Server.swift +++ b/Sources/OpenAPIKit30/Server.swift @@ -11,9 +11,9 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "Server Object" /// - /// See [OpenAPI Server Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#server-object). + /// See [OpenAPI Server Object](https://spec.openapis.org/oas/v3.0.4.html#server-object). /// - public struct Server: Equatable, CodableVendorExtendable { + public struct Server: Equatable, CodableVendorExtendable, Sendable { /// OpenAPI Server URLs can have variable placeholders in them. /// The `urlTemplate` can be asked for a well-formed Foundation /// `URL` if all variables in it have been replaced by constant values. @@ -63,9 +63,9 @@ extension OpenAPI { extension OpenAPI.Server { /// OpenAPI Spec "Server Variable Object" /// - /// See [OpenAPI Server Variable Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#server-variable-object). + /// See [OpenAPI Server Variable Object](https://spec.openapis.org/oas/v3.0.4.html#server-variable-object). /// - public struct Variable: Equatable, CodableVendorExtendable { + public struct Variable: Equatable, CodableVendorExtendable, Sendable { public var `enum`: [String] public var `default`: String public var description: String? @@ -104,7 +104,9 @@ extension OpenAPI.Server: Encodable { try container.encode(variables, forKey: .variables) } - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -180,7 +182,9 @@ extension OpenAPI.Server.Variable: Encodable { try container.encodeIfPresent(description, forKey: .description) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } @@ -245,5 +249,11 @@ extension OpenAPI.Server.Variable { } } +extension OpenAPI.Server: ExternallyDereferenceable { + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + return (self, .init(), []) + } +} + extension OpenAPI.Server: Validatable {} extension OpenAPI.Server.Variable: Validatable {} diff --git a/Sources/OpenAPIKit30/Tag.swift b/Sources/OpenAPIKit30/Tag.swift index 5669c977c..ba5c103e6 100644 --- a/Sources/OpenAPIKit30/Tag.swift +++ b/Sources/OpenAPIKit30/Tag.swift @@ -10,7 +10,7 @@ import OpenAPIKitCore extension OpenAPI { /// OpenAPI Spec "Tag Object" /// - /// See [OpenAPI Tag Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#tag-object). + /// See [OpenAPI Tag Object](https://spec.openapis.org/oas/v3.0.4.html#tag-object). public struct Tag: Equatable, CodableVendorExtendable { public let name: String public let description: String? @@ -55,7 +55,9 @@ extension OpenAPI.Tag: Encodable { try container.encodeIfPresent(externalDocs, forKey: .externalDocs) - try encodeExtensions(to: &container) + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Sources/OpenAPIKit30/Utility/Array+ExternallyDereferenceable.swift b/Sources/OpenAPIKit30/Utility/Array+ExternallyDereferenceable.swift new file mode 100644 index 000000000..ca1f3dd54 --- /dev/null +++ b/Sources/OpenAPIKit30/Utility/Array+ExternallyDereferenceable.swift @@ -0,0 +1,32 @@ +// +// Array+ExternallyDereferenceable.swift +// + +import OpenAPIKitCore + +extension Array where Element: ExternallyDereferenceable & Sendable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + try await withThrowingTaskGroup(of: (Int, (Element, OpenAPI.Components, [Loader.Message])).self) { group in + for (idx, elem) in zip(self.indices, self) { + group.addTask { + return try await (idx, elem.externallyDereferenced(with: loader)) + } + } + + var newElems = Array<(Int, Element)>() + var newComponents = OpenAPI.Components() + var newMessages = [Loader.Message]() + + for try await (idx, (elem, components, messages)) in group { + newElems.append((idx, elem)) + try newComponents.merge(components) + newMessages += messages + } + // things may come in out of order because of concurrency + // so we reorder after completing all entries. + newElems.sort { left, right in left.0 < right.0 } + return (newElems.map { $0.1 }, newComponents, newMessages) + } + } +} diff --git a/Sources/OpenAPIKit30/Utility/Container+DecodeURLAsString.swift b/Sources/OpenAPIKit30/Utility/Container+DecodeURLAsString.swift index ae5fd9fb5..71e3f36a3 100644 --- a/Sources/OpenAPIKit30/Utility/Container+DecodeURLAsString.swift +++ b/Sources/OpenAPIKit30/Utility/Container+DecodeURLAsString.swift @@ -11,19 +11,19 @@ import Foundation extension KeyedDecodingContainerProtocol { internal func decodeURLAsString(forKey key: Self.Key) throws -> URL { let string = try decode(String.self, forKey: key) - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: string, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: string, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: string, encodingInvalidCharacters: false) + url = URL(string: string, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: string) + url = URL(string: string) } -#else - urlCandidate = URL(string: string) -#endif - guard let url = urlCandidate else { + #else + url = URL(string: string) + #endif + guard let url else { throw InconsistencyError( subjectName: key.stringValue, details: "If specified, must be a valid URL", @@ -38,19 +38,19 @@ extension KeyedDecodingContainerProtocol { return nil } - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: string, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: string, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: string, encodingInvalidCharacters: false) + url = URL(string: string, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: string) + url = URL(string: string) } -#else - urlCandidate = URL(string: string) -#endif - guard let url = urlCandidate else { + #else + url = URL(string: string) + #endif + guard let url else { throw InconsistencyError( subjectName: key.stringValue, details: "If specified, must be a valid URL", diff --git a/Sources/OpenAPIKit30/Utility/Dictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit30/Utility/Dictionary+ExternallyDereferenceable.swift new file mode 100644 index 000000000..16ddfcd8d --- /dev/null +++ b/Sources/OpenAPIKit30/Utility/Dictionary+ExternallyDereferenceable.swift @@ -0,0 +1,31 @@ +// +// Dictionary+ExternallyDereferenceable.swift +// OpenAPI +// + +import OpenAPIKitCore + +extension Dictionary where Key: Sendable, Value: ExternallyDereferenceable & Sendable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components, [Loader.Message]).self) { group in + for (key, value) in self { + group.addTask { + let (newRef, components, messages) = try await value.externallyDereferenced(with: loader) + return (key, newRef, components, messages) + } + } + + var newDict = Self() + var newComponents = OpenAPI.Components() + var newMessages = [Loader.Message]() + + for try await (key, newRef, components, messages) in group { + newDict[key] = newRef + try newComponents.merge(components) + newMessages += messages + } + return (newDict, newComponents, newMessages) + } + } +} diff --git a/Sources/OpenAPIKit30/Utility/Optional+ExternallyDereferenceable.swift b/Sources/OpenAPIKit30/Utility/Optional+ExternallyDereferenceable.swift new file mode 100644 index 000000000..87c7a0649 --- /dev/null +++ b/Sources/OpenAPIKit30/Utility/Optional+ExternallyDereferenceable.swift @@ -0,0 +1,13 @@ +// +// Optional+ExternallyDereferenceable.swift +// + +import OpenAPIKitCore + +extension Optional where Wrapped: ExternallyDereferenceable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + guard let wrapped = self else { return (nil, .init(), []) } + return try await wrapped.externallyDereferenced(with: loader) + } +} diff --git a/Sources/OpenAPIKit30/Utility/OrderedDictionary+ExternallyDereferenceable.swift b/Sources/OpenAPIKit30/Utility/OrderedDictionary+ExternallyDereferenceable.swift new file mode 100644 index 000000000..66e78ab4f --- /dev/null +++ b/Sources/OpenAPIKit30/Utility/OrderedDictionary+ExternallyDereferenceable.swift @@ -0,0 +1,36 @@ +// +// OrderedDictionary+ExternallyDereferenceable.swift +// OpenAPI +// +// Created by Mathew Polzin on 08/05/2023. +// + +import OpenAPIKitCore + +extension OrderedDictionary where Key: Sendable, Value: ExternallyDereferenceable & Sendable { + + public func externallyDereferenced(with loader: Loader.Type) async throws -> (Self, OpenAPI.Components, [Loader.Message]) { + try await withThrowingTaskGroup(of: (Key, Value, OpenAPI.Components, [Loader.Message]).self) { group in + for (key, value) in self { + group.addTask { + let (newRef, components, messages) = try await value.externallyDereferenced(with: loader) + return (key, newRef, components, messages) + } + } + + var newDict = Self() + var newComponents = OpenAPI.Components() + var newMessages = [Loader.Message]() + + for try await (key, newRef, components, messages) in group { + newDict[key] = newRef + try newComponents.merge(components) + newMessages += messages + } + // things may come in out of order because of concurrency + // so we reorder after completing all entries. + try newDict.applyOrder(self) + return (newDict, newComponents, newMessages) + } + } +} diff --git a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift index 414d26d24..67151ae52 100644 --- a/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift +++ b/Sources/OpenAPIKit30/Validator/Validation+Builtins.swift @@ -14,7 +14,7 @@ extension Validation { /// `PathItem.Map`. /// /// The OpenAPI Specifcation does not require that the document - /// contain any paths for [security reasons](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#security-filtering) + /// contain any paths for [security reasons](https://spec.openapis.org/oas/v3.0.4.html#security-filtering) /// but documentation that is public in nature might only ever have /// an empty `PathItem.Map` in error. /// @@ -30,7 +30,7 @@ extension Validation { /// one operation. /// /// The OpenAPI Specifcation does not require that path items - /// contain any operations for [security reasons](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#security-filtering) + /// contain any operations for [security reasons](https://spec.openapis.org/oas/v3.0.4.html#security-filtering) /// but documentation that is public in nature might only ever have /// a `PathItem` with no operations in error. /// @@ -160,7 +160,7 @@ extension Validation { /// one response. /// /// The OpenAPI Specifcation requires that Responses Objects - /// contain [at least one response](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object). + /// contain [at least one response](https://spec.openapis.org/oas/v3.0.4.html#responses-object). /// The specification recommends that if there is only one response then /// it be a successful response. /// @@ -178,7 +178,7 @@ extension Validation { /// Validate that the OpenAPI Document's `Tags` all have unique names. /// /// The OpenAPI Specifcation requires that tag names on the Document - /// [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#openapi-object). + /// [are unique](https://spec.openapis.org/oas/v3.0.4.html#openapi-object). /// /// - Important: This is included in validation by default. public static var documentTagNamesAreUnique: Validation { @@ -198,7 +198,7 @@ extension Validation { /// A Path Item Parameter's identity is defined as the pairing of its `name` and /// `location`. /// - /// The OpenAPI Specification requires that these parameters [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object). + /// The OpenAPI Specification requires that these parameters [are unique](https://spec.openapis.org/oas/v3.0.4.html#path-item-object). /// /// - Important: This is included in validation by default. /// @@ -216,7 +216,7 @@ extension Validation { /// An Operation's Parameter's identity is defined as the pairing of its `name` and /// `location`. /// - /// The OpenAPI Specification requires that these parameters [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#operation-object). + /// The OpenAPI Specification requires that these parameters [are unique](https://spec.openapis.org/oas/v3.0.4.html#operation-object). /// /// - Important: This is included in validation by default. /// @@ -230,7 +230,7 @@ extension Validation { /// Validate that all OpenAPI Operation Ids are unique across the whole Document. /// - /// The OpenAPI Specification requires that Operation Ids [are unique](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#operation-object). + /// The OpenAPI Specification requires that Operation Ids [are unique](https://spec.openapis.org/oas/v3.0.4.html#operation-object). /// /// - Important: This is included in validation by default. /// diff --git a/Sources/OpenAPIKit30/XML.swift b/Sources/OpenAPIKit30/XML.swift index 6ee059b49..f7488c309 100644 --- a/Sources/OpenAPIKit30/XML.swift +++ b/Sources/OpenAPIKit30/XML.swift @@ -11,7 +11,7 @@ import Foundation extension OpenAPI { /// OpenAPI Spec "XML Object" /// - /// See [OpenAPI XML Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xml-object). + /// See [OpenAPI XML Object](https://spec.openapis.org/oas/v3.0.4.html#xml-object). public struct XML: Equatable { public let name: String? public let namespace: URL? diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index b963f6042..88192c352 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -20,7 +20,7 @@ private protocol To31 { } extension OpenAPIKit30.OpenAPI.Document { - fileprivate func to31(version: OpenAPIKit.OpenAPI.Document.Version = .v3_1_0) -> OpenAPIKit.OpenAPI.Document { + fileprivate func to31(version: OpenAPIKit.OpenAPI.Document.Version = .v3_1_1) -> OpenAPIKit.OpenAPI.Document { OpenAPIKit.OpenAPI.Document( openAPIVersion: version, info: info.to31(), @@ -169,7 +169,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { let newExamples = examples?.mapValues(eitherRefTo31) switch schema { case .a(let ref): - if let newExamples = newExamples { + if let newExamples { return OpenAPIKit.OpenAPI.Parameter.SchemaContext( schemaReference: .init(ref.to31()), style: style, @@ -187,7 +187,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { ) } case .b(let schema): - if let newExamples = newExamples { + if let newExamples { return OpenAPIKit.OpenAPI.Parameter.SchemaContext( schema.to31(), style: style, @@ -211,7 +211,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext: To31 { extension OpenAPIKit30.OpenAPI.Content.Encoding: To31 { fileprivate func to31() -> OpenAPIKit.OpenAPI.Content.Encoding { OpenAPIKit.OpenAPI.Content.Encoding( - contentType: contentType, + contentTypes: [contentType].compactMap { $0 }, headers: headers?.mapValues(eitherRefTo31), style: style, explode: explode, diff --git a/Sources/OpenAPIKitCore/AnyCodable/AnyCodable.swift b/Sources/OpenAPIKitCore/AnyCodable/AnyCodable.swift index f6e4050d1..32c8c018f 100644 --- a/Sources/OpenAPIKitCore/AnyCodable/AnyCodable.swift +++ b/Sources/OpenAPIKitCore/AnyCodable/AnyCodable.swift @@ -31,19 +31,53 @@ import Foundation You can encode or decode mixed-type values in dictionaries and other collections that require `Encodable` or `Decodable` conformance by declaring their contained type to be `AnyCodable`. + + Note that there are some caveats related to the fact that this type centers around + encoding/decoding values. For example, some technically distinct nil-like types + are all encoded as `nil` and compare equally under the `AnyCodable` type: + - `nil` + - `NSNull()` + - `Void()` */ -public struct AnyCodable { +public struct AnyCodable: @unchecked Sendable { + // IMPORTANT: + // We rely on the fact that AnyCodable can only be constructed with an initializer that knows + // the type of its argument to be Sendable in order to confidently state that AnyCodable itself + // is @unchecked Sendable. public let value: Any - public init(_ value: T?) { + public init(_ value: T?) { self.value = value ?? () } + + // Dangerous, but we use this below where we must transform AnyCodable by e.g. mapping on its value + fileprivate init(trusted value: Any) { + self.value = value + } +} + +protocol _Optional { + var isNil: Bool { get } +} + +extension Optional: _Optional { + var isNil: Bool { self == nil } +} + +extension NSNull: _Optional { + var isNil: Bool { true } } extension AnyCodable: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() + // special nil case + if let optionalValue = value as? _Optional, optionalValue.isNil { + try container.encodeNil() + return + } + switch value { #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case let number as NSNumber: @@ -83,10 +117,12 @@ extension AnyCodable: Encodable { try container.encode(date) case let url as URL: try container.encode(url) - case let array as [Any?]: + case let array as [(any Sendable)?]: try container.encode(array.map { AnyCodable($0) }) - case let dictionary as [String: Any?]: + case let dictionary as [String: (any Sendable)?]: try container.encode(dictionary.mapValues { AnyCodable($0) }) + case let encodableValue as Encodable: + try container.encode(encodableValue) default: let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyCodable value cannot be encoded") throw EncodingError.invalidValue(value, context) @@ -144,20 +180,38 @@ extension AnyCodable: Decodable { } else if let string = try? container.decode(String.self) { self.init(string) } else if let array = try? container.decode([AnyCodable].self) { - self.init(array.map { $0.value }) + self.init(trusted: array.map { $0.value }) } else if let dictionary = try? container.decode([String: AnyCodable].self) { - self.init(dictionary.mapValues { $0.value }) + self.init(trusted: dictionary.mapValues { $0.value }) } else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded") } } } +func isNilEquivalent(value: AnyCodable) -> Bool { + let valueIsNil: Bool + + if let optionalValue = value.value as? _Optional, + optionalValue.isNil { + valueIsNil = true + } else if let _ = value.value as? Void { + valueIsNil = true + } else { + valueIsNil = false + } + + return valueIsNil +} + extension AnyCodable: Equatable { public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { - switch (lhs.value, rhs.value) { - case is (Void, Void): + // special case for nil + if isNilEquivalent(value: lhs) && isNilEquivalent(value: rhs) { return true + } + + switch (lhs.value, rhs.value) { case let (lhs as Bool, rhs as Bool): return lhs == rhs case let (lhs as Int, rhs as Int): @@ -198,6 +252,8 @@ extension AnyCodable: Equatable { return lhs == rhs case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]): return lhs == rhs + case let (lhs as [String: Any], rhs as [String: Any]): + return lhs.mapValues(AnyCodable.init) == rhs.mapValues(AnyCodable.init) case let (lhs as [String], rhs as [String]): return lhs == rhs case let (lhs as [Int], rhs as [Int]): @@ -208,6 +264,23 @@ extension AnyCodable: Equatable { return lhs == rhs case let (lhs as [AnyCodable], rhs as [AnyCodable]): return lhs == rhs + case let (lhs as [Any], rhs as [Any]): + return lhs.map(AnyCodable.init) == rhs.map(AnyCodable.init) + case let (lhs as Encodable, rhs as Encodable): + let encoder = JSONEncoder() + let lhsData: Data + do { + lhsData = try encoder.encode(lhs) + } catch { + return false + } + let rhsData: Data + do { + rhsData = try encoder.encode(rhs) + } catch { + return false + } + return lhsData == rhsData default: return false } @@ -243,12 +316,10 @@ extension AnyCodable: ExpressibleByBooleanLiteral {} extension AnyCodable: ExpressibleByIntegerLiteral {} extension AnyCodable: ExpressibleByFloatLiteral {} extension AnyCodable: ExpressibleByStringLiteral {} -extension AnyCodable: ExpressibleByArrayLiteral {} -extension AnyCodable: ExpressibleByDictionaryLiteral {} extension AnyCodable { public init(nilLiteral _: ()) { - self.init(nil as Any?) + self.init(trusted: (nil as Any?) as Any) } public init(booleanLiteral value: Bool) { @@ -266,12 +337,4 @@ extension AnyCodable { public init(stringLiteral value: String) { self.init(value) } - - public init(arrayLiteral elements: Any...) { - self.init(elements) - } - - public init(dictionaryLiteral elements: (AnyHashable, Any)...) { - self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first })) - } } diff --git a/Sources/OpenAPIKitCore/Either/Either.swift b/Sources/OpenAPIKitCore/Either/Either.swift index 0e8450080..e5c24ad69 100644 --- a/Sources/OpenAPIKitCore/Either/Either.swift +++ b/Sources/OpenAPIKitCore/Either/Either.swift @@ -56,3 +56,5 @@ public enum Either { extension Either: Equatable where A: Equatable, B: Equatable {} +extension Either: Sendable where A: Sendable, B: Sendable {} + diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift index 6b748536e..9861837e7 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/OpenAPIDecodingErrors.swift @@ -37,7 +37,7 @@ public protocol PathContextError { var codingPath: [CodingKey] { get } } -public protocol OpenAPIError: Swift.Error, CustomStringConvertible, PathContextError { +public protocol OpenAPIError: Swift.Error, CustomStringConvertible, PathContextError, Sendable { /// The subject of the error (i.e. the thing being worked with /// when the error occurred). /// diff --git a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift index af8d921ba..776bf23fa 100644 --- a/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift +++ b/Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/RequestDecodingError.swift @@ -6,11 +6,11 @@ // extension Error.Decoding { - public struct Request: OpenAPIError { + public struct Request: OpenAPIError, Sendable { public let context: Context internal let relativeCodingPath: [CodingKey] - public enum Context { + public enum Context: Sendable { case inconsistency(InconsistencyError) case other(Swift.DecodingError) case neither(EitherDecodeNoTypesMatchedError) diff --git a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift index 8779f30ec..ffadaf393 100644 --- a/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OpenAPIKitCore/OrderedDictionary/OrderedDictionary.swift @@ -159,6 +159,30 @@ public struct OrderedDictionary: HasWarnings where Key: Hashable { } return ret } + + struct KeysDontMatch : Swift.Error {} + + /// Given two ordered dictionaries with the exact same keys, + /// apply the ordering of one to the other. This will throw if + /// the dictionary keys are not the same. + public mutating func applyOrder(_ other: Self) throws { + guard other.orderedKeys.count == orderedKeys.count, + other.orderedKeys.allSatisfy({ orderedKeys.contains($0) }) else { + throw KeysDontMatch() + } + + orderedKeys = other.orderedKeys + } + + public mutating func sortKeys(by sort: (Key, Key) throws -> Bool) rethrows { + try orderedKeys.sort(by: sort) + } +} + +extension OrderedDictionary where Key: Comparable { + public mutating func sortKeys() { + orderedKeys.sort() + } } // MARK: - Dictionary Literal @@ -205,6 +229,18 @@ extension OrderedDictionary: Collection { } } +extension OrderedDictionary { + public mutating func merge(_ other: OrderedDictionary, uniquingKeysWith resolve: (Value, Value) throws -> Value) rethrows { + for (key, value) in other { + if let conflict = self[key] { + self[key] = try resolve(conflict, value) + } else { + self[key] = value + } + } + } +} + // MARK: - Iterator extension OrderedDictionary { public struct Iterator: Sequence, IteratorProtocol { @@ -238,8 +274,11 @@ extension OrderedDictionary: Equatable where Value: Equatable { } } -// MARK: - Codable +// MARK: - Sendable +extension OrderedDictionary: Sendable where Key: Sendable, Value: Sendable {} + +// MARK: - Codable public struct AnyCodingKey: CodingKey { public let stringValue: String diff --git a/Sources/OpenAPIKitCore/Shared/CallbackURL.swift b/Sources/OpenAPIKitCore/Shared/CallbackURL.swift index 5e2d6295d..d828a4598 100644 --- a/Sources/OpenAPIKitCore/Shared/CallbackURL.swift +++ b/Sources/OpenAPIKitCore/Shared/CallbackURL.swift @@ -11,9 +11,9 @@ extension Shared { /// A URL template where the placeholders are OpenAPI **Runtime Expressions** instead /// of named variables. /// - /// See [OpenAPI Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#callback-object) and [OpenAPI Runtime Expression](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#runtime-expressions) for more. + /// See [OpenAPI Callback Object](https://spec.openapis.org/oas/v3.0.4.html#callback-object) and [OpenAPI Runtime Expression](https://spec.openapis.org/oas/v3.0.4.html#runtime-expressions) for more. /// - public struct CallbackURL: Hashable, RawRepresentable { + public struct CallbackURL: Hashable, RawRepresentable, Sendable { public let template: URLTemplate /// The string value of the URL without variable replacement. diff --git a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift index f8ff7f48e..454f3f1e5 100644 --- a/Sources/OpenAPIKitCore/Shared/ComponentKey.swift +++ b/Sources/OpenAPIKitCore/Shared/ComponentKey.swift @@ -12,7 +12,7 @@ extension Shared { /// /// These keys must match the regex /// `^[a-zA-Z0-9\.\-_]+$`. - public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider { + public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider, Sendable { public let rawValue: String public init(stringLiteral value: StringLiteralType) { @@ -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,23 @@ 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)" + } + } +} + +extension Shared.ComponentKey: Comparable { + public static func < (lhs: Shared.ComponentKey, rhs: Shared.ComponentKey) -> Bool { + lhs.rawValue < rhs.rawValue + } } diff --git a/Sources/OpenAPIKitCore/Shared/ContentType.swift b/Sources/OpenAPIKitCore/Shared/ContentType.swift index 839759aee..75fc456ce 100644 --- a/Sources/OpenAPIKitCore/Shared/ContentType.swift +++ b/Sources/OpenAPIKitCore/Shared/ContentType.swift @@ -7,7 +7,7 @@ extension Shared { /// The Content Type of an API request or response body. - public struct ContentType: Codable, Equatable, Hashable, RawRepresentable, HasWarnings { + public struct ContentType: Codable, Equatable, Hashable, RawRepresentable, HasWarnings, Sendable { internal let underlyingType: Builtin public let warnings: [Warning] diff --git a/Sources/OpenAPIKitCore/Shared/Discriminator.swift b/Sources/OpenAPIKitCore/Shared/Discriminator.swift index 78af32ca3..b1c1716bd 100644 --- a/Sources/OpenAPIKitCore/Shared/Discriminator.swift +++ b/Sources/OpenAPIKitCore/Shared/Discriminator.swift @@ -8,8 +8,8 @@ extension Shared { /// OpenAPI Spec "Disciminator Object" /// - /// See [OpenAPI Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminator-object). - public struct Discriminator: Equatable { + /// See [OpenAPI Discriminator Object](https://spec.openapis.org/oas/v3.0.4.html#discriminator-object). + public struct Discriminator: Equatable, Sendable { public let propertyName: String public let mapping: OrderedDictionary? diff --git a/Sources/OpenAPIKitCore/Shared/HttpMethod.swift b/Sources/OpenAPIKitCore/Shared/HttpMethod.swift index 316a15ddd..57481265e 100644 --- a/Sources/OpenAPIKitCore/Shared/HttpMethod.swift +++ b/Sources/OpenAPIKitCore/Shared/HttpMethod.swift @@ -9,10 +9,10 @@ extension Shared { /// Represents the HTTP methods supported by the /// OpenAPI Specification. /// - /// See [OpenAPI Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#path-item-object) because the supported + /// See [OpenAPI Path Item Object](https://spec.openapis.org/oas/v3.0.4.html#path-item-object) because the supported /// HTTP methods are enumerated as properties on that /// object. - public enum HttpMethod: String, CaseIterable { + public enum HttpMethod: String, CaseIterable, Sendable { case get = "GET" case post = "POST" case patch = "PATCH" diff --git a/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift b/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift index c6363d646..de0b674cc 100644 --- a/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift +++ b/Sources/OpenAPIKitCore/Shared/JSONSchemaPermissions.swift @@ -6,7 +6,7 @@ // extension Shared { - public enum JSONSchemaPermissions: String, Codable { + public enum JSONSchemaPermissions: String, Codable, Sendable { case readOnly case writeOnly case readWrite diff --git a/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift b/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift index fb5db6591..ce81677ca 100644 --- a/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift +++ b/Sources/OpenAPIKitCore/Shared/JSONTypeFormat.swift @@ -13,7 +13,7 @@ extension Shared { /// type, but it is still important to be able to specify a format without /// a type. This can come into play when writing fragments of schemas /// to be combined later. - public enum AnyFormat: RawRepresentable, Equatable { + public enum AnyFormat: RawRepresentable, Equatable, Sendable { case generic case other(String) @@ -40,7 +40,7 @@ extension Shared { } /// The allowed "format" properties for `.boolean` schemas. - public enum BooleanFormat: RawRepresentable, Equatable { + public enum BooleanFormat: RawRepresentable, Equatable, Sendable { case generic case other(String) @@ -67,7 +67,7 @@ extension Shared { } /// The allowed "format" properties for `.object` schemas. - public enum ObjectFormat: RawRepresentable, Equatable { + public enum ObjectFormat: RawRepresentable, Equatable, Sendable { case generic case other(String) @@ -94,7 +94,7 @@ extension Shared { } /// The allowed "format" properties for `.array` schemas. - public enum ArrayFormat: RawRepresentable, Equatable { + public enum ArrayFormat: RawRepresentable, Equatable, Sendable { case generic case other(String) @@ -121,7 +121,7 @@ extension Shared { } /// The allowed "format" properties for `.number` schemas. - public enum NumberFormat: RawRepresentable, Equatable { + public enum NumberFormat: RawRepresentable, Equatable, Sendable { case generic case float case double @@ -154,7 +154,7 @@ extension Shared { } /// The allowed "format" properties for `.integer` schemas. - public enum IntegerFormat: RawRepresentable, Equatable { + public enum IntegerFormat: RawRepresentable, Equatable, Sendable { case generic case int32 case int64 diff --git a/Sources/OpenAPIKitCore/Shared/OAuthFlows.swift b/Sources/OpenAPIKitCore/Shared/OAuthFlows.swift index 1a8253abe..7eaab4b05 100644 --- a/Sources/OpenAPIKitCore/Shared/OAuthFlows.swift +++ b/Sources/OpenAPIKitCore/Shared/OAuthFlows.swift @@ -10,8 +10,8 @@ import Foundation extension Shared { /// OpenAPI Spec "Oauth Flows Object" /// - /// See [OpenAPI Oauth Flows Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object). - public struct OAuthFlows: Equatable { + /// See [OpenAPI Oauth Flows Object](https://spec.openapis.org/oas/v3.0.4.html#oauth-flows-object). + public struct OAuthFlows: Equatable, Sendable { public let implicit: Implicit? public let password: Password? public let clientCredentials: ClientCredentials? @@ -35,13 +35,13 @@ extension Shared.OAuthFlows { public typealias Scope = String public typealias ScopeDescription = String - public struct CommonFields: Equatable { + public struct CommonFields: Equatable, Sendable { public let refreshUrl: URL? public let scopes: OrderedDictionary } @dynamicMemberLookup - public struct Implicit: Equatable { + public struct Implicit: Equatable, Sendable { private let common: CommonFields public let authorizationUrl: URL @@ -56,7 +56,7 @@ extension Shared.OAuthFlows { } @dynamicMemberLookup - public struct Password: Equatable { + public struct Password: Equatable, Sendable { private let common: CommonFields public let tokenUrl: URL @@ -71,7 +71,7 @@ extension Shared.OAuthFlows { } @dynamicMemberLookup - public struct ClientCredentials: Equatable { + public struct ClientCredentials: Equatable, Sendable { private let common: CommonFields public let tokenUrl: URL @@ -86,7 +86,7 @@ extension Shared.OAuthFlows { } @dynamicMemberLookup - public struct AuthorizationCode: Equatable { + public struct AuthorizationCode: Equatable, Sendable { private let common: CommonFields public let authorizationUrl: URL public let tokenUrl: URL diff --git a/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift b/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift index e28e9c5be..a3166d6dc 100644 --- a/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift +++ b/Sources/OpenAPIKitCore/Shared/ParameterSchemaContextStyle.swift @@ -6,7 +6,7 @@ // extension Shared { - public enum ParameterSchemaContextStyle: String, CaseIterable, Codable { + public enum ParameterSchemaContextStyle: String, CaseIterable, Codable, Sendable { case form case simple case matrix diff --git a/Sources/OpenAPIKitCore/Shared/Path.swift b/Sources/OpenAPIKitCore/Shared/Path.swift index 4389173e0..d3d84916f 100644 --- a/Sources/OpenAPIKitCore/Shared/Path.swift +++ b/Sources/OpenAPIKitCore/Shared/Path.swift @@ -8,9 +8,9 @@ extension Shared { /// OpenAPI Spec "Paths Object" path field pattern support. /// - /// See [OpenAPI Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#paths-object) - /// and [OpenAPI Patterned Fields](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#patterned-fields). - public struct Path: RawRepresentable, Equatable, Hashable { + /// See [OpenAPI Paths Object](https://spec.openapis.org/oas/v3.0.4.html#paths-object) + /// and [OpenAPI Patterned Fields](https://spec.openapis.org/oas/v3.0.4.html#patterned-fields). + public struct Path: RawRepresentable, Equatable, Hashable, Sendable { public let components: [String] public let trailingSlash: Bool @@ -43,3 +43,21 @@ extension Shared.Path: ExpressibleByStringLiteral { self.init(rawValue: value) } } + +extension Shared.Path: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(rawValue) + } +} + +extension Shared.Path: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let rawValue = try container.decode(String.self) + + self.init(rawValue: rawValue) + } +} diff --git a/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift b/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift index 9c69aeb9e..82df5365e 100644 --- a/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift +++ b/Sources/OpenAPIKitCore/Shared/ResponseStatusCode.swift @@ -8,7 +8,7 @@ extension Shared { /// An HTTP Status code or status code range. /// - /// OpenAPI supports one of the following as a key in the [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responses-object): + /// OpenAPI supports one of the following as a key in the [Responses Object](https://spec.openapis.org/oas/v3.0.4.html#responses-object): /// - A `default` entry. /// - A specific status code. /// - A status code range. @@ -18,7 +18,7 @@ extension Shared { /// You can use integer literals to specify an exact status code. /// /// Status code ranges are named in the `StatusCode.Range` enum. For example, the "1XX" range (100-199) can be written as either `.range(.information)` or as `.range(.init(rawValue: "1XX"))`. - public struct ResponseStatusCode: RawRepresentable, Equatable, Hashable, HasWarnings { + public struct ResponseStatusCode: RawRepresentable, Equatable, Hashable, HasWarnings, Sendable { public typealias RawValue = String public let warnings: [Warning] @@ -34,13 +34,13 @@ extension Shared { public static func range(_ range: Range) -> Self { .init(value: .range(range)) } public static func status(code: Int) -> Self { .init(value: .status(code: code)) } - public enum Code: Equatable, Hashable { + public enum Code: Equatable, Hashable, Sendable { case `default` case range(Range) case status(code: Int) } - public enum Range: String { + public enum Range: String, Sendable { /// Status Code `100-199` case information = "1XX" /// Status Code `200-299` @@ -106,3 +106,29 @@ extension Shared.ResponseStatusCode: ExpressibleByIntegerLiteral { warnings = [] } } + +extension Shared.ResponseStatusCode: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(self.rawValue) + } +} + +extension Shared.ResponseStatusCode: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let strVal = try container.decode(String.self) + let val = Shared.ResponseStatusCode(rawValue: strVal) + + guard let value = val else { + throw InconsistencyError( + subjectName: "status code", + details: "Expected the status code to be either an Int, a range like '1XX', or 'default' but found \(strVal) instead", + codingPath: decoder.codingPath + ) + } + + self = value + } +} diff --git a/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift b/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift index 4c24567b5..e230cea58 100644 --- a/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift +++ b/Sources/OpenAPIKitCore/Shared/SecurityScheme.swift @@ -6,7 +6,7 @@ // extension Shared { - public enum SecuritySchemeLocation: String, Codable, Equatable { + public enum SecuritySchemeLocation: String, Codable, Equatable, Sendable { case query case header case cookie diff --git a/Sources/OpenAPIKitCore/URLTemplate/URLTemplate.swift b/Sources/OpenAPIKitCore/URLTemplate/URLTemplate.swift index c9ceb6d4e..164549d3b 100644 --- a/Sources/OpenAPIKitCore/URLTemplate/URLTemplate.swift +++ b/Sources/OpenAPIKitCore/URLTemplate/URLTemplate.swift @@ -41,7 +41,7 @@ import Foundation /// into the template. You can also choose to only replace some of the variables this /// way. /// -public struct URLTemplate: Hashable, RawRepresentable { +public struct URLTemplate: Hashable, RawRepresentable, Sendable { /// The string value of the URL. /// @@ -74,19 +74,19 @@ public struct URLTemplate: Hashable, RawRepresentable { /// Templated URLs with variables in them will not be valid URLs /// and are therefore guaranteed to return `nil`. public var url: URL? { - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: rawValue, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: rawValue, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: rawValue, encodingInvalidCharacters: false) + url = URL(string: rawValue, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: rawValue) + url = URL(string: rawValue) } -#else - urlCandidate = URL(string: rawValue) -#endif - return urlCandidate + #else + url = URL(string: rawValue) + #endif + return url } /// Get the names of all variables in the URL Template. @@ -126,7 +126,7 @@ extension URLTemplate { /// URL Template components are either variables that can take on /// different values depending on the context or they are constant /// unchanging parts of the URL. - public enum Component: Hashable, RawRepresentable { + public enum Component: Hashable, RawRepresentable, Sendable { case variable(name: String) case constant(String) diff --git a/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift b/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift index a1402ddb8..db12e7ed1 100644 --- a/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift +++ b/Sources/OpenAPIKitCore/Utility/Container+DecodeURLAsString.swift @@ -10,19 +10,19 @@ import Foundation extension KeyedDecodingContainerProtocol { internal func decodeURLAsString(forKey key: Self.Key) throws -> URL { let string = try decode(String.self, forKey: key) - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: string, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: string, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: string, encodingInvalidCharacters: false) + url = URL(string: string, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: string) + url = URL(string: string) } -#else - urlCandidate = URL(string: string) -#endif - guard let url = urlCandidate else { + #else + url = URL(string: string) + #endif + guard let url else { throw InconsistencyError( subjectName: key.stringValue, details: "If specified, must be a valid URL", @@ -37,19 +37,19 @@ extension KeyedDecodingContainerProtocol { return nil } - let urlCandidate: URL? -#if canImport(FoundationEssentials) - urlCandidate = URL(string: string, encodingInvalidCharacters: false) -#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + let url: URL? + #if canImport(FoundationEssentials) + url = URL(string: string, encodingInvalidCharacters: false) + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - urlCandidate = URL(string: string, encodingInvalidCharacters: false) + url = URL(string: string, encodingInvalidCharacters: false) } else { - urlCandidate = URL(string: string) + url = URL(string: string) } -#else - urlCandidate = URL(string: string) -#endif - guard let url = urlCandidate else { + #else + url = URL(string: string) + #endif + guard let url else { throw InconsistencyError( subjectName: key.stringValue, details: "If specified, must be a valid URL", diff --git a/Tests/AnyCodableTests/AnyCodableTests.swift b/Tests/AnyCodableTests/AnyCodableTests.swift index 78831567f..3e2971c92 100644 --- a/Tests/AnyCodableTests/AnyCodableTests.swift +++ b/Tests/AnyCodableTests/AnyCodableTests.swift @@ -9,11 +9,17 @@ class AnyCodableTests: XCTestCase { let _: AnyCodable = 10 let _: AnyCodable = 3.4 let _: AnyCodable = "hello" - let _: AnyCodable = ["hi", "there"] - let _: AnyCodable = ["hi": "there"] + let _: AnyCodable = .init(["hi", "there"]) + let _: AnyCodable = .init(["hi": "there"]) } func testEquality() throws { + // nil, NSNull(), and Void() all encode as "null" and + // compare equally. + XCTAssertEqual(AnyCodable(nil), AnyCodable(nil)) + XCTAssertEqual(AnyCodable(nil), AnyCodable(NSNull())) + XCTAssertEqual(AnyCodable(nil), AnyCodable(())) + XCTAssertEqual(AnyCodable(()), AnyCodable(())) XCTAssertEqual(AnyCodable(true), AnyCodable(true)) XCTAssertEqual(AnyCodable(2), AnyCodable(2)) @@ -34,16 +40,100 @@ class AnyCodableTests: XCTestCase { XCTAssertEqual(AnyCodable([AnyCodable("hi"), AnyCodable("there")]), AnyCodable([AnyCodable("hi"), AnyCodable("there")])) XCTAssertEqual(AnyCodable(["hi":1]), AnyCodable(["hi":1])) XCTAssertEqual(AnyCodable(["hi":1.2]), AnyCodable(["hi":1.2])) + XCTAssertEqual(AnyCodable(["hi":true]), AnyCodable(["hi":true])) XCTAssertEqual(AnyCodable(["hi"]), AnyCodable(["hi"])) XCTAssertEqual(AnyCodable([1]), AnyCodable([1])) XCTAssertEqual(AnyCodable([1.2]), AnyCodable([1.2])) XCTAssertEqual(AnyCodable([true]), AnyCodable([true])) + // force the array of Any branch: + XCTAssertEqual(AnyCodable([StringThing(value: "hi")]), AnyCodable([StringThing(value: "hi")])) + + // force the dictionary of Any branch: + XCTAssertEqual(AnyCodable(["hi": StringThing(value: "hi")]), AnyCodable(["hi": StringThing(value: "hi")])) + XCTAssertNotEqual(AnyCodable(()), AnyCodable(true)) } + func testEqualityFromJSON() throws { + let json = """ + { + "boolean": true, + "integer": 1, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "null": null + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let anyCodable0 = try decoder.decode(AnyCodable.self, from: json) + let anyCodable1 = try decoder.decode(AnyCodable.self, from: json) + XCTAssertEqual(anyCodable0, anyCodable1) + } + + struct CustomEncodable: Encodable { + let value1: String + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("hi hi hi " + value1) + } + } + + func test_encodable() throws { + let value = CustomEncodable(value1: "hello") + let anyCodable = AnyCodable(value) + let thing = try JSONEncoder().encode(anyCodable) + XCTAssertEqual(String(data: thing, encoding: .utf8)!, "\"hi hi hi hello\"") + } + func testVoidDescription() { XCTAssertEqual(String(describing: AnyCodable(Void())), "nil") + XCTAssertEqual(AnyCodable(Void()).debugDescription, "AnyCodable(nil)") + } + + func test_encodedDecodedURL() throws { + let value = URL(string: "https://www.google.com") + let anyCodable = AnyCodable(value) + + // URL's absoluteString compares as equal to the wrapped any codable description. + XCTAssertEqual(value?.absoluteString, anyCodable.description) + + let encodedValue = try JSONEncoder().encode(value) + let encodedAnyCodable = try JSONEncoder().encode(anyCodable) + // the URL and the wrapped any codable encode as equals. + XCTAssertEqual(encodedValue, encodedAnyCodable) + + let decodedFromValue = try JSONDecoder().decode(AnyCodable.self, from: encodedValue) + // the URL decoded as any codable has the same description as the original any codable wrapper. + XCTAssertEqual(anyCodable.description, decodedFromValue.description) + + let decodedFromAnyCodable = try JSONDecoder().decode(AnyCodable.self, from: encodedAnyCodable) + // the decoded any codable has the same description as the original any codable wrapper. + XCTAssertEqual(anyCodable.description, decodedFromAnyCodable.description) + + func roundTripEqual(_ a: A, _ b: B) throws -> Bool { + let a = try JSONDecoder().decode(AnyCodable.self, + from: JSONEncoder().encode(a)) + let b = try JSONDecoder().decode(AnyCodable.self, + from: JSONEncoder().encode(b)) + return a == b + } + // if you encode/decode both, the URL and its AnyCodable wrapper are equal. + try XCTAssert(roundTripEqual(anyCodable, value)) + + func encodedEqual(_ a: A, _ b: B) throws -> Bool { + let a = try JSONEncoder().encode(a) + let b = try JSONEncoder().encode(b) + return a == b + } + // if you just compare the encoded data, the URL and its AnyCodable wrapper are equal. + try XCTAssert(encodedEqual(anyCodable, value)) } func testJSONDecoding() throws { @@ -58,7 +148,8 @@ class AnyCodableTests: XCTestCase { "a": "alpha", "b": "bravo", "c": "charlie" - } + }, + "null": null } """.data(using: .utf8)! @@ -71,6 +162,7 @@ class AnyCodableTests: XCTestCase { XCTAssertEqual(dictionary["string"]?.value as! String, "string") XCTAssertEqual(dictionary["array"]?.value as! [Int], [1, 2, 3]) XCTAssertEqual(dictionary["nested"]?.value as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) + XCTAssertEqual(dictionary["null"], AnyCodable(nil)) } func testJSONEncoding() throws { @@ -78,12 +170,15 @@ class AnyCodableTests: XCTestCase { "boolean": true, "integer": 1, "string": "string", - "array": [1, 2, 3], - "nested": [ + "array": .init([1, 2, 3]), + "nested": .init([ "a": "alpha", "b": "bravo", "c": "charlie", - ], + ]), + "null": nil, + "void": .init(Void()), + "nsnull": .init(NSNull()) ] let result = try testStringFromEncoding(of: dictionary) @@ -104,7 +199,10 @@ class AnyCodableTests: XCTestCase { "b" : "bravo", "c" : "charlie" }, - "string" : "string" + "nsnull" : null, + "null" : null, + "string" : "string", + "void" : null } """ ) @@ -153,6 +251,12 @@ class AnyCodableTests: XCTestCase { let string = String(data: data, encoding: .utf8) XCTAssertEqual(string, #"{"value":false}"#) + + let data2 = try JSONEncoder().encode(AnyCodable(false)) + + let string2 = String(data: data2, encoding: .utf8) + + XCTAssertEqual(string2, "false") } func test_encodeInt() throws { @@ -161,6 +265,12 @@ class AnyCodableTests: XCTestCase { let string = String(data: data, encoding: .utf8) XCTAssertEqual(string, #"{"value":2}"#) + + let data2 = try JSONEncoder().encode(AnyCodable(2)) + + let string2 = String(data: data2, encoding: .utf8) + + XCTAssertEqual(string2, "2") } func test_encodeString() throws { @@ -169,6 +279,12 @@ class AnyCodableTests: XCTestCase { let string = String(data: data, encoding: .utf8) XCTAssertEqual(string, #"{"value":"hi"}"#) + + let data2 = try JSONEncoder().encode(AnyCodable("hi")) + + let string2 = String(data: data2, encoding: .utf8) + + XCTAssertEqual(string2, #""hi""#) } func test_encodeURL() throws { @@ -180,6 +296,10 @@ class AnyCodableTests: XCTestCase { } } -fileprivate struct Wrapper: Codable { +fileprivate struct Wrapper: Codable, Equatable { let value: AnyCodable } + +fileprivate struct StringThing: Codable, Equatable { + let value: String +} diff --git a/Tests/OpenAPIKit30ErrorReportingTests/ComponentErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/ComponentErrorTests.swift index a33ed3ca0..53f247b55 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/ComponentErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/ComponentErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class ComponentErrorTests: XCTestCase { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/DocumentErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/DocumentErrorTests.swift index ef2b434b6..30cedeff6 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/DocumentErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/DocumentErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class DocumentErrorTests: XCTestCase { @@ -26,6 +26,7 @@ final class DocumentErrorTests: XCTestCase { let openAPIError = OpenAPI.Error(from: error) XCTAssertEqual(openAPIError.localizedDescription, "Expected to find `openapi` key in the root Document object but it is missing.") + XCTAssertEqual(openAPIError.localizedDescription, openAPIError.description) XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, []) } } diff --git a/Tests/OpenAPIKit30ErrorReportingTests/Helpers.swift b/Tests/OpenAPIKit30ErrorReportingTests/Helpers.swift index f067827ec..2a3cb4e13 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/Helpers.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/Helpers.swift @@ -6,6 +6,6 @@ // import Foundation -import Yams +@preconcurrency import Yams let testDecoder = YAMLDecoder() diff --git a/Tests/OpenAPIKit30ErrorReportingTests/JSONReferenceErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/JSONReferenceErrorTests.swift index a35f01cc4..e6acb4d05 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/JSONReferenceErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/JSONReferenceErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class JSONReferenceErrorTests: XCTestCase { func test_referenceFailedToParse() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/OperationErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/OperationErrorTests.swift index 805273faa..94f876bb6 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/OperationErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/OperationErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class OperationErrorTests: XCTestCase { func test_missingResponses() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift index c69c553cd..bba441e24 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/PathsErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class PathsErrorTests: XCTestCase { func test_missingPaths() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/RequestContentMapErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/RequestContentMapErrorTests.swift index c68ba6bf7..75ac121c0 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/RequestContentMapErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/RequestContentMapErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class RequestContentMapErrorTests: XCTestCase { /** diff --git a/Tests/OpenAPIKit30ErrorReportingTests/RequestContentSchemaErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/RequestContentSchemaErrorTests.swift index 45ec737d3..6aec769f8 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/RequestContentSchemaErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/RequestContentSchemaErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class RequestContentSchemaErrorTests: XCTestCase { func test_wrongTypeContentSchemaTypeProperty() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/RequestErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/RequestErrorTests.swift index 0289fc510..6cb3a10c4 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/RequestErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/RequestErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class RequestErrorTests: XCTestCase { func test_wrongTypeRequest() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/ResponseErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/ResponseErrorTests.swift index 59c33d57a..2d7552afd 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/ResponseErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/ResponseErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class ResponseErrorTests: XCTestCase { func test_headerWithContentAndSchema() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/SchemaErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/SchemaErrorTests.swift index 90f2b197d..3667ed4fc 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/SchemaErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/SchemaErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class SchemaErrorTests: XCTestCase { func test_nonIntegerMaximumForIntegerSchema() { diff --git a/Tests/OpenAPIKit30ErrorReportingTests/SecuritySchemeErrorTests.swift b/Tests/OpenAPIKit30ErrorReportingTests/SecuritySchemeErrorTests.swift index 59989c334..ab03977fc 100644 --- a/Tests/OpenAPIKit30ErrorReportingTests/SecuritySchemeErrorTests.swift +++ b/Tests/OpenAPIKit30ErrorReportingTests/SecuritySchemeErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class SecuritySchemeErrorTests: XCTestCase { func test_missingSecuritySchemeError() { diff --git a/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift index b0d0229f5..3a23f3c1a 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/GitHubAPITests.swift @@ -7,7 +7,7 @@ import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams import Foundation #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift index 6dd077c22..76ccce43e 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/GoogleBooksAPITests.swift @@ -7,7 +7,7 @@ import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams import Foundation #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift index d33d12aa8..c0ee2a65b 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/PetStoreAPITests.swift @@ -7,7 +7,7 @@ import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams import Foundation #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift b/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift index 1344f66b9..c4455dd69 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class SwaggerDocSamplesTests: XCTestCase { func test_allOfExample() throws { diff --git a/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift b/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift index 00e338422..1f717e23f 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/TomTomAPITests.swift @@ -7,7 +7,7 @@ import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams import Foundation #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/Tests/OpenAPIKit30Tests/ComponentsTests.swift b/Tests/OpenAPIKit30Tests/ComponentsTests.swift index 957ebcb2e..bbdd577a7 100644 --- a/Tests/OpenAPIKit30Tests/ComponentsTests.swift +++ b/Tests/OpenAPIKit30Tests/ComponentsTests.swift @@ -309,7 +309,7 @@ extension ComponentsTests { ) ] ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encoded = try orderUnstableTestStringFromEncoding(of: t1) @@ -504,7 +504,7 @@ extension ComponentsTests { ) ] ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKit30Tests/Content/ContentTests.swift b/Tests/OpenAPIKit30Tests/Content/ContentTests.swift index b6c92b549..5d5ee4671 100644 --- a/Tests/OpenAPIKit30Tests/Content/ContentTests.swift +++ b/Tests/OpenAPIKit30Tests/Content/ContentTests.swift @@ -205,7 +205,7 @@ extension ContentTests { func test_exampleAndSchemaContent_encode() { let content = OpenAPI.Content(schema: .init(.object(properties: ["hello": .string])), - example: [ "hello": "world" ]) + example: .init([ "hello": "world" ])) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) assertJSONEquivalent( @@ -260,7 +260,7 @@ extension ContentTests { func test_examplesAndSchemaContent_encode() { let content = OpenAPI.Content(schema: .init(.object(properties: ["hello": .string])), - examples: ["hello": .b(OpenAPI.Example(value: .init([ "hello": "world" ])))]) + examples: ["hello": .b(OpenAPI.Example(value: .init(.init([ "hello": "world" ]))))]) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) assertJSONEquivalent( @@ -405,7 +405,7 @@ extension ContentTests { func test_vendorExtensions_encode() { let content = OpenAPI.Content( schema: .init(.string), - vendorExtensions: [ "x-hello": [ "world": 123 ] ] + vendorExtensions: [ "x-hello": .init([ "world": 123 ]) ] ) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) @@ -428,7 +428,7 @@ extension ContentTests { func test_vendorExtensions_encode_fixKey() { let content = OpenAPI.Content( schema: .init(.string), - vendorExtensions: [ "hello": [ "world": 123 ] ] + vendorExtensions: [ "hello": .init([ "world": 123 ]) ] ) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) diff --git a/Tests/OpenAPIKit30Tests/Document/DereferencedDocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/DereferencedDocumentTests.swift index d46449530..a1ea8115e 100644 --- a/Tests/OpenAPIKit30Tests/Document/DereferencedDocumentTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/DereferencedDocumentTests.swift @@ -28,14 +28,17 @@ final class DereferencedDocumentTests: XCTestCase { servers: [.init(url: URL(string: "http://website.com")!)], paths: [ "/hello/world": .init( + servers: [.init(urlTemplate: URLTemplate(rawValue: "http://{domain}.com")!, variables: ["domain": .init(default: "other")])], get: .init( + operationId: "hi", responses: [ 200: .response(description: "success") ] ) ) ], - components: .noComponents + components: .noComponents, + tags: ["hi"] ).locallyDereferenced() XCTAssertEqual(t1.paths.count, 1) @@ -51,6 +54,13 @@ final class DereferencedDocumentTests: XCTestCase { t1.resolvedEndpointsByPath().keys, ["/hello/world"] ) + + XCTAssertEqual(t1.allOperationIds, ["hi"]) + XCTAssertEqual(t1.allServers, [ + .init(url: URL(string: "http://website.com")!), + .init(urlTemplate: URLTemplate(rawValue: "http://{domain}.com")!, variables: ["domain": .init(default: "other")]), + ]) + XCTAssertEqual(t1.allTags, ["hi"]) } func test_noSecurityReferencedResponseInPath() throws { diff --git a/Tests/OpenAPIKit30Tests/Document/DocumentInfoTests.swift b/Tests/OpenAPIKit30Tests/Document/DocumentInfoTests.swift index 4483be005..9533de090 100644 --- a/Tests/OpenAPIKit30Tests/Document/DocumentInfoTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/DocumentInfoTests.swift @@ -103,7 +103,7 @@ extension DocumentInfoTests { let license = OpenAPI.Document.Info.License( name: "MIT", url: URL(string: "http://website.com")!, - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedLicense = try orderUnstableTestStringFromEncoding(of: license) @@ -142,7 +142,7 @@ extension DocumentInfoTests { OpenAPI.Document.Info.License( name: "MIT", url: URL(string: "http://website.com")!, - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -240,7 +240,7 @@ extension DocumentInfoTests { func test_contact_vendorExtensions_encode() throws { let contact = OpenAPI.Document.Info.Contact( email: "email", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedContact = try orderUnstableTestStringFromEncoding(of: contact) @@ -276,7 +276,7 @@ extension DocumentInfoTests { contact, .init( email: "email", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -508,7 +508,7 @@ extension DocumentInfoTests { title: "title", license: .init(name: "license"), version: "1.0", - vendorExtensions: ["x-speacialFeature": ["hello", "world"]] + vendorExtensions: ["x-speacialFeature": .init(["hello", "world"])] ) let encodedInfo = try orderUnstableTestStringFromEncoding(of: info) @@ -554,7 +554,7 @@ extension DocumentInfoTests { title: "title", license: .init(name: "license"), version: "1.0", - vendorExtensions: ["x-speacialFeature": ["hello", "world"]] + vendorExtensions: ["x-speacialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift index 0d6d7b23d..53a890bd7 100644 --- a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift @@ -41,6 +41,45 @@ final class DocumentTests: XCTestCase { ) } + func test_initOASVersions() { + let t1 = OpenAPI.Document.Version.v3_0_0 + XCTAssertEqual(t1.rawValue, "3.0.0") + + let t2 = OpenAPI.Document.Version.v3_0_1 + XCTAssertEqual(t2.rawValue, "3.0.1") + + let t3 = OpenAPI.Document.Version.v3_0_2 + XCTAssertEqual(t3.rawValue, "3.0.2") + + let t4 = OpenAPI.Document.Version.v3_0_3 + XCTAssertEqual(t4.rawValue, "3.0.3") + + let t5 = OpenAPI.Document.Version.v3_0_4 + XCTAssertEqual(t5.rawValue, "3.0.4") + + let t6 = OpenAPI.Document.Version.v3_0_x(x: 8) + XCTAssertEqual(t6.rawValue, "3.0.8") + + let t7 = OpenAPI.Document.Version(rawValue: "3.0.0") + XCTAssertEqual(t7, .v3_0_0) + + let t8 = OpenAPI.Document.Version(rawValue: "3.0.1") + XCTAssertEqual(t8, .v3_0_1) + + let t9 = OpenAPI.Document.Version(rawValue: "3.0.2") + XCTAssertEqual(t9, .v3_0_2) + + let t10 = OpenAPI.Document.Version(rawValue: "3.0.3") + XCTAssertEqual(t10, .v3_0_3) + + let t11 = OpenAPI.Document.Version(rawValue: "3.0.4") + XCTAssertEqual(t11, .v3_0_4) + + // not a known version: + let t12 = OpenAPI.Document.Version(rawValue: "3.0.8") + XCTAssertNil(t12) + } + func test_getRoutes() { let pi1 = OpenAPI.PathItem( parameters: [], @@ -364,7 +403,7 @@ final class DocumentTests: XCTestCase { let docData = """ { - "openapi": "3.0.0", + "openapi": "3.0.4", "info": { "title": "test", "version": "1.0" @@ -409,7 +448,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { } @@ -426,7 +465,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { } @@ -472,6 +511,33 @@ extension DocumentTests { ) } + func test_specifyUknownOpenAPIVersion_encode() throws { + let document = OpenAPI.Document( + openAPIVersion: .v3_0_x(x: 9), + info: .init(title: "API", version: "1.0"), + servers: [], + paths: [:], + components: .noComponents + ) + let encodedDocument = try orderUnstableTestStringFromEncoding(of: document) + + assertJSONEquivalent( + encodedDocument, + """ + { + "info" : { + "title" : "API", + "version" : "1.0" + }, + "openapi" : "3.0.9", + "paths" : { + + } + } + """ + ) + } + func test_specifyOpenAPIVersion_decode() throws { let documentData = """ @@ -500,6 +566,23 @@ extension DocumentTests { ) } + func test_specifyUnknownOpenAPIVersion_decode() throws { + let documentData = + """ + { + "info" : { + "title" : "API", + "version" : "1.0" + }, + "openapi" : "3.0.9", + "paths" : { + + } + } + """.data(using: .utf8)! + XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.self, from: documentData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value 3.0.9.") } + } + func test_specifyServers_encode() throws { let document = OpenAPI.Document( info: .init(title: "API", version: "1.0"), @@ -517,7 +600,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -539,7 +622,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -580,7 +663,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { "\\/test" : { "summary" : "hi" @@ -599,7 +682,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { "\\/test" : { "summary" : "hi" @@ -649,7 +732,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -682,7 +765,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -729,7 +812,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -751,7 +834,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -797,7 +880,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { } @@ -817,7 +900,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { } @@ -844,7 +927,7 @@ extension DocumentTests { paths: [:], components: .noComponents, externalDocs: .init(url: URL(string: "http://google.com")!), - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedDocument = try orderUnstableTestStringFromEncoding(of: document) @@ -859,7 +942,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -883,7 +966,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.0.0", + "openapi" : "3.0.4", "paths" : { }, @@ -903,7 +986,7 @@ extension DocumentTests { paths: [:], components: .noComponents, externalDocs: .init(url: URL(string: "http://google.com")!), - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift new file mode 100644 index 000000000..00af97ac5 --- /dev/null +++ b/Tests/OpenAPIKit30Tests/Document/ExternalDereferencingDocumentTests.swift @@ -0,0 +1,288 @@ +// +// ExternalDereferencingDocumentTests.swift +// + +import Foundation +@preconcurrency import Yams +import OpenAPIKit30 +import XCTest + +final class ExternalDereferencingDocumentTests: XCTestCase { + // temporarily test with an example of the new interface + func test_example() async throws { + + /// An example of implementing a loader context for loading external references + /// into an OpenAPI document. + struct ExampleLoader: ExternalLoader { + typealias Message = String + + 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)) + + // 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 test makes sure + // the following strategy of using vendor extensions works. + if var extendable = decoded as? VendorExtendable { + extendable.vendorExtensions["x-source-url"] = AnyCodable(url) + finished = extendable as! T + } else { + finished = decoded + } + return (finished, [url.absoluteString]) + } + + static func componentKey(type: T.Type, 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() + .joined(separator: "_") + .replacingOccurrences(of: ".", with: "_") + return try .forceInit(rawValue: urlString) + } + + /// 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 let files: [String: Data] = [ + "params_name_json": """ + { + "name": "name", + "description": "a lonely parameter", + "in": "path", + "required": true, + "schema": { + "$ref": "file://./schemas/string_param.json#" + } + } + """, + "schemas_string_param_json": """ + { + "oneOf": [ + { "type": "string" }, + { "$ref": "file://./schemas/basic_object.json" } + ] + } + """, + "schemas_basic_object_json": """ + { + "type": "object" + } + """, + "paths_webhook_json": """ + { + "summary": "just a webhook", + "get": { + "requestBody": { + "$ref": "file://./requests/webhook.json" + }, + "responses": { + "200": { + "$ref": "file://./responses/webhook.json" + } + } + } + } + """, + "requests_webhook_json": """ + { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "$ref": "file://./schemas/string_param.json" + } + } + }, + "examples": { + "good": { + "$ref": "file://./examples/good.json" + } + }, + "encoding": { + "enc1": { + "headers": { + "head1": { + "$ref": "file://./headers/webhook.json" + } + } + }, + "enc2": { + "style": "form" + } + } + } + } + } + """, + "responses_webhook_json": """ + { + "description": "webhook response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "length": { + "type": "integer", + "minimum": 0 + } + } + } + } + }, + "headers": { + "X-Hello": { + "$ref": "file://./headers/webhook2.json" + } + } + } + """, + "headers_webhook_json": """ + { + "schema": { + "$ref": "file://./schemas/string_param.json" + } + } + """, + "headers_webhook2_json": """ + { + "content": { + "application/json": { + "schema": { + "$ref": "file://./schemas/string_param.json" + } + } + } + } + """, + "examples_good_json": """ + { + "value": "{\\"body\\": \\"request me\\"}" + } + """, + "callbacks_one_json": """ + { + "https://callback.site.com/callback": { + "summary": "just a callback" + } + } + """, + "paths_callback_json": """ + { + "summary": "just a callback", + "get": { + "responses": { + "200": { + "description": "callback response", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "links": { + "link1": { + "$ref": "file://./links/first.json" + } + } + } + } + } + } + """, + "links_first_json": """ + { + "operationId": "helloOp" + } + """ + ].mapValues { $0.data(using: .utf8)! } + } + + let 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")!)) + ], + get: .init( + operationId: "helloOp", + responses: [:], + callbacks: [ + "callback1": .reference(.external(URL(string: "file://./callbacks/one.json")!)) + ] + ) + ), + "/goodbye/{name}": .init( + parameters: [ + .reference(.external(URL(string: "file://./params/name.json")!)) + ] + ), + "/webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)), + "/callback": .reference(.external(URL(string: "file://./paths/callback.json")!)) + ], + components: .init( + schemas: [ + "name_param": .reference(.external(URL(string: "file://./schemas/string_param.json")!)) + ], + // just to show, no parameters defined within document components : + parameters: [:] + ) + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + var docCopy1 = document + try await docCopy1.externallyDereference(with: ExampleLoader.self) + try await docCopy1.externallyDereference(with: ExampleLoader.self) + try await docCopy1.externallyDereference(with: ExampleLoader.self) + try await docCopy1.externallyDereference(with: ExampleLoader.self) + docCopy1.components.sort() + + var docCopy2 = document + try await docCopy2.externallyDereference(with: ExampleLoader.self, depth: 4) + docCopy2.components.sort() + + var docCopy3 = document + let messages = try await docCopy3.externallyDereference(with: ExampleLoader.self, depth: .full) + docCopy3.components.sort() + + XCTAssertEqual(docCopy1, docCopy2) + XCTAssertEqual(docCopy2, docCopy3) + + XCTAssertEqual( + messages.sorted(), + ["file://./callbacks/one.json", + "file://./examples/good.json", + "file://./headers/webhook.json", + "file://./headers/webhook2.json", + "file://./links/first.json", + "file://./params/name.json", + "file://./params/name.json", + "file://./paths/callback.json", + "file://./paths/webhook.json", + "file://./requests/webhook.json", + "file://./responses/webhook.json", + "file://./schemas/basic_object.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json#"] + ) + } +} diff --git a/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift b/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift index 60fee797e..25332acd5 100644 --- a/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKit30Tests/JSONReferenceTests.swift @@ -339,4 +339,52 @@ extension JSONReferenceTests { struct ReferenceWrapper: Codable, Equatable { let reference: JSONReference } + + struct SchemaLoader: ExternalLoader { + typealias Message = String + + static func load(_ url: URL) -> (T, [Message]) where T: Decodable { + return (JSONSchema.string as! T, [url.absoluteString]) + } + + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + return try .forceInit(rawValue: url.absoluteString + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "#", with: "_") + .replacingOccurrences(of: ".", with: "_")) + } + } +} + +// MARK: - External Dereferencing +extension JSONReferenceTests { + func test_externalDerefNoFragment() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json")!) + + let (newReference, components, messages) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json")) + XCTAssertEqual(components, .init(schemas: ["__schema_json": .string])) + XCTAssertEqual(messages, ["./schema.json"]) + } + + func test_externalDerefFragment() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json#/test")!) + + let (newReference, components, messages) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json__test")) + XCTAssertEqual(components, .init(schemas: ["__schema_json__test": .string])) + XCTAssertEqual(messages, ["./schema.json#/test"]) + } + + func test_externalDerefExternalComponents() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json#/components/schemas/test")!) + + let (newReference, components, messages) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json__components_schemas_test")) + XCTAssertEqual(components, .init(schemas: ["__schema_json__components_schemas_test": .string])) + XCTAssertEqual(messages, ["./schema.json#/components/schemas/test"]) + } } diff --git a/Tests/OpenAPIKit30Tests/Operation/OperationTests.swift b/Tests/OpenAPIKit30Tests/Operation/OperationTests.swift index a09c535d7..f773beabc 100644 --- a/Tests/OpenAPIKit30Tests/Operation/OperationTests.swift +++ b/Tests/OpenAPIKit30Tests/Operation/OperationTests.swift @@ -7,7 +7,7 @@ import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class OperationTests: XCTestCase { func test_init() { @@ -126,7 +126,7 @@ extension OperationTests { deprecated: true, security: [[.component(named: "security"): []]], servers: [.init(url: URL(string: "https://google.com")!)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedOperation = try orderUnstableTestStringFromEncoding(of: operation) @@ -298,7 +298,7 @@ extension OperationTests { deprecated: true, security: [[.component(named: "security"): []]], servers: [.init(url: URL(string: "https://google.com")!)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) diff --git a/Tests/OpenAPIKit30Tests/Parameter/ParameterTests.swift b/Tests/OpenAPIKit30Tests/Parameter/ParameterTests.swift index 5a263a46d..58e28d611 100644 --- a/Tests/OpenAPIKit30Tests/Parameter/ParameterTests.swift +++ b/Tests/OpenAPIKit30Tests/Parameter/ParameterTests.swift @@ -827,7 +827,7 @@ extension ParameterTests { context: .path, schema: .string, description: "world", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedParameter = try orderUnstableTestStringFromEncoding(of: parameter) @@ -880,7 +880,7 @@ extension ParameterTests { context: .path, schema: .string, description: "world", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKit30Tests/Path Item/PathItemTests.swift b/Tests/OpenAPIKit30Tests/Path Item/PathItemTests.swift index aee98705d..7a03c7578 100644 --- a/Tests/OpenAPIKit30Tests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKit30Tests/Path Item/PathItemTests.swift @@ -175,7 +175,7 @@ extension PathItemTests { description: "description", servers: [OpenAPI.Server(url: URL(string: "http://google.com")!)], parameters: [.parameter(name: "hello", context: .query, schema: .string)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedPathItem = try orderUnstableTestStringFromEncoding(of: pathItem) @@ -245,7 +245,7 @@ extension PathItemTests { description: "description", servers: [OpenAPI.Server(url: URL(string: "http://google.com")!)], parameters: [.parameter(name: "hello", context: .query, schema: .string)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKit30Tests/Schema Object/JSONSchemaTests.swift b/Tests/OpenAPIKit30Tests/Schema Object/JSONSchemaTests.swift index 673a69a2b..8d5319e63 100644 --- a/Tests/OpenAPIKit30Tests/Schema Object/JSONSchemaTests.swift +++ b/Tests/OpenAPIKit30Tests/Schema Object/JSONSchemaTests.swift @@ -1057,15 +1057,15 @@ final class SchemaObjectTests: XCTestCase { func test_withInitalAllowedValues() { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true, allowedValues: [false])) - let object = JSONSchema.object(.init(format: .unspecified, required: true, allowedValues: [[:]]), .init(properties: [:])) - let array = JSONSchema.array(.init(format: .unspecified, required: true, allowedValues: [[false]]), .init(items: .boolean(.init(format: .unspecified, required: true)))) + let object = JSONSchema.object(.init(format: .unspecified, required: true, allowedValues: [.init([:])]), .init(properties: [:])) + let array = JSONSchema.array(.init(format: .unspecified, required: true, allowedValues: [.init([false])]), .init(items: .boolean(.init(format: .unspecified, required: true)))) let number = JSONSchema.number(.init(format: .unspecified, required: true, allowedValues: [2.5]), .init()) let integer = JSONSchema.integer(.init(format: .unspecified, required: true, allowedValues: [5]), .init()) let string = JSONSchema.string(.init(format: .unspecified, required: true, allowedValues: ["hello"]), .init()) let fragment = JSONSchema.fragment(.init(allowedValues: [false])) XCTAssertEqual(boolean.allowedValues, [false]) - XCTAssertEqual(object.allowedValues, [[:]]) + XCTAssertEqual(object.allowedValues, [.init([:])]) XCTAssertEqual(array.allowedValues?[0].value as! [Bool], [false]) XCTAssertEqual(number.allowedValues, [2.5]) XCTAssertEqual(integer.allowedValues, [5]) @@ -1077,9 +1077,9 @@ final class SchemaObjectTests: XCTestCase { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true)) .with(allowedValues: [false]) let object = JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) - .with(allowedValues: [[:]]) + .with(allowedValues: [.init([:])]) let array = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: true)))) - .with(allowedValues: [[false]]) + .with(allowedValues: [.init([false])]) let number = JSONSchema.number(.init(format: .unspecified, required: true), .init()) .with(allowedValues: [2.5]) let integer = JSONSchema.integer(.init(format: .unspecified, required: true), .init()) @@ -1118,16 +1118,16 @@ final class SchemaObjectTests: XCTestCase { func test_withInitalDefaultValue() { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true, defaultValue: false)) - let object = JSONSchema.object(.init(format: .unspecified, required: true, defaultValue: [:]), .init(properties: [:])) - let array = JSONSchema.array(.init(format: .unspecified, required: true, defaultValue: [false]), .init(items: .boolean(.init(format: .unspecified, required: true)))) + let object = JSONSchema.object(.init(format: .unspecified, required: true, defaultValue: .init([:])), .init(properties: [:])) + let array = JSONSchema.array(.init(format: .unspecified, required: true, defaultValue: .init([false])), .init(items: .boolean(.init(format: .unspecified, required: true)))) let number = JSONSchema.number(.init(format: .unspecified, required: true, defaultValue: 2.5), .init()) let integer = JSONSchema.integer(.init(format: .unspecified, required: true, defaultValue: 5), .init()) let string = JSONSchema.string(.init(format: .unspecified, required: true, defaultValue: "hello"), .init()) let fragment = JSONSchema.fragment(.init(defaultValue: false)) XCTAssertEqual(boolean.defaultValue, false) - XCTAssertEqual(object.defaultValue, [:]) - XCTAssertEqual(array.defaultValue, [false]) + XCTAssertEqual(object.defaultValue, .init([:])) + XCTAssertEqual(array.defaultValue, .init([false])) XCTAssertEqual(number.defaultValue, 2.5) XCTAssertEqual(integer.defaultValue, 5) XCTAssertEqual(string.defaultValue, "hello") @@ -1138,9 +1138,9 @@ final class SchemaObjectTests: XCTestCase { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true)) .with(defaultValue: false) let object = JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) - .with(defaultValue: [:]) + .with(defaultValue: .init([:])) let array = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: true)))) - .with(defaultValue: [false]) + .with(defaultValue: .init([false])) let number = JSONSchema.number(.init(format: .unspecified, required: true), .init()) .with(defaultValue: 2.5) let integer = JSONSchema.integer(.init(format: .unspecified, required: true), .init()) @@ -1163,7 +1163,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(boolean.defaultValue, false) XCTAssertEqual(object.defaultValue, AnyCodable([String: String]())) - XCTAssertEqual(array.defaultValue, [false]) + XCTAssertEqual(array.defaultValue, .init([false])) XCTAssertEqual(number.defaultValue, 2.5) XCTAssertEqual(integer.defaultValue, 5) XCTAssertEqual(string.defaultValue, "hello") @@ -1178,7 +1178,7 @@ final class SchemaObjectTests: XCTestCase { } func test_withInitialExample() { - let object = JSONSchema.object(.init(format: .unspecified, required: true, example: [:]), .init(properties: [:])) + let object = JSONSchema.object(.init(format: .unspecified, required: true, example: .init([:])), .init(properties: [:])) let fragment = JSONSchema.fragment(.init(example: "hi")) // nonsense @@ -1202,7 +1202,7 @@ final class SchemaObjectTests: XCTestCase { let object = try JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) .with(example: AnyCodable([String: String]())) let array = try JSONSchema.array(.init(), .init()) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let boolean = try JSONSchema.boolean(.init(format: .unspecified, required: true)) .with(example: true) @@ -1216,17 +1216,17 @@ final class SchemaObjectTests: XCTestCase { .with(example: "hello world") let allOf = try JSONSchema.all(of: [.string(.init(), .init())]) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let anyOf = try JSONSchema.any(of: [object]) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let oneOf = try JSONSchema.one(of: [object]) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let not = try JSONSchema.not(object) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let fragment = try JSONSchema.fragment(.init()).with(example: "hi") XCTAssertThrowsError(try JSONSchema.reference(.external(URL(string: "hello/world.json#/hello")!)) - .with(example: ["hello"])) + .with(example: .init(["hello"]))) XCTAssertEqual(object.example?.value as? [String: String], [:]) XCTAssertEqual(array.example?.value as? [String], ["hello"]) @@ -1373,6 +1373,28 @@ extension SchemaObjectTests { XCTAssertThrowsError(try orderUnstableDecode(JSONSchema.self, from: readOnlyWriteOnlyData)) } + func test_decodingWithVendorExtensionsTurnedOff() throws { + let vendorExtendedData = """ + { + "type": "object", + "x-hello": "hi" + } + """.data(using: .utf8)! + + let nonVendorExtendedData = """ + { + "type": "object" + } + """.data(using: .utf8)! + + let config = [VendorExtensionsConfiguration.enabledKey: false] + + let vendorExtended = try orderUnstableDecode(JSONSchema.self, from: vendorExtendedData, userInfo: config) + let nonVendorExtended = try orderUnstableDecode(JSONSchema.self, from: nonVendorExtendedData, userInfo: config) + + XCTAssertEqual(vendorExtended, nonVendorExtended) + } + func test_decodingWarnsForTypeAndPropertyConflict() throws { // has type "object" but "items" property that belongs with the "array" type. let badSchema = """ @@ -1790,7 +1812,7 @@ extension SchemaObjectTests { XCTAssertEqual(deprecatedObject, JSONSchema.object(.init(format: .generic, deprecated: true), .init(properties: [:]))) XCTAssertEqual(allowedValueObject.allowedValues?[0].value as! [String: Bool], ["hello": false]) XCTAssertEqual(allowedValueObject.jsonTypeFormat, .object(.generic)) - XCTAssertEqual(defaultValueObject.defaultValue, ["hello": false]) + XCTAssertEqual(defaultValueObject.defaultValue, .init(["hello": false])) XCTAssertEqual(discriminatorObject, JSONSchema.object(discriminator: .init(propertyName: "hello"))) guard case let .object(_, contextB) = allowedValueObject.value else { @@ -2727,7 +2749,7 @@ extension SchemaObjectTests { XCTAssertEqual(nullableObject, JSONSchema.object(.init(format: .generic, nullable: true, example: AnyCodable(["hello": true])), .init(properties: [:]))) XCTAssertEqual(allowedValueObject.allowedValues?[0].value as! [String: Bool], ["hello": false]) XCTAssertEqual(allowedValueObject.jsonTypeFormat, .object(.generic)) - XCTAssertEqual(allowedValueObject.example, ["hello" : true]) + XCTAssertEqual(allowedValueObject.example, .init(["hello" : true])) guard case let .object(_, contextB) = allowedValueObject.value else { XCTFail("expected object to be parsed as object") @@ -3115,9 +3137,9 @@ extension SchemaObjectTests { let writeOnlyArray = JSONSchema.array(.init(format: .unspecified, required: true, permissions: .writeOnly), .init()) let deprecatedArray = JSONSchema.array(.init(format: .unspecified, required: true, deprecated: true), .init()) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init()) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) let defaultValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init()) - .with(defaultValue: [10]) + .with(defaultValue: .init([10])) let discriminatorArray = JSONSchema.array(.init(format: .unspecified, required: true, discriminator: .init(propertyName: "hello")), .init()) testAllSharedSimpleContextEncoding( @@ -3168,7 +3190,7 @@ extension SchemaObjectTests { XCTAssertEqual(writeOnlyArray, JSONSchema.array(.init(format: .generic, permissions: .writeOnly), .init())) XCTAssertEqual(deprecatedArray, JSONSchema.array(.init(format: .generic, deprecated: true), .init())) XCTAssertEqual(allowedValueArray.allowedValues?[0].value as! [Bool], [false]) - XCTAssertEqual(defaultValueArray.defaultValue, [false]) + XCTAssertEqual(defaultValueArray.defaultValue, .init([false])) XCTAssertEqual(discriminatorArray, JSONSchema.array(discriminator: .init(propertyName: "hello"))) guard case let .array(_, contextB) = allowedValueArray.value else { @@ -3201,7 +3223,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(items: .boolean(.init(format: .unspecified, required: false)))) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(items: .boolean(.init(format: .unspecified, required: false)))) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: false)))) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines(entity: requiredArray, propertyLines: [ @@ -3279,7 +3301,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(uniqueItems: true)) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(uniqueItems: true)) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(uniqueItems: true)) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines( entity: requiredArray, @@ -3349,7 +3371,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(maxItems: 2)) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(maxItems: 2)) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(maxItems: 2)) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines(entity: requiredArray, propertyLines: [ @@ -3407,7 +3429,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(minItems: 2)) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(minItems: 2)) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(minItems: 2)) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines(entity: requiredArray, propertyLines: [ @@ -5933,8 +5955,8 @@ extension SchemaObjectTests { "hello": .boolean ], allowedValues: [ - [ "hello": true], - [ "hello": false] + .init([ "hello": true]), + .init([ "hello": false]) ] ) let addProp1 = JSONSchema.object( diff --git a/Tests/OpenAPIKit30Tests/Schema Object/SchemaObjectYamsTests.swift b/Tests/OpenAPIKit30Tests/Schema Object/SchemaObjectYamsTests.swift index 0528c0811..b191ea907 100644 --- a/Tests/OpenAPIKit30Tests/Schema Object/SchemaObjectYamsTests.swift +++ b/Tests/OpenAPIKit30Tests/Schema Object/SchemaObjectYamsTests.swift @@ -13,7 +13,7 @@ import Foundation import XCTest import OpenAPIKit30 -import Yams +@preconcurrency import Yams final class SchemaObjectYamsTests: XCTestCase { func test_floatingPointWholeNumberIntegerDecode() throws { diff --git a/Tests/OpenAPIKit30Tests/ServerTests.swift b/Tests/OpenAPIKit30Tests/ServerTests.swift index ac9638575..0b670697e 100644 --- a/Tests/OpenAPIKit30Tests/ServerTests.swift +++ b/Tests/OpenAPIKit30Tests/ServerTests.swift @@ -170,7 +170,7 @@ extension ServerTests { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -187,7 +187,7 @@ extension ServerTests { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedServer = try orderUnstableTestStringFromEncoding(of: server) diff --git a/Tests/OpenAPIKit30Tests/TestHelpers.swift b/Tests/OpenAPIKit30Tests/TestHelpers.swift index ec2960f9d..336866d5d 100644 --- a/Tests/OpenAPIKit30Tests/TestHelpers.swift +++ b/Tests/OpenAPIKit30Tests/TestHelpers.swift @@ -6,7 +6,7 @@ // import Foundation -import Yams +@preconcurrency import Yams import XCTest fileprivate let foundationTestEncoder = { () -> JSONEncoder in @@ -40,8 +40,9 @@ func orderStableYAMLEncode(_ value: T) throws -> String { return try yamsTestEncoder.encode(value) } -fileprivate let foundationTestDecoder = { () -> JSONDecoder in +fileprivate func buildFoundationTestDecoder(_ userInfo: [CodingUserInfoKey: Any] = [:]) -> JSONDecoder { let decoder = JSONDecoder() + decoder.userInfo = userInfo if #available(macOS 10.12, *) { decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .useDefaultKeys @@ -51,10 +52,12 @@ fileprivate let foundationTestDecoder = { () -> JSONDecoder in decoder.keyDecodingStrategy = .useDefaultKeys #endif return decoder -}() +} + +fileprivate let foundationTestDecoder = { () -> JSONDecoder in buildFoundationTestDecoder() }() -func orderUnstableDecode(_ type: T.Type, from data: Data) throws -> T { - return try foundationTestDecoder.decode(T.self, from: data) +func orderUnstableDecode(_ type: T.Type, from data: Data, userInfo : [CodingUserInfoKey: Any] = [:]) throws -> T { + return try buildFoundationTestDecoder(userInfo).decode(T.self, from: data) } fileprivate let yamsTestDecoder = { () -> YAMLDecoder in diff --git a/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift b/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift index 7ccf0caee..c96acc1cf 100644 --- a/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift +++ b/Tests/OpenAPIKit30Tests/Validator/ValidatorTests.swift @@ -85,7 +85,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -112,7 +112,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -144,7 +144,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -180,7 +180,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -212,7 +212,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -242,7 +242,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -278,7 +278,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -312,7 +312,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -324,7 +324,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -360,7 +360,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -396,7 +396,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -433,7 +433,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -470,7 +470,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -506,7 +506,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -688,7 +688,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -722,7 +722,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -818,7 +818,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -869,7 +869,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -920,7 +920,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -958,7 +958,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -1002,7 +1002,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -1045,7 +1045,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -1335,7 +1335,7 @@ final class ValidatorTests: XCTestCase { vendorExtensions: [ "x-string": "hiya", "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]), "x-float": AnyCodable(22.5 as Float), "x-bool": true @@ -1373,7 +1373,7 @@ final class ValidatorTests: XCTestCase { vendorExtensions: [ "x-string": "hiya", "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]), "x-float": AnyCodable(22.5 as Float), "x-bool": true @@ -1444,6 +1444,12 @@ final class ValidatorTests: XCTestCase { "Inconsistency encountered when parsing ``: \'gzip\' could not be parsed as a Content Type. Content Types should have the format \'/\'." ) XCTAssertEqual(warnings.first?.codingPathString, ".paths[\'/test\'].get.responses.200.content") + XCTAssertNotNil(warnings.first?.underlyingError) + XCTAssertNotNil(warnings.first?.errorCategory) + XCTAssertEqual(warnings.first?.subjectName, "") + XCTAssertEqual(warnings.first?.contextString, "") + + XCTAssertEqual(warnings.first?.localizedDescription, warnings.first?.description) } func test_collectsContentTypeWarningStrict() throws { diff --git a/Tests/OpenAPIKit30Tests/VendorExtendableTests.swift b/Tests/OpenAPIKit30Tests/VendorExtendableTests.swift index 44ea33cdf..229621c29 100644 --- a/Tests/OpenAPIKit30Tests/VendorExtendableTests.swift +++ b/Tests/OpenAPIKit30Tests/VendorExtendableTests.swift @@ -39,13 +39,13 @@ final class VendorExtendableTests: XCTestCase { func test_encodeSuccess() throws { let test = TestStruct(vendorExtensions: [ "x-tension": "hello", - "x-two": [ + "x-two": .init([ "cool", "beans" - ], - "x-three": [ + ]), + "x-three": .init([ "nested": 10 - ] + ]) ]) let _ = try JSONEncoder().encode(test) @@ -85,13 +85,13 @@ extension VendorExtendableTests { func test_encode() throws { let test = TestStruct(vendorExtensions: [ "x-tension": "hello", - "x-two": [ + "x-two": .init([ "cool", "beans" - ], - "x-three": [ + ]), + "x-three": .init([ "nested": 10 - ] + ]) ]) let encoded = try orderUnstableTestStringFromEncoding(of: test) @@ -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 @@ -159,6 +159,9 @@ private struct TestStruct: Codable, CodableVendorExtendable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode("world", forKey: .one) try container.encode("!", forKey: .two) - try encodeExtensions(to: &container) + + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 99617df42..fc0c42e31 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -1233,7 +1233,7 @@ fileprivate func assertEqualNewToOld(_ newExample: OpenAPIKit.OpenAPI.Example, _ } fileprivate func assertEqualNewToOld(_ newEncoding: OpenAPIKit.OpenAPI.Content.Encoding, _ oldEncoding: OpenAPIKit30.OpenAPI.Content.Encoding) throws { - XCTAssertEqual(newEncoding.contentType, oldEncoding.contentType) + XCTAssertEqual(newEncoding.contentTypes.first, oldEncoding.contentType) if let newEncodingHeaders = newEncoding.headers { let oldEncodingHeaders = try XCTUnwrap(oldEncoding.headers) for ((newKey, newHeader), (oldKey, oldHeader)) in zip(newEncodingHeaders, oldEncodingHeaders) { diff --git a/Tests/OpenAPIKitCoreTests/CallbackURLTests.swift b/Tests/OpenAPIKitCoreTests/CallbackURLTests.swift new file mode 100644 index 000000000..01f975b70 --- /dev/null +++ b/Tests/OpenAPIKitCoreTests/CallbackURLTests.swift @@ -0,0 +1,43 @@ +// +// CallbackURLTests.swift +// OpenAPIKit +// +// Created by Mathew Polzin on 2/15/25. +// + +import OpenAPIKitCore +import XCTest + +final class CallbackURLTests: XCTestCase { + func test_init() { + let plainUrl = Shared.CallbackURL(url: URL(string: "https://hello.com")!) + XCTAssertEqual(plainUrl.url, URL(string: "https://hello.com")!) + XCTAssertEqual(plainUrl.template.variables.count, 0) + XCTAssertEqual(plainUrl.rawValue, "https://hello.com") + + let templateUrl = Shared.CallbackURL(rawValue: "https://hello.com/item/{$request.path.id}") + XCTAssertEqual(templateUrl?.template.variables, ["$request.path.id"]) + } + + func test_encode() throws { + let url = Shared.CallbackURL(rawValue: "https://hello.com/item/{$request.path.id}") + + let result = try orderUnstableTestStringFromEncoding(of: url) + + assertJSONEquivalent( + result, + """ + "https:\\/\\/hello.com\\/item\\/{$request.path.id}" + """ + ) + } + + func test_decode() throws { + let json = #""https://hello.com/item/{$request.path.id}""# + let data = json.data(using: .utf8)! + + let url = try orderUnstableDecode(Shared.CallbackURL.self, from: data) + + XCTAssertEqual(url, Shared.CallbackURL(rawValue: "https://hello.com/item/{$request.path.id}")) + } +} diff --git a/Tests/OpenAPIKitCoreTests/ComponentKeyTests.swift b/Tests/OpenAPIKitCoreTests/ComponentKeyTests.swift new file mode 100644 index 000000000..0236e9ee0 --- /dev/null +++ b/Tests/OpenAPIKitCoreTests/ComponentKeyTests.swift @@ -0,0 +1,40 @@ +// +// ComponentKeyTests.swift +// OpenAPIKit +// +// Created by Mathew Polzin on 2/16/25. +// + +import OpenAPIKitCore +import XCTest + +final class ComponentKeyTests: XCTestCase { + func test_init() throws { + let t1 : Shared.ComponentKey = "abcd" + XCTAssertEqual(t1.rawValue, "abcd") + + let t2 = Shared.ComponentKey(rawValue: "abcd") + XCTAssertEqual(t2?.rawValue, "abcd") + + let t3 = Shared.ComponentKey(rawValue: "") + XCTAssertNil(t3) + + let t4 = Shared.ComponentKey(rawValue: "(abcd)") + XCTAssertNil(t4) + + let t5 = try Shared.ComponentKey.forceInit(rawValue: "abcd") + XCTAssertEqual(t5.rawValue, "abcd") + + XCTAssertThrowsError(try Shared.ComponentKey.forceInit(rawValue: nil)) + XCTAssertThrowsError(try Shared.ComponentKey.forceInit(rawValue: "(abcd)")) + } + + func test_problemString() { + let message = Shared.ComponentKey.problem(with: "(abcd)") + + XCTAssertEqual(message, "Keys for components in the Components Object must conform to the regex `^[a-zA-Z0-9\\.\\-_]+$`. '(abcd)' does not..") + + let nonProblem = Shared.ComponentKey.problem(with: "abcd") + XCTAssertNil(nonProblem) + } +} diff --git a/Tests/OpenAPIKitCoreTests/URLTemplate/URLTemplateTests.swift b/Tests/OpenAPIKitCoreTests/URLTemplate/URLTemplateTests.swift index 1020b074a..a0f9c12b6 100644 --- a/Tests/OpenAPIKitCoreTests/URLTemplate/URLTemplateTests.swift +++ b/Tests/OpenAPIKitCoreTests/URLTemplate/URLTemplateTests.swift @@ -377,7 +377,6 @@ fileprivate struct TemplatedURLWrapper: Codable { } // MARK: - Stack Overflow Regression Test -#if swift(>=5.5) import Dispatch extension URLTemplateTests { @@ -392,11 +391,10 @@ extension URLTemplateTests { } """.utf8) - let document = try JSONDecoder().decode( + let _ = try JSONDecoder().decode( StackFoo.self, from: data ) - print(document) } func test_avoid_stack_overflow() async throws { @@ -408,4 +406,3 @@ extension URLTemplateTests { } } } -#endif diff --git a/Tests/OpenAPIKitErrorReportingTests/ComponentErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/ComponentErrorTests.swift index 3c3f726e2..60e010320 100644 --- a/Tests/OpenAPIKitErrorReportingTests/ComponentErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/ComponentErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class ComponentErrorTests: XCTestCase { diff --git a/Tests/OpenAPIKitErrorReportingTests/DocumentErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/DocumentErrorTests.swift index 25ac8ba44..9d3a03d26 100644 --- a/Tests/OpenAPIKitErrorReportingTests/DocumentErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/DocumentErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class DocumentErrorTests: XCTestCase { diff --git a/Tests/OpenAPIKitErrorReportingTests/Helpers.swift b/Tests/OpenAPIKitErrorReportingTests/Helpers.swift index f067827ec..2a3cb4e13 100644 --- a/Tests/OpenAPIKitErrorReportingTests/Helpers.swift +++ b/Tests/OpenAPIKitErrorReportingTests/Helpers.swift @@ -6,6 +6,6 @@ // import Foundation -import Yams +@preconcurrency import Yams let testDecoder = YAMLDecoder() diff --git a/Tests/OpenAPIKitErrorReportingTests/JSONReferenceErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/JSONReferenceErrorTests.swift index 413df97d6..66167bd49 100644 --- a/Tests/OpenAPIKitErrorReportingTests/JSONReferenceErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/JSONReferenceErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class JSONReferenceErrorTests: XCTestCase { func test_referenceFailedToParse() { diff --git a/Tests/OpenAPIKitErrorReportingTests/OperationErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/OperationErrorTests.swift index 1743af2ac..d7e0129e2 100644 --- a/Tests/OpenAPIKitErrorReportingTests/OperationErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/OperationErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class OperationErrorTests: XCTestCase { func test_wrongTypeTags() { diff --git a/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift index 69ccb32da..e5dd3b760 100644 --- a/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/PathsErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class PathsErrorTests: XCTestCase { func test_badPathReference() { diff --git a/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift index 6bc8a6ef2..685b52402 100644 --- a/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class RequestContentMapErrorTests: XCTestCase { /** diff --git a/Tests/OpenAPIKitErrorReportingTests/RequestContentSchemaErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/RequestContentSchemaErrorTests.swift index e6217bfb4..17690166d 100644 --- a/Tests/OpenAPIKitErrorReportingTests/RequestContentSchemaErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/RequestContentSchemaErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class RequestContentSchemaErrorTests: XCTestCase { func test_wrongTypeContentSchemaTypeProperty() { diff --git a/Tests/OpenAPIKitErrorReportingTests/RequestErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/RequestErrorTests.swift index b8edd5b00..184349ee6 100644 --- a/Tests/OpenAPIKitErrorReportingTests/RequestErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/RequestErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class RequestErrorTests: XCTestCase { func test_wrongTypeRequest() { diff --git a/Tests/OpenAPIKitErrorReportingTests/ResponseErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/ResponseErrorTests.swift index 91332e3e7..99323a737 100644 --- a/Tests/OpenAPIKitErrorReportingTests/ResponseErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/ResponseErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class ResponseErrorTests: XCTestCase { func test_headerWithContentAndSchema() { diff --git a/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift index 99074e0dc..ede89a305 100644 --- a/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class SchemaErrorTests: XCTestCase { func test_nonIntegerMaximumForIntegerSchema() { @@ -76,7 +76,7 @@ final class SchemaErrorTests: XCTestCase { XCTAssertEqual(openAPIError.localizedDescription, """ - Inconsistency encountered when parsing `OpenAPI Schema`: Found 'nullable' property. This property is not supported by OpenAPI v3.1.0. OpenAPIKit has translated it into 'type: ["null", ...]'.. at path: .paths['/hello/world'].get.responses.200.content['application/json'].schema + Inconsistency encountered when parsing `OpenAPI Schema`: Found 'nullable' property. This property is not supported by OpenAPI v3.1.x. OpenAPIKit has translated it into 'type: ["null", ...]'.. at path: .paths['/hello/world'].get.responses.200.content['application/json'].schema """) XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [ "paths", diff --git a/Tests/OpenAPIKitErrorReportingTests/SecuritySchemeErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/SecuritySchemeErrorTests.swift index 5e26ad145..f5f2e2081 100644 --- a/Tests/OpenAPIKitErrorReportingTests/SecuritySchemeErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/SecuritySchemeErrorTests.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class SecuritySchemeErrorTests: XCTestCase { func test_missingSecuritySchemeError() { diff --git a/Tests/OpenAPIKitRealSpecSuite/TemplateAPITests.swift b/Tests/OpenAPIKitRealSpecSuite/TemplateAPITests.swift index c9dd6ebff..bfbda7d5d 100644 --- a/Tests/OpenAPIKitRealSpecSuite/TemplateAPITests.swift +++ b/Tests/OpenAPIKitRealSpecSuite/TemplateAPITests.swift @@ -17,7 +17,7 @@ import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams import Foundation #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/Tests/OpenAPIKitTests/ComponentsTests.swift b/Tests/OpenAPIKitTests/ComponentsTests.swift index bf1f7e776..00c5b10bd 100644 --- a/Tests/OpenAPIKitTests/ComponentsTests.swift +++ b/Tests/OpenAPIKitTests/ComponentsTests.swift @@ -319,7 +319,7 @@ extension ComponentsTests { pathItems: [ "ten": .init(get: .init(responses: [200: .response(description: "response")])) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encoded = try orderUnstableTestStringFromEncoding(of: t1) @@ -541,7 +541,7 @@ extension ComponentsTests { pathItems: [ "ten": .init(get: .init(responses: [200: .response(description: "response")])) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKitTests/Content/ContentTests.swift b/Tests/OpenAPIKitTests/Content/ContentTests.swift index 72afe5a15..583cc44b0 100644 --- a/Tests/OpenAPIKitTests/Content/ContentTests.swift +++ b/Tests/OpenAPIKitTests/Content/ContentTests.swift @@ -66,7 +66,7 @@ final class ContentTests: XCTestCase { example: nil, encoding: [ "hello": .init( - contentType: .json, + contentTypes: [.json], headers: [ "world": .init(OpenAPI.Header(schemaOrContent: .init(.header(.string)))) ], @@ -205,7 +205,7 @@ extension ContentTests { func test_exampleAndSchemaContent_encode() { let content = OpenAPI.Content(schema: .init(.object(properties: ["hello": .string])), - example: [ "hello": "world" ]) + example: .init([ "hello": "world" ])) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) assertJSONEquivalent( @@ -260,7 +260,7 @@ extension ContentTests { func test_examplesAndSchemaContent_encode() { let content = OpenAPI.Content(schema: .init(.object(properties: ["hello": .string])), - examples: ["hello": .b(OpenAPI.Example(value: .init([ "hello": "world" ])))]) + examples: ["hello": .b(OpenAPI.Example(value: .init(.init([ "hello": "world" ]))))]) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) assertJSONEquivalent( @@ -355,7 +355,7 @@ extension ContentTests { func test_encodingAndSchema_encode() { let content = OpenAPI.Content( schema: .init(.string), - encoding: ["json": .init(contentType: .json)] + encoding: ["json": .init(contentTypes: [.json])] ) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) @@ -397,7 +397,7 @@ extension ContentTests { content, OpenAPI.Content( schema: .init(.string), - encoding: ["json": .init(contentType: .json)] + encoding: ["json": .init(contentTypes: [.json])] ) ) } @@ -405,7 +405,7 @@ extension ContentTests { func test_vendorExtensions_encode() { let content = OpenAPI.Content( schema: .init(.string), - vendorExtensions: [ "x-hello": [ "world": 123 ] ] + vendorExtensions: [ "x-hello": .init([ "world": 123 ]) ] ) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) @@ -428,7 +428,7 @@ extension ContentTests { func test_vendorExtensions_encode_fixKey() { let content = OpenAPI.Content( schema: .init(.string), - vendorExtensions: [ "hello": [ "world": 123 ] ] + vendorExtensions: [ "hello": .init([ "world": 123 ]) ] ) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) @@ -500,18 +500,18 @@ extension ContentTests { func test_encodingInit() { let _ = OpenAPI.Content.Encoding() - let _ = OpenAPI.Content.Encoding(contentType: .json) + let _ = OpenAPI.Content.Encoding(contentTypes: [.json]) let _ = OpenAPI.Content.Encoding(headers: ["special": .a(.external(URL(string: "hello.yml")!))]) let _ = OpenAPI.Content.Encoding(allowReserved: true) - let _ = OpenAPI.Content.Encoding(contentType: .form, + let _ = OpenAPI.Content.Encoding(contentTypes: [.form], headers: ["special": .a(.external(URL(string: "hello.yml")!))], allowReserved: true) - let _ = OpenAPI.Content.Encoding(contentType: .json, + let _ = OpenAPI.Content.Encoding(contentTypes: [.json], style: .form) - let _ = OpenAPI.Content.Encoding(contentType: .json, + let _ = OpenAPI.Content.Encoding(contentTypes: [.json], style: .form, explode: true) } @@ -544,7 +544,7 @@ extension ContentTests { } func test_encoding_contentType_encode() throws { - let encoding = OpenAPI.Content.Encoding(contentType: .csv) + let encoding = OpenAPI.Content.Encoding(contentTypes: [.csv]) let encodedEncoding = try! orderUnstableTestStringFromEncoding(of: encoding) @@ -567,7 +567,7 @@ extension ContentTests { """.data(using: .utf8)! let encoding = try! orderUnstableDecode(OpenAPI.Content.Encoding.self, from: encodingData) - XCTAssertEqual(encoding, OpenAPI.Content.Encoding(contentType: .csv)) + XCTAssertEqual(encoding, OpenAPI.Content.Encoding(contentTypes: [.csv])) } func test_encoding_multiple_contentTypes_encode() throws { diff --git a/Tests/OpenAPIKitTests/Document/DereferencedDocumentTests.swift b/Tests/OpenAPIKitTests/Document/DereferencedDocumentTests.swift index 941db1093..678223292 100644 --- a/Tests/OpenAPIKitTests/Document/DereferencedDocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DereferencedDocumentTests.swift @@ -28,14 +28,17 @@ final class DereferencedDocumentTests: XCTestCase { servers: [.init(url: URL(string: "http://website.com")!)], paths: [ "/hello/world": .init( + servers: [.init(urlTemplate: URLTemplate(rawValue: "http://{domain}.com")!, variables: ["domain": .init(default: "other")])], get: .init( + operationId: "hi", responses: [ 200: .response(description: "success") ] ) ) ], - components: .noComponents + components: .noComponents, + tags: ["hi"] ).locallyDereferenced() XCTAssertEqual(t1.paths.count, 1) @@ -51,6 +54,13 @@ final class DereferencedDocumentTests: XCTestCase { t1.resolvedEndpointsByPath().keys, ["/hello/world"] ) + + XCTAssertEqual(t1.allOperationIds, ["hi"]) + XCTAssertEqual(t1.allServers, [ + .init(url: URL(string: "http://website.com")!), + .init(urlTemplate: URLTemplate(rawValue: "http://{domain}.com")!, variables: ["domain": .init(default: "other")]), + ]) + XCTAssertEqual(t1.allTags, ["hi"]) } func test_noSecurityReferencedResponseInPath() throws { diff --git a/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift b/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift index 298e7ee58..6106b934e 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentInfoTests.swift @@ -137,7 +137,7 @@ extension DocumentInfoTests { let license = OpenAPI.Document.Info.License( name: "MIT", url: URL(string: "http://website.com")!, - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedLicense = try orderUnstableTestStringFromEncoding(of: license) @@ -176,7 +176,7 @@ extension DocumentInfoTests { OpenAPI.Document.Info.License( name: "MIT", url: URL(string: "http://website.com")!, - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -274,7 +274,7 @@ extension DocumentInfoTests { func test_contact_vendorExtensions_encode() throws { let contact = OpenAPI.Document.Info.Contact( email: "email", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedContact = try orderUnstableTestStringFromEncoding(of: contact) @@ -310,7 +310,7 @@ extension DocumentInfoTests { contact, .init( email: "email", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -584,7 +584,7 @@ extension DocumentInfoTests { title: "title", license: .init(name: "license"), version: "1.0", - vendorExtensions: ["x-speacialFeature": ["hello", "world"]] + vendorExtensions: ["x-speacialFeature": .init(["hello", "world"])] ) let encodedInfo = try orderUnstableTestStringFromEncoding(of: info) @@ -630,7 +630,7 @@ extension DocumentInfoTests { title: "title", license: .init(name: "license"), version: "1.0", - vendorExtensions: ["x-speacialFeature": ["hello", "world"]] + vendorExtensions: ["x-speacialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 4ae5f9070..1de075b87 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -41,6 +41,27 @@ final class DocumentTests: XCTestCase { ) } + func test_initOASVersions() { + let t1 = OpenAPI.Document.Version.v3_1_0 + XCTAssertEqual(t1.rawValue, "3.1.0") + + let t2 = OpenAPI.Document.Version.v3_1_1 + XCTAssertEqual(t2.rawValue, "3.1.1") + + let t3 = OpenAPI.Document.Version.v3_1_x(x: 8) + XCTAssertEqual(t3.rawValue, "3.1.8") + + let t4 = OpenAPI.Document.Version(rawValue: "3.1.0") + XCTAssertEqual(t4, .v3_1_0) + + let t5 = OpenAPI.Document.Version(rawValue: "3.1.1") + XCTAssertEqual(t5, .v3_1_1) + + // not a known version: + let t6 = OpenAPI.Document.Version(rawValue: "3.1.8") + XCTAssertNil(t6) + } + func test_getRoutes() { let pi1 = OpenAPI.PathItem( parameters: [], @@ -390,7 +411,7 @@ final class DocumentTests: XCTestCase { let docData = """ { - "openapi": "3.1.0", + "openapi": "3.1.1", "info": { "title": "test", "version": "1.0" @@ -435,7 +456,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0" + "openapi" : "3.1.1" } """ ) @@ -449,7 +470,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { } @@ -492,6 +513,30 @@ extension DocumentTests { ) } + func test_specifyUknownOpenAPIVersion_encode() throws { + let document = OpenAPI.Document( + openAPIVersion: .v3_1_x(x: 9), + info: .init(title: "API", version: "1.0"), + servers: [], + paths: [:], + components: .noComponents + ) + let encodedDocument = try orderUnstableTestStringFromEncoding(of: document) + + assertJSONEquivalent( + encodedDocument, + """ + { + "info" : { + "title" : "API", + "version" : "1.0" + }, + "openapi" : "3.1.9" + } + """ + ) + } + func test_specifyOpenAPIVersion_decode() throws { let documentData = """ @@ -520,6 +565,23 @@ extension DocumentTests { ) } + func test_specifyUnknownOpenAPIVersion_decode() throws { + let documentData = + """ + { + "info" : { + "title" : "API", + "version" : "1.0" + }, + "openapi" : "3.1.9", + "paths" : { + + } + } + """.data(using: .utf8)! + XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.self, from: documentData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value 3.1.9.") } + } + func test_specifyServers_encode() throws { let document = OpenAPI.Document( info: .init(title: "API", version: "1.0"), @@ -537,7 +599,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "servers" : [ { "url" : "http:\\/\\/google.com" @@ -556,7 +618,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { }, @@ -597,7 +659,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { "\\/test" : { "summary" : "hi" @@ -616,7 +678,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { "\\/test" : { "summary" : "hi" @@ -666,7 +728,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "security" : [ { "security" : [ @@ -696,7 +758,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { }, @@ -743,7 +805,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "tags" : [ { "name" : "hi" @@ -762,7 +824,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { }, @@ -808,7 +870,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0" + "openapi" : "3.1.1" } """ ) @@ -825,7 +887,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { } @@ -852,7 +914,7 @@ extension DocumentTests { paths: [:], components: .noComponents, externalDocs: .init(url: URL(string: "http://google.com")!), - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedDocument = try orderUnstableTestStringFromEncoding(of: document) @@ -867,7 +929,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "x-specialFeature" : [ "hello", "world" @@ -888,7 +950,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { }, @@ -908,7 +970,7 @@ extension DocumentTests { paths: [:], components: .noComponents, externalDocs: .init(url: URL(string: "http://google.com")!), - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -928,7 +990,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "paths" : { }, @@ -980,7 +1042,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "webhooks" : { "webhook-test" : { "delete" : { @@ -1048,7 +1110,7 @@ extension DocumentTests { "title": "API", "version": "1.0" }, - "openapi": "3.1.0", + "openapi": "3.1.1", "paths": { }, "webhooks": { @@ -1118,7 +1180,7 @@ extension DocumentTests { "title" : "API", "version" : "1.0" }, - "openapi" : "3.1.0", + "openapi" : "3.1.1", "webhooks" : { "webhook-test" : { "delete" : { @@ -1164,7 +1226,7 @@ extension DocumentTests { "title": "API", "version": "1.0" }, - "openapi": "3.1.0", + "openapi": "3.1.1", "webhooks": { "webhook-test": { "delete": { diff --git a/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift new file mode 100644 index 000000000..4f977a3d2 --- /dev/null +++ b/Tests/OpenAPIKitTests/Document/ExternalDereferencingDocumentTests.swift @@ -0,0 +1,291 @@ +// +// ExternalDereferencingDocumentTests.swift +// + +import Foundation +@preconcurrency import Yams +import OpenAPIKit +import XCTest + +final class ExternalDereferencingDocumentTests: XCTestCase { + // temporarily test with an example of the new interface + func test_example() async throws { + + /// An example of implementing a loader context for loading external references + /// into an OpenAPI document. + struct ExampleLoader: ExternalLoader { + typealias Message = String + + 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)) + + // 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 test makes sure + // the following strategy of using vendor extensions works. + if var extendable = decoded as? VendorExtendable { + extendable.vendorExtensions["x-source-url"] = AnyCodable(url) + finished = extendable as! T + } else { + finished = decoded + } + return (finished, [url.absoluteString]) + } + + static func componentKey(type: T.Type, 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() + .joined(separator: "_") + .replacingOccurrences(of: ".", with: "_") + return try .forceInit(rawValue: urlString) + } + + /// 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 let files: [String: Data] = [ + "params_name_json": """ + { + "name": "name", + "description": "a lonely parameter", + "in": "path", + "required": true, + "schema": { + "$ref": "file://./schemas/string_param.json#" + } + } + """, + "schemas_string_param_json": """ + { + "oneOf": [ + { "type": "string" }, + { "$ref": "file://./schemas/basic_object.json" } + ] + } + """, + "schemas_basic_object_json": """ + { + "type": "object" + } + """, + "paths_webhook_json": """ + { + "summary": "just a webhook", + "get": { + "requestBody": { + "$ref": "file://./requests/webhook.json" + }, + "responses": { + "200": { + "$ref": "file://./responses/webhook.json" + } + } + } + } + """, + "requests_webhook_json": """ + { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "$ref": "file://./schemas/string_param.json" + } + } + }, + "examples": { + "good": { + "$ref": "file://./examples/good.json" + } + }, + "encoding": { + "enc1": { + "headers": { + "head1": { + "$ref": "file://./headers/webhook.json" + } + } + }, + "enc2": { + "style": "form" + } + } + } + } + } + """, + "responses_webhook_json": """ + { + "description": "webhook response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "length": { + "type": "integer", + "minimum": 0 + } + } + } + } + }, + "headers": { + "X-Hello": { + "$ref": "file://./headers/webhook2.json" + } + } + } + """, + "headers_webhook_json": """ + { + "schema": { + "$ref": "file://./schemas/string_param.json" + } + } + """, + "headers_webhook2_json": """ + { + "content": { + "application/json": { + "schema": { + "$ref": "file://./schemas/string_param.json" + } + } + } + } + """, + "examples_good_json": """ + { + "value": "{\\"body\\": \\"request me\\"}" + } + """, + "callbacks_one_json": """ + { + "https://callback.site.com/callback": { + "$ref": "file://./paths/callback.json" + } + } + """, + "paths_callback_json": """ + { + "summary": "just a callback", + "get": { + "responses": { + "200": { + "description": "callback response", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "links": { + "link1": { + "$ref": "file://./links/first.json" + } + } + } + } + } + } + """, + "links_first_json": """ + { + "operationId": "helloOp" + } + """ + ].mapValues { $0.data(using: .utf8)! } + } + + let 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")!)) + ], + get: .init( + operationId: "helloOp", + responses: [:], + callbacks: [ + "callback1": .reference(.external(URL(string: "file://./callbacks/one.json")!)) + ] + ) + ), + "/goodbye/{name}": .init( + parameters: [ + .reference(.external(URL(string: "file://./params/name.json")!)) + ] + ), + "/webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) + ], + webhooks: [ + "webhook": .reference(.external(URL(string: "file://./paths/webhook.json")!)) + ], + components: .init( + schemas: [ + "name_param": .reference(.external(URL(string: "file://./schemas/string_param.json")!)) + ], + // just to show, no parameters defined within document components : + parameters: [:] + ) + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + var docCopy1 = document + try await docCopy1.externallyDereference(with: ExampleLoader.self) + try await docCopy1.externallyDereference(with: ExampleLoader.self) + try await docCopy1.externallyDereference(with: ExampleLoader.self) + try await docCopy1.externallyDereference(with: ExampleLoader.self) + docCopy1.components.sort() + + var docCopy2 = document + try await docCopy2.externallyDereference(with: ExampleLoader.self, depth: 4) + docCopy2.components.sort() + + var docCopy3 = document + let messages = try await docCopy3.externallyDereference(with: ExampleLoader.self, depth: .full) + docCopy3.components.sort() + + XCTAssertEqual(docCopy1, docCopy2) + XCTAssertEqual(docCopy2, docCopy3) + + XCTAssertEqual( + messages.sorted(), + ["file://./callbacks/one.json", + "file://./examples/good.json", + "file://./headers/webhook.json", + "file://./headers/webhook2.json", + "file://./links/first.json", + "file://./params/name.json", + "file://./params/name.json", + "file://./paths/callback.json", + "file://./paths/webhook.json", + "file://./paths/webhook.json", + "file://./requests/webhook.json", + "file://./responses/webhook.json", + "file://./schemas/basic_object.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json", + "file://./schemas/string_param.json#"] + ) + } +} diff --git a/Tests/OpenAPIKitTests/JSONReferenceTests.swift b/Tests/OpenAPIKitTests/JSONReferenceTests.swift index 53c7a8b64..4587d47cf 100644 --- a/Tests/OpenAPIKitTests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKitTests/JSONReferenceTests.swift @@ -17,6 +17,8 @@ final class JSONReferenceTests: XCTestCase { XCTAssertEqual(t1, t2) XCTAssertTrue(t1.isInternal) XCTAssertFalse(t1.isExternal) + XCTAssertEqual(t1.internalValue, .init(rawValue: "#/hello")) + XCTAssertNil(t1.externalValue) let t3 = JSONReference.component(named: "hello") let t4 = JSONReference.internal(.component(name: "hello")) @@ -27,6 +29,8 @@ final class JSONReferenceTests: XCTestCase { let externalTest = JSONReference.external(URL(string: "hello.json")!) XCTAssertFalse(externalTest.isInternal) XCTAssertTrue(externalTest.isExternal) + XCTAssertNil(externalTest.internalValue) + XCTAssertEqual(externalTest.externalValue, URL(string: "hello.json")) let t5 = JSONReference.InternalReference("#/hello/world") let t6 = JSONReference.InternalReference(rawValue: "#/hello/world") @@ -169,6 +173,10 @@ final class JSONReferenceTests: XCTestCase { XCTAssertEqual(t7.openAPIReference(withDescription: "hi").description, "hi") XCTAssertEqual(t8.openAPIReference(withDescription: "hi").description, "hi") XCTAssertEqual(t9.openAPIReference(withDescription: "hi").description, "hi") + + // test dynamic member lookup: + XCTAssertEqual(t1.openAPIReference().internalValue, .component(name: "hello")) + } } @@ -377,9 +385,55 @@ extension JSONReferenceTests { } } +// MARK: - External Dereferencing +extension JSONReferenceTests { + func test_externalDerefNoFragment() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json")!) + + let (newReference, components, messages) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json")) + XCTAssertEqual(components, .init(schemas: ["__schema_json": .string])) + XCTAssertEqual(messages, ["./schema.json"]) + } + + func test_externalDerefFragment() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json#/test")!) + + let (newReference, components, messages) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json__test")) + XCTAssertEqual(components, .init(schemas: ["__schema_json__test": .string])) + XCTAssertEqual(messages, ["./schema.json#/test"]) + } + + func test_externalDerefExternalComponents() async throws { + let reference: JSONReference = .external(.init(string: "./schema.json#/components/schemas/test")!) + + let (newReference, components, messages) = try await reference.externallyDereferenced(with: SchemaLoader.self) + + XCTAssertEqual(newReference, .component(named: "__schema_json__components_schemas_test")) + XCTAssertEqual(components, .init(schemas: ["__schema_json__components_schemas_test": .string])) + XCTAssertEqual(messages, ["./schema.json#/components/schemas/test"]) + } +} + // MARK: - Test Types extension JSONReferenceTests { struct ReferenceWrapper: Codable, Equatable { let reference: JSONReference } + + struct SchemaLoader: ExternalLoader { + static func load(_ url: URL) -> (T, [String]) { + return (JSONSchema.string as! T, [url.absoluteString]) + } + + static func componentKey(type: T.Type, at url: URL) throws -> OpenAPI.ComponentKey { + return try .forceInit(rawValue: url.absoluteString + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "#", with: "_") + .replacingOccurrences(of: ".", with: "_")) + } + } } diff --git a/Tests/OpenAPIKitTests/Operation/OperationTests.swift b/Tests/OpenAPIKitTests/Operation/OperationTests.swift index 5bd0c88cd..a5bea626f 100644 --- a/Tests/OpenAPIKitTests/Operation/OperationTests.swift +++ b/Tests/OpenAPIKitTests/Operation/OperationTests.swift @@ -7,7 +7,7 @@ import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class OperationTests: XCTestCase { func test_init() { @@ -138,7 +138,7 @@ extension OperationTests { deprecated: true, security: [[.component(named: "security"): []]], servers: [.init(url: URL(string: "https://google.com")!)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedOperation = try orderUnstableTestStringFromEncoding(of: operation) @@ -312,7 +312,7 @@ extension OperationTests { deprecated: true, security: [[.component(named: "security"): []]], servers: [.init(url: URL(string: "https://google.com")!)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) diff --git a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift index e47093f44..6e86c40aa 100644 --- a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift @@ -827,7 +827,7 @@ extension ParameterTests { context: .path, schema: .string, description: "world", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedParameter = try orderUnstableTestStringFromEncoding(of: parameter) @@ -880,7 +880,7 @@ extension ParameterTests { context: .path, schema: .string, description: "world", - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift index db97d6b4a..ae47ff9e0 100644 --- a/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift +++ b/Tests/OpenAPIKitTests/Path Item/PathItemTests.swift @@ -7,6 +7,7 @@ import XCTest import OpenAPIKit +import Foundation final class PathItemTests: XCTestCase { func test_initializePathComponents() { @@ -175,7 +176,7 @@ extension PathItemTests { description: "description", servers: [OpenAPI.Server(url: URL(string: "http://google.com")!)], parameters: [.parameter(name: "hello", context: .query, schema: .string)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedPathItem = try orderUnstableTestStringFromEncoding(of: pathItem) @@ -245,7 +246,7 @@ extension PathItemTests { description: "description", servers: [OpenAPI.Server(url: URL(string: "http://google.com")!)], parameters: [.parameter(name: "hello", context: .query, schema: .string)], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift index b69e4b020..2f7ca5a47 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift @@ -1320,8 +1320,8 @@ final class SchemaObjectTests: XCTestCase { func test_withInitalAllowedValues() { let null = JSONSchema.null(.init(allowedValues: [nil])) let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true, allowedValues: [false])) - let object = JSONSchema.object(.init(format: .unspecified, required: true, allowedValues: [[:]]), .init(properties: [:])) - let array = JSONSchema.array(.init(format: .unspecified, required: true, allowedValues: [[false]]), .init(items: .boolean(.init(format: .unspecified, required: true)))) + let object = JSONSchema.object(.init(format: .unspecified, required: true, allowedValues: [.init([:])]), .init(properties: [:])) + let array = JSONSchema.array(.init(format: .unspecified, required: true, allowedValues: [.init([false])]), .init(items: .boolean(.init(format: .unspecified, required: true)))) let number = JSONSchema.number(.init(format: .unspecified, required: true, allowedValues: [2.5]), .init()) let integer = JSONSchema.integer(.init(format: .unspecified, required: true, allowedValues: [5]), .init()) let string = JSONSchema.string(.init(format: .unspecified, required: true, allowedValues: ["hello"]), .init()) @@ -1329,7 +1329,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(null.allowedValues?[0].description, "nil") XCTAssertEqual(boolean.allowedValues, [false]) - XCTAssertEqual(object.allowedValues, [[:]]) + XCTAssertEqual(object.allowedValues, [.init([:])]) XCTAssertEqual(array.allowedValues?[0].value as! [Bool], [false]) XCTAssertEqual(number.allowedValues, [2.5]) XCTAssertEqual(integer.allowedValues, [5]) @@ -1342,9 +1342,9 @@ final class SchemaObjectTests: XCTestCase { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true)) .with(allowedValues: [false]) let object = JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) - .with(allowedValues: [[:]]) + .with(allowedValues: [.init([:])]) let array = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: true)))) - .with(allowedValues: [[false]]) + .with(allowedValues: [.init([false])]) let number = JSONSchema.number(.init(format: .unspecified, required: true), .init()) .with(allowedValues: [2.5]) let integer = JSONSchema.integer(.init(format: .unspecified, required: true), .init()) @@ -1384,8 +1384,8 @@ final class SchemaObjectTests: XCTestCase { func test_withInitalDefaultValue() { let null = JSONSchema.null(.init(defaultValue: nil)) let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true, defaultValue: false)) - let object = JSONSchema.object(.init(format: .unspecified, required: true, defaultValue: [:]), .init(properties: [:])) - let array = JSONSchema.array(.init(format: .unspecified, required: true, defaultValue: [false]), .init(items: .boolean(.init(format: .unspecified, required: true)))) + let object = JSONSchema.object(.init(format: .unspecified, required: true, defaultValue: .init([:])), .init(properties: [:])) + let array = JSONSchema.array(.init(format: .unspecified, required: true, defaultValue: .init([false])), .init(items: .boolean(.init(format: .unspecified, required: true)))) let number = JSONSchema.number(.init(format: .unspecified, required: true, defaultValue: 2.5), .init()) let integer = JSONSchema.integer(.init(format: .unspecified, required: true, defaultValue: 5), .init()) let string = JSONSchema.string(.init(format: .unspecified, required: true, defaultValue: "hello"), .init()) @@ -1393,8 +1393,8 @@ final class SchemaObjectTests: XCTestCase { XCTAssertNil(null.defaultValue) XCTAssertEqual(boolean.defaultValue, false) - XCTAssertEqual(object.defaultValue, [:]) - XCTAssertEqual(array.defaultValue, [false]) + XCTAssertEqual(object.defaultValue, .init([:])) + XCTAssertEqual(array.defaultValue, .init([false])) XCTAssertEqual(number.defaultValue, 2.5) XCTAssertEqual(integer.defaultValue, 5) XCTAssertEqual(string.defaultValue, "hello") @@ -1406,9 +1406,9 @@ final class SchemaObjectTests: XCTestCase { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true)) .with(defaultValue: false) let object = JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) - .with(defaultValue: [:]) + .with(defaultValue: .init([:])) let array = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: true)))) - .with(defaultValue: [false]) + .with(defaultValue: .init([false])) let number = JSONSchema.number(.init(format: .unspecified, required: true), .init()) .with(defaultValue: 2.5) let integer = JSONSchema.integer(.init(format: .unspecified, required: true), .init()) @@ -1432,7 +1432,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(null.defaultValue!, nil) XCTAssertEqual(boolean.defaultValue, false) XCTAssertEqual(object.defaultValue, AnyCodable([String: String]())) - XCTAssertEqual(array.defaultValue, [false]) + XCTAssertEqual(array.defaultValue, .init([false])) XCTAssertEqual(number.defaultValue, 2.5) XCTAssertEqual(integer.defaultValue, 5) XCTAssertEqual(string.defaultValue, "hello") @@ -1447,7 +1447,7 @@ final class SchemaObjectTests: XCTestCase { } func test_withInitialExample() { - let object = JSONSchema.object(.init(format: .unspecified, required: true, examples: [[:]]), .init(properties: [:])) + let object = JSONSchema.object(.init(format: .unspecified, required: true, examples: [.init([:])]), .init(properties: [:])) let fragment = JSONSchema.fragment(.init(examples: ["hi"])) let null = JSONSchema.null(.init(examples: ["null"])) @@ -1477,7 +1477,7 @@ final class SchemaObjectTests: XCTestCase { let object = try JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) .with(example: AnyCodable([String: String]())) let array = try JSONSchema.array(.init(), .init()) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let boolean = try JSONSchema.boolean(.init(format: .unspecified, required: true)) .with(example: true) @@ -1491,13 +1491,13 @@ final class SchemaObjectTests: XCTestCase { .with(example: "hello world") let allOf = try JSONSchema.all(of: [.string(.init(), .init())]) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let anyOf = try JSONSchema.any(of: [object]) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let oneOf = try JSONSchema.one(of: [object]) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let not = try JSONSchema.not(object) - .with(example: ["hello"]) + .with(example: .init(["hello"])) let fragment = try JSONSchema.fragment(.init()).with(example: "hi") let reference = try JSONSchema.reference(.external(URL(string: "hello/world.json#/hello")!),.init()).with(example: "hi") @@ -1741,14 +1741,12 @@ extension SchemaObjectTests { } """.data(using: .utf8)! - VendorExtensionsConfiguration.isEnabled = false + let config = [VendorExtensionsConfiguration.enabledKey: false] - let vendorExtended = try orderUnstableDecode(JSONSchema.self, from: vendorExtendedData) - let nonVendorExtended = try orderUnstableDecode(JSONSchema.self, from: nonVendorExtendedData) + let vendorExtended = try orderUnstableDecode(JSONSchema.self, from: vendorExtendedData, userInfo: config) + let nonVendorExtended = try orderUnstableDecode(JSONSchema.self, from: nonVendorExtendedData, userInfo: config) XCTAssertEqual(vendorExtended, nonVendorExtended) - - VendorExtensionsConfiguration.isEnabled = true } func test_decodingWarnsForTypeAndPropertyConflict() throws { @@ -2358,7 +2356,7 @@ extension SchemaObjectTests { XCTAssertEqual(constValueObject.allowedValues?[0].value as! [String: Bool], ["hello": false]) XCTAssertEqual(allowedValueObject.allowedValues?[0].value as! [String: Bool], ["hello": false]) XCTAssertEqual(allowedValueObject.jsonTypeFormat, .object(.generic)) - XCTAssertEqual(defaultValueObject.defaultValue, ["hello": false]) + XCTAssertEqual(defaultValueObject.defaultValue, .init(["hello": false])) XCTAssertEqual(discriminatorObject, JSONSchema.object(discriminator: .init(propertyName: "hello"))) guard case let .object(_, contextB) = allowedValueObject.value else { @@ -3408,7 +3406,7 @@ extension SchemaObjectTests { XCTAssertEqual(nullableObject, JSONSchema.object(.init(format: .generic, nullable: true, examples: [AnyCodable(["hello": true])]), .init(properties: [:]))) XCTAssertEqual(allowedValueObject.allowedValues?[0].value as! [String: Bool], ["hello": false]) XCTAssertEqual(allowedValueObject.jsonTypeFormat, .object(.generic)) - XCTAssertEqual(allowedValueObject.examples, [["hello" : true]]) + XCTAssertEqual(allowedValueObject.examples, [.init(["hello" : true])]) guard case let .object(_, contextB) = allowedValueObject.value else { XCTFail("expected object to be parsed as object") @@ -3795,9 +3793,9 @@ extension SchemaObjectTests { let writeOnlyArray = JSONSchema.array(.init(format: .unspecified, required: true, permissions: .writeOnly), .init()) let deprecatedArray = JSONSchema.array(.init(format: .unspecified, required: true, deprecated: true), .init()) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init()) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) let defaultValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init()) - .with(defaultValue: [10]) + .with(defaultValue: .init([10])) let discriminatorArray = JSONSchema.array(.init(format: .unspecified, required: true, discriminator: .init(propertyName: "hello")), .init()) testAllSharedSimpleContextEncoding( @@ -3854,7 +3852,7 @@ extension SchemaObjectTests { XCTAssertEqual(writeOnlyArray, JSONSchema.array(.init(format: .generic, permissions: .writeOnly), .init())) XCTAssertEqual(deprecatedArray, JSONSchema.array(.init(format: .generic, deprecated: true), .init())) XCTAssertEqual(allowedValueArray.allowedValues?[0].value as! [Bool], [false]) - XCTAssertEqual(defaultValueArray.defaultValue, [false]) + XCTAssertEqual(defaultValueArray.defaultValue, .init([false])) XCTAssertEqual(discriminatorArray, JSONSchema.array(discriminator: .init(propertyName: "hello"))) guard case let .array(_, contextB) = allowedValueArray.value else { @@ -3889,7 +3887,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(items: .boolean(.init(format: .unspecified, required: false)))) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(items: .boolean(.init(format: .unspecified, required: false)))) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: false)))) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines(entity: requiredArray, propertyLines: [ @@ -3967,7 +3965,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(uniqueItems: true)) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(uniqueItems: true)) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(uniqueItems: true)) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines( entity: requiredArray, @@ -4037,7 +4035,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(maxItems: 2)) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(maxItems: 2)) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(maxItems: 2)) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines(entity: requiredArray, propertyLines: [ @@ -4095,7 +4093,7 @@ extension SchemaObjectTests { let optionalArray = JSONSchema.array(.init(format: .unspecified, required: false), .init(minItems: 2)) let nullableArray = JSONSchema.array(.init(format: .unspecified, required: true, nullable: true), .init(minItems: 2)) let allowedValueArray = JSONSchema.array(.init(format: .unspecified, required: true), .init(minItems: 2)) - .with(allowedValues: [[10]]) + .with(allowedValues: [.init([10])]) testEncodingPropertyLines(entity: requiredArray, propertyLines: [ @@ -6832,8 +6830,8 @@ extension SchemaObjectTests { "hello": .boolean ], allowedValues: [ - [ "hello": true], - [ "hello": false] + .init([ "hello": true]), + .init([ "hello": false]) ], anchor: "test", dynamicAnchor: "test2", diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift index 12c7c10cd..17bf08876 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift @@ -13,9 +13,23 @@ import Foundation import XCTest import OpenAPIKit -import Yams +@preconcurrency import Yams final class SchemaObjectYamsTests: XCTestCase { + func test_nullTypeDecode() throws { + let nullString = + """ + type: 'null' + """ + + let null = try YAMLDecoder().decode(JSONSchema.self, from: nullString) + + XCTAssertEqual( + null, + JSONSchema.null() + ) + } + func test_floatingPointWholeNumberIntegerDecode() throws { let integerString = """ diff --git a/Tests/OpenAPIKitTests/ServerTests.swift b/Tests/OpenAPIKitTests/ServerTests.swift index a9874050b..ea3a034d9 100644 --- a/Tests/OpenAPIKitTests/ServerTests.swift +++ b/Tests/OpenAPIKitTests/ServerTests.swift @@ -170,7 +170,7 @@ extension ServerTests { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) ) } @@ -187,7 +187,7 @@ extension ServerTests { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let encodedServer = try orderUnstableTestStringFromEncoding(of: server) diff --git a/Tests/OpenAPIKitTests/TestHelpers.swift b/Tests/OpenAPIKitTests/TestHelpers.swift index ec2960f9d..336866d5d 100644 --- a/Tests/OpenAPIKitTests/TestHelpers.swift +++ b/Tests/OpenAPIKitTests/TestHelpers.swift @@ -6,7 +6,7 @@ // import Foundation -import Yams +@preconcurrency import Yams import XCTest fileprivate let foundationTestEncoder = { () -> JSONEncoder in @@ -40,8 +40,9 @@ func orderStableYAMLEncode(_ value: T) throws -> String { return try yamsTestEncoder.encode(value) } -fileprivate let foundationTestDecoder = { () -> JSONDecoder in +fileprivate func buildFoundationTestDecoder(_ userInfo: [CodingUserInfoKey: Any] = [:]) -> JSONDecoder { let decoder = JSONDecoder() + decoder.userInfo = userInfo if #available(macOS 10.12, *) { decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .useDefaultKeys @@ -51,10 +52,12 @@ fileprivate let foundationTestDecoder = { () -> JSONDecoder in decoder.keyDecodingStrategy = .useDefaultKeys #endif return decoder -}() +} + +fileprivate let foundationTestDecoder = { () -> JSONDecoder in buildFoundationTestDecoder() }() -func orderUnstableDecode(_ type: T.Type, from data: Data) throws -> T { - return try foundationTestDecoder.decode(T.self, from: data) +func orderUnstableDecode(_ type: T.Type, from data: Data, userInfo : [CodingUserInfoKey: Any] = [:]) throws -> T { + return try buildFoundationTestDecoder(userInfo).decode(T.self, from: data) } fileprivate let yamsTestDecoder = { () -> YAMLDecoder in diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index eb683ccee..d4cd8b3d0 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -330,7 +330,7 @@ final class BuiltinValidationTests: XCTestCase { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let document = OpenAPI.Document( info: .init(title: "test", version: "1.0"), @@ -338,7 +338,7 @@ final class BuiltinValidationTests: XCTestCase { paths: [:], components: .noComponents ) - let validator = Validator.blank.validating(.serverVarialbeEnumIsValid) + let validator = Validator.blank.validating(.serverVariableEnumIsValid) XCTAssertThrowsError(try document.validate(using: validator)) { error in let error = error as? ValidationErrorCollection XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Server Variable's enum is either not defined or is non-empty (if defined).") @@ -357,7 +357,7 @@ final class BuiltinValidationTests: XCTestCase { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let document = OpenAPI.Document( info: .init(title: "test", version: "1.0"), @@ -365,7 +365,7 @@ final class BuiltinValidationTests: XCTestCase { paths: [:], components: .noComponents ) - let validator = Validator.blank.validating(.serverVarialbeEnumIsValid) + let validator = Validator.blank.validating(.serverVariableEnumIsValid) try document.validate(using: validator) } @@ -381,7 +381,7 @@ final class BuiltinValidationTests: XCTestCase { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let document = OpenAPI.Document( info: .init(title: "test", version: "1.0"), @@ -389,7 +389,7 @@ final class BuiltinValidationTests: XCTestCase { paths: [:], components: .noComponents ) - let validator = Validator.blank.validating(.serverVarialbeDefaultExistsInEnum) + let validator = Validator.blank.validating(.serverVariableDefaultExistsInEnum) XCTAssertThrowsError(try document.validate(using: validator)) { error in let error = error as? ValidationErrorCollection XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: Server Variable's default must exist in enum, if enum is defined.") @@ -408,7 +408,7 @@ final class BuiltinValidationTests: XCTestCase { vendorExtensions: [ "x-otherThing": 1234 ] ) ], - vendorExtensions: ["x-specialFeature": ["hello", "world"]] + vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) let document = OpenAPI.Document( info: .init(title: "test", version: "1.0"), @@ -416,7 +416,7 @@ final class BuiltinValidationTests: XCTestCase { paths: [:], components: .noComponents ) - let validator = Validator.blank.validating(.serverVarialbeDefaultExistsInEnum) + let validator = Validator.blank.validating(.serverVariableDefaultExistsInEnum) try document.validate(using: validator) } @@ -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/Validator/ValidatorTests.swift b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift index 3f49c5e4f..d8351cade 100644 --- a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift +++ b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift @@ -85,7 +85,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -112,7 +112,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -144,7 +144,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -180,7 +180,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -212,7 +212,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -242,7 +242,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -278,7 +278,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -312,7 +312,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -324,7 +324,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -360,7 +360,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -396,7 +396,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -433,7 +433,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -470,7 +470,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -506,7 +506,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -688,7 +688,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -722,7 +722,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -818,7 +818,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -869,7 +869,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hello", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -920,7 +920,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -958,7 +958,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -1002,7 +1002,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -1045,7 +1045,7 @@ final class ValidatorTests: XCTestCase { "x-string": "hiya", "x-int": 2244, "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]) ] ) @@ -1339,7 +1339,7 @@ final class ValidatorTests: XCTestCase { vendorExtensions: [ "x-string": "hiya", "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]), "x-float": AnyCodable(22.5 as Float), "x-bool": true @@ -1377,7 +1377,7 @@ final class ValidatorTests: XCTestCase { vendorExtensions: [ "x-string": "hiya", "x-double": 10.5, - "x-dict": [ "string": "world"], + "x-dict": .init([ "string": "world"]), "x-array": AnyCodable(["hello", nil, "world"]), "x-float": AnyCodable(22.5 as Float), "x-bool": true diff --git a/Tests/OpenAPIKitTests/VendorExtendableTests.swift b/Tests/OpenAPIKitTests/VendorExtendableTests.swift index 5a49e5714..b6fe00ed2 100644 --- a/Tests/OpenAPIKitTests/VendorExtendableTests.swift +++ b/Tests/OpenAPIKitTests/VendorExtendableTests.swift @@ -39,13 +39,13 @@ final class VendorExtendableTests: XCTestCase { func test_encodeSuccess() throws { let test = TestStruct(vendorExtensions: [ "x-tension": "hello", - "x-two": [ + "x-two": .init([ "cool", "beans" - ], - "x-three": [ + ]), + "x-three": .init([ "nested": 10 - ] + ]) ]) let _ = try JSONEncoder().encode(test) @@ -85,13 +85,13 @@ extension VendorExtendableTests { func test_encode() throws { let test = TestStruct(vendorExtensions: [ "x-tension": "hello", - "x-two": [ + "x-two": .init([ "cool", "beans" - ], - "x-three": [ + ]), + "x-three": .init([ "nested": 10 - ] + ]) ]) let encoded = try orderUnstableTestStringFromEncoding(of: test) @@ -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 @@ -159,6 +159,9 @@ private struct TestStruct: Codable, CodableVendorExtendable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode("world", forKey: .one) try container.encode("!", forKey: .two) - try encodeExtensions(to: &container) + + if VendorExtensionsConfiguration.isEnabled(for: encoder) { + try encodeExtensions(to: &container) + } } } diff --git a/Tests/OrderedDictionaryTests/OrderedDictionaryTests.swift b/Tests/OrderedDictionaryTests/OrderedDictionaryTests.swift index 06235553e..3b5b3903b 100644 --- a/Tests/OrderedDictionaryTests/OrderedDictionaryTests.swift +++ b/Tests/OrderedDictionaryTests/OrderedDictionaryTests.swift @@ -7,7 +7,7 @@ import OpenAPIKitCore import XCTest -import Yams +@preconcurrency import Yams final class OrderedDictionaryTests: XCTestCase { func test_initGrouping() { diff --git a/documentation/specification_coverage.md b/documentation/specification_coverage.md index 097596a5e..e390c7f65 100644 --- a/documentation/specification_coverage.md +++ b/documentation/specification_coverage.md @@ -1,6 +1,6 @@ ## Specification Coverage -The list below is organized like the [OpenAPI Specification 3.1.x](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md) reference. Types that have OpenAPIKit representations are checked off. Types that have different names in OpenAPIKit than they do in the specification have their OpenAPIKit names in parenthesis. +The list below is organized like the [OpenAPI Specification 3.1.x](https://spec.openapis.org/oas/v3.1.1.html) reference. Types that have OpenAPIKit representations are checked off. Types that have different names in OpenAPIKit than they do in the specification have their OpenAPIKit names in parenthesis. For more information on the OpenAPIKit types, see the [full type documentation](https://github.com/mattpolzin/OpenAPIKit/wiki). diff --git a/documentation/v4_migration_guide.md b/documentation/v4_migration_guide.md new file mode 100644 index 000000000..d4b4a61a5 --- /dev/null +++ b/documentation/v4_migration_guide.md @@ -0,0 +1,81 @@ +## OpenAPIKit v4 Migration Guide +For general information on the v4 release, see the release notes on GitHub. The +rest of this guide will be formatted as a series of changes and what options you +have to migrate code from v3 to v4. You can also refer back to the release notes +for each of the v4 pre-releases for the most thorough look at what changed. + +This guide will not spend time on strictly additive features of version 4. See +the release notes, README, and documentation for information on new features. + +### Swift version support +OpenAPIKit v4.0 drops support for Swift versions prior to 5.8 (i.e. it supports +v5.8 and greater). + +### Yams version support +Yams is only a test dependency of OpenAPIKit, but since it is still a dependency +it will still impact dependency resolution of downstream projects. Yams 5.1.0+ +is now required. + +### MacOS version support +Only relevant when compiling OpenAPIKit on macOS: Now v10_15+ is required. + +### OpenAPI Specification Versions +The OpenAPIKit module's `OpenAPI.Document.Version` enum gained `v3_1_1` and the +OpenAPIKit30 module's `OpenAPI.Document.Version` enum gained `v3_0_4`. + +The `OpenAPI.Document.Version` enum in both modules gained a new case +(`v3_0_x(x: Int)` and `v3_1_x(x: Int)` respectively) that represents future OAS +versions not released at the time of the given OpenAPIKit release. This allows +non-breaking addition of support for those new versions. + +If you have exhaustive switches over values of those types then your switch +statements will need to be updated. + +### Typo corrections +The following typo corrections were made to OpenAPIKit code. These amount to +breaking changes only in so far as you need to correct the same names if they +appear in your codebase. + +- `public static Validation.serverVarialbeEnumIsValid` -> `.serverVariableEnumIsValid` +- `spublic static Validation.erverVarialbeDefaultExistsInEnum` -> `.serverVariableDefaultExistsInEnum` + +### `AnyCodable` +**NOTE** that the `AnyCodable` type is used extensively for OpenAPIKit examples +and vendor extensions so that is likely where this note will be relevant to you. + +1. The constructor for `AnyCodable` now requires knowledge at compile time that + the value it is initialized with is `Sendable`. +2. Array and Dictionary literal protocol conformances had to be dropped. + Anywhere you were relying on implicit conversion from e.g. `["hello": 1]` to + an `AnyCodable`, wrap the literal with an explicit call to init: + `.init(["hello": 1])`. + +### Vendor Extensions +1. The `vendorExtensions` property of any `VendorExtendable` type must now + implement a setter as well as a getter. This is not likely to impact + downstream projects, but technically possible. +2. If you are disabling `vendorExtensions` support via the + `VendorExtensionsConfiguration.isEnabled` property, you need to switch to + using encoder/decoder `userInfo` to disable vendor extensions. The + `isEnabled` property has been removed. See the example below. + +To set an encoder or decoder up to disable vendor extensions use code like the +following before using the encoder or decoder with an OpenAPIKit type: +```swift +let userInfo = [VendorExtensionsConfiguration.enabledKey: false] +let encoder = JSONEncoder() +encoder.userInfo = userInfo + +let decoder = JSONDecoder() +decoder.userInfo = userInfo +``` + +### `OpenAPI.Content.Encoding` +The `contentType` property has been removed in favor of the newer `contentTypes` +property (plural). + +### `JSONSchemaContext` +The default (fallback) implementations of `inferred`, `anchor`, and +`dynamicAnchor` have been removed. Almost no downstream code will break because +of this, but if you've implemented the `JSONSchemaContext` protocol yourself +then this note is for you.