Skip to content

Commit

Permalink
OpenAPI support (#122)
Browse files Browse the repository at this point in the history
* Prepare to support multiple versions of the specification

* Preparing createSpecification

* Accept versions in definitions

* Improve factory method

* Start tests on open api specification support

* Example api-with-examples

* Example callback-example

* Link example

* Remove repetition

* Update documentation

* Correct function documentation

* Include a lock file for dependencies.

Please enter the commit message for your changes. Lines starting
  • Loading branch information
kalinchernev authored Aug 1, 2018
1 parent 9e55349 commit 2d2475e
Show file tree
Hide file tree
Showing 46 changed files with 9,351 additions and 249 deletions.
5 changes: 0 additions & 5 deletions .jscsrc

This file was deleted.

22 changes: 0 additions & 22 deletions .jsdocconf

This file was deleted.

3 changes: 0 additions & 3 deletions .jshintignore

This file was deleted.

13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ documentation, servers, clients, tests and much more based on the rich [OpenAPI
**swagger-jsdoc** enables you to integrate [Swagger](http://swagger.io)
using [`JSDoc`](http://usejsdoc.org/) comments in your code. Just add `@swagger` on top of your DocBlock and declare the meaning of your code in `yaml` complying to the OpenAPI specification.

`swagger-jsdoc` will parse your documentation from
your actual living code and output an OpenAPI specification to integrate any server and client
technology as long as both sides comply with the specification.
`swagger-jsdoc` will parse your documentation from your actual living code and output an OpenAPI specification to integrate any server and client technology as long as both sides comply with the specification.

Thus, the `swagger-jsdoc` project assumes that you want document your existing working code in a way to "give life" to it, generating a specification which can then be feeded into other Swagger tools, and not the vice-versa.
Thus, the `swagger-jsdoc` project assumes that you want document your existing working code in a way to "give life" to it, generating a specification which can then be fed into other Swagger tools, and not the vice-versa.

If you prefer to write the OpenAPI specification first and separately, you might check other projects facilitating this, such as

Expand All @@ -26,7 +24,10 @@ If you prefer to write the OpenAPI specification first and separately, you might

## Supported versions

- [OpenAPI 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) (previously known as Swagger)
- OpenAPI 3.x
- Swagger 2.0

You can find the corresponding documentation in the [official repository of the specification](https://github.com/OAI/OpenAPI-Specification).

## Install

Expand Down Expand Up @@ -70,5 +71,3 @@ You can also use the tool via [command line interface](./docs/CLI.md).
- Branch for each separate feature
- Write detailed commit messages, comment unclear code blocks and update unit tests
- Push to your own repository and create a new PR to merge back into this repository

Note: If there are additions to the swagger definition object ensure that the output object keys comply with the swagger specification. If there are keys that do not comply add them to the `excludedSwaggerProperties` list in `lib/swagger-helpers.js`.
32 changes: 21 additions & 11 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
`swagger-jsdoc` returns the validated OpenAPI specification as JSON or YAML.

```javascript
var swaggerJSDoc = require('swagger-jsdoc');
const swaggerJSDoc = require('swagger-jsdoc');

var options = {
swaggerDefinition: {
const options = {
definition: {
info: {
title: 'Hello World', // Title (required)
version: '1.0.0', // Version (required)
Expand All @@ -16,10 +16,15 @@ var options = {
};

// Initialize swagger-jsdoc -> returns validated swagger spec in json format
var swaggerSpec = swaggerJSDoc(options);
const swaggerSpec = swaggerJSDoc(options);
```

At this time you can do with the swaggerSpec whatever you want.
Notes:

- `options.definition` could be also `options.swaggerDefinition`
- paths given in `options.apis` are resolved with [node-glob](https://github.com/isaacs/node-glob) in the background. Try to limit your patterns smartly to speed up discovery of files.

At this time you can do with the `swaggerSpec` whatever you want.
The simplest way would be serving it straight to the outside world:

```javascript
Expand All @@ -29,11 +34,11 @@ app.get('/api-docs.json', function(req, res) {
});
```

You could also use a framework like [swagger-tools](https://www.npmjs.com/package/swagger-tools) to serve the spec and a swagger-ui.
You could also use a framework like [swagger-tools](https://www.npmjs.com/package/swagger-tools) to serve the spec and a `swagger-ui`.

### How to document the API

The API can now be documented in JSDoc-style with swagger spec in YAML.
The API can be documented in JSDoc-style with swagger spec in YAML.

```javascript
/**
Expand Down Expand Up @@ -66,10 +71,10 @@ app.post('/login', function(req, res) {
### Re-using Model Definitions

A model may be the same for multiple endpoints (Ex. User POST,PUT responses).
In place of writing (or copy and pasting) the same code into multiple locations,
which can be error prone when adding a new field to the schema. You can define
a model and re-use it across multiple endpoints. You can also reference another

In place of writing (or copy and pasting) the same code into multiple locations, which can be error prone when adding a new field to the schema. You can define a model and re-use it across multiple endpoints. You can also reference another
model and add fields.

```javascript
/**
* @swagger
Expand Down Expand Up @@ -149,9 +154,12 @@ model and add fields.
});
```

Keep in mind that since v3 of the specification, you can use [components](https://swagger.io/docs/specification/components/) in order to definite and reuse resources.

### Load external definitions

You can load external definitions or paths after ``swaggerJSDoc()`` function.
You can load external definitions or paths after `swaggerJSDoc()` function.

```javascript
// Initialize swagger-jsdoc -> returns validated swagger spec in json format
var swaggerSpec = swaggerJSDoc(options);
Expand All @@ -161,3 +169,5 @@ swaggerSpec.definitions.out_login = require("config/schemajson/out.login.schema.
// or set manual paths
swaggerSpec.paths["api/v1/cool"] = {"get" : { ... } }
```

If you need more examples, feel free to browse the repository and its tests and examples.
5 changes: 2 additions & 3 deletions example/app.js → example/v2/app.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint import/no-extraneous-dependencies: 0 */
// This file is an example, it's not functionally used by the module.

// Dependencies
const express = require('express');
const bodyParser = require('body-parser');
const routes = require('./routes');
const routes2 = require('./routes2');
const swaggerJSDoc = require('../');
const swaggerJSDoc = require('../..');

// Initialize express
const app = express();
Expand Down Expand Up @@ -37,7 +36,7 @@ const options = {
// Import swaggerDefinitions
swaggerDefinition,
// Path to the API docs
apis: ['./example/routes*.js', './example/parameters.yaml'],
apis: ['./example/v2/routes*.js', './example/v2/parameters.yaml'],
};

// Initialize swagger-jsdoc -> returns validated swagger spec in json format
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions lib/helpers/convertGlobPaths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const glob = require('glob');

/**
* Converts an array of globs to full paths
* @function
* @param {array} globs - Array of globs and/or normal paths
* @return {array} Array of fully-qualified paths
* @requires glob
*/
function convertGlobPaths(globs) {
return globs
.map(globString => glob.sync(globString))
.reduce((previous, current) => previous.concat(current), []);
}

module.exports = convertGlobPaths;
45 changes: 45 additions & 0 deletions lib/helpers/createSpecification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint no-self-assign: 0 */

/**
* Adds necessary properties for a given specification.
* @see https://goo.gl/Eoagtl
* @function
* @param {object} definition - The `definition` or `swaggerDefinition` from options.
* @returns {object} Object containing required properties of a given specification version.
*/
function createSpecification(definition) {
const specification = JSON.parse(JSON.stringify(definition));

// Properties corresponding to their specifications.
const v2 = [
'paths',
'definitions',
'responses',
'parameters',
'securityDefinitions',
];
const v3 = [...v2, 'components'];

if (specification.openapi) {
specification.openapi = specification.openapi;
v3.forEach(property => {
specification[property] = specification[property] || {};
});
} else if (specification.swagger) {
specification.swagger = specification.swagger;
v2.forEach(property => {
specification[property] = specification[property] || {};
});
} else {
specification.swagger = '2.0';
v2.forEach(property => {
specification[property] = specification[property] || {};
});
}

specification.tags = specification.tags || [];

return specification;
}

module.exports = createSpecification;
26 changes: 26 additions & 0 deletions lib/helpers/filterJsDocComments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const jsYaml = require('js-yaml');

/**
* Filters JSDoc comments for those tagged with '@swagger'
* @function
* @param {array} jsDocComments - JSDoc comments
* @returns {array} JSDoc comments tagged with '@swagger'
* @requires js-yaml
*/
function filterJsDocComments(jsDocComments) {
const swaggerJsDocComments = [];

for (let i = 0; i < jsDocComments.length; i += 1) {
const jsDocComment = jsDocComments[i];
for (let j = 0; j < jsDocComment.tags.length; j += 1) {
const tag = jsDocComment.tags[j];
if (tag.title === 'swagger') {
swaggerJsDocComments.push(jsYaml.safeLoad(tag.description));
}
}
}

return swaggerJsDocComments;
}

module.exports = filterJsDocComments;
71 changes: 71 additions & 0 deletions lib/helpers/getSpecificationObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const parser = require('swagger-parser');
const createSpecification = require('./createSpecification');
const specHelper = require('./specification');
const parseApiFile = require('./parseApiFile');
const filterJsDocComments = require('./filterJsDocComments');
const convertGlobPaths = require('./convertGlobPaths');

function isEmptyObject(obj) {
// eslint-disable-next-line
Object.keys(obj).forEach(key => {
if (key in obj) return false;
});

return true;
}

/**
* OpenAPI specification validator does not accept empty values for a few properties.
* Solves validator error: "Schema error should NOT have additional properties"
* @function
* @param {object} inputSpec - The swagger/openapi specification
* @param {object} improvedSpec - The cleaned version of the inputSpec
*/
function cleanUselessProperties(inputSpec) {
const improvedSpec = JSON.parse(JSON.stringify(inputSpec));
const toClean = [
'definitions',
'responses',
'parameters',
'securityDefinitions',
];

toClean.forEach(unncessaryProp => {
if (isEmptyObject(improvedSpec[unncessaryProp])) {
delete improvedSpec[unncessaryProp];
}
});

return improvedSpec;
}

function getSpecificationObject(options) {
// Get input definition and prepare the specification's skeleton
const definition = options.swaggerDefinition || options.definition;
let specification = createSpecification(definition);

// Parse the documentation containing information about APIs.
const apiPaths = convertGlobPaths(options.apis);

for (let i = 0; i < apiPaths.length; i += 1) {
const files = parseApiFile(apiPaths[i]);
const swaggerJsDocComments = filterJsDocComments(files.jsdoc);

specHelper.addDataToSwaggerObject(specification, files.yaml);
specHelper.addDataToSwaggerObject(specification, swaggerJsDocComments);
}

parser.parse(specification, (err, api) => {
if (!err) {
specification = api;
}
});

if (specification.openapi) {
specification = cleanUselessProperties(specification);
}

return specification;
}

module.exports = getSpecificationObject;
38 changes: 38 additions & 0 deletions lib/helpers/parseApiFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const fs = require('fs');
const path = require('path');
const doctrine = require('doctrine');
const jsYaml = require('js-yaml');

/**
* Parses the provided API file for JSDoc comments.
* @function
* @param {string} file - File to be parsed
* @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files
* @requires doctrine
*/
function parseApiFile(file) {
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm;
const fileContent = fs.readFileSync(file, { encoding: 'utf8' });
const ext = path.extname(file);
const yaml = [];
const jsDocComments = [];

if (ext === '.yaml' || ext === '.yml') {
yaml.push(jsYaml.safeLoad(fileContent));
} else {
const regexResults = fileContent.match(jsDocRegex);
if (regexResults) {
for (let i = 0; i < regexResults.length; i += 1) {
const jsDocComment = doctrine.parse(regexResults[i], { unwrap: true });
jsDocComments.push(jsDocComment);
}
}
}

return {
yaml,
jsdoc: jsDocComments,
};
}

module.exports = parseApiFile;
Loading

0 comments on commit 2d2475e

Please sign in to comment.