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 43 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
413 changes: 0 additions & 413 deletions dynamic.js

This file was deleted.

4 changes: 1 addition & 3 deletions examples/test-package.json
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
{
"version": "3.1.0"
}
{}
5 changes: 3 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FastifyPlugin } from 'fastify';
import * as SwaggerSchema from 'swagger-schema-official';
import { OpenAPIV2, OpenAPIV3 } from 'openapi-types';

declare module 'fastify' {
interface FastifyInstance {
Expand Down Expand Up @@ -68,12 +69,12 @@ export interface StaticPathSpec {
}

export interface StaticDocumentSpec {
document: string;
document: OpenAPIV2.Document | OpenAPIV3.Document;
}

export interface FastifyStaticSwaggerOptions extends FastifySwaggerOptions {
mode: 'static';
specification: StaticPathSpec | StaticDocumentSpec;
}

export default fastifySwagger;
export default fastifySwagger;
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
31 changes: 31 additions & 0 deletions lib/dynamic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict'

const { addHook, resolveSwaggerFunction } = require('./dynamicUtil')

module.exports = function (fastify, opts, done) {
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 = {
object: null,
string: null
}

const swagger = resolveSwaggerFunction(opts, routes, Ref, cache, done)
fastify.decorate('swagger', swagger)

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

const fs = require('fs')
const path = require('path')
const Ref = require('json-schema-resolver')

function addHook (fastify) {
const routes = []
const sharedSchemasMap = new Map()

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

fastify.addHook('onRegister', async (instance) => {
// we need to wait the ready event to get all the .getSchemas()
// otherwise it will be empty
// TODO: better handle for schemaId
// when schemaId is the same in difference instance
// the latter will lost
instance.addHook('onReady', (done) => {
const allSchemas = instance.getSchemas()
for (const schemaId of Object.keys(allSchemas)) {
if (!sharedSchemasMap.has(schemaId)) {
sharedSchemasMap.set(schemaId, allSchemas[schemaId])
}
}
done()
})
})

return {
routes,
Ref () {
const externalSchemas = Array.from(sharedSchemasMap.values())
// TODO: hardcoded applicationUri is not a ideal solution
return Ref({ clone: true, applicationUri: 'todo.com', externalSchemas })
}
}
}

function resolveSwaggerFunction (opts, routes, Ref, cache, done) {
let build
if (Object.keys(opts.openapi).length > 0 && opts.openapi.constructor === Object) {
build = require('./openapi')
} else {
build = require('./swagger')
}
return build(opts, routes, Ref, cache, done)
}

// 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) {
let start = url.indexOf('/:')
if (start === -1) return url

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

function readPackageJson (done) {
try {
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')))
} catch (err) {
return done(err)
}
}

module.exports = {
addHook,
resolveSwaggerFunction,
formatParamUrl,
readPackageJson
}
200 changes: 200 additions & 0 deletions lib/openapi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
'use strict'

const yaml = require('js-yaml')
const { formatParamUrl, readPackageJson } = require('./dynamicUtil')
const { getBodyParams, getCommonParams, generateResponse, stripBasePathByServers } = require('./openapiUtil')

// TODO: refactor for readability, reference in below
// https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two
// https://www.informit.com/articles/article.aspx?p=1398607
module.exports = function (opts, routes, Ref, cache, done) {
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.string) return cache.string
} else {
if (cache.object) return cache.object
}

const openapiObject = {}
const pkg = readPackageJson(done)

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

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

ref = Ref()
openapiObject.components.schemas = {
...openapiObject.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(openapiObject.components.schemas)
.forEach(_ => { delete _.$id })

openapiObject.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, openapiObject.servers)
: route.url
const url = formatParamUrl(path)

const swaggerRoute = openapiObject.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) {
getCommonParams('query', parameters, schema.querystring, ref, openapiObject.components.schemas)
}

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

if (schema.params) {
getCommonParams('path', parameters, schema.params, ref, openapiObject.components.schemas)
}

if (schema.headers) {
getCommonParams('header', parameters, schema.headers, ref, openapiObject.components.schemas)
}
climba03003 marked this conversation as resolved.
Show resolved Hide resolved

// TODO: need to documentation, we treat it same as the querystring
// fastify do not support cookies schema in first place
if (schema.cookies) {
getCommonParams('cookie', parameters, schema.cookies, ref, openapiObject.components.schemas)
}

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 = generateResponse(schema ? schema.response : null, schema ? schema.produces : null, ref)

openapiObject.paths[url] = swaggerRoute
}

if (opts && opts.yaml) {
const openapiString = yaml.safeDump(openapiObject, { skipInvalid: true })
cache.string = openapiString
return openapiString
}

cache.object = openapiObject
return openapiObject
}
}
Loading