You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
5.7 KiB
205 lines
5.7 KiB
'use strict'
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const Ref = require('json-schema-resolver')
|
|
const cloner = require('rfdc')({ proto: true, circles: false })
|
|
const { rawRequired } = require('../symbols')
|
|
const { xConsume } = require('../constants')
|
|
|
|
function addHook (fastify, pluginOptions) {
|
|
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 = cloner(Array.from(sharedSchemasMap.values()))
|
|
return Ref(Object.assign(
|
|
{ applicationUri: 'todo.com' },
|
|
pluginOptions.refResolver,
|
|
{ clone: true, externalSchemas })
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function shouldRouteHide (schema, opts) {
|
|
const { hiddenTag, hideUntagged } = opts
|
|
|
|
if (schema && schema.hide) {
|
|
return true
|
|
}
|
|
|
|
const tags = (schema && schema.tags) || []
|
|
|
|
if (tags.length === 0 && hideUntagged) {
|
|
return true
|
|
}
|
|
|
|
if (tags.includes(hiddenTag)) {
|
|
return schema.tags.includes(hiddenTag)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// 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}'
|
|
// custom verbs at the end of a url are okay => /user::watch but should be rendered as /user:watch in swagger
|
|
const COLON = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
|
|
function formatParamUrl (str) {
|
|
let i, char
|
|
let state = 'skip'
|
|
let path = ''
|
|
let param = ''
|
|
let level = 0
|
|
// count for regex if no param exist
|
|
let regexp = 0
|
|
for (i = 0; i < str.length; i++) {
|
|
char = str[i]
|
|
switch (state) {
|
|
case 'colon': {
|
|
// we only accept a-zA-Z0-9_ in param
|
|
if (COLON.indexOf(char) !== -1) {
|
|
param += char
|
|
} else if (char === '(') {
|
|
state = 'regexp'
|
|
level++
|
|
} else {
|
|
// end
|
|
state = 'skip'
|
|
path += '{' + param + '}'
|
|
path += char
|
|
param = ''
|
|
}
|
|
break
|
|
}
|
|
case 'regexp': {
|
|
if (char === '(') {
|
|
level++
|
|
} else if (char === ')') {
|
|
level--
|
|
}
|
|
// we end if the level reach zero
|
|
if (level === 0) {
|
|
state = 'skip'
|
|
if (param === '') {
|
|
regexp++
|
|
param = 'regexp' + String(regexp)
|
|
}
|
|
path += '{' + param + '}'
|
|
param = ''
|
|
}
|
|
break
|
|
}
|
|
default: {
|
|
// we check if we need to change state
|
|
if (char === ':' && str[i + 1] === ':') {
|
|
// double colon -> single colon
|
|
path += char
|
|
// skip one more
|
|
i++
|
|
} else if (char === ':') {
|
|
// single colon -> state colon
|
|
state = 'colon'
|
|
} else if (char === '(') {
|
|
state = 'regexp'
|
|
level++
|
|
} else if (char === '*') {
|
|
// * -> wildcard
|
|
// should be exist once only
|
|
path += '{wildcard}'
|
|
} else {
|
|
path += char
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// clean up
|
|
if (state === 'colon' && param !== '') {
|
|
path += '{' + param + '}'
|
|
}
|
|
return path
|
|
}
|
|
|
|
function resolveLocalRef (jsonSchema, externalSchemas) {
|
|
if (typeof jsonSchema.type !== 'undefined' && typeof jsonSchema.properties !== 'undefined') {
|
|
// for the shorthand querystring/params/headers declaration
|
|
const propertiesMap = Object.keys(jsonSchema.properties).reduce((acc, headers) => {
|
|
const rewriteProps = {}
|
|
rewriteProps.required = (Array.isArray(jsonSchema.required) && jsonSchema.required.indexOf(headers) >= 0) || false
|
|
// save raw required for next restore in the content/<media-type>
|
|
if (jsonSchema.properties[headers][xConsume]) {
|
|
rewriteProps[rawRequired] = jsonSchema.properties[headers].required
|
|
}
|
|
const newProps = Object.assign({}, jsonSchema.properties[headers], rewriteProps)
|
|
|
|
return Object.assign({}, acc, { [headers]: newProps })
|
|
}, {})
|
|
|
|
return propertiesMap
|
|
}
|
|
|
|
// for oneOf, anyOf, allOf support in querystring/params/headers
|
|
if (jsonSchema.oneOf || jsonSchema.anyOf || jsonSchema.allOf) {
|
|
const schemas = jsonSchema.oneOf || jsonSchema.anyOf || jsonSchema.allOf
|
|
return schemas.reduce(function (acc, schema) {
|
|
const json = resolveLocalRef(schema, externalSchemas)
|
|
return { ...acc, ...json }
|
|
}, {})
|
|
}
|
|
|
|
// $ref is in the format: #/definitions/<resolved definition>/<optional fragment>
|
|
const localRef = jsonSchema.$ref.split('/')[2]
|
|
return resolveLocalRef(externalSchemas[localRef], externalSchemas)
|
|
}
|
|
|
|
function readPackageJson () {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json')))
|
|
} catch (err) {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function resolveSwaggerFunction (opts, cache, routes, Ref, done) {
|
|
if (typeof opts.openapi === 'undefined' || opts.openapi === null) {
|
|
return require('../spec/swagger')(opts, cache, routes, Ref, done)
|
|
} else {
|
|
return require('../spec/openapi')(opts, cache, routes, Ref, done)
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
addHook,
|
|
shouldRouteHide,
|
|
readPackageJson,
|
|
formatParamUrl,
|
|
resolveLocalRef,
|
|
resolveSwaggerFunction
|
|
}
|