Skip to content

Commit

Permalink
Merge pull request #10 from seriousme/openapiv3
Browse files Browse the repository at this point in the history
Openapi V3 support
  • Loading branch information
Hans Klunder authored Jun 24, 2018
2 parents 48bbf9b + 50488f7 commit 0f6eb69
Show file tree
Hide file tree
Showing 21 changed files with 3,454 additions and 947 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![Build Status](https://travis-ci.org/seriousme/fastify-swaggergen.svg?branch=master)](https://travis-ci.org/seriousme/fastify-swaggergen)
[![Greenkeeper badge](https://badges.greenkeeper.io/seriousme/fastify-swaggergen.svg)](https://greenkeeper.io/)

A plugin for [fastify](https://www.fastify.io) to autogenerate a configuration based on a [Swagger](https://swagger.io/) (v2) specification.
A plugin for [fastify](https://www.fastify.io) to autogenerate a configuration based on a [OpenApi](https://www.openapis.org/)(v2/v3) specification.

<a name="install"></a>
## Install:
Expand Down Expand Up @@ -30,10 +30,10 @@ const options = {
fastify.register(swaggerGen, options);
```

All schema and routes will be taken from the Swagger specification listed in the options. No need to specify them in your code.
All schema and routes will be taken from the OpenApispecification listed in the options. No need to specify them in your code.
<a name="pluginOptions"></a>
### Options
- `swaggerSpec`: this can be a JSON object, or the name of a JSON or YAML file containing a valid Swagger (v2) file
- `swaggerSpec`: this can be a JSON object, or the name of a JSON or YAML file containing a valid OpenApi(v2/v3) file
- `service`: this can be a javascript object or class, or the name of a javascript file containing such an object. If the import of the file results in a function instead of an object then the function will be executed during import.
- `fastifySwagger`: an object containing the options for the [fastify-swagger](https://github.com/fastify/fastify-swagger) plugin. To avoid registering this plugin pass `{ fastifySwagger: { disabled: true }}`

Expand All @@ -43,18 +43,18 @@ See the [examples](#examples) section for a demo.
<a name="generator"></a>
## Generator

To make life even more easy there is the `swaggergen` cli. The `swaggergen` cli takes a valid Swagger (v2) file (JSON or YAML) and generates a project including a fastify flugin that you can use on any fastify server, a stub of the service class and a skeleton of a test harness to test the plugin.
To make life even more easy there is the `swaggergen` cli. The `swaggergen` cli takes a valid OpenApi (v2/v3) file (JSON or YAML) and generates a project including a fastify flugin that you can use on any fastify server, a stub of the service class and a skeleton of a test harness to test the plugin.

<a name="generatorUsage"></a>
### Usage
```
swaggergen [options] <swagger specification>
swaggergen [options] <OpenApi specification>
```
or if you don't have `swaggergen` installed:
```
npx github:seriousme/fastify-swaggergen <swagger specification>
npx github:seriousme/fastify-swaggergen <OpenApi specification>
```
This will generate a project based on the provided swagger specification.
This will generate a project based on the provided OpenApi specification.
Any existing files in the project folder will be overwritten!
See the [generator examples](#examples) section for a demo.
<a name="generatorOptions"></a>
Expand Down
2 changes: 1 addition & 1 deletion examples/generatedProject/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This directory contains a fastify plugin that was autogenerated using
[fastify-swaggergen](https://github.com/seriousme/fastify-swaggergen) and
the swagger specifation in [swagger.json](swagger.json)
the OpenApi specifation in [openApi.json](openApi.json)

In this directory use:
+ "npm install" to install its dependencies
Expand Down
2 changes: 1 addition & 1 deletion examples/generatedProject/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const swaggerGen = require("../../index.js");

const options = {
swaggerSpec: `${__dirname}/swagger.json`,
swaggerSpec: `${__dirname}/openApi.json`,
service: `${__dirname}/service.js`,
fastifySwagger: {
disabled: false
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/generatedProject/test/test-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const test = t.test;
const Fastify = require("fastify");
const fastifyPlugin = require("../index.js");

const swaggerSpec = "../swagger.json";
const swaggerSpec = "../openApi.json";
const service = require("../service.js");

const opts = {
Expand Down
10 changes: 5 additions & 5 deletions lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,21 @@ class generator {
const testDir = path.join(projectDir, "test");
const dirMode = 0o755;

const swaggerFile = "swagger.json";
const specFile = "openApi.json";
const serviceFile = "service.js";
const plugin = "index.js";
const files = {
swagger: contents(
spec: contents(
projectDir,
swaggerFile,
specFile,
JSON.stringify(this.specification, null, 2)
),
service: contents(projectDir, serviceFile, this.generateService()),
plugin: contents(
projectDir,
plugin,
this.generatePlugin(
swaggerFile,
specFile,
serviceFile,
this.localPlugin ? pathLocal(projectDir) : undefined
)
Expand All @@ -138,7 +138,7 @@ class generator {
testPlugin: contents(
testDir,
"test-plugin.js",
this.generateTest(swaggerFile, serviceFile, plugin)
this.generateTest(specFile, serviceFile, plugin)
)
};

Expand Down
22 changes: 10 additions & 12 deletions lib/parser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// this class is to bridge various parser versions
const swp = require("swagger-parser");
const ParserV2 = require("./parser.v2");
// const ParserV3 = require("./parser.v3"); // once available, for now only v2
const ParserV3 = require("./parser.v3");

class parser {
/** constructor */
Expand All @@ -23,34 +23,32 @@ class parser {
* @returns {object} fastify configuration information
*/
async parse(specification) {
let swagger, data;
let spec, data;
try {
// parse first, to avoid dereferencing of $ref's
data = await swp.parse(specification);
// save the original (with $refs) because swp.validate modifies its input
this.original = JSON.parse(JSON.stringify(data, null, 2));
// and validate
swagger = await swp.validate(data);
spec = await swp.validate(data);
} catch (e) {
data = {};
swagger = {};
spec = {};
}

const version = swagger.swagger;

const version = spec.swagger || spec.openapi;
switch (version) {
case "2.0":
const parserV2 = new ParserV2();
return parserV2.parse(swagger);
return parserV2.parse(spec);

// case "3.0":
// const parserV3 = new ParserV3();
// return parserV3.parse(swagger);
// break;
case "3.0.0":
const parserV3 = new ParserV3();
return parserV3.parse(spec);

default:
throw new Error(
"'swaggerSpec' parameter must contain a swagger version 2.0 specification"
"'swaggerSpec' parameter must contain a valid version 2.0 or 3.0.0 specification"
);
}
}
Expand Down
22 changes: 11 additions & 11 deletions lib/parser.v2.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// a class for parsing swagger V2 data into config data for fastify
// a class for parsing openapi V2 data into config data for fastify

class parserV2 {
constructor() {
Expand All @@ -20,7 +20,7 @@ class parserV2 {
}

makeURL(path) {
// fastify wants 'path/:param' instead of swaggers 'path/{param}'
// fastify wants 'path/:param' instead of openapis 'path/{param}'
return path.replace(/{(\w+)}/g, ":$1");
}

Expand Down Expand Up @@ -123,35 +123,35 @@ class parserV2 {
return schema;
}

processOperation(base, path, operation, data) {
processOperation(path, operation, data) {
const route = {
method: operation.toUpperCase(),
url: this.makeURL(path),
schema: this.makeSchema(data),
operationId: data.operationId || this.makeOperationId(operation, path),
swaggerSource: data
openapiSource: data
};
this.config.routes.push(route);
}

processPaths(base, paths) {
processPaths(paths) {
for (let path in paths) {
for (let operation in paths[path]) {
this.processOperation(base, path, operation, paths[path][operation]);
this.processOperation(path, operation, paths[path][operation]);
}
}
}

parse(swagger) {
for (let item in swagger) {
parse(spec) {
for (let item in spec) {
switch (item) {
case "paths":
this.processPaths(swagger.basePath, swagger.paths);
this.processPaths(spec.paths);
break;
case "basePath":
this.config.prefix = swagger[item];
this.config.prefix = spec[item];
default:
this.config.generic[item] = swagger[item];
this.config.generic[item] = spec[item];
}
}
return this.config;
Expand Down
161 changes: 161 additions & 0 deletions lib/parser.v3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// a class for parsing openapi v3 data into config data for fastify

class parserV3 {
constructor() {
this.config = { generic: {}, routes: [] };
}

makeOperationId(operation, path) {
// make a nice camelCase operationID
// e.g. get /user/{name} becomes getUserByName
const firstUpper = str => str.substr(0, 1).toUpperCase() + str.substr(1);
const by = (matched, p1) => "By" + firstUpper(p1);
const parts = path.split("/").slice(1);
const opId = parts
.map((item, i) => (i > 0 ? firstUpper(item) : item))
.join("")
.replace(/{(\w+)}/g, by)
.replace(/[^a-z]/gi, "");
return opId;
}

makeURL(path) {
// fastify wants 'path/:param' instead of openapis 'path/{param}'
return path.replace(/{(\w+)}/g, ":$1");
}

copyProps(source, target, list) {
list.forEach(item => {
if (source[item]) target[item] = source[item];
});
}

parseParams(data) {
const params = {
type: "object",
properties: {}
};
const required = [];
data.forEach(item => {
// item.type "file" breaks ajv, so treat is as a special here
if (item.type === "file") {
item.type = "string";
item.isFile = true;
}
//
params.properties[item.name] = item.schema;
this.copyProps(item, params.properties[item.name], ["description"]);
// ajv wants "required" to be an array, which seems to be too strict
// see https://github.com/json-schema/json-schema/wiki/Properties-and-required
if (item.required) {
required.push(item.name);
}
});
if (required.length > 0) {
params.required = required;
}
return params;
}

parseParameters(schema, data) {
const params = [];
const querystring = [];
const headers = [];
// const formData = [];
data.forEach(item => {
switch (item.in) {
// case "body":
// schema.body = item.schema;
// break;
// case "formData":
// formData.push(item);
// break;
case "path":
params.push(item);
break;
case "query":
querystring.push(item);
break;
case "header":
headers.push(item);
break;
}
});
if (params.length > 0) schema.params = this.parseParams(params);
if (querystring.length > 0)
schema.querystring = this.parseParams(querystring);
if (headers.length > 0) schema.headers = this.parseParams(headers);
}

parseBody(data) {
let schema;
if (data && data.content) {
for (let mimeType in data.content) {
if (mimeType !== "application/json") {
console.log(`body type: ${mimeType} found`);
}
schema = data.content[mimeType].schema;
}
}
return schema;
}

parseResponses(responses) {
const result = {};
let hasResponse = false;
for (let httpCode in responses) {
const body = this.parseBody(responses[httpCode]);
if (body !== undefined) {
result[httpCode] = body;
hasResponse = true;
}
}
return hasResponse ? result : null;
}

makeSchema(data) {
const schema = {};
const copyItems = ["tags", "summary", "operationId"];
this.copyProps(data, schema, copyItems);
if (data.parameters) this.parseParameters(schema, data.parameters);
const body = this.parseBody(data.requestBody);
if (body) schema.body = body;
const response = this.parseResponses(data.responses);
if (response) schema.response = response;
return schema;
}

processOperation(path, operation, data) {
const route = {
method: operation.toUpperCase(),
url: this.makeURL(path),
schema: this.makeSchema(data),
operationId: data.operationId || this.makeOperationId(operation, path),
openapiSource: data
};
this.config.routes.push(route);
}

processPaths(paths) {
for (let path in paths) {
for (let operation in paths[path]) {
this.processOperation(path, operation, paths[path][operation]);
}
}
}

parse(spec) {
for (let item in spec) {
switch (item) {
case "paths":
this.processPaths(spec.paths);
break;
default:
this.config.generic[item] = spec[item];
}
}
return this.config;
}
}

module.exports = parserV3;
2 changes: 1 addition & 1 deletion lib/templateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const utils = {
"req.params": route.schema.params,
"req.query": route.schema.querystring,
"req.body": route.schema.body,
"valid responses": route.swaggerSource.responses
"valid responses": route.openapiSource.responses
};

const commentize = label => {
Expand Down
2 changes: 1 addition & 1 deletion lib/templates/README.md.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ module.exports = (projectName, instructions) => `# ${projectName}
This directory contains a fastify plugin that was autogenerated using
[fastify-swaggergen](https://github.com/seriousme/fastify-swaggergen) and
the swagger specifation in [swagger.json](swagger.json)
the OpenApi specifation in [openApi.json](openApi.json)
${instructions}
`;
Loading

0 comments on commit 0f6eb69

Please sign in to comment.