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 10, 2023
1 parent 5b5fb42 commit 74a53c4
Show file tree
Hide file tree
Showing 11 changed files with 503 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 ((!schema || !schema.params) && hasParams(url)) {
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 ((!schema || !schema.params) && hasParams(url)) {
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
40 changes: 40 additions & 0 deletions lib/util/generate-params-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

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

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

function paramType () {
return 'string'
}

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

// Generates default parameters schema from the given URL. (ex: /example/{userId})
function generateParamsSchema (url) {
const params = matchParams(url)
const schema = {
params: {
type: 'object',
properties: {}
}
}

schema.params.properties = params.reduce((acc, param) => {
const name = paramName(param)
acc[name] = {
type: paramType()
}
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
}
30 changes: 30 additions & 0 deletions test/spec/openapi/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -862,3 +862,33 @@ test('path params on relative url', async (t) => {
}
])
})

test('verify generated path param definition with route prefixing', async (t) => {
const opts = {
schema: {}
}

const fastify = Fastify()

await fastify.register(fastifySwagger, openapiRelativeOptions)
await fastify.register(function (app, _, done) {
app.get('/:userId', opts, () => {})

done()
}, { prefix: '/v1' })
await fastify.ready()

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

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

t.same(definedPath.parameters, [{
schema: {
type: 'string'
},
in: 'path',
name: 'userId',
required: true
}])
})
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'
}
})
})
28 changes: 28 additions & 0 deletions test/spec/swagger/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,31 @@ test('security querystrings ignored when declared in security and securityScheme
t.notOk(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'apiKey')))
t.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'authKey')))
})

test('verify generated path param definition with route prefixing', async (t) => {
const opts = {
schema: {}
}

const fastify = Fastify()

await fastify.register(fastifySwagger, swaggerOption)
await fastify.register(function (app, _, done) {
app.get('/:userId', opts, () => {})

done()
}, { prefix: '/v1' })
await fastify.ready()

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

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

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

0 comments on commit 74a53c4

Please sign in to comment.