Skip to content

Commit

Permalink
feat: generate path params definition when missing
Browse files Browse the repository at this point in the history
  • Loading branch information
msebastianb committed Oct 9, 2023
1 parent 5b5fb42 commit e6046d2
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 4 deletions.
2 changes: 1 addition & 1 deletion lib/spec/openapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ module.exports = function (opts, cache, routes, Ref, done) {

const openapiRoute = Object.assign({}, openapiObject.paths[url])

const openapiMethod = prepareOpenapiMethod(schema, ref, openapiObject)
const openapiMethod = prepareOpenapiMethod(schema, ref, openapiObject, url)

if (route.links) {
for (const statusCode of Object.keys(route.links)) {
Expand Down
11 changes: 10 additions & 1 deletion lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const { formatParamUrl } = require('../../util/format-param-url')
const { resolveLocalRef } = require('../../util/resolve-local-ref')
const { xResponseDescription, xConsume, xExamples } = require('../../constants')
const { rawRequired } = require('../../symbols')
const { generateParamsSchema } = require('../../util/generate-params-schema')
const { hasParams } = require('../../util/match-params')

function prepareDefaultOptions (opts) {
const openapi = opts.openapi
Expand Down Expand Up @@ -359,7 +361,7 @@ function resolveResponse (fastifyResponseJson, produces, ref) {
return responsesContainer
}

function prepareOpenapiMethod (schema, ref, openapiObject) {
function prepareOpenapiMethod (schema, ref, openapiObject, url) {
const openapiMethod = {}
const parameters = []

Expand Down Expand Up @@ -407,6 +409,13 @@ function prepareOpenapiMethod (schema, ref, openapiObject) {
}
}

// If there is no schema or schema.params, we need to generate them
if (hasParams(url) && (!schema || !schema.params)) {
const schema = generateParamsSchema(url)
resolveCommonParams('path', parameters, schema.params, ref, openapiObject.definitions)
openapiMethod.parameters = parameters
}

openapiMethod.responses = resolveResponse(schema ? schema.response : null, schema ? schema.produces : null, ref)

return openapiMethod
Expand Down
2 changes: 1 addition & 1 deletion lib/spec/swagger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ module.exports = function (opts, cache, routes, Ref, done) {

const swaggerRoute = Object.assign({}, swaggerObject.paths[url])

const swaggerMethod = prepareSwaggerMethod(schema, ref, swaggerObject)
const swaggerMethod = prepareSwaggerMethod(schema, ref, swaggerObject, url)

if (route.links) {
throw new Error('Swagger (Open API v2) does not support Links. Upgrade to OpenAPI v3 (see @fastify/swagger readme)')
Expand Down
11 changes: 10 additions & 1 deletion lib/spec/swagger/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const { readPackageJson } = require('../../util/read-package-json')
const { formatParamUrl } = require('../../util/format-param-url')
const { resolveLocalRef } = require('../../util/resolve-local-ref')
const { xResponseDescription, xConsume } = require('../../constants')
const { generateParamsSchema } = require('../../util/generate-params-schema')
const { hasParams } = require('../../util/match-params')

function prepareDefaultOptions (opts) {
const swagger = opts.swagger
Expand Down Expand Up @@ -254,7 +256,7 @@ function resolveResponse (fastifyResponseJson, ref) {
return responsesContainer
}

function prepareSwaggerMethod (schema, ref, swaggerObject) {
function prepareSwaggerMethod (schema, ref, swaggerObject, url) {
const swaggerMethod = {}
const parameters = []

Expand Down Expand Up @@ -302,6 +304,13 @@ function prepareSwaggerMethod (schema, ref, swaggerObject) {
}
}

// If there is no schema or schema.params, we need to generate them
if (hasParams(url) && (!schema || !schema.params)) {
const schema = generateParamsSchema(url)
resolveCommonParams('path', parameters, schema.params, ref, swaggerObject.definitions)
swaggerMethod.parameters = parameters
}

swaggerMethod.responses = resolveResponse(schema ? schema.response : null, ref)

return swaggerMethod
Expand Down
39 changes: 39 additions & 0 deletions lib/util/generate-params-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict'

const { matchParams } = require('./match-params')

const schema = {
params: {
type: 'object',
properties: {}
}
}

const namePattern = /\{([^}]+)\}/

function paramType (param) {
return param ? 'string' : undefined
}

function paramName (param) {
return param.replace(namePattern, (_, captured) => captured)
}

// Generates default parameters schema from the given URL.
function generateParamsSchema (url) {
const params = matchParams(url)
schema.params.properties = params.reduce((acc, param) => {
acc[paramName(param)] = {
type: paramType(paramName(param))
}
return acc
}, {})

return schema
}

module.exports = {
generateParamsSchema,
paramType,
paramName
}
19 changes: 19 additions & 0 deletions lib/util/match-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict'

const paramPattern = /\{[^{}]+\}/g

function hasParams (url) {
if (!url) return false
const matches = url.match(paramPattern)
return matches !== null && matches.length > 0
}

function matchParams (url) {
if (!url) return []
return url.match(paramPattern) || []
}

module.exports = {
hasParams,
matchParams
}
114 changes: 114 additions & 0 deletions test/spec/openapi/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -911,3 +911,117 @@ test('support query serialization params', async t => {
t.equal(api.paths['/'].get.parameters[0].explode, false)
t.equal(api.paths['/'].get.parameters[0].allowReserved, true)
})

test('add default properties for url params when missing schema', async t => {
const opt = {}

const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: true
})
fastify.get('/:userId', opt, () => { })
await fastify.ready()

const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)

const definedPath = api.paths['/{userId}'].get

t.same(definedPath.parameters[0], {
in: 'path',
name: 'userId',
required: true,
schema: {
type: 'string'
}
})
})

test('add default properties for url params when missing schema.params', async t => {
const opt = {
schema: {
body: {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
}
}
}

const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: true
})
fastify.post('/:userId', opt, () => { })
await fastify.ready()

const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)

const definedPath = api.paths['/{userId}'].post

t.same(definedPath.parameters[0], {
in: 'path',
name: 'userId',
required: true,
schema: {
type: 'string'
}
})
t.same(definedPath.requestBody.content['application/json'].schema.properties, {
bio: {
type: 'string'
}
})
})

test('avoid overwriting params when schema.params is provided', async t => {
const opt = {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
},
body: {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
}
}
}

const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: true
})
fastify.post('/:userId', opt, () => { })
await fastify.ready()

