Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on: [pull_request]
jobs:
codecov:
container:
image: swift:6.2
image: swift:6.3
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
container:
image: swift:6.2
image: swift:6.3

steps:
- uses: actions/checkout@v5
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ jobs:
fail-fast: false
matrix:
image:
- swift:6.1-focal
- swift:6.1-jammy
- swift:6.1-noble
- swift:6.2-jammy
- swift:6.2-noble
- swift:6.3-jammy
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 6.1
// swift-tools-version: 6.2

import PackageDescription

Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ versions and key features are supported by which OpenAPIKit versions.

| OpenAPIKit | Swift | OpenAPI v3.0, v3.1 | OpenAPI v3.2 | Package Traits |
|------------|-------|--------------------|--------------|----------------|
| v4.x | 5.8+ | ✅ | | |
| v5.x | 5.10+ | ✅ | ✅ | |
| v6.x | 6.1+ | ✅ | ✅ | ✅ |
| v7.x | 6.2+ | ✅ | ✅ | ✅ |

- [Usage](#usage)
- [Migration](#migration)
- [Older Versions](#older-versions)
- [3.x to 4.x](#3x-to-4x)
- [4.x to 5.x](#4x-to-5x)
- [5.x to 6.x](#5x-to-6x)
- [6.x to 7.x](#6x-to-7x)
- [Decoding OpenAPI Documents](#decoding-openapi-documents)
- [Decoding Errors](#decoding-errors)
- [Encoding OpenAPI Documents](#encoding-openapi-documents)
Expand Down Expand Up @@ -58,13 +58,7 @@ versions and key features are supported by which OpenAPIKit versions.
#### Older Versions
- [`1.x` to `2.x`](./documentation/migration_guides/v2_migration_guide.md)
- [`2.x` to `3.x`](./documentation/migration_guides/v3_migration_guide.md)

#### 3.x to 4.x
If you are migrating from OpenAPIKit 3.x to OpenAPIKit 4.x, check out the
[v4 migration guide](./documentation/migration_guides/v4_migration_guide.md).

Be aware of the changes to minimum Swift version and minimum Yams version
(although Yams is only a test dependency of OpenAPIKit).
- [`3.x` to `4.x`](./documentation/migration_guides/v4_migration_guide.md)

#### 4.x to 5.x
If you are migrating from OpenAPIKit 4.x to OpenAPIKit 5.x, check out the
Expand All @@ -78,6 +72,12 @@ If you are migrating from OpenAPIKit 5.x to OpenAPIKit 6.x, check out the

Be aware of the change to minimum Swift version, now Swift 6.1.

#### 6.x to 7.x
If you are migrating from OpenAPIKit 6.x to OpenAPIKit 7.x, check out the
[v7 migration guide](./documentation/migration_guides/v7_migration_guide.md).

Be aware of the change to minimum Swift version, now Swift 6.2.

### Decoding OpenAPI Documents

Most documentation will focus on what it looks like to work with the
Expand Down
107 changes: 107 additions & 0 deletions Sources/OpenAPIKit/JSONDynamicReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import OpenAPIKitCore

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// A `JSONDynamicReference` represents a JSON Schema `$dynamicRef`
/// (JSON Schema 2020-12, [§7.7](https://json-schema.org/draft/2020-12/json-schema-core#section-7.7)).
///
/// Like `JSONReference`, a dynamic reference can point either to a component
/// in the Components Object, to another location within the same document
/// (including a `$dynamicAnchor`), or to another file.
///
/// OpenAPIKit parses and round-trips `$dynamicRef`. Dynamic-scope *evaluation*
/// is a runtime concern belonging to JSON Schema validators; local
/// dereferencing (`locallyDereferenced()`) does not resolve `$dynamicRef` and
/// fails if it encounters one that cannot be inlined.
@dynamicMemberLookup
public struct JSONDynamicReference: Equatable, Hashable, Sendable {
public let jsonReference: JSONReference<JSONSchema>

public init(_ reference: JSONReference<JSONSchema>) {
self.jsonReference = reference
}

public subscript<T>(dynamicMember path: KeyPath<JSONReference<JSONSchema>, T>) -> T {
return jsonReference[keyPath: path]
}

/// Reference a `$dynamicAnchor` (or `$anchor`) local to this document.
///
/// - Important: `anchor` does not contain a leading '#'.
public static func anchor(_ anchor: String) -> Self {
return .init(.internal(.anchor(anchor)))
}
}

// MARK: - Codable

extension JSONDynamicReference {
private enum CodingKeys: String, CodingKey {
case dynamicRef = "$dynamicRef"
}
}

extension JSONDynamicReference: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch jsonReference {
case .internal(let reference):
try container.encode(reference.rawValue, forKey: .dynamicRef)
case .external(let url):
try container.encode(url.absoluteString, forKey: .dynamicRef)
}
}
}

extension JSONDynamicReference: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let referenceString = try container.decode(String.self, forKey: .dynamicRef)

guard !referenceString.isEmpty else {
throw DecodingError.dataCorruptedError(forKey: .dynamicRef, in: container, debugDescription: "Expected a reference string, but found an empty string instead.")
}

if referenceString.first == "#" {
guard let internalReference = JSONReference<JSONSchema>.InternalReference(rawValue: referenceString) else {
throw GenericError(
subjectName: "JSON Dynamic Reference",
details: "Failed to parse a JSON Dynamic Reference from '\(referenceString)'",
codingPath: container.codingPath
)
}
self = .init(.internal(internalReference))
} else {
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, *) {
externalReference = URL(string: referenceString, encodingInvalidCharacters: false)
} else {
externalReference = URL(string: referenceString)
}
#else
externalReference = URL(string: referenceString)
#endif
guard let externalReference else {
throw GenericError(
subjectName: "JSON Dynamic Reference",
details: "Failed to parse a valid URI for a JSON Dynamic Reference from '\(referenceString)'",
codingPath: container.codingPath
)
}
self = .init(.external(externalReference))
}
}
}

// Conforms for parity with JSONReference; lets downstream code key
// Validations on dynamic references (a `Validation`'s `Subject` must be `Validatable`).
extension JSONDynamicReference: Validatable {}
12 changes: 11 additions & 1 deletion Sources/OpenAPIKit/JSONReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
case component(name: String)
/// The reference refers to some path outside the Components Object.
case path(Path)
/// The reference refers to a plain URI fragment identifying a
/// `$dynamicAnchor` or `$anchor` (e.g. `#category`).
case anchor(String)

/// Get the name of the referenced object.
///
Expand All @@ -149,6 +152,8 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
return name
case .path(let path):
return path.components.last?.stringValue
case .anchor(let name):
return name
}
}

Expand All @@ -166,7 +171,10 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
}
let fragment = rawValue.dropFirst()
guard fragment.starts(with: "/components") else {
self = .path(Path(rawValue: String(fragment)))
// A fragment without a leading '/' is a plain anchor (#category).
self = fragment.first == "/"
? .path(Path(rawValue: String(fragment)))
: .anchor(String(fragment))
return
}
guard fragment.starts(with: "/components/\(ReferenceType.openAPIComponentsKey)") else {
Expand All @@ -192,6 +200,8 @@ public enum JSONReference<ReferenceType: ComponentDictionaryLocatable>: Equatabl
return "#/components/\(ReferenceType.openAPIComponentsKey)/\(name)"
case .path(let path):
return "#\(path.rawValue)"
case .anchor(let name):
return "#\(name)"
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,16 @@ extension JSONSchema: LocallyDereferenceable {
dereferenced = dereferenced.with(vendorExtensions: extensions)

return dereferenced
case .dynamicReference(let reference, _):
// A `DereferencedJSONSchema` must not contain references. Dynamic-scope
// resolution is not yet implemented (see #359), so a `$dynamicRef`
// cannot be inlined; dereferencing fails rather than retaining the
// reference and breaking the `Dereferenced...` invariant.
throw GenericError(
subjectName: "JSONSchema",
details: "Cannot dereference `$dynamicRef` ('\(reference.absoluteString)'): dynamic references are not resolved by local dereferencing.",
codingPath: []
)
case .boolean(let context):
return .boolean(addComponentNameExtension(to: context))
case .object(let coreContext, let objectContext):
Expand Down Expand Up @@ -665,6 +675,12 @@ extension JSONSchema: ExternallyDereferenceable {
newSchema = .init(
schema: .reference(newReference, core)
)
case .dynamicReference:
// TODO: external dereferencing of `$dynamicRef` is not implemented;
// deferred alongside local dynamic-scope resolution (see #359).
newComponents = .noComponents
newSchema = self
newMessages = []
case .fragment(_):
newComponents = .noComponents
newSchema = self
Expand Down
6 changes: 5 additions & 1 deletion Sources/OpenAPIKit/Schema Object/JSONSchema+Combining.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ internal struct FragmentCombiner {
self.combinedFragment = .array(try leftCoreContext.combined(with: rightCoreContext), arrayContext)
case (.fragment(let leftCoreContext), .object(let rightCoreContext, let objectContext)):
self.combinedFragment = .object(try leftCoreContext.combined(with: rightCoreContext), objectContext)
case (.fragment(let leftCoreContext), .dynamicReference(let reference, let rightCoreContext)):
self.combinedFragment = .dynamicReference(reference, try leftCoreContext.combined(with: rightCoreContext))

case (.boolean(let leftCoreContext), .boolean(let rightCoreContext)):
self.combinedFragment = .boolean(try leftCoreContext.combined(with: rightCoreContext))
Expand Down Expand Up @@ -202,6 +204,8 @@ internal struct FragmentCombiner {

case (_, .any), (.any, _), (_, .not), (.not, _), (_, .one), (.one, _):
throw JSONSchemaResolutionError(.unsupported(because: "not, any(of:), and one(of:) are not yet supported for schema resolution"))
case (_, .dynamicReference), (.dynamicReference, _):
throw JSONSchemaResolutionError(.unsupported(because: "$dynamicRef is not supported for schema simplification"))
case (.boolean, _),
(.integer, _),
(.number, _),
Expand Down Expand Up @@ -238,7 +242,7 @@ internal struct FragmentCombiner {

let jsonSchema: JSONSchema
switch combinedFragment.value {
case .fragment, .reference, .null:
case .fragment, .reference, .dynamicReference, .null:
jsonSchema = combinedFragment
case .boolean(let coreContext):
jsonSchema = .boolean(try coreContext.validatedContext())
Expand Down
Loading