Skip to content

Releases: mattpolzin/OpenAPIKit

Version 2.0.0 Alpha 1

16 Aug 01:16
5fdfa37
Compare
Choose a tag to compare
Version 2.0.0 Alpha 1 Pre-release
Pre-release

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 a dereferenced(in:) method that takes OpenAPI.Components as its argument and performs a recursive deference operation on self -- the same behavior exposed on OpenAPI.Document via its locallyDereferenced() method.
  • Arrays of schema fragments ([JSONSchemaFragment]) like those found under the JSONSchema all(of:) case can be resolved into a JSONSchema representation using the array extension resolved(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 the dereferenced(in:) method of the originating OpenAPI type. For example, to get a DereferencedPathItem, you use the OpenAPI.PathItem dereferenced(in:) method.
  • DereferencedJSONSchema no longer has an all(of:) case. Instead, part of dereferencing a schema is now the process of resolving an all(of:) schema. More on this below.

Changes

  • OpenAPI.Components forceDereference(_:) was renamed to lookup(_:). More on this below.
  • DereferencedJSONSchema underlyingJSONSchema property was renamed to jsonSchema to avoid the misconception that this property stores the JSONSchema that produced the given DereferencedJSONSchema. In reality, this property will build the JSONSchema representing the given DereferencedJSONSchema.
  • JSONSchema.Context, DereferencedJSONSchema.Context, and JSONSchemaFragment.GeneralContext have been renamed to JSONSchema.CoreContext, DereferencedJSONSchema.CoreContext, and JSONSchemaFragment.CoreContext to align and settle on a better name for "the context shared by most cases." With that, the JSONSchema generalContext property has been renamed to coreContext.
  • 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 the required 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, the Validatable 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 that URL is encoded as a string for compatibility with the broadest range of encoders and therefore URL is not Validatable. You can still use String as a validation subject or write validations in terms of the OpenAPIKit types that contain URL properties. See the full Validation Documentation for more.
  • The OpenAPI.Server type's url property was renamed urlTemplate and its type changed from a Foundation URL to a URLTemplate. 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 Foundation URL from a URLTemplate via the url property but only urls without any variable placeholders can be turned into Foundation URLs directly.
  • The OpenAPI.Content type's schema 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

15 Aug 16:38
9c08102
Compare
Choose a tag to compare

The OpenAPI.Server.Variable type incorrectly required that the enum property be defined (on decoding). This has now been fixed.

Statusfied

03 Aug 15:22
4b5f646
Compare
Choose a tag to compare

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

01 Aug 01:25
e0b5e64
Compare
Choose a tag to compare

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

29 Jul 01:17
05258e4
Compare
Choose a tag to compare

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

28 Jul 00:32
e347c68
Compare
Choose a tag to compare

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

25 Jul 23:22
31daf62
Compare
Choose a tag to compare

Fixes #98 wherein an allOf Schema Object with a Reference Object would not parse and could not be represented in OpenAPIKit.

⚠️ Breaking Change ⚠️
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

15 Jul 01:13
b0f5ab3
Compare
Choose a tag to compare

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

13 Jul 05:44
bc8b663
Compare
Choose a tag to compare

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

03 Jul 04:02
201586d
Compare
Choose a tag to compare

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