const swaggerObject = fastify.swagger()

const definedPath = swaggerObject.paths['/{userId}'].post

t.same(definedPath.parameters[0], {
in: 'path',
name: 'id',
required: true,
schema: {
type: 'string'
}
})
t.same(definedPath.requestBody.content['application/json'].schema.properties, {
bio: {
type: 'string'
}
})
})
108 changes: 108 additions & 0 deletions test/spec/swagger/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,111 @@ test('support "exposeHeadRoutes" option at route level', async (t) => {
'Expected Response'
)
})

test('add default properties for url params when missing schema', async t => {
const opt = {}

const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/:userId', opt, () => { })
await fastify.ready()

const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)

const definedPath = api.paths['/{userId}'].get

t.same(definedPath.parameters[0], {
type: 'string',
required: true,
in: 'path',
name: 'userId'
})
})

test('add default properties for url params when missing schema.params', async t => {
const opt = {
schema: {
body: {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
}
}
}

const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/:userId', opt, () => { })
await fastify.ready()

const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)

const definedPath = api.paths['/{userId}'].post

t.same(definedPath.parameters[0].schema, {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
})
t.same(definedPath.parameters[1], {
in: 'path',
name: 'userId',
type: 'string',
required: true
})
})

test('avoid overwriting params when schema.params is provided', async t => {
const opt = {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
},
body: {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
}
}
}

const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/:userId', opt, () => { })
await fastify.ready()

const swaggerObject = fastify.swagger()

const definedPath = swaggerObject.paths['/{userId}'].post

t.same(definedPath.parameters[0].schema, {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
})
t.same(definedPath.parameters[1], {
in: 'path',
name: 'id',
type: 'string',
required: true
})
})
Loading

0 comments on commit e6046d2

Please sign in to comment.