Skip to content

Support schema reference description overrides #299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 26, 2023
Merged
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
10 changes: 10 additions & 0 deletions Sources/OpenAPIKit/JSONReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,16 @@ extension OpenAPI {
}
}

public extension JSONReference {
/// Create an OpenAPI.Reference from the given JSONReference.
func openAPIReference(withDescription description: String? = nil) -> OpenAPI.Reference<ReferenceType> {
OpenAPI.Reference(
self,
description: description
)
}
}

/// `SummaryOverridable` exists to provide a parent protocol to `OpenAPIDescribable`
/// and `OpenAPISummarizable`. The structure is designed to provide default no-op
/// implementations of both the members of this protocol to all types that implement either
Expand Down
34 changes: 34 additions & 0 deletions Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,36 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {

// See `JSONSchemaContext`
public var deprecated: Bool { jsonSchema.deprecated }

/// Returns a version of this `DereferencedJSONSchema` that has the given description.
public func with(description: String) -> DereferencedJSONSchema {
switch self {
case .null:
return .null
case .boolean(let context):
return .boolean(context.with(description: description))
case .object(let coreContext, let objectContext):
return .object(coreContext.with(description: description), objectContext)
case .array(let coreContext, let arrayContext):
return .array(coreContext.with(description: description), arrayContext)
case .number(let coreContext, let numberContext):
return .number(coreContext.with(description: description), numberContext)
case .integer(let coreContext, let integerContext):
return .integer(coreContext.with(description: description), integerContext)
case .string(let coreContext, let stringContext):
return .string(coreContext.with(description: description), stringContext)
case .all(of: let schemas, core: let coreContext):
return .all(of: schemas, core: coreContext.with(description: description))
case .one(of: let schemas, core: let coreContext):
return .one(of: schemas, core: coreContext.with(description: description))
case .any(of: let schemas, core: let coreContext):
return .any(of: schemas, core: coreContext.with(description: description))
case .not(let schema, core: let coreContext):
return .not(schema, core: coreContext.with(description: description))
case .fragment(let context):
return .fragment(context.with(description: description))
}
}
}

extension DereferencedJSONSchema {
Expand Down Expand Up @@ -368,6 +398,10 @@ extension JSONSchema: LocallyDereferenceable {
if !context.required {
dereferenced = dereferenced.optionalSchemaObject()
}
if let refDescription = context.description {
dereferenced = dereferenced.with(description: refDescription)
}
// TODO: consider which other core context properties to override here as with description ^
return dereferenced
case .boolean(let context):
return .boolean(context)
Expand Down
50 changes: 25 additions & 25 deletions Sources/OpenAPIKit/Schema Object/JSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable {
public static func not(_ schema: JSONSchema, core: CoreContext<JSONTypeFormat.AnyFormat>) -> Self {
.init(schema: .not(schema, core: core))
}
public static func reference(_ reference: JSONReference<JSONSchema>, _ context: ReferenceContext) -> Self {
public static func reference(_ reference: JSONReference<JSONSchema>, _ context: CoreContext<JSONTypeFormat.AnyFormat>) -> Self {
.init(schema: .reference(reference, context))
}
/// Schemas without a `type`.
Expand All @@ -90,7 +90,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable {
indirect case one(of: [JSONSchema], core: CoreContext<JSONTypeFormat.AnyFormat>)
indirect case any(of: [JSONSchema], core: CoreContext<JSONTypeFormat.AnyFormat>)
indirect case not(JSONSchema, core: CoreContext<JSONTypeFormat.AnyFormat>)
case reference(JSONReference<JSONSchema>, ReferenceContext)
case reference(JSONReference<JSONSchema>, CoreContext<JSONTypeFormat.AnyFormat>)
/// Schemas without a `type`.
case fragment(CoreContext<JSONTypeFormat.AnyFormat>) // This allows for the "{}" case and also fragments of schemas that will later be combined with `all(of:)`.
}
Expand Down Expand Up @@ -196,7 +196,9 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable {
.any(of: _, core: let context as JSONSchemaContext),
.not(_, core: let context as JSONSchemaContext):
return context.description
case .reference, .null:
case .reference(_, let referenceContext):
return referenceContext.description
case .null:
return nil
}
}
Expand Down Expand Up @@ -375,7 +377,9 @@ extension JSONSchema {
.any(of: _, core: let context as JSONSchemaContext),
.not(_, core: let context as JSONSchemaContext):
return context
case .reference, .null:
case .reference(_, let context as JSONSchemaContext):
return context
case .null:
return nil
}
}
Expand Down Expand Up @@ -433,15 +437,6 @@ extension JSONSchema {
}
return context
}

/// Get the context specific to a `reference` schema. If not a
/// reference schema, returns `nil`.
public var referenceContext: ReferenceContext? {
guard case .reference(_, let context) = value else {
return nil
}
return context
}
}

