@financialforcedev/orizuru-openapi
Version:
OpenAPI generator for Orizuru.
264 lines (222 loc) • 7.02 kB
JavaScript
/**
* Copyright (c) 2017, FinancialForce.com, inc
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the FinancialForce.com, inc nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
;
const
_ = require('lodash'),
avsc = require('avsc'),
V2 = '2.0',
CONTENT_TYPE = 'application/json',
OPENAPI_TYPE_BOOLEAN = 'boolean',
OPENAPI_TYPE_INTEGER = 'integer',
OPENAPI_TYPE_NUMBER = 'number',
OPENAPI_TYPE_STRING = 'string',
OPENAPI_TYPE_BYTE = 'byte',
OPENAPI_TYPE_OBJECT = 'object',
OPEN_API_TYPE_ARRAY = 'array',
REF_TAG = '$ref',
DEF_ROOT = '#/definitions/',
RESPONSE_RECORD_NAME = 'Response',
getRecordName = (fullyQualifiedName) => {
return fullyQualifiedName.split('.').pop();
},
booleanTypeMapper = (definitionsState, type) => {
return {
type: OPENAPI_TYPE_BOOLEAN
};
},
stringTypeMapper = (definitionsState, type) => {
return {
type: OPENAPI_TYPE_STRING
};
},
integerTypeMapper = (definitionsState, type) => {
return {
type: OPENAPI_TYPE_INTEGER
};
},
numberTypeMapper = (definitionsState, type) => {
return {
type: OPENAPI_TYPE_NUMBER
};
},
byteTypeMapper = (definitionsState, type) => {
return {
type: OPENAPI_TYPE_BYTE
};
},
recordReferenceMapper = (definitionsState, type) => {
const
recordName = getRecordName(type.name);
definitionsState.refs.push(type);
return {
[REF_TAG]: DEF_ROOT +
recordName
};
},
recordMapper = (definitionsState, type) => {
const
recordName = getRecordName(type.name),
fieldToProperty = (properties, field) => {
const
mapper = definitionsState.typeMappers[field.type.typeName];
properties[field.name] = mapper(definitionsState, field.type);
return properties;
},
properties = type.fields.reduce(fieldToProperty, {}),
definition = {
type: OPENAPI_TYPE_OBJECT,
properties: properties
},
required = {
required: type.fields.map(field => field.name)
};
definitionsState.definitions.push({
name: recordName,
value: Object.assign(definition, type.fields.length ? required : {})
});
},
arrayMapper = (definitionsState, type, typeMappers) => {
return {
type: OPEN_API_TYPE_ARRAY,
items: definitionsState.typeMappers[type.itemsType.typeName](definitionsState, type.itemsType)
};
},
schemasToPaths = (paths, [name, avroSchema]) => {
const
recordName = getRecordName(avroSchema.name);
paths['/' + name] = {
post: {
description: `Raise a ${recordName} event.`,
operationId: name,
parameters: [{
name: recordName,
'in': 'body',
description: avroSchema.doc,
required: true,
schema: {
$ref: DEF_ROOT + recordName
}
}],
responses: {
200: {
description: `${name} response`,
schema: {
$ref: DEF_ROOT + RESPONSE_RECORD_NAME
}
},
'default': {
description: 'Error'
}
}
}
};
return paths;
},
schemaToDefinitions = (definitionsState, [name, avroSchema]) => {
const
responseSchema = avsc.Type.forSchema({
name: RESPONSE_RECORD_NAME,
doc: RESPONSE_RECORD_NAME,
type: 'record',
fields: [{
name: 'id',
type: 'string'
}]
}),
recordName = getRecordName(avroSchema.name);
if (recordName === RESPONSE_RECORD_NAME) {
throw new Error(`Schema record name clashes with the generated response record name ${RESPONSE_RECORD_NAME}`);
}
recordMapper(definitionsState, avroSchema);
while (definitionsState.refs.length > 0) {
recordMapper(definitionsState, definitionsState.refs.pop());
}
recordMapper(definitionsState, responseSchema);
return definitionsState;
},
typeMappers = {
'boolean': booleanTypeMapper,
'int': integerTypeMapper,
'long': integerTypeMapper,
'float': numberTypeMapper,
'double': numberTypeMapper,
bytes: byteTypeMapper,
string: stringTypeMapper,
array: arrayMapper,
record: recordReferenceMapper
};
/**
* OpenAPI Generator.
* @module
*/
module.exports = {
/**
* An express request handler.
* @typedef RequestHandler
* @type {function}
* @param {Object} req - The request.
* @param {Object} res - The response.
*/
/**
* Returns an express RequestHandler function that will send an OpenAPI 2.0 document
* as a JSON response. The template is merged with the paths and definitions
* generated from the Avro schema map.
* A generated response definition is created with the name 'Response' this must
* not clash with any schemas passed in.
*
* @param {Object} template - The OpenAPI 2.0 template.
* @param {Object} schemaNameToDefinition - Map of name to Avro schema JSON object.
* @returns {RequestHandler} - The handler.
*/
generateV2: (template, schemaNameToDefinition) => (req, res) => {
const
entries = Object.keys(schemaNameToDefinition).map(name => [name, schemaNameToDefinition[name]]),
parsedSchemas = entries.map(([name, value]) => [name, avsc.Type.forSchema(value)]),
paths = parsedSchemas.reduce(schemasToPaths, {}),
definitionsState = parsedSchemas.reduce(schemaToDefinitions, {
definitions: [],
refs: [],
typeMappers: typeMappers
}),
// Reverse the order so dependencies come before dependants.
defs = definitionsState.definitions.reverse().reduce((accum, def) => {
accum[def.name] = def.value;
return accum;
}, {}),
document = _.merge({}, {
swagger: V2,
info: {},
host: '',
basePath: '',
schemes: ['https'],
consumes: [CONTENT_TYPE],
produces: [CONTENT_TYPE],
paths,
definitions: defs
}, template);
res.json(document);
}
};