diff --git a/lib/apidocToSwagger.js b/lib/apidocToSwagger.js index f2590b8..5fae1e4 100644 --- a/lib/apidocToSwagger.js +++ b/lib/apidocToSwagger.js @@ -1,304 +1,194 @@ var _ = require('lodash'); -var pathToRegexp = require('path-to-regexp'); var swagger = { - swagger : "2.0", - info : {}, - paths : {}, - definitions: {} + swagger: "2.0", + info: {}, + paths: {}, + definitions: {} }; function toSwagger(apidocJson, projectJson) { - swagger.info = addInfo(projectJson); - swagger.paths = extractPaths(apidocJson); - return swagger; -} - -var tagsRegex = /(<([^>]+)>)/ig; -// Removes

tags from text -function removeTags(text) { - return text ? text.replace(tagsRegex, "") : text; + swagger.info = addInfo(projectJson); + swagger.paths = extractPaths(apidocJson); + swagger.definitions = extractDefinitions(apidocJson); + return swagger; } +/** + * add project info loaded from package.json + * @param projectJson + * @returns {{}} + */ function addInfo(projectJson) { - var info = {}; - info["title"] = projectJson.title || projectJson.name; - info["version"] = projectJson.version; - info["description"] = projectJson.description; - return info; + var info = {}; + info["title"] = projectJson.title; + info["version"] = projectJson.version; + info["description"] = projectJson.description; + return info; } /** * Extracts paths provided in json format - * post, patch, put request parameters are extracted in body - * get and delete are extracted to path parameters * @param apidocJson * @returns {{}} */ -function extractPaths(apidocJson){ - var apiPaths = groupByUrl(apidocJson); - var paths = {}; - for (var i = 0; i < apiPaths.length; i++) { - var verbs = apiPaths[i].verbs; - var url = verbs[0].url; - var pattern = pathToRegexp(url, null); - var matches = pattern.exec(url); - - // Surrounds URL parameters with curly brackets -> :email with {email} - var pathKeys = []; - for (var j = 1; j < matches.length; j++) { - var key = matches[j].substr(1); - url = url.replace(matches[j], "{"+ key +"}"); - pathKeys.push(key); - } - - for(var j = 0; j < verbs.length; j++) { - var verb = verbs[j]; - var type = verb.type; - - var obj = paths[url] = paths[url] || {}; - - if (type == 'post' || type == 'patch' || type == 'put') { - _.extend(obj, createPostPushPutOutput(verb, swagger.definitions, pathKeys)); - } else { - _.extend(obj, createGetDeleteOutput(verb, swagger.definitions)); - } - } - } - return paths; -} - -function createPostPushPutOutput(verbs, definitions, pathKeys) { - var pathItemObject = {}; - var verbDefinitionResult = createVerbDefinitions(verbs,definitions); - - var params = []; - var pathParams = createPathParameters(verbs, pathKeys); - pathParams = _.filter(pathParams, function(param) { - var hasKey = pathKeys.indexOf(param.name) !== -1; - return !(param.in === "path" && !hasKey) - }); - - params = params.concat(pathParams); - var required = verbs.parameter && verbs.parameter.fields && verbs.parameter.fields.Parameter.length > 0; - - params.push({ - "in": "body", - "name": "body", - "description": removeTags(verbs.description), - "required": required, - "schema": { - "$ref": "#/definitions/" + verbDefinitionResult.topLevelParametersRef - } - }); - - pathItemObject[verbs.type] = { - tags: [verbs.group], - summary: removeTags(verbs.description), - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], - parameters: params - } - - if (verbDefinitionResult.topLevelSuccessRef) { - pathItemObject[verbs.type].responses = { - "200": { - "description": "successful operation", - "schema": { - "type": verbDefinitionResult.topLevelSuccessRefType, - "items": { - "$ref": "#/definitions/" + verbDefinitionResult.topLevelSuccessRef - } - } - } - }; - }; - - return pathItemObject; -} - -function createVerbDefinitions(verbs, definitions) { - var result = { - topLevelParametersRef : null, - topLevelSuccessRef : null, - topLevelSuccessRefType : null - }; - var defaultObjectName = verbs.name; - - var fieldArrayResult = {}; - if (verbs && verbs.parameter && verbs.parameter.fields) { - fieldArrayResult = createFieldArrayDefinitions(verbs.parameter.fields.Parameter, definitions, verbs.name, defaultObjectName); - result.topLevelParametersRef = fieldArrayResult.topLevelRef; - }; - - if (verbs && verbs.success && verbs.success.fields) { - fieldArrayResult = createFieldArrayDefinitions(verbs.success.fields["Success 200"], definitions, verbs.name, defaultObjectName); - result.topLevelSuccessRef = fieldArrayResult.topLevelRef; - result.topLevelSuccessRefType = fieldArrayResult.topLevelRefType; - }; - - return result; +function extractPaths(apidocJson) { + var paths = {}; + var apiPaths = groupByUrl(apidocJson); + for (var key in apiPaths) { + paths[key] = extractMethods(apiPaths[key]); + } + return paths; } -function createFieldArrayDefinitions(fieldArray, definitions, topLevelRef, defaultObjectName) { - var result = { - topLevelRef : topLevelRef, - topLevelRefType : null - } - - if (!fieldArray) { - return result; - } - - for (var i = 0; i < fieldArray.length; i++) { - var parameter = fieldArray[i]; - - var nestedName = createNestedName(parameter.field); - var objectName = nestedName.objectName; - if (!objectName) { - objectName = defaultObjectName; - } - var type = parameter.type; - if (i == 0) { - result.topLevelRefType = type; - if(parameter.type == "Object") { - objectName = nestedName.propertyName; - nestedName.propertyName = null; - } else if (parameter.type == "Array") { - objectName = nestedName.propertyName; - nestedName.propertyName = null; - result.topLevelRefType = "array"; - } - result.topLevelRef = objectName; - }; - definitions[objectName] = definitions[objectName] || - { properties : {}, required : [] }; - - if (nestedName.propertyName) { - var prop = { type: (parameter.type || "").toLowerCase(), description: removeTags(parameter.description) }; - if(parameter.type == "Object") { - prop.$ref = "#/definitions/" + parameter.field; - } - - var typeIndex = type.indexOf("[]"); - if(typeIndex !== -1 && typeIndex === (type.length - 2)) { - prop.type = "array"; - prop.items = { - type: type.slice(0, type.length-2) - }; - } - - definitions[objectName]['properties'][nestedName.propertyName] = prop; - if (!parameter.optional) { - var arr = definitions[objectName]['required']; - if(arr.indexOf(nestedName.propertyName) === -1) { - arr.push(nestedName.propertyName); - } - }; - - }; - } - - return result; +/** + * In order to work with schemas in swagger we need to extract schema definitions for post patch and put requests + * Creating definitions object + * @param apidocJson + * @returns {{}} + */ +function extractDefinitions(apidocJson) { + var definitions = {}; + for (var i = 0; i < apidocJson.length; i++) { + if (apidocJson[i].parameter) { + definitions[apidocJson[i].name] = createSchemaDefinition(apidocJson[i].parameter.fields); + } + if (apidocJson[i].success) { + definitions[apidocJson[i].name + "Success"] = createSchemaDefinition(apidocJson[i].success.fields); + } + + } + return definitions; } -function createNestedName(field) { - var propertyName = field; - var objectName; - var propertyNames = field.split("."); - if(propertyNames && propertyNames.length > 1) { - propertyName = propertyNames[propertyNames.length-1]; - propertyNames.pop(); - objectName = propertyNames.join("."); - } - - return { - propertyName: propertyName, - objectName: objectName - } +/** + * Extracts parameters from method and creates schema definitions block + * @param method + * @returns {{properties: {}, required: Array}} + */ +function createSchemaDefinition(fields) { + var pathItemObject = {}; + var required = []; + //create input schema definitions + for (var type in fields) { + if (type != "path" && type != "query") { + for (var i = 0; i < fields[type].length; i++) { + pathItemObject[fields[type][i].field] = + { + type: fields[type][i].type.toLowerCase(), + description: fields[type][i].description + } + //all required fields are pushed to required object + if (!fields[type][i].optional) { + required.push(fields[type][i].field); + } + } + } + } + return {properties: pathItemObject, required: required}; } /** - * Generate get, delete method output + * Generate method output * @param verbs * @returns {{}} */ -function createGetDeleteOutput(verbs,definitions) { - var pathItemObject = {}; - verbs.type = verbs.type === "del" ? "delete" : verbs.type; - - var verbDefinitionResult = createVerbDefinitions(verbs,definitions); - pathItemObject[verbs.type] = { - tags: [verbs.group], - summary: removeTags(verbs.description), - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], - parameters: createPathParameters(verbs) - } - if (verbDefinitionResult.topLevelSuccessRef) { - pathItemObject[verbs.type].responses = { - "200": { - "description": "successful operation", - "schema": { - "type": verbDefinitionResult.topLevelSuccessRefType, - "items": { - "$ref": "#/definitions/" + verbDefinitionResult.topLevelSuccessRef - } - } - } - }; - }; - return pathItemObject; +function extractMethods(methods) { + var pathItemObject = {}; + for (var i = 0; i < methods.length; i++) { + pathItemObject[methods[i].type] = { + tags: [methods[i].group], + summary: methods[i].description, + consumes: [ + "application/json" + ], + produces: [ + "application/json" + ], + parameters: extractParameters(methods[i]), + responses: extractMethodResponses(methods[i]) + } + } + return pathItemObject; } /** - * Iterate through all method parameters and create array of parameter objects which are stored as path parameters + * Iterate through all methods parameters and create array of parameter objects * @param verbs * @returns {Array} */ -function createPathParameters(verbs, pathKeys) { - pathKeys = pathKeys || []; - - var pathItemObject = []; - if (verbs.parameter && verbs.parameter.fields) { - - for (var i = 0; i < verbs.parameter.fields.Parameter.length; i++) { - var param = verbs.parameter.fields.Parameter[i]; - var field = param.field; - var type = param.type; - pathItemObject.push({ - name: field, - in: type === "file" ? "formData" : "path", - required: !param.optional, - type: param.type.toLowerCase(), - description: removeTags(param.description) - }); +function extractParameters(method) { + var pathItemObject = []; + if (method.parameter) { + for (var type in method.parameter.fields) { + //body params are packed together and schema is defined in definitions with method name + if (type == "body") { + pathItemObject.push({ + name: type, + in: type, + required: true, + type: type.toLowerCase(), + description: method.description, + "schema": { + "$ref": "#/definitions/" + method.name + } + }); + } else { + //path params are created separately + for (var i = 0; i < method.parameter.fields[type].length; i++) { + pathItemObject.push({ + name: method.parameter.fields[type][i].field, + in: type === 'file' ? 'formData' : method.parameter.fields[type][i].group.toLowerCase(), + required: !method.parameter.fields[type][i].optional, + type: method.parameter.fields[type][i].type.toLowerCase(), + description: method.parameter.fields[type][i].description + }); + } + } + } + } + return pathItemObject; +} - } - } - return pathItemObject; +/** + * extract method response codes + * @param method + */ +function extractMethodResponses(method) { + var res = {}; + //extract success + if (method.success && method.success.fields) { + var key = Object.keys(method.success.fields)[0]; + res[key] = { + "description": "successful operation", + "schema": {"$ref": "#/definitions/" + method.name + "Success"} + }; + } + + //extract errors + if (method.error && method.error.fields) { + key = Object.keys(method.error.fields)[0]; + for (err in method.error.fields[key]) { + res[method.error.fields[key][err].field] = {"description": method.error.fields[key][err].description}; + } + } + return res; } +/** + * In order to generate swagger for all methods they must be grouped by URL, and type(get, put, delete) is subobject + * See swagger specs for details. + * @param apidocJson + * @returns {*} + */ function groupByUrl(apidocJson) { - return _.chain(apidocJson) - .groupBy("url") - .pairs() - .map(function (element) { - return _.object(_.zip(["url", "verbs"], element)); - }) - .value(); + return _.groupBy(apidocJson, function (b) { + return b.url; + }); } module.exports = { - toSwagger: toSwagger + toSwagger: toSwagger }; \ No newline at end of file