Skip to content

Commit

Permalink
feat: basic support for openapi 3.1 (#109)
Browse files Browse the repository at this point in the history
this adds basic support for openapi 3.1 definitions, based on the
migration blog post from openapis.org.

the primary change is allowing `null` as a `type`, and that `type` can
be an array.

the other changes listed relating to file uploads, and
`exclusiveMinimum` aren't applicable as these aren't really supported at
all yet (#51,
#53)

there's probably a bunch of other gaps in general JSON schema support,
such as the `if` / `else` things mentioned, but there's relatively few
examples of complex `3.1.0` definitions to test against.

I stumbled across https://github.com/APIs-guru/openapi-directory looking
for samples and I've tested these changes against these definitions:
-
https://github.com/APIs-guru/openapi-directory/blob/dec74da7a6785d5d5b83bc6a4cebc07336d67ec9/APIs/vercel.com/0.0.1/openapi.yaml
-
https://github.com/APIs-guru/openapi-directory/blob/dec74da7a6785d5d5b83bc6a4cebc07336d67ec9/APIs/discourse.local/latest/openapi.yaml
-
https://github.com/APIs-guru/openapi-directory/blob/dec74da7a6785d5d5b83bc6a4cebc07336d67ec9/APIs/adyen.com/CheckoutService/70/openapi.yaml

It appears to be giving reasonable output - no compile errors at least,
and nothing obviously wrong doing a quick scan of output.

ref:
https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0
  • Loading branch information
mnahkies authored Nov 12, 2023
1 parent 2ff114a commit 60838d9
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
*/

import {describe, it, expect} from "@jest/globals"
import {unitTestInput} from "../test/input.test-utils"
import {testVersions, unitTestInput} from "../test/input.test-utils"
import {buildDependencyGraph} from "./dependency-graph"
import {getSchemaNameFromRef} from "./openapi-utils"

describe("core/dependency-graph", () => {
describe.each(testVersions)("%s - core/dependency-graph", (version) => {
it("works", async () => {
const {input} = await unitTestInput()
const {input} = await unitTestInput(version)

const graph = buildDependencyGraph(input, getSchemaNameFromRef)

Expand Down
35 changes: 27 additions & 8 deletions packages/openapi-code-generator/src/core/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,14 @@ function normalizeMediaType(mediaTypes: {[contentType: string]: MediaType} = {})
// Sometimes people pass `{}` as the MediaType for 204 responses, filter these out
.filter(([, mediaType]) => Boolean(mediaType.schema))
.map(([contentType, mediaType]) => {
return [
contentType,
{
schema: normalizeSchemaObject(mediaType.schema),
encoding: mediaType.encoding,
},
]
}))
return [
contentType,
{
schema: normalizeSchemaObject(mediaType.schema),
encoding: mediaType.encoding,
},
]
}))
}

function normalizeParameterObject(parameterObject: Parameter): IRParameter {
Expand All @@ -195,13 +195,30 @@ function normalizeSchemaObject(schemaObject: Schema | Reference): MaybeIRModel {
return schemaObject satisfies IRRef
}

// TODO: HACK: translates a type array into a a oneOf - unsure if this makes sense,
// or is the cleanest way to do it. I'm fairly sure this will work fine
// for most things though.
if (Array.isArray(schemaObject.type)) {
const nullable = Boolean(schemaObject.type.find(it => it === "null"))
return normalizeSchemaObject({
oneOf: schemaObject.type
.filter(it => it !== "null")
.map(it => normalizeSchemaObject({
...schemaObject,
type: it,
nullable,
})),
})
}

const base: IRModelBase = {
nullable: schemaObject.nullable || false,
readOnly: schemaObject.readOnly || false,
}

switch (schemaObject.type) {
case undefined:
case "null": // TODO: HACK
case "object": {
const properties = normalizeProperties(schemaObject.properties)
const allOf = normalizeAllOf(schemaObject.allOf)
Expand All @@ -222,6 +239,8 @@ function normalizeSchemaObject(schemaObject: Schema | Reference): MaybeIRModel {

return {
...base,
// TODO: HACK
nullable: base.nullable || schemaObject.type === "null",
type: "object",
allOf,
oneOf,
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-code-generator/src/core/openapi-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export interface Schema {
minProperties?: number
required?: string[] /* [] */
enum?: string[] | number[]
type?: "integer" | "number" | "string" | "boolean" | "object" | "array" /* object */
type?: "null" | "integer" | "number" | "string" | "boolean" | "object" | "array" /* object */
not?: Schema | Reference
allOf?: (Schema | Reference)[]
oneOf?: (Schema | Reference)[]
Expand Down
18 changes: 16 additions & 2 deletions packages/openapi-code-generator/src/test/input.test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ import {Input} from "../core/input"
import path from "path"
import yaml from "js-yaml"

export async function unitTestInput(skipValidation = false) {
type Version = "3.0.x" | "3.1.x"

export const testVersions = ["3.0.x", "3.1.x"] satisfies Version[]

function fileForVersion(version: Version) {
switch (version) {
case "3.0.x":
return path.join(__dirname, "unit-test-inputs-3.0.3.yaml")
case "3.1.x":
return path.join(__dirname, "unit-test-inputs-3.1.0.yaml")
default: throw new Error(`unsupported test version '${version}'`)
}
}

export async function unitTestInput(version: Version, skipValidation = false) {
const validator = await OpenapiValidator.create()

if (skipValidation) {
jest.spyOn(validator, "validate").mockResolvedValue()
}

const file = path.join(__dirname, "unit-test-inputs.yaml")
const file = fileForVersion(version)
const loader = await OpenapiLoader.create(file, validator)

return {input: new Input(loader), file}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0.0"
openapi: "3.0.3"
info:
title: unit-test-inputs
version: '0.0.1'
Expand Down
179 changes: 179 additions & 0 deletions packages/openapi-code-generator/src/test/unit-test-inputs-3.1.0.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
openapi: "3.1.0"
info:
title: unit-test-inputs
version: '0.0.1'
components:
schemas:
SimpleObject:
required:
- str
- num
- date
- datetime
- required_nullable
properties:
str:
type: string
num:
type: number
date:
type: string
format: date
datetime:
type: string
format: date-time
optional_str:
type: string
required_nullable:
type:
- string
- "null"
NamedNullableStringEnum:
type:
- string
- "null"
enum:
- ''
- one
- two
- three
- null
ObjectWithRefs:
required:
- requiredObject
properties:
optionalObject:
$ref: '#/components/schemas/SimpleObject'
requiredObject:
$ref: '#/components/schemas/SimpleObject'

ObjectWithComplexProperties:
required:
- requiredOneOf
- requiredOneOfRef
properties:
requiredOneOf:
oneOf:
- type: string
- type: number
requiredOneOfRef:
$ref: '#/components/schemas/OneOf'
optionalOneOf:
oneOf:
- type: string
- type: number
optionalOneOfRef:
$ref: '#/components/schemas/OneOf'

OneOf:
oneOf:
- type: object
properties:
strs:
type: array
items:
type: string
minItems: 1
required:
- strs
- type: array
items:
type: string
minItems: 1
- type: string

AnyOf:
type:
- number
- string

AllOf:
allOf:
- $ref: '#/components/schemas/Base'
- type: object
required:
- id
properties:
id:
type: integer
format: int64
Base:
type: object
required:
- name
properties:
name:
type: string
breed:
type: string

Ordering:
type: object
required:
- dependency1
- dependency2
properties:
dependency1:
$ref: "#/components/schemas/ZOrdering"
dependency2:
$ref: "#/components/schemas/AOrdering"

ZOrdering:
type: object
required:
- dependency1
properties:
name:
type: string
dependency1:
$ref: "#/components/schemas/AOrdering"
AOrdering:
type: object
properties:
name:
type: string

AdditionalPropertiesBool:
type: object
additionalProperties: true

AdditionalPropertiesSchema:
type: object
properties:
name:
type: string
additionalProperties:
$ref: "#/components/schemas/NamedNullableStringEnum"

Recursive:
type: object
properties:
child:
$ref: "#/components/schemas/Recursive"

OneOfAllOf:
type: object
oneOf:
- allOf:
- $ref: '#/components/schemas/AOrdering'
- $ref: '#/components/schemas/ZOrdering'

Enums:
properties:
str:
type:
- string
- "null"
enum:
- null
- "foo"
- "bar"
num:
type:
- number
- "null"
enum:
- null
- "ignored"
- 10
- 20
Loading

0 comments on commit 60838d9

Please sign in to comment.