Releases: mattpolzin/OpenAPIKit
Version 2.0.0 Alpha 1
Version 2 of OpenAPIKit adds a few features, fixes some bugs requiring breaking changes, lays groundwork for future work, and renames some types that had started to feel inconsistent with other parts of OpenAPIKit.
As with any alpha release, do not expect the changes in this release to represent a finished stable API. That said, most of the breaking changes intended for this major version are introduced with this first alpha.
Additions
- Most types are now
LocallyDereferenceable
which means they offer adereferenced(in:)
method that takesOpenAPI.Components
as its argument and performs a recursive deference operation onself
-- the same behavior exposed onOpenAPI.Document
via itslocallyDereferenced()
method. - Arrays of schema fragments (
[JSONSchemaFragment]
) like those found under theJSONSchema
all(of:)
case can be resolved into aJSONSchema
representation using the array extensionresolved(against:)
. - A new
URLTemplate
type supports templated URLs like Server Objects use. It will be the basis for future additions like variable replacement and template URL resolution.
Removals
OpenAPI.Components
dereference(_:)
was removed in favor of the subscript with the same signature.- The various
Dereferenced
versions of types lose their public initializer. These types should only be constructed using thedereferenced(in:)
method of the originatingOpenAPI
type. For example, to get aDereferencedPathItem
, you use theOpenAPI.PathItem
dereferenced(in:)
method. DereferencedJSONSchema
no longer has anall(of:)
case. Instead, part of dereferencing a schema is now the process of resolving anall(of:)
schema. More on this below.
Changes
OpenAPI.Components
forceDereference(_:)
was renamed tolookup(_:)
. More on this below.DereferencedJSONSchema
underlyingJSONSchema
property was renamed tojsonSchema
to avoid the misconception that this property stores theJSONSchema
that produced the givenDereferencedJSONSchema
. In reality, this property will build theJSONSchema
representing the givenDereferencedJSONSchema
.JSONSchema.Context
,DereferencedJSONSchema.Context
, andJSONSchemaFragment.GeneralContext
have been renamed toJSONSchema.CoreContext
,DereferencedJSONSchema.CoreContext
, andJSONSchemaFragment.CoreContext
to align and settle on a better name for "the context shared by most cases." With that, theJSONSchema
generalContext
property has been renamed tocoreContext
.- The default optionality of
JSONSchema
values when decoded has been swapped from optional to required. This has no effect on encoding/decoding (perhaps counter-intuitively) because in any JSON Schema structure, the optionality of a property on an object is actually determined by therequired
array on that object and this fact remains true despite the default changing. More on this below. - Instead of allowing
Validation
to use any type as a subject, theValidatable
protocol has been introduced with all types that will work for validation contexts getting conformance. This allows the type checker to steer you away from writing validations that will never be run. NOTE thatURL
is encoded as a string for compatibility with the broadest range of encoders and thereforeURL
is notValidatable
. You can still useString
as a validation subject or write validations in terms of the OpenAPIKit types that containURL
properties. See the full Validation Documentation for more. - The
OpenAPI.Server
type'surl
property was renamedurlTemplate
and its type changed from a FoundationURL
to aURLTemplate
. This change fixes the bug that OpenAPIKit did not used to be able to decode or represent OpenAPI Template URLs (those with variables in them). You can get a FoundationURL
from aURLTemplate
via theurl
property but only urls without any variable placeholders can be turned into FoundationURL
s directly. - The
OpenAPI.Content
type'sschema
property has become optional. Requiring the schema property was not compliant with the OpenAPI specification (see the Media Item Object definition).
Details
Dereferencing
In OpenAPIKit v1, the OpenAPI.Components
type offered the methods dereference(_:)
and forceDereference(_:)
to perform lookup of components by their references. These were overloaded to allow looking up Either
types representing either a reference to a component or the component itself.
In OpenAPIKit v1.4, true dereferencing was introduced. True dereferencing does not just turn a reference into the value it refers to, it removes references recursively for all properties of the given value. That made the use of the word dereference in the OpenAPI.Components
type's methods misleading -- this methods "looked up" values but did not "dereference" them.
OpenAPIKit v2 fixes this confusing naming by supporting component lookup via subscript
(non-throwing) and lookup(_:)
(throwing) methods and not offering any methods that truly dereference types. At the same time, OpenAPIKit v2 adds the dereferenced(in:)
method to most OpenAPI types. This new method takes an OpenAPI.Components
value and returns a fully dereferenced version of self
. The dereferenced(in:)
method offers the same recursive dereferencing behavior exposed by the OpenAPI.Document
locallyDereferenced()
method that was added in OpenAPIKit v1.4.
Resolving all(of:)
schemas
The JSONSchema
all(of:)
case is unique amongst the combination cases because all of the schema fragments under it can effectively be merged into a new schema. This is what "resolving" that schema means in OpenAPIKit. The result of resolving an all(of:)
schema is a DereferencedJSONSchema
.
For the most part, resolution will just happen as part of dereferencing or resolving anything that contains a JSONSchema
that happens to have all(of:)
cases but you do also have the ability to take an array of JSONSchemaFragment
and resolve it directly with the array extension resolved(against:)
which takes an OpenAPI.Components
to resolve the array of schema fragments against.
let schemaData = """
{
"allOf": [
{
"type": "object",
"description": "A person",
"required": [ "name" ],
"properties": {
"name": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"favoriteColor": {
"type": "string",
"enum": [ "red", "green", "blue" ]
}
}
}
]
}
""".data(using: .utf8)!
let personSchema = try JSONDecoder().decode(JSONSchema .self, from: schemaData)
.dereferenced(in: .noComponents)
// results in:
let personSchemaInCode = JSONSchema.object(
description: "A person",
properties: [
"name": .string,
"favoriteColor": try JSONSchema.string(required: false, allowedValues: "red", "green", "blue")
]
)
Default schema optionality
In OpenAPIKit v1, schemas created in-code defaulted to required:true
. While decoding, however, they would default to required:false
and then if a JSON Schema .object
had a required
array containing a certain property, that property's required
boolean
would get flipped to true
. This is somewhat intuitive at face value, but it has the unintuitive side effect of all root schema nodes (i.e. a JSON Schema that does not live within another .object
) will have required: false
because there was no parent required
array to cause OpenAPIKit to flip its required
boolean
.
Again, this has no effect on the accuracy of encoding/decoding because the required
boolean
of a JSONSchema
is only encoded as part of a parent schema's required
array.
OpenAPIKit v2 swaps this decoding default to required: true
and instead flips the boolean
to false
for all properties not found in a parent object's required
array. This approach has the same effect upon encoding/decoding except for root schemas having required: true
which both aligns better with the default in-code required
boolean
and also makes more intuitive sense.
let schemaData = """
{
"type": "object",
"required": [
"test"
],
"properties": {
"test": {
"type": "string"
}
}
}
""".data(using: .utf8)!
//
// OpenAPIKit v1
//
let schema1 = try JSONDecoder().decode(JSONSchema.self, from: schemaData)
// results in:
let schema1InCode = JSONSchema.object(
required: false, // <-- note that this is unintuitive even though it has no effect on the correctness of the schema when encoded.
properties: [
"test": .string(required: true) // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
]
)
//
// OpenAPIKit v2
//
let schema2 = try JSONDecoder().decode(JSONSchema.self, from: schemaData)
// results in:
let schema2InCode = JSONSchema.object(
required: true, // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
properties: [
"test": .string(required: true) // <-- note that this `required: true` could be omitted, it is the default for in-code construction.
]
)
Enum-be-gone
The OpenAPI.Server.Variable
type incorrectly required that the enum
property be defined (on decoding). This has now been fixed.
Statusfied
Adds a subscript overload to OrderedDictionary
when the Key
is an OpenAPI.Respose.StatusCode
so that elements can be accessed using the status
keyword.
Because OrderedDictionary
is both a dictionary and a collection, it has always offered subscript access via both Key
and Index
(Int
). OpenAPI.Response.StatusCode
is ExpressibleByIntegerLiteral
so accessing ordered dictionaries of OpenAPI.Response
from the OpenAPI.Operation
responses
property by Int
is ambiguous:
operation.responses[200] // <- is this `StatusCode` 200 or the 200th index of the collection?
Now, code accessing the dictionary with a status key can be written:
operation.responses[status: 200]
Of course, you can still use any other method of constructing a status code as well:
operation.responses[.status(code: 200)] // <- equivalent to above
operation.responses[.range(.success)] // <- means the OpenAPI status code string "2XX"
operation.responses[.default]
Operation: Callback
Don't fail to decode documents that have callbacks
properties in their Operation Objects (although callbacks are not yet supported by OpenAPIKit yet).
Heading the Right Direction
Improved error reporting around decoding invalid specification extensions and allowed some Header Object properties that were previously disallowed by accident when specification extension support was added to that type.
Unsupported Support
This fix makes types that are not yet supported by OpenAPIKit (see the Specification Coverage) ignored silently instead of letting them cause decoding to fail.
The idea is, it's better to still be able to parse documentation even if not all of it has OpenAPIKit representation yet. OpenAPIKit is really close to supporting all OpenAPI types but not quite there yet. At least now the existence of those types in a document won't render the document invalid in the eyes of OpenAPIKit.
Fragmented References
Fixes #98 wherein an allOf Schema Object with a Reference Object would not parse and could not be represented in OpenAPIKit.
The fix involved adding the .reference
case to the JSONSchemaFragment
type. Although the surface area here is small, it is possible that this breaks code -- specifically, code that switches over the JSONSchemaFragment
enum.
Required Stability
JSONSchema
objects' requiredProperties
/optionalProperties
are now sorted so that their ordering is stable. These properties are dynamically created from the properties
dictionary and therefore the order has never been defined before.
Even now, the order is not publicly guaranteed, but now it will at least be stable which can help with diffing.
U.R.Love
URLs now encode as strings and decode from strings no matter what encoder/decoder you use. This is the treatment already given to URLs by popular encoders and decoders like Foundation's JSON options and Yams's YAML options but now OpenAPIKit takes the liberty of passing URLs to the encoder (and requesting them from the decoder) as strings to make sure that they always get this intuitive treatment.
True Forms
This is a big release that closes two longstanding gaps: An inability to dereference an OpenAPI Document and the lack of any canonical representation of the components of the API described by an OpenAPI Document.
Dereferencing
The OpenAPI specification supports the use of JSON References in a number of places throughout the Document. These references allow authors to reuse one component in multiple places or even just make the route definition section of the document more concise by storing some components in the Components Object.
When traversing an OpenAPI document, these references are often more of an annoyance than anything else; Every time you reach a part of the document where a reference is allowed, you must check whether you are working with a reference or not or maybe even reach out to the Components Object and look a reference up.
This release introduces the OpenAPI.Document
locallyDereferenced()
method. This method traverses the whole document resolving all references to components found in the Components Object. The result is a document that has replaced many types with dereferenced variants -- anywhere you would have seen Either<JSONReference<Thing>, Thing>
you will now just see Thing
(or a DereferencedThing
, anyway). The dereferenced variants will always expose the properties of the original OpenAPI
type.
Before:
let document: OpenAPI.Document = ...
let anOperation: OpenAPI.Operation = document
.paths["/hello/world"]!
.get!
let parametersOrReferences = anOperation.parameters
// print the name of all parameters that happen to be inlined:
for parameter in parametersOrReferences.compactMap({ $0.parameterValue }) {
print(parameter.name)
}
// resolve parameters in order to print the name of them all
for parameter in parametersOrReferences.compactMap(document.components.dereference) {
print(parameter.name)
}
Now:
let document: OpenAPI.Document = ...
let anOperation = try document
.locallyDereferenced() // new
.paths["/hello/world"]!
.get!
let parameters = anOperation.parameters
// loop over all parameters and print their names
for parameter in parameters {
print(parameter.name)
}
Resolving (to canonical representations)
The OpenAPI Specification leaves numerous opportunities (including JSON References) for authors to write the same documentation in different ways. Sometimes when analyzing an OpenAPI Document or using it to produce something new (a la code generation) you really just need to know what the API looks like, not how the author of the documentation chose to structure the OpenAPI document.
This release introduces the resolved()
method on the DereferencedDocument
(the result of the locallyDereferenced()
method on an OpenAPI.Document
). A ResolvedDocument
collects information from all over the OpenAPI.Document
to form canonical definitions of the routes and endpoints found within. In a resolved document, you work with ResolvedRoute
(the canonical counterpart to the OpenAPI.PathItem
) and ResolvedEndpoint
(the canonical counterpart to the OpenAPI.Operation
).
To show the power of a resolved type, let's look at the hypothetical need to look at all parameters for a particular endpoint. To achieve this without resolved types, we need to collect parameters on the Path Item Object and combine them with those on the Operation Object. Even worse, to really get this right we would need to let the parameters of the Operation override those of the Path Item if the parameter names & locations were the same.
Before:
let document: OpenAPI.Document = ...
let aPathItem = try document
.locallyDereferenced()
.paths["/hello/world"]!
let anOperation = aPathItem.get!
// going to oversimplify here and not worry
// about collisions between the Path Item and
// Operation parameters.
let parameters = aPathItem.parameters + anOperation.parameters
After:
let document: OpenAPI.Document = ...
let anEndpoint = try document
.locallyDereferenced()
.resolved() // new
.routesByPath["/hello/world"]! // new
.get!
// no oversimplification here, this is going to get
// us the totally correct comprehensive list of
// parameters for the endpoint.
let parameters = anEndpoint.parameters