@whook/whook
Version:
Build strong and efficient REST web services.
132 lines • 6.06 kB
JavaScript
import { YHTTPError } from 'yhttperror';
import contentType from 'content-type';
import preferredCharsets from 'negotiator/lib/charset.js';
import preferredMediaType from 'negotiator/lib/encoding.js';
import { YError } from 'yerror';
import { pickFirstHeaderValue, pickAllHeaderValues } from './headers.js';
import { ensureResolvedObject } from 'ya-open-api-types';
const { parse: parseContentType } = contentType;
export async function extractConsumableMediaTypes(API, operation) {
if (!('requestBody' in operation && operation.requestBody)) {
return [];
}
const requestBody = await ensureResolvedObject(API, operation.requestBody);
if (!('content' in requestBody && requestBody.content)) {
return [];
}
// Per spec contents, the `content` property should always
// be present so not checking before using it
// https://swagger.io/specification/#requestBodyObject
return Object.keys(requestBody.content);
}
export async function extractProduceableMediaTypes(API, operation) {
if (!('responses' in operation && operation.responses)) {
return [];
}
const produceableMediaTypes = [];
for (const response of Object.values(operation.responses)) {
if (!(typeof response === 'object' && response)) {
continue;
}
const resolvedResponse = await ensureResolvedObject(API, response);
if (!('content' in resolvedResponse && resolvedResponse.content)) {
continue;
}
produceableMediaTypes.push(...Object.keys(resolvedResponse.content || {}));
}
return [...new Set(produceableMediaTypes)];
}
export function extractBodySpec(request, consumableMediaTypes, consumableCharsets) {
const contentLengthValues = pickAllHeaderValues('content-length', request.headers);
const bodySpec = {
contentType: '',
contentLength: contentLengthValues.length
? Number(contentLengthValues[0])
: 0,
charset: 'utf-8',
};
if (request.headers['content-type']) {
const baseContentType = pickFirstHeaderValue('content-type', request.headers);
try {
if (typeof baseContentType === 'string') {
const parsedContentType = parseContentType(baseContentType);
bodySpec.contentType = parsedContentType.type;
if (parsedContentType.parameters &&
parsedContentType.parameters.charset) {
if (['utf-8', 'utf8'].includes(parsedContentType.parameters.charset.toLowerCase())) {
bodySpec.charset = 'utf-8';
}
else {
throw new YError('E_UNSUPPORTED_CHARSET', parsedContentType.parameters.charset);
}
}
if (parsedContentType.parameters &&
parsedContentType.parameters.boundary) {
bodySpec.boundary = parsedContentType.parameters.boundary;
}
}
else {
throw new YError('E_EMPTY_CONTENT_TYPE');
}
}
catch (err) {
throw YHTTPError.wrap(err, 400, 'E_BAD_CONTENT_TYPE', request.headers['content-type']);
}
}
checkBodyCharset(bodySpec, consumableCharsets);
checkBodyMediaType(bodySpec, consumableMediaTypes);
return bodySpec;
}
export function checkBodyCharset(bodySpec, consumableCharsets) {
if (bodySpec.contentLength &&
bodySpec.charset &&
!consumableCharsets.includes(bodySpec.charset)) {
throw new YHTTPError(406, 'E_UNSUPPORTED_CHARSET', bodySpec.charset, consumableCharsets);
}
}
export function checkBodyMediaType(bodySpec, consumableMediaTypes) {
if (bodySpec.contentLength &&
bodySpec.contentType &&
!consumableMediaTypes.includes(bodySpec.contentType)) {
throw new YHTTPError(415, 'E_UNSUPPORTED_MEDIA_TYPE', bodySpec.contentType, consumableMediaTypes);
}
}
export function extractResponseSpec(operation, request, supportedMediaTypes, supportedCharsets) {
const accept = pickFirstHeaderValue('accept', request.headers) || '*';
const responseSpec = {
charsets: request.headers['accept-charset']
? preferredCharsets(pickFirstHeaderValue('accept-charset', request.headers), supportedCharsets)
: supportedCharsets,
contentTypes: preferredMediaType(accept.replace(/(^|,)\*\/\*($|,|;)/g, '$1*$2'), supportedMediaTypes),
};
return responseSpec;
}
export function checkResponseMediaType(request, responseSpec, produceableMediaTypes) {
if (0 === responseSpec.contentTypes.length) {
throw new YHTTPError(406, 'E_UNACCEPTABLE_MEDIA_TYPE', request.headers.accept, produceableMediaTypes);
}
}
export function checkResponseCharset(request, responseSpec, produceableCharsets) {
if (0 === responseSpec.charsets.length) {
throw new YHTTPError(406, 'E_UNACCEPTABLE_CHARSET', request.headers['accept-charset'], responseSpec.charsets, produceableCharsets);
}
}
export async function executeHandler(definition, handler, parameters) {
const responsePromise = handler(parameters, definition);
if (!(responsePromise && responsePromise.then)) {
throw new YHTTPError(500, 'E_NO_RESPONSE_PROMISE', definition.operation.operationId, definition.method, definition.path);
}
const response = await responsePromise;
if (!response) {
throw new YHTTPError(500, 'E_NO_RESPONSE', definition.operation.operationId, definition.method, definition.path);
}
if ('undefined' === typeof response.status) {
throw new YHTTPError(500, 'E_NO_RESPONSE_STATUS', definition.operation.operationId, definition.method, definition.path);
}
if ('number' !== typeof response.status) {
throw new YHTTPError(500, 'E_NON_NUMERIC_STATUS', definition.operation.operationId, definition.method, definition.path);
}
response.headers = response.headers || {};
return response;
}
//# sourceMappingURL=router.js.map