// MARK: - Transformations
Expand Down Expand Up @@ -1053,7 +1048,13 @@ extension JSONSchema {
schema: .fragment(fragment.with(description: description)),
vendorExtensions: vendorExtensions
)
case .reference, .null:
case .reference(let ref, let referenceContext):
return .init(
warnings: warnings,
schema: .reference(ref, referenceContext.with(description: description)),
vendorExtensions: vendorExtensions
)
case .null:
return self
}
}
Expand Down Expand Up @@ -1701,9 +1702,10 @@ extension JSONSchema {
/// Construct a reference schema
public static func reference(
_ reference: JSONReference<JSONSchema>,
required: Bool = true
required: Bool = true,
description: String? = nil
) -> JSONSchema {
return .reference(reference, .init(required: required))
return .reference(reference, .init(required: required, description: description))
}
}

Expand Down Expand Up @@ -1795,10 +1797,9 @@ extension JSONSchema: Encodable {
try container.encode(node, forKey: .not)
try core.encode(to: encoder)

case .reference(let reference, _):
var container = encoder.singleValueContainer()

try container.encode(reference)
case .reference(let reference, let core):
try core.encode(to: encoder)
try reference.encode(to: encoder)

case .fragment(let context):
var container = encoder.singleValueContainer()
Expand Down Expand Up @@ -1833,11 +1834,10 @@ extension JSONSchema: Decodable {

public init(from decoder: Decoder) throws {

if let singleValueContainer = try? decoder.singleValueContainer() {
if let ref = try? singleValueContainer.decode(JSONReference<JSONSchema>.self) {
self = .reference(ref, required: true)
return
}
if let ref = try? JSONReference<JSONSchema>(from: decoder) {
let coreContext = try CoreContext<JSONTypeFormat.AnyFormat>(from: decoder)
self = .reference(ref, coreContext)
return
}

let container = try decoder.container(keyedBy: SubschemaCodingKeys.self)
Expand Down
6 changes: 5 additions & 1 deletion Sources/OpenAPIKitCompat/Compat30To31.swift
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,11 @@ extension OpenAPIKit30.JSONSchema: To31 {
case .not(let not, core: let core):
schema = .not(not.to31(), core: core.to31())
case .reference(let ref, let context):
schema = .reference(ref.to31(), context)
let coreContext: OpenAPIKit.JSONSchema.CoreContext<OpenAPIKit.JSONTypeFormat.AnyFormat>
coreContext = .init(
required: context.required
)
schema = .reference(ref.to31(), coreContext)
case .fragment(let core):
schema = .fragment(core.to31())
}
Expand Down
42 changes: 42 additions & 0 deletions Tests/OpenAPIKitTests/JSONReferenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,48 @@ final class JSONReferenceTests: XCTestCase {
XCTAssertEqual(JSONReference<OpenAPI.Callbacks>.component(named: "hello").absoluteString, "#/components/callbacks/hello")
XCTAssertEqual(JSONReference<OpenAPI.PathItem>.component(named: "hello").absoluteString, "#/components/pathItems/hello")
}

func test_toOpenAPIReference() {
let t1 = JSONReference<JSONSchema>.component(named: "hello")
let t2 = JSONReference<OpenAPI.Response>.component(named: "hello")
let t3 = JSONReference<OpenAPI.Parameter>.component(named: "hello")
let t4 = JSONReference<OpenAPI.Example>.component(named: "hello")
let t5 = JSONReference<OpenAPI.Request>.component(named: "hello")
let t6 = JSONReference<OpenAPI.Header>.component(named: "hello")
let t7 = JSONReference<OpenAPI.SecurityScheme>.component(named: "hello")
let t8 = JSONReference<OpenAPI.Callbacks>.component(named: "hello")
let t9 = JSONReference<OpenAPI.PathItem>.component(named: "hello")

XCTAssertEqual(t1.openAPIReference().jsonReference, t1)
XCTAssertEqual(t2.openAPIReference().jsonReference, t2)
XCTAssertEqual(t3.openAPIReference().jsonReference, t3)
XCTAssertEqual(t4.openAPIReference().jsonReference, t4)
XCTAssertEqual(t5.openAPIReference().jsonReference, t5)
XCTAssertEqual(t6.openAPIReference().jsonReference, t6)
XCTAssertEqual(t7.openAPIReference().jsonReference, t7)
XCTAssertEqual(t8.openAPIReference().jsonReference, t8)
XCTAssertEqual(t9.openAPIReference().jsonReference, t9)

XCTAssertNil(t1.openAPIReference().description)
XCTAssertNil(t2.openAPIReference().description)
XCTAssertNil(t3.openAPIReference().description)
XCTAssertNil(t4.openAPIReference().description)
XCTAssertNil(t5.openAPIReference().description)
XCTAssertNil(t6.openAPIReference().description)
XCTAssertNil(t7.openAPIReference().description)
XCTAssertNil(t8.openAPIReference().description)
XCTAssertNil(t9.openAPIReference().description)

XCTAssertEqual(t1.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t2.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t3.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t4.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t5.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t6.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t7.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t8.openAPIReference(withDescription: "hi").description, "hi")
XCTAssertEqual(t9.openAPIReference(withDescription: "hi").description, "hi")
}
}

// MARK: Codable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ final class DereferencedSchemaObjectTests: XCTestCase {
XCTAssertEqual(t1, .string(.init(), .init()))
}

func test_throwingReferenceWithOverriddenDescription() throws {
let components = OpenAPI.Components(
schemas: ["test": .string]
)
let t1 = try JSONSchema.reference(.component(named: "test"), description: "hello").dereferenced(in: components)
XCTAssertEqual(t1, .string(.init(description: "hello"), .init()))
}

func test_optionalObjectWithoutReferences() {
let t1 = JSONSchema.object(properties: ["test": .string]).dereferenced()
XCTAssertEqual(
Expand Down Expand Up @@ -504,4 +512,37 @@ final class DereferencedSchemaObjectTests: XCTestCase {
)
}
}

func test_withDescription() throws {
let null = JSONSchema.null.dereferenced()!.with(description: "test")
let object = JSONSchema.object.dereferenced()!.with(description: "test")
let array = JSONSchema.array.dereferenced()!.with(description: "test")

let boolean = JSONSchema.boolean.dereferenced()!.with(description: "test")
let number = JSONSchema.number.dereferenced()!.with(description: "test")
let integer = JSONSchema.integer.dereferenced()!.with(description: "test")
let string = JSONSchema.string.dereferenced()!.with(description: "test")
let fragment = JSONSchema.fragment(.init()).dereferenced()!.with(description: "test")
let all = JSONSchema.all(of: .string).dereferenced()!.with(description: "test")
let one = JSONSchema.one(of: .string).dereferenced()!.with(description: "test")
let any = JSONSchema.any(of: .string).dereferenced()!.with(description: "test")
let not = JSONSchema.not(.string).dereferenced()!.with(description: "test")

XCTAssertEqual(object.description, "test")
XCTAssertEqual(array.description, "test")

XCTAssertEqual(boolean.description, "test")
XCTAssertEqual(number.description, "test")
XCTAssertEqual(integer.description, "test")
XCTAssertEqual(string.description, "test")
XCTAssertEqual(fragment.description, "test")

XCTAssertEqual(all.description, "test")
XCTAssertEqual(one.description, "test")
XCTAssertEqual(any.description, "test")
XCTAssertEqual(not.description, "test")

XCTAssertNil(null.description)
}

}
Loading