Skip to content
33 changes: 33 additions & 0 deletions Sources/OpenAPIKit/Document/Document+URIResolution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import OpenAPIKitCore

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

public extension OpenAPI.Document {
/// Establish the base URI for this document.
///
/// If the document has a `$self`, it is resolved against the retrieval URI.
/// Otherwise, the retrieval URI itself is the established base URI.
func baseURI(relativeTo retrievalURI: URL? = nil) -> URL? {
selfURI?.resolvedURI(relativeTo: retrievalURI) ?? retrievalURI
}

/// Resolve a JSON reference against this document's established base URI.
func resolvedURI<ReferenceType>(
for reference: JSONReference<ReferenceType>,
relativeTo retrievalURI: URL? = nil
) -> URL {
reference.resolvedURI(relativeTo: baseURI(relativeTo: retrievalURI))
}

/// Resolve an OpenAPI reference against this document's established base URI.
func resolvedURI<ReferenceType>(
for reference: OpenAPI.Reference<ReferenceType>,
relativeTo retrievalURI: URL? = nil
) -> URL {
reference.resolvedURI(relativeTo: baseURI(relativeTo: retrievalURI))
}
}
42 changes: 42 additions & 0 deletions Sources/OpenAPIKit/JSONReference+URIResolution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import OpenAPIKitCore

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

public extension JSONReference {
/// A URI-reference representing this JSON reference.
///
/// Internal references are represented as fragment-only URLs.
var uriReference: URL {
switch self {
case .internal(let reference):
return URL(string: reference.rawValue)!
case .external(let url):
return url
}
}

/// Resolve this reference against the given base URI.
///
/// If `baseURI` is `nil`, relative URI-references remain relative.
func resolvedURI(relativeTo baseURI: URL?) -> URL {
uriReference.resolvedURI(relativeTo: baseURI)
}
}

public extension OpenAPI.Reference {
/// A URI-reference representing this OpenAPI reference.
var uriReference: URL {
jsonReference.uriReference
}

/// Resolve this reference against the given base URI.
///
/// If `baseURI` is `nil`, relative URI-references remain relative.
func resolvedURI(relativeTo baseURI: URL?) -> URL {
jsonReference.resolvedURI(relativeTo: baseURI)
}
}
50 changes: 49 additions & 1 deletion Sources/OpenAPIKit/Schema Object/JSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

import OpenAPIKitCore

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

/// OpenAPI "Schema Object"
///
/// See [OpenAPI Schema Object](https://spec.openapis.org/oas/v3.2.0.html#schema-object).
Expand Down Expand Up @@ -211,6 +217,11 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, Sendable {
return coreContext.externalDocs
}

// See `JSONSchemaContext`
public var id: URL? {
return coreContext.id
}

// See `JSONSchemaContext`
public var allowedValues: [AnyCodable]? {
return coreContext.allowedValues
Expand Down Expand Up @@ -493,6 +504,13 @@ extension JSONSchema: VendorExtendable {
schema: value.with(vendorExtensions: vendorExtensions)
)
}

public func with(id: URL) -> JSONSchema {
.init(
warnings: warnings,
schema: value.with(id: id)
)
}
}

extension JSONSchema.Schema {
Expand Down Expand Up @@ -526,6 +544,37 @@ extension JSONSchema.Schema {
return .fragment(context.with(vendorExtensions: vendorExtensions))
}
}

public func with(id: URL) -> JSONSchema.Schema {
switch self {
case .null(let context):
return .null(context.with(id: id))
case .boolean(let context):
return .boolean(context.with(id: id))
case .number(let contextA, let contextB):
return .number(contextA.with(id: id), contextB)
case .integer(let contextA, let contextB):
return .integer(contextA.with(id: id), contextB)
case .string(let contextA, let contextB):
return .string(contextA.with(id: id), contextB)
case .object(let contextA, let contextB):
return .object(contextA.with(id: id), contextB)
case .array(let contextA, let contextB):
return .array(contextA.with(id: id), contextB)
case .all(of: let of, core: let core):
return .all(of: of, core: core.with(id: id))
case .one(of: let of, core: let core):
return .one(of: of, core: core.with(id: id))
case .any(of: let of, core: let core):
return .any(of: of, core: core.with(id: id))
case .not(let of, core: let core):
return .not(of, core: core.with(id: id))
case .reference(let context, let coreContext):
return .reference(context, coreContext.with(id: id))
case .fragment(let context):
return .fragment(context.with(id: id))
}
}
}

// MARK: - Transformations
Expand Down Expand Up @@ -2006,7 +2055,6 @@ extension JSONSchema: Decodable {
}

