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
141 changes: 140 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ Please specify `type: 'null'` for the response otherwise Fastify itself will fai
<a name="route.openapi"></a>
#### OpenAPI Parameter Options

**Note:** OpenAPI's terminology differs from Fastify's. OpenAPI uses "parameter" to refer to parts of a request that in [Fastify's validation documentation](https://www.fastify.io/docs/latest/Validation-and-Serialization/#validation) are called "querystring", "params", and "headers".
**Note:** OpenAPI's terminology differs from Fastify's. OpenAPI uses "parameter" to refer to parts of a request that in [Fastify's validation documentation](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/) are called "querystring", "params", and "headers".

OpenAPI provides some options beyond those provided by the [JSON schema specification](https://json-schema.org/specification.html) for specifying the shape of parameters. A prime example of this is the `collectionFormat` option for specifying how to encode parameters that should be handled as arrays of values.

Expand Down Expand Up @@ -698,6 +698,145 @@ Will generate this in the OpenAPI v3 schema's `paths`:
}
```

##### Route parameters

Route parameters in Fastify are called params, these are values included in the URL of the requests, for example:

```js
fastify.route({
method: 'GET',
url: '/:id',
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
}
},
handler (request, reply) {
reply.send(request.params.id)
}
})
```

Will generate this in the Swagger (OpenAPI v2) schema's `paths`:

```json
{
"/{id}": {
"get": {
"parameters": [
{
"type": "string",
"description": "user id",
"required": true,
"in": "path",
"name": "id"
}
],
"responses": {
"200": {
"description": "Default Response"
}
}
}
}
}
```

Will generate this in the OpenAPI v3 schema's `paths`:

```json
{
"/{id}": {
"get": {
"parameters": [
{
"schema": {
"type": "string"
},
"in": "path",
"name": "id",
"required": true,
"description": "user id"
}
],
"responses": {
"200": {
"description": "Default Response"
}
}
}
}
}
```

Whether `params` is not present in the schema, or a schema is not provided, parameters are automatically generated, for example:

```js
fastify.route({
method: 'POST',
url: '/:id',
handler (request, reply) {
reply.send(request.params.id)
}
})
```

Will generate this in the Swagger (OpenAPI v2) schema's `paths`:

```json
{
"/{id}": {
"get": {
"parameters": [
{
"type": "string",
"required": true,
"in": "path",
"name": "id"
}
],
"responses": {
"200": {
"description": "Default Response"
}
}
}
}
}
```

Will generate this in the OpenAPI v3 schema's `paths`:

```json
{
"/{id}": {
"get": {
"parameters": [
{
"schema": {
"type": "string"
},
"in": "path",
"name": "id",
"required": true
}
],
"responses": {
"200": {
"description": "Default Response"
}
}
}
}
}
```

<a name="route.links"></a>
#### Links

Expand Down
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
35 changes: 35 additions & 0 deletions lib/util/generate-params-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'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,
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
}])
})
Loading