Skip to content

Commit

Permalink
$ref support - OAS 2.0 compliant (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eomm authored May 24, 2020
1 parent be1fcfe commit 1bd5447
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 127 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ npm run prepare
So that [swagger-ui](https://github.com/swagger-api/swagger-ui) static folder will be generated for you.
#### How work under the hood
`fastify-static` serve the `swagger-ui` static files, then it calls `/docs/json` to get the swagger file and render it.
<a name="seealso"></a>
## See also
Sometimes you already have a Swagger definition and you need to build Fastify routes from that.
Expand Down
278 changes: 172 additions & 106 deletions dynamic.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,33 @@
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const Ref = require('json-schema-resolver')

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

const routes = []
const sharedSchemasMap = new Map()
let ref

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

opts = opts || {}

opts.swagger = opts.swagger || {}
Expand Down Expand Up @@ -74,6 +91,8 @@ module.exports = function (fastify, opts, next) {
if (consumes) swaggerObject.consumes = consumes
if (produces) swaggerObject.produces = produces
if (definitions) swaggerObject.definitions = definitions
else swaggerObject.definitions = {}

if (securityDefinitions) {
swaggerObject.securityDefinitions = securityDefinitions
}
Expand All @@ -87,6 +106,22 @@ module.exports = function (fastify, opts, next) {
swaggerObject.externalDocs = externalDocs
}

if (!ref) {
const externalSchemas = Array.from(sharedSchemasMap.values())

ref = Ref({ clone: true, applicationUri: 'todo.com', externalSchemas })
swaggerObject.definitions = {
...swaggerObject.definitions,
...(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.definitions)
.forEach(_ => { delete _.$id })
}

swaggerObject.paths = {}
for (var route of routes) {
if (route.schema && route.schema.hide) {
Expand Down Expand Up @@ -190,128 +225,84 @@ module.exports = function (fastify, opts, next) {

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
const queryProperties = Object.keys(query.properties).reduce((acc, h) => {
const required = (query.required && query.required.indexOf(h) >= 0) || false
const newProps = Object.assign({}, query.properties[h], { required })
return Object.assign({}, acc, { [h]: newProps })
}, {})
function getBodyParams (parameters, body) {
const bodyResolved = ref.resolve(body)

return getQueryParams(parameters, queryProperties)
}
const param = {}
param.name = 'body'
param.in = 'body'
param.schema = bodyResolved
parameters.push(param)
}

Object.keys(query).forEach(prop => {
const obj = query[prop]
const param = obj
param.name = prop
param.in = 'query'
parameters.push(param)
})
}
function getFormParams (parameters, form) {
const resolved = ref.resolve(form)
const add = plainJsonObjectToSwagger2('formData', resolved, swaggerObject.definitions)
add.forEach(_ => parameters.push(_))
}

function getBodyParams (parameters, body) {
const param = {}
param.name = 'body'
param.in = 'body'
param.schema = body
parameters.push(param)
}
function getQueryParams (parameters, query) {
const resolved = ref.resolve(query)
const add = plainJsonObjectToSwagger2('query', resolved, swaggerObject.definitions)
add.forEach(_ => parameters.push(_))
}

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, path) {
const resolved = ref.resolve(path)
const add = plainJsonObjectToSwagger2('path', resolved, swaggerObject.definitions)
add.forEach(_ => parameters.push(_))
}

function getPathParams (parameters, params) {
if (params.type && params.properties) {
// for the shorthand querystring declaration
return getPathParams(parameters, params.properties)
}
function getHeaderParams (parameters, headers) {
const resolved = ref.resolve(headers)
const add = plainJsonObjectToSwagger2('header', resolved, swaggerObject.definitions)
add.forEach(_ => parameters.push(_))
}

Object.keys(params).forEach(p => {
const param = Object.assign({}, params[p])
param.name = p
param.in = 'path'
param.required = true
parameters.push(param)
})
}
// https://swagger.io/docs/specification/2-0/describing-responses/
function genResponse (fastifyResponseJson) {
// if the user does not provided an out schema
if (!fastifyResponseJson) {
return { 200: { description: 'Default Response' } }
}

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 })
}, {})
const responsesContainer = {}

return getHeaderParams(parameters, headerProperties)
}
Object.keys(fastifyResponseJson).forEach(key => {
// 2xx is not supported by swagger

Object.keys(headers).forEach(h =>
parameters.push({
name: h,
in: 'header',
required: headers[h].required,
description: headers[h].description,
type: headers[h].type
})
)
}
const rawJsonSchema = fastifyResponseJson[key]
const resolved = ref.resolve(rawJsonSchema)

function genResponse (response) {
// if the user does not provided an out schema
if (!response) {
return { 200: { description: 'Default Response' } }
}
if (resolved.type || resolved.$ref) {
responsesContainer[key] = {
schema: resolved
}
} else {
responsesContainer[key] = resolved
}

// remove previous references
response = Object.assign({}, response)
if (!responsesContainer[key].description) {
responsesContainer[key].description = 'Default 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
return responsesContainer
}
}

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

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

// The swagger standard does not accept the url param with ':'
Expand All @@ -330,3 +321,78 @@ function formatParamUrl (url) {
return formatParamUrl(url.slice(0, start) + '{' + url.slice(++start, end) + '}' + url.slice(end))
}
}

