UNPKG

hapi-openapi

Version:

Design-driven apis with OpenAPI (formerly Swagger) 2.0 and hapi.

337 lines (282 loc) 10.4 kB
'use strict'; const Enjoi = require('enjoi'); const Joi = require('joi'); const extensions = [ { type: 'int64', base: Joi.string().regex(/^\d+$/) }, { type: 'byte', base: Joi.string().base64() }, { type: 'date-time', base: Joi.date().iso() }, { type: 'file', base: Joi.object({ value: Joi.binary().required(true), consumes: Joi.array().items( Joi.string().regex(/multipart\/form-data|application\/x-www-form-urlencoded/) ).required(true), in: Joi.string().regex(/formData/).required(true) }) } ]; const refineType = function (type, format) { if (type === 'integer') { type = 'number'; } switch (format) { case 'int64': case 'byte': case 'binary': case 'date': case 'date-time': return format; default: return type; } }; const refineSchema = function (joiSchema, jsonSchema) { if (jsonSchema.nullable) { return joiSchema.allow(null); } return joiSchema; }; const enjoi = Enjoi.defaults({ extensions, refineType, refineSchema }); const create = function (options = {}) { const makeValidator = function (parameter, consumes, openapi, allowUnknownProperties = false) { const coerce = coercion(parameter, consumes); let schema; if ((parameter.in === 'body' || parameter.in === 'formData') && parameter.schema) { schema = enjoi.schema(parameter.schema); if (schema.type === 'object') { schema = schema.unknown(allowUnknownProperties); } } else { let template = { required: parameter.required, enum: parameter.enum, type: parameter.type, schema: undefined, items: parameter.items, properties: parameter.properties, pattern: parameter.pattern, format: parameter.format, allowEmptyValue: parameter.allowEmptyValue, collectionFormat: parameter.collectionFormat, default: parameter.default, maximum: parameter.maximum, minimum: parameter.minimum, maxLength: parameter.maxLength, minLength: parameter.minLength, maxItems: parameter.maxItems, minItems: parameter.minItems, uniqueItems: parameter.uniqueItems, multipleOf: parameter.multipleOf }; if (openapi) { template = { ...template, ...parameter.schema }; } else { template.schema = parameter.schema; } schema = enjoi.schema(template); } if (parameter.type === 'array') { schema = schema.single(true); } if (parameter.required) { schema = schema.required(); } if (parameter.in !== 'body' && parameter.allowEmptyValue) { schema = schema.allow('').optional(); } return { parameter, schema, routeExt: function (request, h) { const p = parameter.in === 'query' ? 'query' : 'params'; if (request[p][parameter.name] !== undefined) { request[p][parameter.name] = coerce && request[p][parameter.name] && coerce(request[p][parameter.name]); } return h.continue; }, validate: function (value) { const data = coerce && value && coerce(value); const result = schema.validate(data); if (result && result.error) { result.error.message = result.error.message.replace('value', parameter.name); result.error.details.forEach((detail) => { detail.message = detail.message.replace('value', parameter.name); detail.path = [parameter.name]; }); throw result.error; } return result.value; } }; }; const makeResponseValidator = function (responses, openapi) { const schemas = {}; for (const [code, response] of Object.entries(responses)) { if (!isNaN(code)) { let schemaDesc; if (openapi && response.content) { for (const mediaType of Object.keys(response.content)) { // Applying first available schema to all media types if (response.content[mediaType].schema) { schemaDesc = response.content[mediaType].schema; break; } } } else { schemaDesc = response.schema; } if (schemaDesc) { const schema = enjoi.schema(schemaDesc); if (schemaDesc === 'array') { schema.single(true); } schemas[code] = schema; } } } return schemas; }; const makeAll = function (parameters = [], requestBody, consumes, openapi, allowUnknownProperties = false) { const routeExt = []; const validate = {}; const formValidators = {}; let headers = {}; const formValidator = function (value) { const result = this.validate(value); if (result.error) { throw result.error; } return this.parameter.type === 'file' ? result.value : result; }; if (openapi && requestBody && requestBody.content) { consumes = Object.keys(requestBody.content); for (const mediaType of consumes) { // Applying first available schema to all media types if (requestBody.content[mediaType].schema) { const parameter = { in: 'body', schema: requestBody.content[mediaType].schema, name: 'body' }; const validator = makeValidator(parameter, consumes, openapi, allowUnknownProperties); validate.payload = validator.validate; break; } } } for (const parameter of parameters) { const validator = makeValidator(parameter, consumes, openapi, allowUnknownProperties); switch (validator.parameter.in) { case 'header': headers = headers || {}; headers[validator.parameter.name] = validator.schema; break; case 'query': validate.query = validate.query || {}; validate.query[validator.parameter.name] = validator.schema; routeExt.push(validator.routeExt); break; case 'path': validate.params = validate.params || {}; validate.params[validator.parameter.name.replace(/(\*[0-9]*|\?)$/, '')] = validator.schema; routeExt.push(validator.routeExt); break; case 'body': validate.payload = validator.validate; break; case 'formData': formValidators[validator.parameter.name] = formValidator.bind(validator); break; default: break; } if (headers && Object.keys(headers).length > 0) { validate.headers = Joi.object(headers).options({ allowUnknown: true }); } if (!validate.payload && Object.keys(formValidators).length > 0) { validate.payload = async function (value) { const results = {}; for (const [param, data] of Object.entries(value)) { results[param] = await formValidators[param](data); } return results; }; } } for (const [key, value] of Object.entries(validate)) { if (typeof value === 'object' && !Joi.isSchema(value)) { validate[key] = Joi.object(value); } } return { validate, routeExt }; }; return { makeAll, makeValidator, makeResponseValidator }; }; const pathsep = function (format) { switch (format) { case 'csv': return ','; case 'ssv': return ' '; case 'tsv': return '\t'; case 'pipes': return '|'; case 'multi': return '&'; } }; const coercion = function (parameter, consumes) { let fn; switch (parameter.type) { case 'array': fn = function (data) { if (Array.isArray(data)) { return data; } const sep = pathsep(parameter.collectionFormat || 'csv'); return data.split(sep); }; break; case 'integer': case 'number': fn = function (data) { if (parameter.format === 'int64') { return data; } return Number(data); }; break; case 'string': //TODO: handle date, date-time, binary, byte formats. fn = String; break; case 'boolean': fn = function (data) { return (data === 'true') || (data === '1') || (data === true); }; break; case 'file': { fn = function (data) { return { value: data, consumes, in: parameter.in }; }; break; } } if (!fn && parameter.schema) { fn = (data) => data; } return fn; }; module.exports = { create };