@superhero/http-server-using-oas
Version:
Integrates the HTTP server and OAS (OpenAPI Specification) @superhero components
201 lines (176 loc) • 6.92 kB
JavaScript
export function locate(locator)
{
const specification = locator.config.find('oas')
return new OptionsDispatcher(specification)
}
export default class OptionsDispatcher
{
constructor(specification)
{
this.specification = specification
}
dispatch(request, session)
{
const [ validResource, ...validOperations ] = session.route.operation.operationId.split('#')
const
output = {},
components = {},
paths = {},
parameters = {},
requestBodies = {},
responses = {},
schemas = {},
depth = request.url.pathname.split('/').length
// Loop through all paths in the OpenAPI Specification and find the ones that
// match the defined operation, or the beginning of the request path if no operation is defined...
for(const path in this.specification.paths || {})
{
let valid = false
// validate the path depending on if the operation-id specify a specific operation, or not...
if(validResource)
{
valid = validResource === path
}
// validate the beginning of the request path if no specific operation is defined...
else
{
const
partial = path.split('/').slice(0, depth).join('/'),
regexp = partial.replace(/{[^}]+}/g, '([^/]*)')
valid = new RegExp(`^${regexp}$`).test(request.url.pathname)
}
// Only include a scoped version of the specification
if(valid)
{
paths[path] = { ...this.specification.paths[path] }
// Operations
for(let method in paths[path])
{
const lowerCasedMethod = method.toLowerCase()
switch(lowerCasedMethod)
{
case 'get':
case 'put':
case 'post':
case 'delete':
case 'options':
case 'head':
case 'patch':
case 'trace':
{
if(validOperations.langth === 0 // if not specified, then not concidered restrictive...
|| validOperations.map(validMethod => validMethod.toLowerCase()).includes(lowerCasedMethod))
{
// break to proceed to process
break
}
else
{
// hide restricted methods
delete paths[path][method]
continue
}
}
default:
{
// ignore any non HTTP method
continue
}
}
const operation = paths[path][method]
// Parameters
for(let parameter of operation.parameters || [])
{
parameter = this.#augment(parameters, parameter.$ref) || parameter
// Parameters - schemas
this.#augmentSchemasRecursively(schemas, parameter.schema)
}
// Request Bodies
let
requestBody = operation.requestBody || {}
requestBody = this.#augment(requestBodies, requestBody.$ref) || requestBody
this.#augment(requestBodies, requestBody.$ref)
// Request Bodies - schemas
for(const contentType in requestBody.content || {})
{
this.#augmentSchemasRecursively(schemas, operation.requestBody.content[contentType].schema)
}
// Responses
for(const status in operation.responses || {})
{
let
response = operation.responses[status] || {}
response = this.#augment(responses, response.$ref) || response
// Responses - schemas
for(const contentType in response.content || {})
{
this.#augmentSchemasRecursively(schemas, response.content[contentType].schema)
}
}
}
}
}
// Aport with a 404 error if no paths were found in the specification
if(false === Object.keys(paths).length)
{
const error = new Error(`No endpoints found matching the requested path "${request.url.pathname}"`)
error.code = 'E_OAS_NO_ENDPOINTS_FOUND'
error.status = 404
return session.abortion.abort(error)
}
output.openapi = this.specification.openapi
output.info = this.specification.info
output.paths = paths
if(Object.keys(parameters) .length) components.parameters = parameters
if(Object.keys(requestBodies) .length) components.requestBodies = requestBodies
if(Object.keys(responses) .length) components.responses = responses
if(Object.keys(schemas) .length) components.schemas = schemas
if(Object.keys(components) .length) output.components = components
this.view.body = output
}
#augment(component, ref)
{
// only augment if the reference is defined
if('string' !== typeof ref) return
const [ uri, pointer ] = ref.split('#')
// only augment local references
if(true === !!uri) return
// component name
const name = pointer.split('/').pop()
// avoid redundant traversals and augmentations
if(name in component) return
// augment the component with the branch at the traversed pointer in the specification
const traversePath = pointer.split('/').filter(Boolean)
return component[name] = traversePath.reduce((obj, key) => obj && obj[key], this.specification)
}
#augmentSchemasRecursively(component, schema)
{
// avoid undefined schemas
if(false === !!schema) return
// loop through the schemas if it is an array
if(Array.isArray(schema)) return schema.forEach(schema => this.#augmentSchemasRecursively(component, schema))
// replace the schema with the augmented one, if the schema defined a reference
if(schema.$ref) schema = this.#augment(component, schema.$ref)
// avoid redundant processing
if(false === !!schema) return
this.#augmentSchemasRecursively(component, Object.values(schema.properties || {}))
this.#augmentSchemasRecursively(component, schema.additionalProperties)
this.#augmentSchemasRecursively(component, schema.propertyNames)
this.#augmentSchemasRecursively(component, schema.items)
this.#augmentSchemasRecursively(component, schema.allOf)
this.#augmentSchemasRecursively(component, schema.anyOf)
this.#augmentSchemasRecursively(component, schema.oneOf)
this.#augmentSchemasRecursively(component, schema.if)
this.#augmentSchemasRecursively(component, schema.not)
this.#augmentSchemasRecursively(component, schema.then)
this.#augmentSchemasRecursively(component, schema.else)
}
onError(reason, _, session)
{
const error = new Error(`Server error occured while attempting to declare API options`)
error.code = 'E_OAS_INVALID_REQUEST_PARAMETERS'
error.cause = reason
error.status = 500
session.abortion.abort(error)
}
}