// For supported keys read:
// https://swagger.io/docs/specification/2-0/describing-parameters/
function plainJsonObjectToSwagger2 (container, jsonSchema, externalSchemas) {
const obj = localRefResolve(jsonSchema, externalSchemas)
let toSwaggerProp
switch (container) {
case 'query':
toSwaggerProp = function (properyName, jsonSchemaElement) {
jsonSchemaElement.in = container
jsonSchemaElement.name = properyName
return jsonSchemaElement
}
break
case 'formData':
toSwaggerProp = function (properyName, jsonSchemaElement) {
delete jsonSchemaElement.$id
jsonSchemaElement.in = container
jsonSchemaElement.name = properyName

// https://json-schema.org/understanding-json-schema/reference/non_json_data.html#contentencoding
if (jsonSchemaElement.contentEncoding === 'binary') {
delete jsonSchemaElement.contentEncoding // Must be removed
jsonSchemaElement.type = 'file'
}

return jsonSchemaElement
}
break
case 'path':
toSwaggerProp = function (properyName, jsonSchemaElement) {
jsonSchemaElement.in = container
jsonSchemaElement.name = properyName
jsonSchemaElement.required = true
return jsonSchemaElement
}
break
case 'header':
toSwaggerProp = function (properyName, jsonSchemaElement) {
return {
in: 'header',
name: properyName,
required: jsonSchemaElement.required,
description: jsonSchemaElement.description,
type: jsonSchemaElement.type
}
}
break
}

return Object.keys(obj).reduce((acc, propKey) => {
acc.push(toSwaggerProp(propKey, obj[propKey]))
return acc
}, [])
}

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

return propertiesMap
}

if (jsonSchema.$ref) {
// $ref is in the format: #/definitions/<resolved definition>/<optional fragment>
const localReference = jsonSchema.$ref.split('/')[2]
return localRefResolve(externalSchemas[localReference], externalSchemas)
}
return jsonSchema
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"homepage": "https://github.com/fastify/fastify-swagger#readme",
"devDependencies": {
"@types/node": "^14.0.1",
"fastify": "^3.0.0-rc.1",
"fastify": "^3.0.0-rc.2",
"fs-extra": "^9.0.0",
"joi": "^14.3.1",
"joi-to-json-schema": "^5.1.0",
Expand All @@ -46,7 +46,8 @@
"@types/swagger-schema-official": "^2.0.20",
"fastify-plugin": "^2.0.0",
"fastify-static": "^3.0.0",
"js-yaml": "^3.12.1"
"js-yaml": "^3.12.1",
"json-schema-resolver": "^1.2.0"
},
"standard": {
"ignore": [
Expand Down
Loading

0 comments on commit 1bd5447

Please sign in to comment.