Skip to content
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

feat: support local reference in schema, support definitions keyword #676

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
26 changes: 18 additions & 8 deletions lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,18 @@ function transformDefsToComponents (jsonSchema) {
Object.keys(jsonSchema[key]).forEach(function (prop) {
jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop])
})
// definitions are not allowed in openapi schema, so we mutate them to properties
} else if (key === 'definitions') {
jsonSchema.properties = {
...transformDefsToComponents(jsonSchema[key]),
...jsonSchema.properties
}
delete jsonSchema[key]
} else if (key === '$ref') {
// replace the top-lvl path
jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas')
// replace the path for nested defs
jsonSchema[key] = jsonSchema[key].replaceAll('definitions', 'properties')
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) {
jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples)
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) {
Expand Down Expand Up @@ -402,16 +412,16 @@ function prepareOpenapiSchemas (schemas, ref) {
return Object.entries(schemas)
.reduce((res, [name, schema]) => {
const _ = { ...schema }
const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] }))

// Swagger doesn't accept $id on /definitions schemas.
// The $ids are needed by Ref() to check the URI so we need
// to remove them at the end of the process
// definitions are added by resolve but they are replace by components.schemas
delete resolved.$id
delete resolved.definitions
const resolvedRef = ref.resolve(_, { externalSchemas: [schemas] })

// swagger doesn't accept $id on components schemas
// $ids are needed only for ref.resolve to check the URI
delete resolvedRef.$id

const components = transformDefsToComponents(resolvedRef)

res[name] = resolved
res[name] = components
return res
}, {})
}
Expand Down
36 changes: 36 additions & 0 deletions test/spec/openapi/refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,39 @@ test('uses examples if has property required in body', async (t) => {
t.ok(schema.parameters)
t.same(schema.parameters[0].in, 'query')
})

test('support absolute $ref inside the json-schema definitions', async (t) => {
const fastify = Fastify()

await fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({
$id: 'NestedSchema',
definitions: {
SchemaA: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
SchemaB: {
type: 'object',
properties: {
example: { $ref: 'NestedSchema#/definitions/SchemaA' }
asc11cat marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
})
instance.post('/url1/:test', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, params: { $ref: 'NestedSchema#/definitions/SchemaA' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {})
})

await fastify.ready()

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')

// ref must be prefixed by '#/components/schemas/'
t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA')

await Swagger.validate(openapiObject)
})
106 changes: 106 additions & 0 deletions test/spec/openapi/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ const {
schemaAllOf
} = require('../../../examples/options')

const openapiOptionWithResolver = {
openapi: {},
refResolver: {
buildLocalReference: (json, baseUri, fragment, i) => {
return json.$id || `def-${i}`
}
}
}

test('support - oneOf, anyOf, allOf', async (t) => {
t.plan(2)
const fastify = Fastify()
Expand Down Expand Up @@ -785,3 +794,100 @@ test('support query serialization params', async t => {
t.equal(api.paths['/'].get.parameters[0].style, 'deepObject')
t.equal(api.paths['/'].get.parameters[0].explode, false)
})

test('support json-schema definitions keyword in schema(merge)', async t => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOptionWithResolver)

const schema = {
$id: 'NestedSchema',
properties: {
id: { type: 'string' }
},
definitions: {
SchemaA: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
SchemaB: {
type: 'object',
properties: {
id: { type: 'string' }
}
}
}
}

fastify.register(async (instance) => {
instance.addSchema(schema)
})

await fastify.ready()

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')

// definitions are getting merged to properties
t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema'])
t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties'])
t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB', 'id'])

await Swagger.validate(openapiObject)
})

test('prefer properties when merging json-schema definitions with properties', async t => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOptionWithResolver)

const schema = {
$id: 'NestedSchema',
properties: {
SchemaA: {
$id: 'SchemaA',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' }
}
}
},
definitions: {
SchemaA: {
$id: 'SchemaA',
type: 'object',
properties: {
id: { type: 'string' }
}
},
SchemaB: {
$id: 'SchemaB',
type: 'object',
properties: {
id: { type: 'string' }
}
}
}
}

fastify.register(async (instance) => {
instance.addSchema(schema)
})

await fastify.ready()

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')

t.same(openapiObject.components.schemas.NestedSchema.properties.SchemaA, {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' }
}
})
await Swagger.validate(openapiObject)
})