├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── lib ├── index.js └── swagger-doc.js ├── package.json └── test ├── index_test.js └── swaggerDoc_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | coverage.html 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": false, 17 | "strict": false, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "laxcomma" : true, 21 | "expr" : true, 22 | "predef": [ 23 | "define", 24 | "require", 25 | "expect", 26 | "it", 27 | "describe", 28 | "_gaq", 29 | "spyOn", 30 | "afterEach", 31 | "ok", 32 | "console", 33 | "beforeEach", 34 | "before", 35 | "after" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - "npm install -g grunt-cli" 6 | script: 7 | - "grunt travis" 8 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | // load all grunt tasks 3 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | release: { 8 | options: { 9 | tagName: 'v<%= version %>' 10 | } 11 | }, 12 | mochacli: { 13 | options: { 14 | reporter: 'dot' 15 | }, 16 | all: ['test/{,*/}*.js'] 17 | }, 18 | watch: { 19 | scripts: { 20 | files: ['<%= jshint.all %>'], 21 | tasks: ['default'] 22 | } 23 | }, 24 | jshint: { 25 | options: { 26 | jshintrc: '.jshintrc' 27 | }, 28 | all: [ 29 | 'Gruntfile.js', 30 | 'lib/*.js', 31 | 'test/*.js', 32 | 'index.js' 33 | ] 34 | }, 35 | mochacov: { 36 | coveralls: { 37 | options: { 38 | coveralls: { 39 | serviceName: 'travis-ci' 40 | } 41 | } 42 | }, 43 | coverage: { 44 | options: { 45 | reporter: 'html-cov', 46 | output: './coverage.html' 47 | } 48 | }, 49 | options: { 50 | files: 'test/*.js', 51 | require: ['should'] 52 | } 53 | } 54 | }); 55 | 56 | // Default task. 57 | grunt.registerTask('test', ['jshint', 'mochacli']); 58 | grunt.registerTask('travis', ['test', 'mochacov:coveralls']); 59 | grunt.registerTask('coverage', ['test', 'mochacov:coverage']); 60 | grunt.registerTask('default', ['test']); 61 | }; 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Timo Behrmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-restify-swagger 2 | ======================= 3 | 4 | [![Build Status](https://travis-ci.org/z0mt3c/node-restify-swagger.png)](https://travis-ci.org/z0mt3c/node-restify-swagger) 5 | [![Coverage Status](https://coveralls.io/repos/z0mt3c/node-restify-swagger/badge.png?branch=master)](https://coveralls.io/r/z0mt3c/node-restify-swagger?branch=master) 6 | [![Dependency Status](https://gemnasium.com/z0mt3c/node-restify-swagger.png)](https://gemnasium.com/z0mt3c/node-restify-swagger) 7 | 8 | ## Requirements 9 | This project depends on https://github.com/z0mt3c/node-restify-validation. 10 | 11 | ## Example 12 | 13 | var restify = require('restify'); 14 | var restifySwagger = require('node-restify-swagger'); 15 | var restifyValidation = require('node-restify-validation'); 16 | 17 | var server = restify.createServer(); 18 | server.use(restify.queryParser()); 19 | server.use(restifyValidation.validationPlugin({ 20 | errorsAsArray: false, 21 | })); 22 | restifySwagger.configure(server, { 23 | description: 'Description of my API', 24 | title: 'Title of my API', 25 | allowMethodInModelNames: true 26 | }); 27 | 28 | server.post({ 29 | url: '/animals', 30 | swagger: { 31 | summary: 'Add animal', 32 | docPath: 'zoo' 33 | }, 34 | validation: { 35 | name: { isRequired: true, isAlpha:true, scope: 'body' }, 36 | locations: { isRequired: true, type:'array', swaggerType: 'Location', scope: 'body' } 37 | }, 38 | models: { 39 | Location: { 40 | id: 'Location', 41 | properties: { 42 | name: { type: 'string' }, 43 | continent: { type: 'string' } 44 | } 45 | }, 46 | } 47 | }, function (req, res, next) { 48 | res.send(req.params); 49 | }); 50 | 51 | restifySwagger.loadRestifyRoutes(); 52 | server.listen(8001, function () { 53 | console.log('%s listening at %s', server.name, server.url); 54 | }); 55 | 56 | 57 | Above will validate and accept at POST /animals: 58 | 59 | { 60 | "name": "Tiger", 61 | "location": [ 62 | { "name": "India", continent: "Asia" }, 63 | { "name": "China", continent: "Asia" } 64 | ] 65 | } 66 | 67 | And produce swagger spec doc at http://localhost:8001/swagger/resources.json 68 | 69 | { 70 | "swaggerVersion": "1.2", 71 | "apiVersion": [], 72 | "basePath": "http://localhost:8001", 73 | "apis": [ 74 | { 75 | "path": "/swagger/zoo", 76 | "description": "" 77 | } 78 | ] 79 | } 80 | 81 | And endpoint documentation at http://localhost:8001/swagger/zoo 82 | 83 | { 84 | "swaggerVersion": "1.2", 85 | "apiVersion": [], 86 | "basePath": "http://localhost:8001", 87 | "resourcePath": "/swagger/zoo", 88 | "apis": [ 89 | { 90 | "path": "/animals", 91 | "description": "", 92 | "operations": [ 93 | { 94 | "notes": null, 95 | "nickname": "Animals", 96 | "produces": [ 97 | "application/json" 98 | ], 99 | "consumes": [ 100 | "application/json" 101 | ], 102 | "responseMessages": [ 103 | { 104 | "code": 500, 105 | "message": "Internal Server Error" 106 | } 107 | ], 108 | "parameters": [ 109 | { 110 | "name": "Body", 111 | "required": true, 112 | "dataType": "POSTAnimals", 113 | "paramType": "body" 114 | } 115 | ], 116 | "summary": "Add animal", 117 | "httpMethod": "POST", 118 | "method": "POST" 119 | } 120 | ] 121 | } 122 | ], 123 | "models": { 124 | "Location": { 125 | "id": "Location", 126 | "properties": { 127 | "name": { 128 | "type": "string" 129 | }, 130 | "continent": { 131 | "type": "string" 132 | } 133 | } 134 | }, 135 | "POSTAnimals": { 136 | "properties": { 137 | "name": { 138 | "type": "string", 139 | "dataType": "string", 140 | "name": "name", 141 | "required": true 142 | }, 143 | "locations": { 144 | "type": "array", 145 | "dataType": "Location", 146 | "name": "locations", 147 | "items": { 148 | "$ref": "Location" 149 | }, 150 | "required": true 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | 158 | ## Install 159 | 160 | npm install node-restify-swagger 161 | 162 | 163 | ## License 164 | 165 | 166 | The MIT License (MIT) 167 | 168 | Copyright (c) 2013 Timo Behrmann 169 | 170 | Permission is hereby granted, free of charge, to any person obtaining a copy 171 | of this software and associated documentation files (the "Software"), to deal 172 | in the Software without restriction, including without limitation the rights 173 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 174 | copies of the Software, and to permit persons to whom the Software is 175 | furnished to do so, subject to the following conditions: 176 | 177 | The above copyright notice and this permission notice shall be included in 178 | all copies or substantial portions of the Software. 179 | 180 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 181 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 182 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 183 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 184 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 185 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 186 | THE SOFTWARE. -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Timo Behrmann. All rights reserved. 3 | */ 4 | 5 | var _ = require('underscore'); 6 | var swagger = module.exports.swagger = require('./swagger-doc'); 7 | var assert = require('assert'); 8 | var lingo = require('lingo'); 9 | var path = require('path'); 10 | var restifyValidation = require('node-restify-validation'); 11 | var validationUtils = restifyValidation.utils; 12 | 13 | module.exports.swaggerPathPrefix = '/swagger/'; 14 | module.exports.apiDescriptions = {}; 15 | 16 | var getApiDescription = module.exports.getApiDescription = function(path) { 17 | if (path && path.indexOf(module.exports.swaggerPathPrefix) === 0) { 18 | path = path.substr(module.exports.swaggerPathPrefix.length); 19 | } 20 | 21 | return module.exports.apiDescriptions[path]; 22 | }; 23 | 24 | var defaultOptions = { 25 | discoveryUrl: module.exports.swaggerPathPrefix+'resources.json' 26 | }; 27 | 28 | var convertToSwagger = module.exports._convertToSwagger = function (path) { 29 | return path.replace(/:([^/]+)/g, '{$1}'); 30 | }; 31 | 32 | var mapToSwaggerType = module.exports._mapToSwaggerType = function (value) { 33 | var type = 'string'; 34 | 35 | if (!value) { 36 | // don't care 37 | } else if (_.has(value, 'swaggerType')) { 38 | type = value.swaggerType; 39 | } else if (value.isDate) { 40 | type = 'dateTime'; 41 | } else if (value.isBoolean) { 42 | type = 'boolean'; 43 | } else if (value.isInt || value.isNumeric) { 44 | type = 'integer'; 45 | } else if (value.isFloat || value.isDecimal) { 46 | type = 'float'; 47 | } else if (value && value.isJSONObject) { 48 | type = 'object'; 49 | } else if (value && value.isJSONArray) { 50 | type = 'array'; 51 | } 52 | 53 | return type; 54 | }; 55 | 56 | module.exports.configure = function (server, options) { 57 | this.options = _.extend(defaultOptions, options); 58 | this.server = server; 59 | 60 | if (this.options.apiDescriptions) { 61 | module.exports.apiDescriptions = this.options.apiDescriptions; 62 | } 63 | 64 | swagger.configure(this.server, this.options); 65 | }; 66 | 67 | module.exports.findOrCreateResource = function (resource, options) { 68 | assert.ok(swagger.resources, 'Swagger not initialized! Execution of configure required!'); 69 | 70 | var found = _.find(swagger.resources, function (myResource) { 71 | return _.isEqual(resource, myResource.path); 72 | }); 73 | 74 | if (found && options.models) { 75 | _.extend(found.models, options.models); 76 | } 77 | 78 | var docs = found || swagger.createResource(resource, options || { models: {}, description: getApiDescription(resource) }); 79 | return docs; 80 | }; 81 | 82 | var pushPathParameters = module.exports._pushPathParameters = function (item, validationModel, parameters) { 83 | var hasPathParameters = false; 84 | 85 | _.each(item.path.restifyParams, function (param) { 86 | if (!_.has(validationModel, param)) { 87 | parameters.push({name: param, description: null, required: true, dataType: 'String', paramType: 'path'}); 88 | hasPathParameters = true; 89 | } 90 | }); 91 | 92 | return hasPathParameters; 93 | }; 94 | 95 | var extractSubtypes = module.exports._extractSubtypes = function (model, swaggerDoc) { 96 | _.each(model.properties, function (element, key) { 97 | var isSubtype = !(element.type && element.dataType); 98 | var submodelName = lingo.capitalize(lingo.camelcase(key)); 99 | 100 | if (isSubtype) { 101 | if (!_.has(swaggerDoc, submodelName)) { 102 | swaggerDoc.models[submodelName] = { properties: element }; 103 | extractSubtypes(swaggerDoc.models[submodelName], swaggerDoc); 104 | } 105 | model.properties[key] = { type: submodelName }; 106 | } 107 | }); 108 | }; 109 | 110 | 111 | module.exports.loadRestifyRoutes = function () { 112 | var self = this; 113 | var defined = {}; 114 | 115 | _.each(this.server.router.mounts, function (item) { 116 | 117 | var spec = item.spec; 118 | var validationModel = spec.validation; 119 | 120 | if (validationModel) { 121 | var url = spec.url || item.path; 122 | var name = lingo.camelcase(url.replace(/[\/_]/g, ' ')); 123 | var method = spec.method; 124 | var specSwagger = spec.swagger || {}; 125 | var mySwaggerPathParts = specSwagger.docPath || url.split(path.sep)[1]; 126 | var mySwaggerPath = module.exports.swaggerPathPrefix + mySwaggerPathParts; 127 | var models = spec.models || {}; 128 | 129 | if (!_.contains(self.options.blacklist, mySwaggerPathParts)) { 130 | var swaggerDoc = self.findOrCreateResource(mySwaggerPath, { models: models, description: getApiDescription(mySwaggerPath) }); 131 | var parameters = []; 132 | var modelName = name; 133 | // to allow generetion of unique models for request with same url but different method 134 | if (self.options.allowMethodInModelNames) { 135 | modelName = method + name; 136 | } 137 | var model = { properties: { } }; 138 | 139 | var hasPathParameters = false; 140 | var hasBodyParameters = false; 141 | var hasQueryParameters = false; 142 | 143 | // Add missing but required path variables - even if they are not specified 144 | var restifyPathParameters = ( item.path.restifyParams && item.path.restifyParams.length > 0 ) ? item.path.restifyParams : []; 145 | hasPathParameters = pushPathParameters(item, validationModel, parameters); 146 | 147 | _.each(validationModel, function (valueArray, key) { 148 | var value = _.reduce(_.isArray(valueArray) ? valueArray : [valueArray], 149 | function (memo, entry) { 150 | return _.extend(memo, entry); 151 | }, {}); 152 | 153 | var swaggerType = mapToSwaggerType(value); 154 | var myProperty = { 155 | type: swaggerType, 156 | dataType: value.swaggerType || swaggerType, 157 | name: key, 158 | description: value.description || undefined 159 | }; 160 | 161 | if (value.type === 'array') { 162 | myProperty.type = 'array'; 163 | myProperty.items = { 164 | '$ref': swaggerType 165 | }; 166 | } 167 | 168 | if (_.isArray(value.isIn)) { 169 | myProperty.allowableValues = { 170 | 'valueType': 'LIST', 171 | 'values': value.isIn 172 | }; 173 | if (value.defaultValue) { 174 | myProperty.defaultValue = value.defaultValue; 175 | } 176 | 177 | } 178 | 179 | if (_.isBoolean(value.isRequired) && value.isRequired) { 180 | myProperty.required = true; 181 | } 182 | 183 | if (_.isEqual(value.scope, 'path')) { 184 | myProperty.paramType = 'path'; 185 | hasPathParameters = true; 186 | parameters.push(myProperty); 187 | 188 | } else if (_.isEqual(value.swaggerType, 'file')) { 189 | myProperty.paramType = value.swaggerScope || value.scope; 190 | hasQueryParameters = true; 191 | parameters.push(myProperty); 192 | 193 | } else if (_.isEqual(value.scope, 'body')) { 194 | model.properties[key] = myProperty; 195 | hasBodyParameters = true; 196 | } else if (_.isEqual(value.scope, 'header')) { 197 | myProperty.paramType = 'header'; 198 | hasPathParameters = false; 199 | parameters.push(myProperty); 200 | } else { 201 | myProperty.paramType = 'query'; 202 | hasQueryParameters = true; 203 | parameters.push(myProperty); 204 | } 205 | }); 206 | 207 | if (hasBodyParameters) { 208 | model.properties = validationUtils.deflat(model.properties); 209 | extractSubtypes(model, swaggerDoc); 210 | swaggerDoc.models[modelName] = model; 211 | parameters.push({ 212 | name: 'Body', 213 | description: swagger.summary, 214 | required: true, 215 | dataType: modelName, 216 | paramType: 'body' 217 | }); 218 | } 219 | 220 | // avoid duplicated routes 221 | if (!defined[spec.method+spec.url]) { 222 | swaggerDoc[method.toLowerCase()](convertToSwagger(url), specSwagger.summary, { 223 | notes: specSwagger.notes || null, 224 | nickname: specSwagger.nickname || name, 225 | responseClass: specSwagger.responseClass || undefined, 226 | produces: specSwagger.produces || swagger.produces || [ 227 | 'application/json' 228 | ], 229 | consumes: specSwagger.consumes || swagger.consumes || [ 230 | 'application/json' 231 | ], 232 | responseMessages: specSwagger.responseMessages || swagger.responseMessages || [ 233 | { 234 | code: 500, 235 | message: 'Internal Server Error' 236 | } 237 | ], 238 | parameters: parameters 239 | }); 240 | defined[spec.method+spec.url] = true; 241 | } 242 | } 243 | } 244 | }); 245 | }; 246 | 247 | -------------------------------------------------------------------------------- /lib/swagger-doc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2012 Eirikur Nilsson 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | 'use strict'; 27 | 28 | var SWAGGER_METHODS = ['get', 'patch', 'post', 'put', 'delete'], 29 | SWAGGER_VERSION = '1.2'; 30 | 31 | function Resource(path, options) { 32 | options = options || {}; 33 | 34 | this.path = path; 35 | this.models = options.models || {}; 36 | this.apis = {}; 37 | this.description = options.description; 38 | } 39 | 40 | Resource.prototype.getApi = function(path) { 41 | if (!(path in this.apis)) { 42 | this.apis[path] = { 43 | path: path, 44 | description: '', 45 | operations: [] 46 | }; 47 | } 48 | return this.apis[path]; 49 | }; 50 | 51 | var operationType = function(method) { 52 | method = method.toUpperCase(); 53 | 54 | return function(path, summary, operation) { 55 | if (!operation) { 56 | operation = summary; 57 | summary = ''; 58 | } else { 59 | operation.summary = summary; 60 | } 61 | operation.httpMethod = method; 62 | operation.method = method; 63 | 64 | var api = this.getApi(path); 65 | api.operations.push(operation); 66 | }; 67 | }; 68 | 69 | for (var i = 0; i < SWAGGER_METHODS.length; i++) { 70 | var m = SWAGGER_METHODS[i]; 71 | Resource.prototype[m] = operationType(m); 72 | } 73 | 74 | 75 | var swagger = module.exports = {}; 76 | 77 | swagger.Resource = Resource; 78 | 79 | swagger.resources = []; 80 | 81 | /** 82 | * Configures swagger-doc for a express or restify server. 83 | * @param {Server} server A server object from express or restify. 84 | * @param {{discoveryUrl: string, version: string, basePath: string}} options Options 85 | */ 86 | swagger.configure = function(server, options) { 87 | options = options || {}; 88 | 89 | var discoveryUrl = options.discoveryUrl || '/resources.json', 90 | self = this; 91 | 92 | this.server = server; 93 | this.apiVersion = options.version || this.server.versions || '1.0.0'; 94 | this.basePath = options.basePath; 95 | this.info = options.info; 96 | this.responseMessages = options.responseMessages; 97 | 98 | this.server.get(discoveryUrl, function(req, res) { 99 | var result = self._createResponse(req); 100 | result.apis = self.resources.map(function(r) { return {path: r.path, description: r.description || '' }; }); 101 | res.header('Access-Control-Allow-Origin', '*'); 102 | res.header('Access-Control-Allow-Methods', 'GET, PATCH, POST, DELETE, PUT'); 103 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 104 | res.send(result); 105 | }); 106 | }; 107 | 108 | /** 109 | * Registers a Resource with the specified path and options. 110 | * @param {!String} path The path of the resource. 111 | * @param {{models}} options Optional options that can contain models. 112 | * @return {Resource} The new resource. 113 | */ 114 | swagger.createResource = function(path, options) { 115 | var resource = new Resource(path, options), 116 | self = this; 117 | this.resources.push(resource); 118 | 119 | this.server.get(path, function(req, res) { 120 | var result = self._createResponse(req); 121 | result.resourcePath = path; 122 | result.apis = Object.keys(resource.apis).map(function(k) { return resource.apis[k]; }); 123 | result.models = resource.models; 124 | res.header('Access-Control-Allow-Origin', '*'); 125 | res.header('Access-Control-Allow-Methods', 'GET, PATCH, POST, DELETE, PUT'); 126 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 127 | res.send(result); 128 | }); 129 | 130 | return resource; 131 | }; 132 | 133 | swagger._createResponse = function(req) { 134 | var basePath = this.basePath || 'http://' + req.headers.host; 135 | return { 136 | swaggerVersion: SWAGGER_VERSION, 137 | apiVersion: this.apiVersion, 138 | basePath: basePath, 139 | info: this.info 140 | }; 141 | }; 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-restify-swagger", 3 | "version": "0.1.7", 4 | "author": { 5 | "name": "Timo Behrmann" 6 | }, 7 | "license": "MIT", 8 | "keywords": [ 9 | "rest", 10 | "api", 11 | "restify", 12 | "validation" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/z0mt3c/node-restify-swagger.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/z0mt3c/node-restify-swagger/issues" 20 | }, 21 | "scripts": { 22 | "test": "grunt test", 23 | "blanket": { 24 | "pattern": "//^(?!.*node_modules.*$).*lib//" 25 | } 26 | }, 27 | "main": "lib/index", 28 | "readmeFilename": "README.md", 29 | "dependencies": { 30 | "underscore": "~1.7.0", 31 | "underscore.string": "~2.3.3", 32 | "node-restify-validation": "~0.1.0", 33 | "lingo": "0.0.5" 34 | }, 35 | "devDependencies": { 36 | "restify": "2.8.3", 37 | "grunt": "~0.4.1", 38 | "grunt-contrib-jshint": "~0.10.0", 39 | "grunt-contrib-watch": "~0.6.1", 40 | "grunt-mocha-cli": "~1.10.0", 41 | "matchdep": "~0.3.0", 42 | "should": "~4.2.1", 43 | "sinon": "~1.11.1", 44 | "grunt-mocha-cov": "~0.3.0", 45 | "grunt-release": "~0.7.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/index_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Timo Behrmann. All rights reserved. 3 | */ 4 | 5 | var assert = require('assert'); 6 | var should = require('should'); 7 | var sinon = require('sinon'); 8 | var _ = require('underscore'); 9 | var index = require('../lib/index'); 10 | var restify = require('restify'); 11 | 12 | describe('test', function () { 13 | beforeEach(function() { 14 | index.swagger.resources = []; 15 | }); 16 | 17 | it('_convertToSwagger', function (done) { 18 | index._convertToSwagger.should.have.type('function'); 19 | index._convertToSwagger('hello/:firstParam/:secondParam').should.equal('hello/{firstParam}/{secondParam}'); 20 | index._convertToSwagger('hello/:firstParam/test/:secondParam').should.equal('hello/{firstParam}/test/{secondParam}'); 21 | index._convertToSwagger('hello/:firstParam/test/:secondParam/asdf').should.equal('hello/{firstParam}/test/{secondParam}/asdf'); 22 | done(); 23 | }); 24 | 25 | it('_mapToSwaggerType', function (done) { 26 | index._mapToSwaggerType.should.have.type('function'); 27 | index._mapToSwaggerType({ isJSONObject: true }).should.equal('object'); 28 | index._mapToSwaggerType({ isJSONArray: true }).should.equal('array'); 29 | index._mapToSwaggerType({ isDate: true }).should.equal('dateTime'); 30 | index._mapToSwaggerType({ isFloat: true }).should.equal('float'); 31 | index._mapToSwaggerType({ isBoolean: true }).should.equal('boolean'); 32 | index._mapToSwaggerType({ swaggerType: 'asdf' }).should.equal('asdf'); 33 | index._mapToSwaggerType({ isInt: true }).should.equal('integer'); 34 | index._mapToSwaggerType({ }).should.equal('string'); 35 | index._mapToSwaggerType('asdfasdf').should.equal('string'); 36 | index._mapToSwaggerType(null).should.equal('string'); 37 | done(); 38 | }); 39 | 40 | it('configure', function (done) { 41 | var server = sinon.stub(index.swagger, 'configure', function (myServer, myOptions) { 42 | myServer.should.equal(server); 43 | myOptions.discoveryUrl.should.exist; 44 | myOptions.abc.should.be.ok; 45 | }); 46 | 47 | index.configure(server, { abc: true }); 48 | server.called.should.be.ok; 49 | server.restore(); 50 | done(); 51 | }); 52 | 53 | it('findOrCreateResource', function (done) { 54 | 55 | 56 | var resource = '/test'; 57 | var options = {}; 58 | 59 | var server = sinon.stub(index.swagger, 'createResource', function (myResource, myOptions) { 60 | myResource.should.equal(resource); 61 | myOptions.should.equal(options); 62 | return true; 63 | }); 64 | 65 | index.findOrCreateResource(resource, options).should.be.ok; 66 | server.called.should.be.ok; 67 | server.calledOnce.should.be.ok; 68 | 69 | var resourceObj = { path: resource }; 70 | index.swagger.resources = [ resourceObj ]; 71 | 72 | index.findOrCreateResource(resource, options).should.equal(resourceObj); 73 | server.calledTwice.should.not.be.ok; 74 | server.restore(); 75 | index.swagger.resources = []; 76 | done(); 77 | }); 78 | 79 | it('pushPathParameters', function (done) { 80 | var item = { path: { restifyParams: []}}, 81 | validationModel = {}, 82 | parameters = []; 83 | 84 | index._pushPathParameters(item, validationModel, parameters).should.not.be.ok; 85 | 86 | item.path.restifyParams = [ 'test' ]; 87 | index._pushPathParameters(item, validationModel, parameters).should.be.ok; 88 | 89 | validationModel = { test: {} }; 90 | index._pushPathParameters(item, validationModel, parameters).should.not.be.ok; 91 | parameters.length.should.equal(1); 92 | parameters[0].name.should.equal('test'); 93 | 94 | done(); 95 | }); 96 | 97 | it('loadRestifyRoutes', function (done) { 98 | var server = restify.createServer({}); 99 | server.get({ url: '/asdf/:p1/:p2', 100 | swagger: { 101 | summary: 'summary', 102 | notes: 'notes', 103 | nickname: 'nickname' 104 | }, 105 | validation: { 106 | q1: { isRequired: true, isIn: ['asdf'], scope: 'query', description: 'description q1'}, 107 | b1: { isRequired: true, isIn: ['asdf'], defaultValue: 'asdf', scope: 'body', description: 'description b1'}, 108 | p2: { isRequired: true, isIn: ['asdf'], defaultValue: 'asdf', scope: 'path', description: 'description p2'}, 109 | p3: { isRequired: true, swaggerType: 'file', scope: 'body', description: 'description p2'}, 110 | h1: { isRequired: true, isIn: ['asdf'], scope: 'header', description: 'description h1'} 111 | } 112 | }, function (req, res, next) { 113 | // not called 114 | false.should.be.ok; 115 | }); 116 | 117 | index.configure(server, { apiDescriptions: { 118 | 'asdf': 'asdf' 119 | }}); 120 | index.loadRestifyRoutes(); 121 | 122 | index.swagger.resources.length.should.equal(1); 123 | var swaggerResource = index.swagger.resources[0]; 124 | swaggerResource.models.AsdfP1P2.should.exist; 125 | swaggerResource.models.AsdfP1P2.properties.b1.should.exist; 126 | swaggerResource.models.AsdfP1P2.properties.b1.defaultValue.should.equal('asdf'); 127 | swaggerResource.models.AsdfP1P2.properties.b1.allowableValues.should.exist; 128 | swaggerResource.models.AsdfP1P2.properties.b1.allowableValues.values[0].should.equal('asdf'); 129 | swaggerResource.models.AsdfP1P2.properties.b1.required.should.be.ok; 130 | 131 | var swaggerApi = swaggerResource.apis['/asdf/{p1}/{p2}']; 132 | swaggerApi.operations.length.should.equal(1); 133 | 134 | var swaggerOperation = swaggerApi.operations[0]; 135 | swaggerOperation.notes.should.equal('notes'); 136 | swaggerOperation.nickname.should.equal('nickname'); 137 | swaggerOperation.parameters.length.should.equal(6); 138 | _.difference(['q1', 'p1', 'p2', 'p3', 'h1', 'Body'], _.pluck(swaggerOperation.parameters, 'name')).length.should.equal(0); 139 | 140 | done(); 141 | }); 142 | it('loadRestifyRoutesWithResponseModel', function (done) { 143 | var server = restify.createServer(); 144 | 145 | var Models = { 146 | Model : { 147 | properties: { 148 | inputValue: { 149 | type: 'string', 150 | name: 'name', 151 | description: 'description', 152 | required: true 153 | } 154 | } 155 | } 156 | }; 157 | 158 | server.get({ url: '/model', 159 | models: Models, 160 | swagger: { 161 | summary: 'summary', 162 | notes: 'notes', 163 | nickname: 'nickname', 164 | responseClass: 'Model', 165 | }, 166 | validation: { 167 | 168 | } 169 | }, function (req, res, next) { 170 | // not called 171 | false.should.be.ok; 172 | }); 173 | 174 | index.configure(server, {}); 175 | index.loadRestifyRoutes(); 176 | 177 | index.swagger.resources.length.should.equal(1); 178 | var swaggerResource = index.swagger.resources[0]; 179 | 180 | swaggerResource.models.Model.should.exist; 181 | swaggerResource.models.Model.properties.inputValue.should.exist; 182 | swaggerResource.models.Model.properties.inputValue.name.should.exist; 183 | 184 | 185 | done(); 186 | }); 187 | it('loadRestifyRoutesWithResponseModelSecondRoute', function (done) { 188 | var server = restify.createServer(); 189 | 190 | var ModelsV1 = { 191 | Model : { 192 | properties: { 193 | inputValue: { 194 | type: 'string', 195 | name: 'name', 196 | description: 'description', 197 | required: true 198 | } 199 | } 200 | } 201 | }; 202 | 203 | server.get({ url: '/model', 204 | models: ModelsV1, 205 | swagger: { 206 | summary: 'summary', 207 | notes: 'notes', 208 | nickname: 'nickname', 209 | responseClass: 'Model', 210 | }, 211 | validation: { 212 | 213 | } 214 | }, function (req, res, next) { 215 | // not called 216 | false.should.be.ok; 217 | }); 218 | 219 | var ModelsV2 = { 220 | DetailModel : { 221 | properties: { 222 | inputValue: { 223 | type: 'string', 224 | name: 'name', 225 | description: 'description', 226 | required: true 227 | } 228 | } 229 | } 230 | }; 231 | 232 | server.get({ url: '/model/detail', 233 | models: ModelsV2, 234 | swagger: { 235 | summary: 'summary', 236 | notes: 'notes', 237 | nickname: 'nickname', 238 | responseClass: 'DetailModel', 239 | }, 240 | validation: { 241 | 242 | } 243 | }, function (req, res, next) { 244 | // not called 245 | false.should.be.ok; 246 | }); 247 | 248 | index.configure(server, {}); 249 | index.loadRestifyRoutes(); 250 | 251 | index.swagger.resources.length.should.equal(1); 252 | var swaggerResource = index.swagger.resources[0]; 253 | 254 | swaggerResource.models.DetailModel.should.exist; 255 | swaggerResource.models.DetailModel.properties.inputValue.should.exist; 256 | swaggerResource.models.DetailModel.properties.inputValue.name.should.exist; 257 | 258 | swaggerResource.models.Model.should.exist; 259 | swaggerResource.models.Model.properties.inputValue.should.exist; 260 | swaggerResource.models.Model.properties.inputValue.name.should.exist; 261 | 262 | 263 | done(); 264 | }); 265 | it('loadRestifyRoutesWithResponseModelAsArray', function (done) { 266 | var server = restify.createServer(); 267 | 268 | var Models = { 269 | Model : { 270 | properties: { 271 | inputValue: { 272 | type: 'string', 273 | name: 'name', 274 | description: 'description', 275 | required: true 276 | } 277 | } 278 | } 279 | }; 280 | 281 | server.get({ url: '/modelarray', 282 | models: Models, 283 | swagger: { 284 | summary: 'summary', 285 | notes: 'notes', 286 | nickname: 'nickname' 287 | }, 288 | validation: { 289 | models: { isRequired: true, swaggerType: 'Model', type: 'array', scope: 'body' } 290 | } 291 | }, function (req, res, next) { 292 | // not called 293 | false.should.be.ok; 294 | }); 295 | 296 | index.configure(server, {}); 297 | index.loadRestifyRoutes(); 298 | 299 | index.swagger.resources.length.should.equal(1); 300 | var swaggerResource = index.swagger.resources[0]; 301 | 302 | swaggerResource.models.Modelarray.should.exist; 303 | swaggerResource.models.Modelarray.properties.models.type.should.equal('array'); 304 | swaggerResource.models.Modelarray.properties.models.dataType.should.equal('Model'); 305 | 306 | done(); 307 | }); 308 | it('should allow same paths with different method names have different models', function (done) { 309 | var server = restify.createServer(); 310 | 311 | // defining /model route where only one parameter is provided in validation 312 | server.del({ url: '/model', 313 | swagger: { 314 | summary: 'summary', 315 | notes: 'notes', 316 | nickname: 'nickname', 317 | responseClass: 'Model', 318 | }, 319 | validation: { 320 | id: { isRequired: true, isInteger: true, scope: 'body' } 321 | } 322 | }, function (req, res, next) { 323 | // not called 324 | false.should.be.ok; 325 | }); 326 | 327 | // defining route with same path but different method - validation model is different 328 | server.put({ url: '/model', 329 | swagger: { 330 | summary: 'summary', 331 | notes: 'notes', 332 | nickname: 'nickname', 333 | responseClass: 'Model', 334 | }, 335 | validation: { 336 | foo: { isRequired: true, isBoolean:true, scope: 'body' }, 337 | bar: { isRequired: true, isBoolean:true, scope: 'body' } 338 | } 339 | }, function (req, res, next) { 340 | // not called 341 | false.should.be.ok; 342 | }); 343 | 344 | index.configure(server, { 345 | allowMethodInModelNames: true 346 | }); 347 | index.loadRestifyRoutes(); 348 | 349 | //index.swagger.resources.length.should.equal(2); 350 | var swaggerResource = index.swagger.resources[0]; 351 | 352 | Object.keys(swaggerResource.models).length.should.equal(2); 353 | Object.keys(swaggerResource.models.DELETEModel.properties).length.should.equal(1); 354 | Object.keys(swaggerResource.models.PUTModel.properties).length.should.equal(2); 355 | 356 | done(); 357 | }); 358 | }); 359 | -------------------------------------------------------------------------------- /test/swaggerDoc_test.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, beforeEach*/ 2 | var assert = require('assert'); 3 | var Resource = require('../lib/swagger-doc.js').Resource; 4 | 5 | describe('Resource', function() { 6 | var resource; 7 | 8 | beforeEach(function() { 9 | resource = new Resource('/users'); 10 | resource.getApi('/users/old'); 11 | }); 12 | 13 | describe('#path', function() { 14 | it('should be initialized in constructor', function() { 15 | assert.equal(resource.path, '/users'); 16 | }); 17 | }); 18 | 19 | describe('#models', function() { 20 | it('is an empty object by default', function() { 21 | assert.equal(Object.keys(resource.models).length, 0); 22 | }); 23 | 24 | it('can be initialized in constructor', function() { 25 | resource = new Resource('/users', {models: {Tag: {id: 'Tag'}}}); 26 | assert.equal(resource.models.Tag.id, 'Tag'); 27 | }); 28 | }); 29 | 30 | describe('#getApi()', function() { 31 | it('returns an empty api for new paths', function() { 32 | assert(!('/users/new' in resource.apis)); 33 | var api = resource.getApi('/users/new'); 34 | 35 | assert('/users/new' in resource.apis); 36 | assert.equal(api.path, '/users/new'); 37 | assert.equal(api.description, ''); 38 | assert.equal(api.operations.length, 0); 39 | }); 40 | 41 | it('returns same api instance for existing paths', function() { 42 | var oldApi = resource.getApi('/users/old'); 43 | var api = resource.getApi('/users/old'); 44 | 45 | assert.equal(oldApi, api); 46 | }); 47 | }); 48 | 49 | describe('#get()', function() { 50 | it('registers an operation on an api', function() { 51 | resource.get('/users', { 52 | summary: 'Returns all users', 53 | parameters: [ 54 | {name: 'sort', required: false, dataType: 'string', paramType: 'get'} 55 | ] 56 | }); 57 | 58 | var op = resource.getApi('/users').operations[0]; 59 | assert.equal(op.summary, 'Returns all users'); 60 | assert.equal(op.httpMethod, 'GET'); 61 | assert.equal(op.parameters.length, 1); 62 | }); 63 | 64 | it('supports summary as optional argument', function() { 65 | resource.get('/users', 'Returns all users', {}); 66 | 67 | var op = resource.getApi('/users').operations[0]; 68 | assert.equal(op.summary, 'Returns all users'); 69 | }); 70 | }); 71 | }); --------------------------------------------------------------------------------