├── .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 |
--------------------------------------------------------------------------------