UNPKG

baucis-swagger2

Version:

Generate customizable swagger version 2.0 definitions for your Baucis REST API.

430 lines (395 loc) 14.4 kB
// This is a Controller mixin to add methods for generating Swagger data. // __Dependencies__ var mongoose = require('mongoose'); var utils = require('./utils'); var params = require('./parameters'); // __Private Members__ // __Module Definition__ module.exports = function () { var controller = this; // __Private Instance Members__ function buildTags(resourceName) { return [ resourceName ]; } function buildResponsesFor(isInstance, verb, resourceName, pluralName) { var responses = {}; //default errors on baucis httpStatus code + string responses.default = { description: 'Unexpected error.', schema: { 'type': 'string' } }; if (isInstance || verb==='post') { responses['200'] = { description: 'Sucessful response. Single resource.', schema: { '$ref': '#/definitions/' + utils.capitalize(resourceName) } }; } else { responses['200'] = { description: 'Sucessful response. Collection of resources.', schema: { type: 'array', items: { $ref: '#/definitions/' + utils.capitalize(resourceName) } } }; } // Add other errors if needed: (400, 403, 412 etc. ) responses['404'] = { description: (isInstance) ? 'No ' + resourceName + ' was found with that ID.' : 'No ' + pluralName + ' matched that query.', schema: { 'type': 'string' //'$ref': '#/definitions/ErrorModel' } }; if (verb === 'put' || verb==='post' || verb==='patch') { responses['422'] = { description: 'Validation error.', schema: { type: 'array', items: { '$ref': '#/definitions/ValidationError' } } }; } return responses; } function buildSecurityFor() { return null; //no security defined } function buildOperationInfo(res, operationId, summary, description) { res.operationId = operationId; res.summary = summary; res.description = description; return res; } function buildBaseOperation(mode, verb, controller) { var resourceName = controller.model().singular(); var pluralName = controller.model().plural(); var isInstance = (mode === 'instance'); var resourceKey = utils.capitalize(resourceName); var res = { //consumes: ['application/json'], //if used overrides global definition //produces: ['application/json'], //if used overrides global definition parameters: params.generateOperationParameters(isInstance, verb, controller), responses: buildResponsesFor(isInstance, verb, resourceName, pluralName) }; if (res.parameters.length === 0) { delete(res.parameters); } var sec = buildSecurityFor(); if (sec) { res.security = sec; } if (isInstance) { return buildBaseOperationInstance(verb, res, resourceKey, resourceName); } else { //collection return buildBaseOperationCollection(verb, res, resourceKey, pluralName); } } function buildBaseOperationInstance(verb, res, resourceKey, resourceName) { if ('get' === verb) { return buildOperationInfo(res, 'get' + resourceKey + 'ById', 'Get a ' + resourceName + ' by its unique ID', 'Retrieve a ' + resourceName + ' by its ID' + '.'); } else if ('put' === verb) { return buildOperationInfo(res, 'update' + resourceKey, 'Modify a ' + resourceName + ' by its unique ID', 'Update an existing ' + resourceName + ' by its ID' + '.'); } else if ('delete' === verb) { return buildOperationInfo(res, 'delete' + resourceKey + 'ById', 'Delete a ' + resourceName + ' by its unique ID', 'Deletes an existing ' + resourceName + ' by its ID' + '.'); } } function buildBaseOperationCollection(verb, res, resourceKey, pluralName) { if ('get' === verb) { return buildOperationInfo(res, 'query' + resourceKey, 'Query some ' + pluralName, 'Query over ' + pluralName + '.'); } else if ('post' === verb) { return buildOperationInfo(res, 'create' + resourceKey, 'Create some ' + pluralName, 'Create one or more ' + pluralName + '.'); } else if ('delete' === verb) { return buildOperationInfo(res, 'delete' + resourceKey + 'ByQuery', 'Delete some ' + pluralName + ' by query', 'Delete all ' + pluralName + ' matching the specified query.'); } } function buildOperation(containerPath, mode, verb) { var resourceName = controller.model().singular(); var operation = buildBaseOperation(mode, verb, controller); operation.tags = buildTags(resourceName); containerPath[verb] = operation; return operation; } // Convert a Mongoose type into a Swagger type function swagger20TypeFor(type) { if (!type) { return null; } if (type === Number) { return 'number'; } if (type === Boolean) { return 'boolean'; } if (type === String || type === Date || type === mongoose.Schema.Types.ObjectId || type === mongoose.Schema.Types.Oid) { return 'string'; } if (type === mongoose.Schema.Types.Array || Array.isArray(type) || type.name === "Array") { return 'array'; } if (type === Object || type instanceof Object || type === mongoose.Schema.Types.Mixed || type === mongoose.Schema.Types.Buffer) { return null; } throw new Error('Unrecognized type: ' + type); } function swagger20TypeFormatFor(type) { if (!type) { return null; } if (type === Number) { return 'double'; } if (type === Date) { return 'date-time'; } /* if (type === String) { return null; } if (type === Boolean) { return null; } if (type === mongoose.Schema.Types.ObjectId) { return null; } if (type === mongoose.Schema.Types.Oid) { return null; } if (type === mongoose.Schema.Types.Array) { return null; } if (Array.isArray(type) || type.name === "Array") { return null; } if (type === Object) { return null; } if (type instanceof Object) { return null; } if (type === mongoose.Schema.Types.Mixed) { return null; } if (type === mongoose.Schema.Types.Buffer) { return null; } */ return null; } function skipProperty(name, path, controller) { var select = controller.select(); var mode = (select && select.match(/(?:^|\s)[-]/g)) ? 'exclusive' : 'inclusive'; var exclusiveNamePattern = new RegExp('\\B-' + name + '\\b', 'gi'); var inclusiveNamePattern = new RegExp('(?:\\B[+]|\\b)' + name + '\\b', 'gi'); // Keep deselected paths private if (path.selected === false) { return true; } // _id always included unless explicitly excluded? // If it's excluded, skip this one. if (select && mode === 'exclusive' && select.match(exclusiveNamePattern)) { return true; } // If the mode is inclusive but the name is not present, skip this one. if (select && mode === 'inclusive' && name !== '_id' && !select.match(inclusiveNamePattern)) { return true; } return false; } // A method used to generated a Swagger property for a model function generatePropertyDefinition(name, path, definitionName) { var property = {}; var type = path.options.type ? swagger20TypeFor(path.options.type) : 'string'; // virtuals don't have type if (skipProperty(name, path, controller)) { return; } // Configure the property if (path.options.type === mongoose.Schema.Types.ObjectId) { if ("_id" === name) { property.type = 'string'; } else if (path.options.ref) { property.$ref = '#/definitions/' + utils.capitalize(path.options.ref); } } else if (path.schema) { //Choice (1. embed schema here or 2. reference and publish as a root definition) property.type = 'array'; property.items = { //2. reference $ref: '#/definitions/'+ definitionName + utils.capitalize(name) }; } else { property.type = type; if ('array' === type) { if (isArrayOfRefs(path.options.type)) { property.items = { type: 'string' //handle references as string (serialization for objectId) }; } else { var resolvedType = referenceForType(path.options.type); if (resolvedType.isPrimitive) { property.items = { type: resolvedType.type }; } else { property.items = { $ref: resolvedType.type }; } } } var format = swagger20TypeFormatFor(path.options.type); if (format) { property.format = format; } if ('__v' === name) { property.format = 'int32'; } } /* // Set enum values if applicable if (path.enumValues && path.enumValues.length > 0) { // Pending: property.allowableValues = { valueType: 'LIST', values: path.enumValues }; } // Set allowable values range if min or max is present if (!isNaN(path.options.min) || !isNaN(path.options.max)) { // Pending: property.allowableValues = { valueType: 'RANGE' }; } if (!isNaN(path.options.min)) { // Pending: property.allowableValues.min = path.options.min; } if (!isNaN(path.options.max)) { // Pending: property.allowableValues.max = path.options.max; } */ if (!property.type && !property.$ref) { warnInvalidType(name, path); property.type = 'string'; } return property; } function referenceForType(type) { if (type && type.length>0 && type[0]) { var sw2Type = swagger20TypeFor(type[0]); if (sw2Type) { return { isPrimitive: true, type: sw2Type //primitive type }; } else { return { isPrimitive: false, type: '#/definitions/' + type[0].name //not primitive: asume complex type def and reference }; } } return { isPrimitive: true, type: 'string' }; //No info provided } function isArrayOfRefs(type) { return (type && type.length > 0 && type[0] && type[0].ref && type[0].type && type[0].type.name === 'ObjectId'); } function warnInvalidType(name, path) { console.log('Warning: That field type is not yet supported in baucis Swagger definitions, using "string."'); console.log('Path name: %s.%s', utils.capitalize(controller.model().singular()), name); console.log('Mongoose type: %s', path.options.type); } function mergePaths(definition, pathsCollection, definitionName) { Object.keys(pathsCollection).forEach(function (name) { var path = pathsCollection[name]; var property = generatePropertyDefinition(name, path, definitionName); definition.properties[name] = property; if (path.options.required) { definition.required.push(name); } }); } // A method used to generate a Swagger model definition for a controller function generateModelDefinition(schema, definitionName) { var definition = { required: [], properties: {} }; mergePaths(definition, schema.paths, definitionName); mergePaths(definition, schema.virtuals, definitionName); //remove empty arrays -> swagger 2.0 validates if (definition.required.length === 0) { delete(definition.required); } if (definition.properties.length === 0) { delete(definition.properties); } return definition; } function mergePathsForInnerDef(defs, collectionPaths, definitionName) { Object.keys(collectionPaths).forEach(function (name) { var path = collectionPaths[name]; if (path.schema) { var newdefinitionName = definitionName + utils.capitalize(name); //<-- synthetic name (no info for this in input model) var def = generateModelDefinition(path.schema, newdefinitionName); defs[newdefinitionName] = def; } }); } function addInnerModelDefinitions(defs, definitionName) { var schema = controller.model().schema; mergePathsForInnerDef(defs, schema.paths, definitionName); mergePathsForInnerDef(defs, schema.virtuals, definitionName); } // __Build the Definition__ controller.generateSwagger2 = function () { if (controller.swagger2) { return controller; } var modelName = utils.capitalize(controller.model().singular()); controller.swagger2 = { paths: {}, definitions: {} }; // Add Resource Model controller.swagger2.definitions[modelName] = generateModelDefinition(controller.model().schema, modelName); addInnerModelDefinitions(controller.swagger2.definitions, modelName); // Paths var pluralName = controller.model().plural(); var collectionPath = '/' + pluralName; var instancePath = '/' + pluralName + '/{id}'; var paths = {}; buildPathParams(paths, instancePath, true); buildPathParams(paths, collectionPath, false); buildOperation(paths[instancePath], 'instance', 'get'); buildOperation(paths[instancePath], 'instance', 'put'); buildOperation(paths[instancePath], 'instance', 'delete'); buildOperation(paths[collectionPath], 'collection', 'get'); buildOperation(paths[collectionPath], 'collection', 'post'); buildOperation(paths[collectionPath], 'collection', 'delete'); controller.swagger2.paths = paths; return controller; }; function buildPathParams(pathContainer, path, isInstance) { var pathParams = params.generatePathParameters(isInstance); if (pathParams.length > 0) { pathContainer[path] = { parameters : pathParams }; } } return controller; };