Skip to content

Commit

Permalink
feat: generate path params definition when missing (#761)
Browse files Browse the repository at this point in the history
* feat: generate path params definition when missing

* chore: rename schema to schemaGenerated

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* chore: rename schema to schemaGenerated swagger

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* chore: remove paramType call

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* chore: remove paramType function

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* chore: replace match with test on url

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* fix: remove paramType from exports and tests

* chore: use strictSame instead of same where needed

* fix: broken website url in readme

* chore(docs): add route parameters documentation

---------

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
  • Loading branch information
msebastianb and Eomm committed Oct 15, 2023
1 parent 5b5fb42 commit a1963e5
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 5 deletions.
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)

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) {
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
}

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

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

0 comments on commit a1963e5

Please sign in to comment.