Skip to content

Commit

Permalink
Add support for nullish option in OpenAPI 3.0.3 (#10)
Browse files Browse the repository at this point in the history
- Handle nullish with a union of Typebox's Type.Null() and the actual type so that the schema accepts null as value
  • Loading branch information
iivo-k authored Aug 19, 2024
1 parent d6ddeba commit b4782a2
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 23 deletions.
73 changes: 50 additions & 23 deletions src/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,11 @@ export const write = async (source, opts = {}) => {
}

function writeType(schema, isRequired = false) {
const isNullable = !!schema.nullable
schema = cleanupSchema(schema)

if (schema[kRef]) {
writeRef(schema, isRequired)
writeRef(schema, isRequired, isNullable)
return
}

Expand All @@ -167,20 +168,20 @@ export const write = async (source, opts = {}) => {
}

if (schema.const) {
writeLiteral(schema, isRequired)
writeLiteral(schema, isRequired, isNullable)
return
}

if (schema.enum) {
writeCompound({
...schema,
anyOf: schema.enum,
}, isRequired)
}, isRequired, isNullable)
return
}

if (schema.anyOf || schema.allOf || schema.oneOf) {
writeCompound(schema, isRequired)
writeCompound(schema, isRequired, isNullable)
return
}

Expand All @@ -194,21 +195,39 @@ export const write = async (source, opts = {}) => {
}

if (schema.type === 'object') {
writeObject(schema, isRequired)
writeObject(schema, isRequired, isNullable)
return
}

if (schema.type === 'array') {
writeArray(schema, isRequired)
writeArray(schema, isRequired, isNullable)
return
}

if (schema.type in scalarTypes) {
writeScalar(schema, isRequired)
writeScalar(schema, isRequired, isNullable)
}
}

function writeLiteral(schema, isRequired = false) {
/**
* @param {boolean} isRequired
* @param {boolean} isNullable
*/
function startNullish(isRequired, isNullable) {
if (!isRequired) w.write('T.Optional(')
if (isNullable) w.write('T.Union([T.Null(), ')
}

/**
* @param {boolean} isRequired
* @param {boolean} isNullable
*/
function endNullish(isRequired, isNullable) {
if (isNullable) w.write('])')
if (!isRequired) w.write(')')
}

function writeLiteral(schema, isRequired = false, isNullable = false) {
let { const: value } = schema

let options = extractSchemaOptions(schema)
Expand All @@ -221,10 +240,12 @@ export const write = async (source, opts = {}) => {
options = ''
}

w.write(`${isRequired ? '' : 'T.Optional('}T.Literal(${value}${options})${isRequired ? '' : ')'}`)
startNullish(isRequired, isNullable)
w.write(`T.Literal(${value}${options})`)
endNullish(isRequired, isNullable)
}

function writeRef(schema, isRequired = false) {
function writeRef(schema, isRequired = false, isNullable = false) {
let options = extractSchemaOptions(schema)
if (Object.keys(options).length) {
options = `,${JSON.stringify(options)}`
Expand All @@ -233,13 +254,15 @@ export const write = async (source, opts = {}) => {
}

const value = `CloneType(${schema[kRef]}${options})`
w.write(`${isRequired ? '' : 'T.Optional('}${value}${isRequired ? '' : ')'}`)
startNullish(isRequired, isNullable)
w.write(`${value}`)
endNullish(isRequired, isNullable)
}

function writeCompound(schema, isRequired = false) {
function writeCompound(schema, isRequired = false, isNullable = false) {
const { enum: _, type, anyOf, allOf, oneOf, ...options } = schema

if (!isRequired) w.write('T.Optional(')
startNullish(isRequired, isNullable)

const compoundType = anyOf
? 'T.Union' // anyOf
Expand Down Expand Up @@ -271,13 +294,13 @@ export const write = async (source, opts = {}) => {

w.write(')')

if (!isRequired) w.write(')')
endNullish(isRequired, isNullable)
}

function writeObject(schema, isRequired = false) {
function writeObject(schema, isRequired = false, isNullable = false) {
const { type, properties = {}, required = [], ...options } = schema

if (!isRequired) w.write('T.Optional(')
startNullish(isRequired, isNullable)

let optionsString
const optionsKeys = Object.keys(options)
Expand Down Expand Up @@ -330,14 +353,16 @@ export const write = async (source, opts = {}) => {
w.write(')')
}

if (!isRequired) w.write(')')
endNullish(isRequired, isNullable)
}

function writeScalar(schema, isRequired = false) {
function writeScalar(schema, isRequired = false, isNullable = false) {
let { type, ...options } = schema

if (type === 'string' && options?.format === 'binary') {
w.write(`${isRequired ? '' : 'T.Optional('}Binary()${isRequired ? '' : ')'}`)
startNullish(isRequired, isNullable)
w.write('Binary()')
endNullish(isRequired, isNullable)
return
}

Expand All @@ -347,13 +372,15 @@ export const write = async (source, opts = {}) => {
options = ''
}

w.write(`${isRequired ? '' : 'T.Optional('}T.${scalarTypes[type]}(${options})${isRequired ? '' : ')'}`)
startNullish(isRequired, isNullable)
w.write(`T.${scalarTypes[type]}(${options})`)
endNullish(isRequired, isNullable)
}

function writeArray(schema, isRequired = false) {
function writeArray(schema, isRequired = false, isNullable = false) {
const { type, items, ...options } = schema

if (!isRequired) w.write('T.Optional(')
startNullish(isRequired, isNullable)

const isArray = Array.isArray(items)

Expand Down Expand Up @@ -386,7 +413,7 @@ export const write = async (source, opts = {}) => {

w.write(')')

if (!isRequired) w.write(')')
endNullish(isRequired, isNullable)
}

function buildSchema(paths, pathKey, method) {
Expand Down
10 changes: 10 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,13 @@ test('parse some openapi examples', async (t) => {
await write('https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml')
})
})

test('nullable test', async (t) => {
await writeFile('./tmp/test-nullable.yaml.js', await write('./test/test-nullable.yaml'))
t.assert.snapshot(await readFile('./tmp/petstore.yaml.js', 'utf8'))
const { components } = await import('../tmp/test-nullable.yaml.js')
assert.deepEqual(components.schemas.Test, Type.Union([Type.Null(), Type.Object({
testStr: Type.Optional(Type.Union([Type.Null(), Type.String({ minLength: 2, maxLength: 2 })])),
testArr: Type.Union([Type.Null(), Type.Array(Type.Number())]),
})]))
})
Loading

0 comments on commit b4782a2

Please sign in to comment.