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: generate path params definition when missing #761

Merged
merged 10 commits into from
Oct 15, 2023
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 schemaGenerated = generateParamsSchema(url)
resolveCommonParams('path', parameters, schemaGenerated.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)
Eomm marked this conversation as resolved.
Show resolved Hide resolved

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 schemaGenerated = generateParamsSchema(url)
resolveCommonParams('path', parameters, schemaGenerated.params, ref, swaggerObject.definitions)
swaggerMethod.parameters = parameters
}

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

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

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

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

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

// Generates default parameters schema from the given URL. (ex: /example/{userId})
function generateParamsSchema (url) {
Eomm marked this conversation as resolved.
Show resolved Hide resolved
const params = matchParams(url)
const schema = {
params: {
type: 'object',
properties: {}
}
}

schema.params.properties = params.reduce((acc, param) => {
const name = paramName(param)
acc[name] = {
type: 'string'
}
return acc
}, {})

return schema
Eomm marked this conversation as resolved.
Show resolved Hide resolved
}

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

const paramPattern = /\{[^{}]+\}/g
Eomm marked this conversation as resolved.
Show resolved Hide resolved

function hasParams (url) {
if (!url) return false
return paramPattern.test(url)
}

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], {
msebastianb marked this conversation as resolved.
Show resolved Hide resolved
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
Loading