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 : add openapi 3 support #333

Merged
merged 47 commits into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7511cb7
refactor: better file structure
climba03003 Jan 11, 2021
4d4b915
refactor: extract common function
climba03003 Jan 11, 2021
3e5c54c
refactor: split dynamic handle to swagger and openapi
climba03003 Jan 11, 2021
1b2973f
feat: add openapi 3 support
climba03003 Jan 11, 2021
9d75d9a
test: add openapi 3 test case
climba03003 Jan 11, 2021
6eb9e6a
feat: update openapi from 3.0.0 to 3.0.3
climba03003 Jan 12, 2021
ee190b1
refactor: typo
climba03003 Jan 12, 2021
0130700
refactor: more clear argument
climba03003 Jan 12, 2021
729f4dc
refactor: use map instead of reduce
climba03003 Jan 12, 2021
a8ec317
refactor: use done instead of next
climba03003 Jan 12, 2021
984e3c0
refactor: group functions
climba03003 Jan 12, 2021
460ee0d
refactor: genResponse to generateResponse
climba03003 Jan 12, 2021
ac41bef
chore: remove invalid comment
climba03003 Jan 12, 2021
aa229df
refactor: better structure
climba03003 Jan 12, 2021
2862f27
feat: generate response according to produces
climba03003 Jan 12, 2021
92d81d5
feat: use package json as default info
climba03003 Jan 12, 2021
fc6ce2c
refactor: better cache name
climba03003 Jan 12, 2021
ee1e6b4
chore: add comment to address different issue
climba03003 Jan 12, 2021
5453c3b
feat: add oneOf, allOf, anyOf support in query, header, path, formData
climba03003 Jan 12, 2021
ba31090
Fix type of StaticDocumentSpec.document (#328)
radzom Jan 14, 2021
a9f4d3b
refactor: better file structure
climba03003 Jan 11, 2021
37cc39b
refactor: extract common function
climba03003 Jan 11, 2021
00cb007
refactor: split dynamic handle to swagger and openapi
climba03003 Jan 11, 2021
fb23cdc
feat: add openapi 3 support
climba03003 Jan 11, 2021
d1660c7
test: add openapi 3 test case
climba03003 Jan 11, 2021
bad28b6
feat: update openapi from 3.0.0 to 3.0.3
climba03003 Jan 12, 2021
abc1a90
refactor: typo
climba03003 Jan 12, 2021
780ed14
refactor: more clear argument
climba03003 Jan 12, 2021
4c6806a
refactor: use map instead of reduce
climba03003 Jan 12, 2021
c11a936
refactor: use done instead of next
climba03003 Jan 12, 2021
cf9bec5
refactor: group functions
climba03003 Jan 12, 2021
c66fd2c
refactor: genResponse to generateResponse
climba03003 Jan 12, 2021
4afe782
chore: remove invalid comment
climba03003 Jan 12, 2021
dd7d500
refactor: better structure
climba03003 Jan 12, 2021
66b0fca
feat: generate response according to produces
climba03003 Jan 12, 2021
85f5136
feat: use package json as default info
climba03003 Jan 12, 2021
18a31f1
refactor: better cache name
climba03003 Jan 12, 2021
f291f31
chore: add comment to address different issue
climba03003 Jan 12, 2021
4edb67b
feat: add oneOf, allOf, anyOf support in query, header, path, formData
climba03003 Jan 12, 2021
c0439b6
refactor: better argument name
climba03003 Jan 14, 2021
76f5858
feat: add cookies schema support for openapi 3
climba03003 Jan 14, 2021
30ec49e
fix: typo
climba03003 Jan 14, 2021
f09965d
Merge branch 'feat-openapi-3' of https://github.com/climba03003/fasti…
climba03003 Jan 14, 2021
b405dd4
chore: add openapi typings
climba03003 Jan 18, 2021
33bed43
docs: add openapi support docs
climba03003 Jan 18, 2021
03777ad
docs: update as suggestion
climba03003 Jan 18, 2021
35a9be8
fix: openapi
climba03003 Jan 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const fp = require('fastify-plugin')

const setup = { dynamic: require('./dynamic'), static: require('./static') }
const setup = { dynamic: require('./lib/dynamic'), static: require('./lib/static') }

function fastifySwagger (fastify, opts, next) {
opts = opts || {}
Expand Down
36 changes: 36 additions & 0 deletions lib/dynamic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

const { addHook } = require('./util')
const buildSwagger = require('./swagger')
const buildOpenapi = require('./openapi')

module.exports = function (fastify, opts, next) {
const { routes, Ref } = addHook(fastify)

opts = Object.assign({}, {
exposeRoute: false,
hiddenTag: 'X-HIDDEN',
stripBasePath: true,
openapi: {},
swagger: {},
transform: null
}, opts || {})

if (opts.exposeRoute === true) {
const prefix = opts.routePrefix || '/documentation'
fastify.register(require('./routes'), { prefix })
}

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

if (Object.keys(opts.openapi).length > 0 && opts.openapi.constructor === Object) {
fastify.decorate('swagger', buildOpenapi(opts, routes, Ref, cache, next))
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
} else {
fastify.decorate('swagger', buildSwagger(opts, routes, Ref, cache, next))
}

next()
}
262 changes: 262 additions & 0 deletions lib/openapi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
'use strict'

const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const { formatParamUrl, plainJsonObjectToSwagger2, swagger2ParametersToOpenapi3, stripBasePathByServers } = require('./util')

module.exports = function (opts, routes, Ref, cache, next) {
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
let ref

const info = opts.openapi.info || null
const servers = opts.openapi.servers || null
const components = opts.openapi.components || null
const tags = opts.openapi.tags || null
const externalDocs = opts.openapi.externalDocs || null

const stripBasePath = opts.stripBasePath
const transform = opts.transform
const hiddenTag = opts.hiddenTag
const extensions = []

for (const [key, value] of Object.entries(opts.openapi)) {
if (key.startsWith('x-')) {
extensions.push([key, value])
}
}

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

const swaggerObject = {}
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
let pkg

try {
pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')))
} catch (err) {
return next(err)
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
}

// Base Openapi info
// this info is displayed in the swagger file
// in the same order as here
swaggerObject.openapi = '3.0.0'
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
if (info) {
swaggerObject.info = info
} else {
swaggerObject.info = {
version: '1.0.0',
title: pkg.name || ''
}
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
}
if (servers) {
swaggerObject.servers = servers
}
if (components) {
swaggerObject.components = Object.assign({}, components, { schemas: Object.assign({}, components.schemas) })
} else {
swaggerObject.components = { schemas: {} }
}
if (tags) {
swaggerObject.tags = tags
}
if (externalDocs) {
swaggerObject.externalDocs = externalDocs
}

for (const [key, value] of extensions) {
swaggerObject[key] = value
}

ref = Ref()
swaggerObject.components.schemas = {
...swaggerObject.components.schemas,
...(ref.definitions().definitions)
}

// Swagger doesn't accept $id on /definitions schemas.
// The $ids are needed by Ref() to check the URI so we need
// to remove them at the end of the process
Object.values(swaggerObject.components.schemas)
.forEach(_ => { delete _.$id })

swaggerObject.paths = {}

for (const route of routes) {
const schema = transform
? transform(route.schema)
: route.schema

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

if (schema && schema.tags && schema.tags.includes(hiddenTag)) {
continue
}

const path = stripBasePath
? stripBasePathByServers(route.url, swaggerObject.servers)
: route.url
const url = formatParamUrl(path)

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

const swaggerMethod = {}
const parameters = []

// route.method should be either a String, like 'POST', or an Array of Strings, like ['POST','PUT','PATCH']
const methods = typeof route.method === 'string' ? [route.method] : route.method

for (const method of methods) {
swaggerRoute[method.toLowerCase()] = swaggerMethod
}

// 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.operationId) {
swaggerMethod.operationId = schema.operationId
}

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

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

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

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

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

if (schema.body) {
swaggerMethod.requestBody = {
content: {}
}
getBodyParams(swaggerMethod.requestBody.content, schema.body, schema.consumes)
}

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
}

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

for (const key of Object.keys(schema)) {
if (key.startsWith('x-')) {
swaggerMethod[key] = schema[key]
}
}
}

swaggerMethod.responses = genResponse(schema ? schema.response : null)

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

function getBodyParams (parameters, body, consumes) {
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
const bodyResolved = ref.resolve(body)

if ((Array.isArray(consumes) && consumes.length === 0) || typeof consumes === 'undefined') {
consumes = ['application/json']
}

consumes.forEach((consume) => {
parameters[consume] = {
schema: bodyResolved
}
})
}

function getQueryParams (parameters, query) {
const resolved = ref.resolve(query)
const add = plainJsonObjectToSwagger2('query', resolved, swaggerObject.components.schemas)
add.forEach(_ => parameters.push(swagger2ParametersToOpenapi3(_)))
}

function getPathParams (parameters, path) {
const resolved = ref.resolve(path)
const add = plainJsonObjectToSwagger2('path', resolved, swaggerObject.components.schemas)
add.forEach(_ => parameters.push(swagger2ParametersToOpenapi3(_)))
}

function getHeaderParams (parameters, headers) {
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
const resolved = ref.resolve(headers)
const add = plainJsonObjectToSwagger2('header', resolved, swaggerObject.components.schemas)
add.forEach(_ => parameters.push(swagger2ParametersToOpenapi3(_)))
}

// https://swagger.io/docs/specification/2-0/describing-responses/
function genResponse (fastifyResponseJson) {
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
// if the user does not provided an out schema
if (!fastifyResponseJson) {
return { 200: { description: 'Default Response' } }
}

const responsesContainer = {}

Object.keys(fastifyResponseJson).forEach(key => {
// 2xx is not supported by swagger
climba03003 marked this conversation as resolved.
Show resolved Hide resolved

const rawJsonSchema = fastifyResponseJson[key]
const resolved = ref.resolve(rawJsonSchema)

const content = {
'application/json': {}
}
climba03003 marked this conversation as resolved.
Show resolved Hide resolved

content['application/json'] = {
schema: resolved
}

responsesContainer[key] = {
content,
description: rawJsonSchema.description || 'Default Response'
}
})

return responsesContainer
}
}
}
4 changes: 2 additions & 2 deletions routes.js → lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ function fastifySwagger (fastify, opts, next) {

// serve swagger-ui with the help of fastify-static
fastify.register(fastifyStatic, {
root: path.join(__dirname, 'static'),
root: path.join(__dirname, '..', 'static'),
prefix: staticPrefix,
decorateReply: false
})

fastify.register(fastifyStatic, {
root: opts.baseDir || __dirname,
root: opts.baseDir || path.join(__dirname, '..'),
serve: false
})

Expand Down
File renamed without changes.
Loading