Skip to content

Commit

Permalink
introducing static mode for the fastify-swagger (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
PavelPolyakov authored and delvedor committed Mar 15, 2018
1 parent 0f89566 commit 8ae17e5
Show file tree
Hide file tree
Showing 9 changed files with 621 additions and 306 deletions.
77 changes: 52 additions & 25 deletions README.md
Expand Up @@ -86,53 +86,80 @@ fastify.ready(err => {
fastify.swagger()
})
```

<a name="api"></a>
## API
#### register options
```js
{
swagger: {
info: {
title: String,
description: String,
version: String
},
host: String,
schemes: [ String ],
consumes: [ String ],
produces: [ String ],
securityDefinitions: Object
<a name="register.options"></a>
### register options
<a name="modes"></a>
#### modes
`fastify-swagger` supports two registration modes `dynamic` and `static`:
<a name="mode.dynamic"></a>
##### dynamic
`dynamic` mode is the default one, if you use the plugin this way - swagger specification would be gathered from your routes definitions.
```js
{
swagger: {
info: {
title: String,
description: String,
version: String
},
host: String,
schemes: [ String ],
consumes: [ String ],
produces: [ String ],
securityDefinitions: Object
}
}
}
```
*All the above parameters are optional.*
You can use all the properties of the [swagger specification](https://swagger.io/specification/), if you find anything missing, please open an issue or a pr!
```

<a name="options"></a>
#### swagger options
Calling `fastify.swagger` will return to you a JSON object representing your api, if you pass `{ yaml: true }` to `fastify.swagger`, it will return you a yaml string.
*All the above parameters are optional.*
You can use all the properties of the [swagger specification](https://swagger.io/specification/), if you find anything missing, please open an issue or a pr!

Example of the `fastify-swagger` usage in the `dynamic` mode is available [here](examples/dynamic.js).
<a name="mode.static"></a>

If you pass `{ exposeRoute: true }` the plugin will expose the documentation with the following apis:
##### static
`static` mode should be configured explicitly. In this mode `fastify-swagger` serves given specification, you should craft it yourselfe.
```js
{
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml'
}
}
```
Example of the `fastify-swagger` usage in the `static` mode is available [here](examples/static-file.js).
<a name="additional"></a>
#### additional
If you pass `{ exposeRoute: true }` during the registration the plugin will expose the documentation with the following apis:

| url | description |
|-------|----------------|
|`'/documentation/json'` | the json object representing the api |
|`'/documentation/yaml'` | the yaml object representing the api |
|`'/documentation'` | the swagger ui |

<a name="swagger.options"></a>
### swagger options
Calling `fastify.swagger` will return to you a JSON object representing your api, if you pass `{ yaml: true }` to `fastify.swagger`, it will return you a yaml string.

<a name="hide"></a>
#### Hide a route
### Hide a route
Sometimes you may need to hide a certain route from the documentation, just pass `{ hide: true }` to the schema object inside the route declaration.

#### Security
<a name="security"></a>
### Security
Global security definitions and route level security provide documentation only. It does not implement authentication nor route security for you. Once your authentication is implemented, along with your defined security, users will be able to successfully authenticate and interact with your API using the user interfaces of the documentation.

<a name="anknowledgements"></a>
## Acknowledgements

This project is kindly sponsored by:
- [nearForm](http://nearform.com)
- [LetzDoIt](http://www.letzdoitapp.com/)

<a name="license"></a>
## License

Licensed under [MIT](./LICENSE).
290 changes: 290 additions & 0 deletions dynamic.js
@@ -0,0 +1,290 @@
'use strict'

const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')

module.exports = function (fastify, opts, next) {
fastify.decorate('swagger', swagger)

const routes = []

fastify.addHook('onRoute', (routeOptions) => {
routes.push(routeOptions)
})

opts = opts || {}

opts.swagger = opts.swagger || {}

const info = opts.swagger.info || null
const host = opts.swagger.host || null
const schemes = opts.swagger.schemes || null
const consumes = opts.swagger.consumes || null
const produces = opts.swagger.produces || null
const basePath = opts.swagger.basePath || null
const securityDefinitions = opts.swagger.securityDefinitions || null

if (opts.exposeRoute === true) {
fastify.register(require('./routes'))
}

const cache = {
swaggerObject: null,
swaggerString: null
}

function swagger (opts) {
if (opts && opts.yaml) {
if (cache.swaggerString) return cache.swaggerString
} else {
if (cache.swaggerObject) return cache.swaggerObject
}

const swaggerObject = {}
var pkg

try {
pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')))
} catch (err) {
return next(err)
}

// Base swagger info
// this info is displayed in the swagger file
// in the same order as here
swaggerObject.swagger = '2.0'
if (info) {
swaggerObject.info = info
} else {
swaggerObject.info = {
version: '1.0.0',
title: pkg.name || ''
}
}
if (host) swaggerObject.host = host
if (schemes) swaggerObject.schemes = schemes
if (basePath) swaggerObject.basePath = basePath
if (consumes) swaggerObject.consumes = consumes
if (produces) swaggerObject.produces = produces
if (securityDefinitions) {
swaggerObject.securityDefinitions = securityDefinitions
}

swaggerObject.paths = {}
for (var route of routes) {
const url = formatParamUrl(route.url)
const method = route.method.toLowerCase()
const schema = route.schema

const swaggerRoute = swaggerObject.paths[url] || {}

if (schema && schema.hide) {
continue
}

const swaggerMethod = swaggerRoute[method] = {}
const parameters = []

// All the data the user can give us, is via the schema object
if (schema) {
// the resulting schema will be in this order
if (schema.summary) {
swaggerMethod.summary = schema.summary
}

if (schema.description) {
swaggerMethod.description = schema.description
}

if (schema.tags) {
swaggerMethod.tags = schema.tags
}

if (schema.consumes) {
swaggerMethod.consumes = schema.consumes
}

if (schema.querystring) {
getQueryParams(parameters, schema.querystring)
}

if (schema.body) {
const consumesAllFormOnly =
consumesFormOnly(schema) || consumesFormOnly(swaggerObject)
consumesAllFormOnly
? getFormParams(parameters, schema.body)
: getBodyParams(parameters, schema.body)
}

if (schema.params) {
getPathParams(parameters, schema.params)
}

if (schema.headers) {
getHeaderParams(parameters, schema.headers)
}

if (parameters.length) {
swaggerMethod.parameters = parameters
}

if (schema.deprecated) {
swaggerMethod.deprecated = schema.deprecated
}

if (schema.security) {
swaggerMethod.security = schema.security
}
}

swaggerMethod.responses = genResponse(schema ? schema.response : null)
if (swaggerRoute) {
swaggerObject.paths[url] = swaggerRoute
}
}

if (opts && opts.yaml) {
const swaggerString = yaml.safeDump(swaggerObject, { skipInvalid: true })
cache.swaggerString = swaggerString
return swaggerString
}

cache.swaggerObject = swaggerObject
return swaggerObject
}

next()
}

function consumesFormOnly (schema) {
const consumes = schema.consumes
return (
consumes &&
consumes.length === 1 &&
(consumes[0] === 'application/x-www-form-urlencoded' ||
consumes[0] === 'multipart/form-data')
)
}

function getQueryParams (parameters, query) {
if (query.type && query.properties) {
// for the shorthand querystring declaration
return getQueryParams(parameters, query.properties)
}

Object.keys(query).forEach(prop => {
const obj = query[prop]
const param = obj
param.name = prop
param.in = 'query'
parameters.push(param)
})
}

function getBodyParams (parameters, body) {
const param = {}
param.name = 'body'
param.in = 'body'
param.schema = body
parameters.push(param)
}

function getFormParams (parameters, body) {
const formParamsSchema = body.properties
if (formParamsSchema) {
Object.keys(formParamsSchema).forEach(name => {
const param = formParamsSchema[name]
delete param.$id
param.in = 'formData'
param.name = name
parameters.push(param)
})
}
}

function getPathParams (parameters, params) {
if (params.type && params.properties) {
// for the shorthand querystring declaration
return getPathParams(parameters, params.properties)
}

Object.keys(params).forEach(p => {
const param = {}
param.name = p
param.in = 'path'
param.required = true
param.description = params[p].description
param.type = params[p].type
parameters.push(param)
})
}

function getHeaderParams (parameters, headers) {
if (headers.type && headers.properties) {
// for the shorthand querystring declaration
const headerProperties = Object.keys(headers.properties).reduce((acc, h) => {
const required = (headers.required && headers.required.indexOf(h) >= 0) || false
const newProps = Object.assign({}, headers.properties[h], { required })
return Object.assign({}, acc, { [h]: newProps })
}, {})

return getHeaderParams(parameters, headerProperties)
}

Object.keys(headers).forEach(h =>
parameters.push({
name: h,
in: 'header',
required: headers[h].required,
description: headers[h].description,
type: headers[h].type
})
)
}

function genResponse (response) {
// if the user does not provided an out schema
if (!response) {
return { 200: { description: 'Default Response' } }
}

// remove previous references
response = Object.assign({}, response)

Object.keys(response).forEach(key => {
if (response[key].type) {
var rsp = response[key]
var description = response[key].description
var headers = response[key].headers
response[key] = {
schema: rsp
}
response[key].description = description || 'Default Response'
if (headers) response[key].headers = headers
}

if (!response[key].description) {
response[key].description = 'Default Response'
}
})

return response
}

// The swagger standard does not accept the url param with ':'
// so '/user/:id' is not valid.
// This function converts the url in a swagger compliant url string
// => '/user/{id}'
function formatParamUrl (url) {
var start = url.indexOf('/:')
if (start === -1) return url

var end = url.indexOf('/', ++start)

if (end === -1) {
return url.slice(0, start) + '{' + url.slice(++start) + '}'
} else {
return formatParamUrl(url.slice(0, start) + '{' + url.slice(++start, end) + '}' + url.slice(end))
}
}
File renamed without changes.

0 comments on commit 8ae17e5

Please sign in to comment.