├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── api ├── controllers │ ├── ContactController.js │ ├── GroupController.js │ └── SwaggerController.js ├── hooks │ └── swagger │ │ └── index.js └── models │ ├── Contact.js │ └── Group.js ├── config ├── marlinspike.js └── swagger.js ├── dist └── api │ └── controllers │ ├── ContactController.js │ └── GroupController.js ├── gulpfile.js ├── lib ├── spec.js └── xfmr.js ├── package.json └── test ├── bootstrap.test.js └── xfmr.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | *.sw* 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailsjs/sails-swagger/ac0ffb27ecf324658f49edfe06aa245b7d951bb1/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | 5 | sudo: false 6 | 7 | deploy: 8 | provider: npm 9 | email: tjwebb@balderdash.co 10 | api_key: 11 | secure: K0RUyAG988D2xdNC6AD+k8xcreihwRicpuROzl6BE4JFsVTB8OmGzSU1slI/jNFduOjL44tq+p0XGEjeIp7qe13DgLmmWuZIwNkBSmvGgD2mkjUHAdb6i8C5IVlW6kkpy9dq6tpdOn0i7ZNSwNd8RCuQ4sI8AZAJB/izhYSODP8= 12 | on: 13 | tags: true 14 | repo: tjwebb/sails-swagger 15 | all_branches: true 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sails-swagger 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][ci-image]][ci-url] 5 | [![Dependency Status][daviddm-image]][daviddm-url] 6 | [![Code Climate][codeclimate-image]][codeclimate-url] 7 | 8 | 9 | [swagger.io](http://swagger.io/) (v2.0) hook for Sails. The application's models, controllers, and routes are aggregated and transformed into a Swagger Document. Supports the Swagger 2.0 specification. 10 | 11 | ## Install 12 | 13 | ```sh 14 | $ npm install sails-swagger --save 15 | ``` 16 | 17 | ## Configuration 18 | ```js 19 | // config/swagger.js 20 | module.exports.swagger = { 21 | /** 22 | * require() the package.json file for your Sails app. 23 | */ 24 | pkg: require('../package'), 25 | ui: { 26 | url: 'http://swagger.balderdash.io' 27 | } 28 | }; 29 | ``` 30 | 31 | ## Usage 32 | After installing and configuring swagger, you can find the docs output on the [/swagger/doc](http://localhost:1337/swagger/doc) route. 33 | 34 | You may also specify additional swagger endpoints by specifying the swagger spec in config/routes.js 35 | 36 | ``` 37 | /** 38 | * Route Mappings 39 | * @file config/routes.js 40 | * (sails.config.routes) 41 | * 42 | * Your routes map URLs to views and controllers. 43 | */ 44 | 45 | module.exports.routes = { 46 | 47 | /*************************************************************************** 48 | * * 49 | * Make the view located at `views/homepage.ejs` (or `views/homepage.jade`, * 50 | * etc. depending on your default view engine) your home page. * 51 | * * 52 | * (Alternatively, remove this and add an `index.html` file in your * 53 | * `assets` directory) * 54 | * * 55 | ***************************************************************************/ 56 | 57 | '/': { 58 | view: 'homepage' 59 | }, 60 | 61 | /*************************************************************************** 62 | * * 63 | * Custom routes here... * 64 | * * 65 | * If a request to a URL doesn't match any of the custom routes above, it * 66 | * is matched against Sails route blueprints. See `config/blueprints.js` * 67 | * for configuration options and examples. * 68 | * * 69 | ***************************************************************************/ 70 | 'get /groups/:id': { 71 | controller: 'GroupController', 72 | action: 'test', 73 | skipAssets: 'true', 74 | //swagger path object 75 | swagger: { 76 | methods: ['GET', 'POST'], 77 | summary: ' Get Groups ', 78 | description: 'Get Groups Description', 79 | produces: [ 80 | 'application/json' 81 | ], 82 | tags: [ 83 | 'Groups' 84 | ], 85 | responses: { 86 | '200': { 87 | description: 'List of Groups', 88 | schema: 'Group', // api/model/Group.js, 89 | type: 'array' 90 | } 91 | }, 92 | parameters: [] 93 | 94 | } 95 | }, 96 | 'put /groups/:id': { 97 | controller: 'GroupController', 98 | action: 'test', 99 | skipAssets: 'true', 100 | //swagger path object 101 | swagger: { 102 | methods: ['PUT', 'POST'], 103 | summary: 'Update Groups ', 104 | description: 'Update Groups Description', 105 | produces: [ 106 | 'application/json' 107 | ], 108 | tags: [ 109 | 'Groups' 110 | ], 111 | responses: { 112 | '200': { 113 | description: 'Updated Group', 114 | schema: 'Group' // api/model/Group.js 115 | } 116 | }, 117 | parameters: [ 118 | 'Group' // api/model/Group.js 119 | ] 120 | 121 | } 122 | } 123 | }; 124 | 125 | 126 | ``` 127 | 128 | ## License 129 | MIT 130 | 131 | ## Maintained By 132 | [](http://langa.io) 133 | 134 | [sails-version-image]: https://goo.gl/gTUV5x 135 | [sails-url]: http://sailsjs.org 136 | [npm-image]: https://img.shields.io/npm/v/sails-swagger.svg?style=flat 137 | [npm-url]: https://npmjs.org/package/sails-swagger 138 | [ci-image]: https://img.shields.io/travis/langateam/sails-swagger/master.svg?style=flat 139 | [ci-url]: https://travis-ci.org/langateam/sails-swagger 140 | [daviddm-image]: http://img.shields.io/david/langateam/sails-swagger.svg?style=flat 141 | [daviddm-url]: https://david-dm.org/langateam/sails-swagger 142 | [codeclimate-image]: https://img.shields.io/codeclimate/github/langateam/sails-swagger.svg?style=flat 143 | [codeclimate-url]: https://codeclimate.com/github/langateam/sails-swagger 144 | -------------------------------------------------------------------------------- /api/controllers/ContactController.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test (req, res) { 3 | let contact = Contact.testInstance(); 4 | return res.status(200).jsonx(contact); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/controllers/GroupController.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test (req, res) { 3 | let group = Group.testInstance(); 4 | return res.status(200).jsonx([group]); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/controllers/SwaggerController.js: -------------------------------------------------------------------------------- 1 | const SwaggerController = { 2 | doc(req, res) { 3 | res.status(200).jsonx(sails.hooks.swagger.doc) 4 | }, 5 | 6 | ui (req, res) { 7 | let docUrl = req.protocol + '://' + req.get('Host') + '/swagger/doc' 8 | res.redirect(sails.config.swagger.ui.url + '?doc=' + encodeURIComponent(docUrl)) 9 | } 10 | } 11 | 12 | export default SwaggerController 13 | -------------------------------------------------------------------------------- /api/hooks/swagger/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import _ from 'lodash' 3 | import Marlinspike from 'marlinspike' 4 | import xfmr from '../../../lib/xfmr' 5 | 6 | class Swagger extends Marlinspike { 7 | 8 | defaults (overrides) { 9 | return { 10 | 'swagger': { 11 | pkg: { 12 | name: 'No package information', 13 | description: 'You should set sails.config.swagger.pkg to retrieve the content of the package.json file', 14 | version: '0.0.0' 15 | }, 16 | ui: { 17 | url: 'http://localhost:8080/' 18 | } 19 | }, 20 | 'routes': { 21 | '/swagger/doc': { 22 | controller: 'SwaggerController', 23 | action: 'doc' 24 | } 25 | } 26 | }; 27 | } 28 | 29 | constructor (sails) { 30 | super(sails, module); 31 | } 32 | 33 | initialize (next) { 34 | let hook = this.sails.hooks.swagger 35 | this.sails.after('lifted', () => { 36 | hook.doc = xfmr.getSwagger(this.sails, this.sails.config.swagger.pkg) 37 | }) 38 | 39 | next() 40 | } 41 | } 42 | 43 | export default Marlinspike.createSailsHook(Swagger) 44 | -------------------------------------------------------------------------------- /api/models/Contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Contact.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | testInstance() { 11 | return { 12 | name: 'Contact Name', 13 | group: [ 14 | { 15 | name: 'Group Name' 16 | } 17 | ] 18 | } 19 | }, 20 | 21 | attributes: { 22 | name: { 23 | type: 'string', 24 | defaultsTo: 'Contact Name' 25 | }, 26 | group: { 27 | model: 'Group' 28 | } 29 | 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /api/models/Group.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Group.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | testInstance() { 11 | return { 12 | name: 'Group Name', 13 | contacts: [{ 14 | name: 'Contact 1' 15 | }, { 16 | name: 'Contact 2' 17 | }] 18 | } 19 | }, 20 | 21 | attributes: { 22 | name: { 23 | type: 'string', 24 | defaultsTo: 'Group Name' 25 | }, 26 | contacts: { 27 | collection: 'Contact', 28 | via: 'group' 29 | } 30 | 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /config/marlinspike.js: -------------------------------------------------------------------------------- 1 | module.exports.marlinspike = { 2 | models: false 3 | }; 4 | -------------------------------------------------------------------------------- /config/swagger.js: -------------------------------------------------------------------------------- 1 | // config/swagger.js 2 | module.exports.swagger = { 3 | /** 4 | * require() the package.json file for your Sails app. 5 | */ 6 | pkg: require('../package'), 7 | ui: { 8 | url: 'http://swagger.balderdash.io' 9 | } 10 | }; -------------------------------------------------------------------------------- /dist/api/controllers/ContactController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = { 7 | test: function test(req, res) { 8 | var contact = Contact.testInstance(); 9 | return res.status(200).jsonx(contact); 10 | } 11 | }; 12 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /dist/api/controllers/GroupController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = { 7 | test: function test(req, res) { 8 | var group = Group.testInstance(); 9 | return res.status(200).jsonx([group]); 10 | } 11 | }; 12 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var babel = require('gulp-babel'); 3 | 4 | gulp.task('default', function () { 5 | gulp.src([ 'lib/**' ]) 6 | .pipe(babel()) 7 | .pipe(gulp.dest('dist/lib')); 8 | 9 | gulp.src([ 'api/**' ]) 10 | .pipe(babel()) 11 | .pipe(gulp.dest('dist/api')); 12 | 13 | gulp.src([ 'config/**' ]) 14 | .pipe(babel()) 15 | .pipe(gulp.dest('dist/config')); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#dataTypeFormat 3 | */ 4 | 5 | const types = { 6 | integer: { 7 | type: 'integer', 8 | format: 'int32' 9 | }, 10 | float: { 11 | type: 'number', 12 | format: 'float' 13 | }, 14 | double: { 15 | type: 'number', 16 | format: 'double' 17 | }, 18 | string: { 19 | type: 'string', 20 | format: 'string' 21 | }, 22 | binary: { 23 | type: 'string', 24 | format: 'binary' 25 | }, 26 | boolean: { 27 | type: 'boolean' 28 | }, 29 | date: { 30 | type: 'string', 31 | format: 'date' 32 | }, 33 | datetime: { 34 | type: 'string', 35 | format: 'date-time' 36 | } 37 | } 38 | 39 | const typeMap = { 40 | text: 'string', 41 | json: 'string' 42 | } 43 | 44 | const Spec = { 45 | getPropertyType (wltype) { 46 | return types[typeMap[wltype] || wltype] 47 | } 48 | } 49 | 50 | export default Spec 51 | -------------------------------------------------------------------------------- /lib/xfmr.js: -------------------------------------------------------------------------------- 1 | import hoek from 'hoek' 2 | import _ from 'lodash' 3 | import Spec from './spec' 4 | import pluralize from 'pluralize' 5 | 6 | const methodMap = { 7 | post: 'Create Object(s)', 8 | get: 'Read Object(s)', 9 | put: 'Update Object(s)', 10 | patch: 'Update Object(s)', 11 | delete: 'Destroy Object(s)', 12 | options: 'Get Resource Options', 13 | head: 'Get Resource headers' 14 | } 15 | 16 | function getBlueprintPrefixes() { 17 | // Add a "/" to a prefix if it's missing 18 | function formatPrefix(prefix) { 19 | return (prefix.indexOf('/') !== 0 ? '/' : '') + prefix 20 | } 21 | 22 | let prefixes = [] 23 | // Check if blueprints hook is not removed 24 | if (sails.config.blueprints) { 25 | if (sails.config.blueprints.prefix) { 26 | // Case of blueprints prefix 27 | prefixes.push(formatPrefix(sails.config.blueprints.prefix)) 28 | if (sails.config.blueprints.rest && sails.config.blueprints.restPrefix) { 29 | // Case of blueprints prefix + rest prefix 30 | prefixes.unshift(prefixes[0] + formatPrefix(sails.config.blueprints.restPrefix)) 31 | } 32 | } else if (sails.config.blueprints.rest && sails.config.blueprints.restPrefix) { 33 | // Case of rest prefix 34 | prefixes.push(formatPrefix(sails.config.blueprints.restPrefix)) 35 | } 36 | } 37 | return prefixes 38 | } 39 | 40 | const Transformer = { 41 | 42 | getSwagger(sails, pkg) { 43 | return { 44 | swagger: '2.0', 45 | info: Transformer.getInfo(pkg), 46 | host: sails.config.swagger.host, 47 | tags: Transformer.getTags(sails), 48 | definitions: Transformer.getDefinitions(sails), 49 | paths: Transformer.getPaths(sails) 50 | } 51 | }, 52 | 53 | /** 54 | * Convert a package.json file into a Swagger Info Object 55 | * http://swagger.io/specification/#infoObject 56 | */ 57 | getInfo(pkg) { 58 | return hoek.transform(pkg, { 59 | 'title': 'name', 60 | 'description': 'description', 61 | 'version': 'version', 62 | 63 | 'contact.name': 'author', 64 | 'contact.url': 'homepage', 65 | 66 | 'license.name': 'license' 67 | }) 68 | }, 69 | 70 | /** 71 | * http://swagger.io/specification/#tagObject 72 | */ 73 | getTags(sails) { 74 | return _.map(_.pluck(sails.controllers, 'globalId'), tagName => { 75 | return { 76 | name: tagName 77 | //description: `${tagName} Controller` 78 | } 79 | }) 80 | }, 81 | 82 | /** 83 | * http://swagger.io/specification/#definitionsObject 84 | */ 85 | getDefinitions(sails) { 86 | let definitions = _.transform(sails.models, (definitions, model, modelName) => { 87 | definitions[model.identity] = { 88 | properties: Transformer.getDefinitionProperties(model.definition) 89 | } 90 | }) 91 | 92 | delete definitions['undefined'] 93 | 94 | return definitions 95 | }, 96 | 97 | getDefinitionProperties(definition) { 98 | 99 | return _.mapValues(definition, (def, attrName) => { 100 | let property = _.pick(def, [ 101 | 'type', 'description', 'format', 'model' 102 | ]) 103 | 104 | return property.model && sails.config.blueprints.populate ? { '$ref': Transformer.generateDefinitionReference(property.model) } : Spec.getPropertyType(property.type) 105 | }) 106 | }, 107 | 108 | /** 109 | * Convert the internal Sails route map into a Swagger Paths 110 | * Object 111 | * http://swagger.io/specification/#pathsObject 112 | * http://swagger.io/specification/#pathItemObject 113 | */ 114 | getPaths(sails) { 115 | let routes = sails.router._privateRouter.routes 116 | let pathGroups = _.chain(routes) 117 | .values() 118 | .flatten() 119 | .unique(route => { 120 | return route.path + route.method + JSON.stringify(route.keys) 121 | }) 122 | .reject({ path: '/*' }) 123 | .reject({ path: '/__getcookie' }) 124 | .reject({ path: '/csrfToken' }) 125 | .reject({ path: '/csrftoken' }) 126 | .groupBy('path') 127 | .value() 128 | 129 | pathGroups = _.reduce(pathGroups, function(result, routes, path) { 130 | path = path.replace(/:(\w+)\??/g, '{$1}') 131 | if (result[path]) 132 | result[path] = _.union(result[path], routes) 133 | else 134 | result[path] = routes 135 | return result 136 | }, []) 137 | 138 | let inferredPaths = _.mapValues(pathGroups, pathGroup => { 139 | return Transformer.getPathItem(sails, pathGroup) 140 | }) || []; 141 | return _.merge( inferredPaths , Transformer.getDefinitionsFromRouteConfig(sails) ); 142 | }, 143 | 144 | /** 145 | * Convert the swagger routes defined in sails.config.routes to route map 146 | * Object 147 | * http://swagger.io/specification/#pathsObject 148 | * http://swagger.io/specification/#pathItemObject 149 | */ 150 | getDefinitionsFromRouteConfig(sails) { 151 | let routes = sails.config.routes, 152 | swaggerdefs = _.pick(routes, function(routeConfig, route) { 153 | return _.has(routeConfig, 'swagger'); 154 | }); 155 | 156 | let swaggerDefinitions = _.chain(routes) 157 | .pick(function(routeConfig, route) { 158 | return _.has(routeConfig, 'swagger'); 159 | }).mapValues(function(route, key) { 160 | var swaggerdef = route.swagger || {}; 161 | swaggerdef.responses = _.chain(swaggerdef.responses || {}) 162 | .mapValues(function(response, responseCode) { 163 | if (response.schema || response.model) { 164 | response.schema = response.schema || response.model; 165 | if (typeof response.schema == 'string') { 166 | response.schema = { 167 | '$ref': '#/definitions/' + (response.schema || '').toLowerCase() 168 | }; 169 | }else if(typeof response.schema == 'object'){ 170 | if( (response.schema.type || '').toLowerCase()=='array' ){ 171 | response.schema.items = response.schema.items || { 172 | '$ref': '#/definitions/'+(response.schema.model || '').toLowerCase() 173 | }; 174 | delete response.schema.model; 175 | } 176 | } 177 | } 178 | return response; 179 | }).value(); 180 | swaggerdef.parameters = _.chain(swaggerdef.parameters || []) 181 | .map(function(parameter) { 182 | 183 | if (typeof parameter == 'string') { 184 | return '#/definitions/' + (parameter || '').toLowerCase(); 185 | 186 | } else { 187 | parameter.schema = parameter.schema || null; 188 | return parameter; 189 | } 190 | }).value(); 191 | 192 | var methods = swaggerdef.methods || ['get']; 193 | delete swaggerdef.methods; 194 | var defs = {}; 195 | _.map(methods, function(method) { 196 | defs[(method || '').toLowerCase().trim()] = swaggerdef; 197 | }); 198 | return defs; 199 | }).value(); 200 | 201 | var swaggerPaths = {}; 202 | for (var defRoute in swaggerDefinitions) { 203 | var sPath = (defRoute || '').toLowerCase().replace(/(get|post|put|option|delete)? ?/g, ''); 204 | sPath = sPath.replace(/:(\w+)\??/g, '{$1}'); 205 | swaggerPaths[sPath] = _.merge( swaggerPaths[sPath] || {}, swaggerDefinitions[defRoute] ); 206 | } 207 | 208 | return swaggerPaths || []; 209 | }, 210 | 211 | getModelFromPath(sails, path) { 212 | let [$, parentModelName, parentId, childAttributeName, childId] = path.split('/') 213 | let parentModel = sails.models[parentModelName] || parentModelName ? sails.models[pluralize.singular(parentModelName)] : undefined 214 | let childAttribute = _.get(parentModel, ['attributes', childAttributeName]) 215 | let childModelName = _.get(childAttribute, 'collection') || _.get(childAttribute, 'model') 216 | let childModel = sails.models[childModelName] || childModelName ? sails.models[pluralize.singular(childModelName)] : undefined 217 | 218 | return childModel || parentModel 219 | }, 220 | 221 | getModelIdentityFromPath(sails, path) { 222 | let model = Transformer.getModelFromPath(sails, path) 223 | if (model) { 224 | return model.identity 225 | } 226 | }, 227 | 228 | /** 229 | * http://swagger.io/specification/#definitionsObject 230 | */ 231 | getDefinitionReferenceFromPath(sails, path) { 232 | let model = Transformer.getModelFromPath(sails, path) 233 | if (model) { 234 | return Transformer.generateDefinitionReference(model.identity) 235 | } 236 | }, 237 | 238 | generateDefinitionReference(modelIdentity) { 239 | return '#/definitions/' + modelIdentity 240 | }, 241 | 242 | /** 243 | * http://swagger.io/specification/#pathItemObject 244 | */ 245 | getPathItem(sails, pathGroup) { 246 | let methodGroups = _.chain(pathGroup) 247 | .indexBy('method') 248 | .pick([ 249 | 'get', 'post', 'put', 'head', 'options', 'patch', 'delete' 250 | ]) 251 | .value() 252 | 253 | return _.mapValues(methodGroups, (methodGroup, method) => { 254 | return Transformer.getOperation(sails, methodGroup, method) 255 | }) 256 | }, 257 | 258 | /** 259 | * http://swagger.io/specification/#operationObject 260 | */ 261 | getOperation(sails, methodGroup, method) { 262 | return { 263 | summary: methodMap[method], 264 | consumes: ['application/json'], 265 | produces: ['application/json'], 266 | parameters: Transformer.getParameters(sails, methodGroup), 267 | responses: Transformer.getResponses(sails, methodGroup), 268 | tags: Transformer.getPathTags(sails, methodGroup) 269 | } 270 | }, 271 | 272 | /** 273 | * A list of tags for API documentation control. Tags can be used for logical 274 | * grouping of operations by resources or any other qualifier. 275 | */ 276 | getPathTags(sails, methodGroup) { 277 | return _.unique(_.compact([ 278 | Transformer.getPathModelTag(sails, methodGroup), 279 | Transformer.getPathControllerTag(sails, methodGroup), 280 | Transformer.getControllerFromRoute(sails, methodGroup) 281 | ])) 282 | }, 283 | 284 | getPathModelTag(sails, methodGroup) { 285 | let model = Transformer.getModelFromPath(sails, methodGroup.path) 286 | return model && model.globalId 287 | }, 288 | 289 | getPathControllerTag(sails, methodGroup) { 290 | // Fist check if we can find a controller tag using prefixed blueprint routes 291 | for (var prefix of getBlueprintPrefixes()) { 292 | if (methodGroup.path.indexOf(prefix) === 0) { 293 | let [$, pathToken] = methodGroup.path.replace(prefix, '').split('/') 294 | let tag = _.get(sails.controllers, [pathToken, 'globalId']) 295 | if (tag) return tag 296 | } 297 | } 298 | 299 | let [$, pathToken] = methodGroup.path.split('/') 300 | return _.get(sails.controllers, [pathToken, 'globalId']) 301 | }, 302 | 303 | getControllerFromRoute(sails, methodGroup) { 304 | let route = sails.config.routes[`${methodGroup.method} ${methodGroup.path}`] 305 | if (!route) return 306 | 307 | let pattern = /(.+)Controller/ 308 | let controller = route.controller || (_.isString(route) && route.split('.')[0]) 309 | 310 | if (!controller) return 311 | 312 | let [$, name] = /(.+)Controller/.exec(controller) 313 | 314 | return name 315 | }, 316 | 317 | /** 318 | * http://swagger.io/specification/#parameterObject 319 | */ 320 | getParameters(sails, methodGroup) { 321 | let method = methodGroup.method 322 | let routeKeys = methodGroup.keys 323 | 324 | let canHavePayload = method === 'post' || method === 'put' 325 | 326 | if (!routeKeys.length && !canHavePayload) return [] 327 | 328 | let parameters = _.map(routeKeys, param => { 329 | return { 330 | name: param.name, 331 | in : 'path', 332 | required: true, 333 | type: 'string' 334 | } 335 | }) 336 | 337 | if (canHavePayload) { 338 | let path = methodGroup.path 339 | let modelIdentity = Transformer.getModelIdentityFromPath(sails, path) 340 | 341 | if (modelIdentity) { 342 | parameters.push({ 343 | name: modelIdentity, 344 | in : 'body', 345 | required: true, 346 | schema: { 347 | $ref: Transformer.getDefinitionReferenceFromPath(sails, path) 348 | } 349 | }) 350 | } 351 | } 352 | 353 | return parameters 354 | }, 355 | 356 | /** 357 | * http://swagger.io/specification/#responsesObject 358 | */ 359 | getResponses(sails, methodGroup) { 360 | let $ref = Transformer.getDefinitionReferenceFromPath(sails, methodGroup.path) 361 | let ok = { 362 | description: 'The requested resource' 363 | } 364 | if ($ref) { 365 | ok.schema = { '$ref': $ref } 366 | } 367 | return { 368 | '200': ok, 369 | '404': { description: 'Resource not found' }, 370 | '500': { description: 'Internal server error' } 371 | } 372 | } 373 | } 374 | 375 | export default Transformer 376 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-swagger", 3 | "version": "0.5.1", 4 | "description": "swagger.io integration for sails.js", 5 | "main": "dist/api/hooks/swagger/index.js", 6 | "scripts": { 7 | "test": "gulp && mocha --reporter spec --compilers js:babel/register", 8 | "prepublish": "gulp" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/tjwebb/sails-swagger.git" 13 | }, 14 | "keywords": [ 15 | "sails", 16 | "sailsjs", 17 | "swagger", 18 | "swagger.io", 19 | "api", 20 | "sdk", 21 | "documentation" 22 | ], 23 | "author": "Travis Webb ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/tjwebb/sails-swagger/issues" 27 | }, 28 | "homepage": "https://github.com/tjwebb/sails-swagger", 29 | "devDependencies": { 30 | "babel": "^5.8.21", 31 | "gulp": "^3.9.0", 32 | "gulp-babel": "^5.2.1", 33 | "mocha": "^2.2.5", 34 | "sails": "balderdashy/sails", 35 | "sails-disk": "^0.10.8" 36 | }, 37 | "dependencies": { 38 | "hoek": "^2.14.0", 39 | "lodash": "^3.10.1", 40 | "marlinspike": "^0.12.10", 41 | "pluralize": "^1.2.1" 42 | }, 43 | "sails": { 44 | "isHook": true, 45 | "hookName": "swagger" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/bootstrap.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import _ from 'lodash' 3 | import Sails from 'sails' 4 | 5 | const config = { 6 | appPath: path.resolve(__dirname, '..'), 7 | hooks: { grunt: false }, 8 | log: { level: 'silent' }, 9 | models: { migrate: 'drop' }, 10 | port: 1339, 11 | swagger: { 12 | pkg: require('../package') 13 | } 14 | } 15 | 16 | before(function (done) { 17 | this.timeout(30000); 18 | 19 | Sails.lift(config, function(err, server) { 20 | global.sails = server; 21 | done(err) 22 | }); 23 | }); 24 | 25 | after(function () { 26 | global.sails.lower(); 27 | }); 28 | -------------------------------------------------------------------------------- /test/xfmr.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import _ from 'lodash' 3 | import xfmr from '../lib/xfmr' 4 | import pkg from '../package' 5 | 6 | 7 | describe('xfmr', () => { 8 | 9 | 10 | describe('#getSwagger', () => { 11 | it('should generate complete and correct Swagger doc', () => { 12 | let swagger = xfmr.getSwagger(sails, pkg) 13 | 14 | assert(swagger) 15 | }) 16 | }) 17 | 18 | describe('#getInfo', () => { 19 | it('should correctly transform package.json', () => { 20 | let info = xfmr.getInfo(pkg) 21 | 22 | assert(_.isObject(info)) 23 | assert.equal(info.title, 'sails-swagger') 24 | assert(_.isObject(info.contact)) 25 | assert(_.isString(info.version)) 26 | }) 27 | }) 28 | 29 | describe('#getPaths', () => { 30 | it('should be able to access Sails routes', () => { 31 | assert(_.isObject(sails.router._privateRouter.routes)) 32 | }) 33 | it('should transform routes to paths', () => { 34 | let paths = xfmr.getPaths(sails) 35 | 36 | assert(paths) 37 | }) 38 | }) 39 | 40 | describe('#getPathItem', () => { 41 | it('should generate a Swagger Path Item object from a Sails route', () => { 42 | let pathGroup = [{ 43 | "path": "/group", 44 | "method": "get", 45 | "callbacks": [ 46 | null 47 | ], 48 | "keys": [], 49 | "regexp": {} 50 | }, { 51 | "path": "/group", 52 | "method": "post", 53 | "callbacks": [ 54 | null 55 | ], 56 | "keys": [], 57 | "regexp": {} 58 | }] 59 | 60 | let pathItem = xfmr.getPathItem(sails, pathGroup) 61 | 62 | assert(_.isObject(pathItem)) 63 | assert(_.isObject(pathItem.get)) 64 | assert(_.isObject(pathItem.post)) 65 | assert(_.isUndefined(pathItem.put)) 66 | }) 67 | }) 68 | 69 | describe('#getOperation', () => { 70 | it('should generate a Swagger Operation object from a Sails route', () => { 71 | let route = sails.router._privateRouter.routes.get[0] 72 | let swaggerOperation = xfmr.getOperation(sails, route, 'get') 73 | 74 | assert(_.isObject(swaggerOperation)) 75 | }) 76 | }) 77 | 78 | describe('#getParameters', () => { 79 | it('should generate an empty array for a Sails route with no keys', () => { 80 | let route = sails.router._privateRouter.routes.get[0] 81 | let params = xfmr.getParameters(sails, route) 82 | 83 | assert(_.isArray(params)) 84 | assert(_.isEmpty(params)) 85 | }) 86 | it('should generate a Swagger Parameters object from a Sails route', () => { 87 | let route = _.findWhere(sails.router._privateRouter.routes.get, { path: '/contact/:id' }) 88 | let params = xfmr.getParameters(sails, route) 89 | 90 | assert(_.isArray(params)) 91 | assert.equal(params[0].name, 'id') 92 | }) 93 | it('should generate a Swagger Parameters object from a Sails model for POST endpoints', () => { 94 | let route = _.findWhere(sails.router._privateRouter.routes.post, { path: '/contact' }) 95 | let params = xfmr.getParameters(sails, route) 96 | 97 | assert(_.isArray(params)) 98 | assert.equal(params.length, 1) 99 | assert.equal(params[0].name, 'contact'); 100 | }) 101 | it('should generate a Swagger Parameters object from a Sails model for PUT endpoints', () => { 102 | let route = _.findWhere(sails.router._privateRouter.routes.put, { path: '/contact/:id' }) 103 | let params = xfmr.getParameters(sails, route) 104 | 105 | assert(_.isArray(params)) 106 | assert.equal(params.length, 2) 107 | assert.equal(params[0].name, 'id') 108 | assert.equal(params[1].name, 'contact'); 109 | }) 110 | it('should not generate a Swagger Parameters object when there is not a Sails model', () => { 111 | let route = _.findWhere(sails.router._privateRouter.routes.post, { path: '/swagger/doc' }) 112 | let params = xfmr.getParameters(sails, route) 113 | 114 | assert(_.isArray(params)) 115 | assert(_.isEmpty(params)) 116 | }) 117 | }) 118 | 119 | describe('#getResponses', () => { 120 | it('should generate a Swagger Responses object from a Sails route', () => { 121 | let route = sails.router._privateRouter.routes.get[0] 122 | let swaggerResponses = xfmr.getResponses(sails, route) 123 | 124 | assert(_.isObject(swaggerResponses)) 125 | }) 126 | it('should generate a Swagger Responses object from a Sails route with body', () => { 127 | let route = _.findWhere(sails.router._privateRouter.routes.post, { path: '/contact' }) 128 | let swaggerResponses = xfmr.getResponses(sails, route) 129 | 130 | assert(_.isObject(swaggerResponses['200'].schema)) 131 | }) 132 | }) 133 | 134 | describe('#getDefinitions()', () => { 135 | it('should generate a Swagger Definitions object', () => { 136 | let swaggerDefinitions = xfmr.getDefinitions(sails); 137 | 138 | assert(_.isObject(swaggerDefinitions)) 139 | assert(_.isObject(swaggerDefinitions.contact)) 140 | assert(_.isObject(swaggerDefinitions.group)) 141 | }) 142 | it('should generate a Swagger Definitions object with nested schemas', () => { 143 | let swaggerDefinitions = xfmr.getDefinitions(sails); 144 | 145 | assert(_.isObject(swaggerDefinitions.contact)) 146 | assert.deepEqual({ '$ref': '#/definitions/group' }, swaggerDefinitions.contact.properties.group) 147 | }) 148 | 149 | context('populate turned off', () => { 150 | before(() => { 151 | sails.config.blueprints.populate = false 152 | }) 153 | it('should generate a Swagger Definitions object with nested schemas', () => { 154 | let swaggerDefinitions = xfmr.getDefinitions(sails); 155 | 156 | assert(_.isObject(swaggerDefinitions.contact)) 157 | assert.deepEqual({ type: 'integer', format: 'int32' }, swaggerDefinitions.contact.properties.group) 158 | }) 159 | after(() => { 160 | sails.config.blueprints.populate = true 161 | }) 162 | }) 163 | }) 164 | 165 | describe('#getDefinitionReferenceFromPath()', () => { 166 | it('should generate a Swagger $ref from a simple path /contact', () => { 167 | assert.equal('#/definitions/contact', xfmr.getDefinitionReferenceFromPath(sails, '/contact')) 168 | }) 169 | it('should generate a Swagger $ref from a simple path /contact/:id', () => { 170 | assert.equal('#/definitions/contact', xfmr.getDefinitionReferenceFromPath(sails, '/contact/:id')) 171 | }) 172 | it('should generate a Swagger $ref from an association path /contact/:parentid/groups', () => { 173 | assert.equal('#/definitions/group', xfmr.getDefinitionReferenceFromPath(sails, '/contact/:parentid/group')) 174 | }) 175 | it('should generate a Swagger $ref from an association path /group/:parentid/contacts/:id', () => { 176 | assert.equal('#/definitions/contact', xfmr.getDefinitionReferenceFromPath(sails, '/group/:parentid/contacts/:id')) 177 | }) 178 | it('should generate a Swagger $ref from a pluralized association path /users', () => { 179 | sails.models['user'] = { identity: 'user' } 180 | assert.equal('#/definitions/user', xfmr.getDefinitionReferenceFromPath(sails, '/users')) 181 | }) 182 | it('should generate a Swagger $ref from a pluralized association path /memories', () => { 183 | sails.models['memory'] = { identity: 'memory' } 184 | assert.equal('#/definitions/memory', xfmr.getDefinitionReferenceFromPath(sails, '/memories')) 185 | }) 186 | }) 187 | 188 | describe('#getDefinitionsFromRouteConfig()', () => { 189 | before(() => { 190 | sails.config.routes = _.extend(sails.config.routes, { 191 | 'get /groups/:id': { 192 | controller: 'GroupController', 193 | action: 'test', 194 | skipAssets: 'true', 195 | //swagger path object 196 | swagger: { 197 | methods: ['GET'], 198 | 199 | summary: ' Get Groups ', 200 | description: 'Get Groups Description', 201 | produces: [ 202 | 'application/json' 203 | ], 204 | tags: [ 205 | 'Groups' 206 | ], 207 | responses: { 208 | '200': { 209 | description: 'List of Groups', 210 | schema: 'Group', //model, 211 | type: 'array' 212 | } 213 | }, 214 | parameters: [{ 215 | "name": "id", 216 | "in": "path", 217 | "description": "ID of pet to use", 218 | "required": true, 219 | "type": "array", 220 | "items": { 221 | "type": "string" 222 | }, 223 | "collectionFormat": "csv" 224 | }, { 225 | "name": "name", 226 | "in": "query", 227 | "description": "ID of pet to use", 228 | "required": true, 229 | "type": "string" 230 | }, 'Contact'] 231 | 232 | } 233 | } 234 | }) 235 | }) 236 | 237 | it('should get swagger definitions from route config', () => { 238 | var results = xfmr.getDefinitionsFromRouteConfig(sails); 239 | 240 | var expectedSwaggerSpec = { 241 | "/groups/{id}": { 242 | "get": { 243 | "summary": " Get Groups ", 244 | "description": "Get Groups Description", 245 | "produces": [ 246 | "application/json" 247 | ], 248 | "tags": [ 249 | "Groups" 250 | ], 251 | "responses": { 252 | "200": { 253 | "description": "List of Groups", 254 | "schema": { 255 | "$ref": "#/definitions/group" 256 | }, 257 | "type": "array" 258 | } 259 | }, 260 | "parameters": [{ 261 | "name": "id", 262 | "in": "path", 263 | "description": "ID of pet to use", 264 | "required": true, 265 | "type": "array", 266 | "items": { 267 | "type": "string" 268 | }, 269 | "collectionFormat": "csv", 270 | "schema": null 271 | }, { 272 | "name": "name", 273 | "in": "query", 274 | "description": "ID of pet to use", 275 | "required": true, 276 | "type": "string", 277 | "schema": null 278 | }, 279 | "#/definitions/contact" 280 | ] 281 | } 282 | } 283 | }; 284 | 285 | assert.equal(true, _.isEqual(results, expectedSwaggerSpec)); 286 | 287 | }) 288 | 289 | }) 290 | }) 291 | --------------------------------------------------------------------------------