public init(from decoder: Decoder) throws {

if let ref = try? JSONReference<JSONSchema>(from: decoder) {
let coreContext = try CoreContext<JSONTypeFormat.AnyFormat>(from: decoder)
self = .init(warnings: coreContext.warnings, schema: .reference(ref, coreContext))
Expand Down
66 changes: 66 additions & 0 deletions Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

import OpenAPIKitCore

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

// MARK: - Core Context

/// A schema context stores information about a schema.
Expand Down Expand Up @@ -66,6 +72,9 @@ public protocol JSONSchemaContext: Sendable {
/// Get the external docs, if specified. If unspecified, returns `nil`.
var externalDocs: OpenAPI.ExternalDocumentation? { get }

/// An identifier for this schema, if one is defined.
var id: URL? { get }

/// The OpenAPI spec calls this "enum"
///
/// If not specified, it is assumed that any
Expand Down Expand Up @@ -138,6 +147,18 @@ public protocol JSONSchemaContext: Sendable {
var vendorExtensions: [String: AnyCodable] { get }
}

public extension JSONSchemaContext {
var id: URL? { nil }

/// Establish the base URI for this schema context.
///
/// If the schema defines `$id`, it is resolved against the parent base URI.
/// Otherwise, the parent base URI is carried forward unchanged.
func baseURI(relativeTo parentBaseURI: URL?) -> URL? {
id?.resolvedURI(relativeTo: parentBaseURI) ?? parentBaseURI
}
}

extension JSONSchema {
/// The context that applies to all schemas.
public struct CoreContext<Format: OpenAPIFormat>: JSONSchemaContext, HasWarnings {
Expand All @@ -153,6 +174,7 @@ extension JSONSchema {
public let title: String?
public let description: String?
public let externalDocs: OpenAPI.ExternalDocumentation?
public let id: URL?

public let discriminator: OpenAPI.Discriminator?

Expand Down Expand Up @@ -221,6 +243,7 @@ extension JSONSchema {
&& title == nil
&& _deprecated == nil
&& externalDocs == nil
&& id == nil
&& allowedValues == nil
&& defaultValue == nil
&& examples.isEmpty
Expand All @@ -244,6 +267,7 @@ extension JSONSchema {
description: String? = nil,
discriminator: OpenAPI.Discriminator? = nil,
externalDocs: OpenAPI.ExternalDocumentation? = nil,
id: URL? = nil,
allowedValues: [AnyCodable]? = nil,
defaultValue: AnyCodable? = nil,
examples: [AnyCodable] = [],
Expand All @@ -264,6 +288,7 @@ extension JSONSchema {
self.description = description
self.discriminator = discriminator
self.externalDocs = externalDocs
self.id = id
self.allowedValues = allowedValues
self.defaultValue = defaultValue
self.examples = examples
Expand All @@ -285,6 +310,7 @@ extension JSONSchema {
description: String? = nil,
discriminator: OpenAPI.Discriminator? = nil,
externalDocs: OpenAPI.ExternalDocumentation? = nil,
id: URL? = nil,
allowedValues: [AnyCodable]? = nil,
defaultValue: AnyCodable? = nil,
examples: [String],
Expand All @@ -304,6 +330,7 @@ extension JSONSchema {
self.description = description
self.discriminator = discriminator
self.externalDocs = externalDocs
self.id = id
self.allowedValues = allowedValues
self.defaultValue = defaultValue
self.examples = examples.map(AnyCodable.init)
Expand All @@ -327,6 +354,7 @@ extension JSONSchema.CoreContext: Equatable {
&& lhs.title == rhs.title
&& lhs.description == rhs.description
&& lhs.externalDocs == rhs.externalDocs
&& lhs.id == rhs.id
&& lhs.discriminator == rhs.discriminator
&& lhs.allowedValues == rhs.allowedValues
&& lhs.defaultValue == rhs.defaultValue
Expand Down Expand Up @@ -355,6 +383,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -379,6 +408,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -403,6 +433,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -427,6 +458,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -451,6 +483,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -475,6 +508,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: [example],
Expand All @@ -499,6 +533,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -523,6 +558,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -547,6 +583,7 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand All @@ -571,6 +608,32 @@ extension JSONSchema.CoreContext {
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
anchor: anchor,
dynamicAnchor: dynamicAnchor,
defs: defs,
xml: xml,
vendorExtensions: vendorExtensions,
_inferred: inferred
)
}

/// Return this context with the given identifier.
public func with(id: URL) -> JSONSchema.CoreContext<Format> {
return .init(
format: format,
required: required,
nullable: nullable,
permissions: _permissions,
deprecated: _deprecated,
title: title,
description: description,
discriminator: discriminator,
externalDocs: externalDocs,
id: id,
allowedValues: allowedValues,
defaultValue: defaultValue,
examples: examples,
Expand Down Expand Up @@ -883,6 +946,7 @@ extension JSONSchema {
case description
case discriminator
case externalDocs
case id = "$id"
case allowedValues = "enum"
case const
case defaultValue = "default"
Expand Down Expand Up @@ -919,6 +983,7 @@ extension JSONSchema.CoreContext: Encodable {
try container.encodeIfPresent(description, forKey: .description)
try container.encodeIfPresent(discriminator, forKey: .discriminator)
try container.encodeIfPresent(externalDocs, forKey: .externalDocs)
try container.encodeIfPresent(id?.absoluteString, forKey: .id)
if !examples.isEmpty {
try container.encode(examples, forKey: .examples)
}
Expand Down Expand Up @@ -989,6 +1054,7 @@ extension JSONSchema.CoreContext: Decodable {
description = try container.decodeIfPresent(String.self, forKey: .description)
discriminator = try container.decodeIfPresent(OpenAPI.Discriminator.self, forKey: .discriminator)
externalDocs = try container.decodeIfPresent(OpenAPI.ExternalDocumentation.self, forKey: .externalDocs)
id = try container.decodeURLAsStringIfPresent(forKey: .id)
if Format.self == JSONTypeFormat.StringFormat.self {
if nullable {
allowedValues = try Self.decodeAllowedValuesOrConst(String?.self, inContainer: container)?.map(AnyCodable.init)
Expand Down
Loading
Loading