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 {}
28 changes: 23 additions & 5 deletions 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 Expand Up @@ -588,16 +598,24 @@ extension JSONReference: LocallyDereferenceable where ReferenceType: LocallyDere
in components: OpenAPI.Components,
following references: Set<AnyHashable>,
dereferencedFromComponentNamed name: String?
) throws -> ReferenceType.DereferencedSelf {
try _dereferenced(in: components, following: references) { resolved, refs, name in
try resolved._dereferenced(in: components, following: refs, dereferencedFromComponentNamed: name)
}
}

internal func _dereferenced(
in components: OpenAPI.Components,
following references: Set<AnyHashable>,
then: (ReferenceType, Set<AnyHashable>, String?) throws -> ReferenceType.DereferencedSelf
) throws -> ReferenceType.DereferencedSelf {
var newReferences = references
let (inserted, _) = newReferences.insert(self)
guard inserted else {
throw OpenAPI.Components.ReferenceCycleError(ref: self.absoluteString)
}

return try components
.lookup(self)
._dereferenced(in: components, following: newReferences, dereferencedFromComponentNamed: self.name)
let resolved = try components.lookup(self)
return try then(resolved, newReferences, self.name)
}
}

Expand Down
Loading