Skip to content

Commit

Permalink
Merge branch 'use-swagger-v2'
Browse files Browse the repository at this point in the history
  • Loading branch information
JennieJi committed Jul 30, 2020
2 parents d049194 + 9c07734 commit 6a2d5c1
Show file tree
Hide file tree
Showing 20 changed files with 6,483 additions and 213 deletions.
60 changes: 10 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@ Then you may render it easily with [SwaggerUI](https://github.com/swagger-api/sw

# What is supported

- customSchema in OAS v2 or v3 formats
- convert _service_ to paths
- convert _enum_, _message_ into components, paths will reference to the components schema
- basic types mapping to JS type _number_, _string_, _boolean_ ( long types will be mapped to _string_)
- recognize fields:
- [OperationObject](https://swagger.io/specification/#operationObject).requestBody.\$proto
Replace requestBody with a [Reference Object](https://swagger.io/specification/#referenceObject)
- [OperationObject](https://swagger.io/specification/#operationObject).responses.\$proto
Replace responses['200'] with a [Reference Object](https://swagger.io/specification/#referenceObject)

# Install

Expand All @@ -35,60 +32,23 @@ Example:

```javascript
module.exports = {
file: 'test.proto',
// or multiple files
files: ['test1.proto', 'test2.proto'],
dist: 'apischema.json',
formatServicePath: (path) => path.replace(/\./g, '/'),
customSchema: {
// Similar to openapi v3 format
info: {
title: 'API',
version: '1.0.0',
contact: {
name: 'Jennie Ji',
email: '[email protected]',
url: 'jennieji.github.io',
},
},
tags: [
{
name: 'test',
description: '',
},
],
swagger: '2.0',
paths: {
'/api/test': {
get: {
requestBody: {
$proto: 'GetDataRequest', // Tell me the protobuf message name
},
responses: {
$proto: 'GetDataResponse', // Tell me the protobuf message name
'200': {
schema: {
$ref: 'GetDataResponse', // Tell me the protobuf message name
},
},
},
params: [],
},
// or customize
post: {
requestBody: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/GetDataRequest'
}
}
}
},
responses: {
'200': {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/GetDataResponse'
}
}
}
}
}
}
},
},
components: {
Expand Down
8 changes: 6 additions & 2 deletions bin/protobuf2swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

const path = require('path');
const program = require('commander');
const pkg = require('../package.json');
const fs = require('fs');
const convert = require('../utils/convert.js');
const convert = require('../lib/convert.js');

program
.version('0.0.0', '-v --version')
.version(pkg.version, '-v --version')
.arguments('[config_file]')
.usage('[config_file]')
.on('--help', () => {
Expand All @@ -22,6 +23,9 @@ const [configPath] = program.args;
const cwd = process.cwd();
const DEFAULT_CONFIG_PATH = 'protobuf2swagger.config.js';
const config = require(path.resolve(cwd, configPath || DEFAULT_CONFIG_PATH));
if (config.file) {
config.files = config.files ? [config.file, ...config.files] : [config.file];
}

(async () => {
const content = await convert(config);
Expand Down
File renamed without changes.
66 changes: 66 additions & 0 deletions lib/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const protobufjs = require('protobufjs');
const fs = require('fs');
const path = require('path');
const converter = require('swagger2openapi');
const processComponents = require('./processComponents');
const service2Paths = require('./service2Paths');

async function convert({ files, customSchema = {}, formatServicePath }) {
const protobuf = new protobufjs.Root();
if (files) {
files.forEach((file) => {
protobuf.loadSync(file, {
alternateCommentMode: true,
});
});
}
if (customSchema.swagger) {
customSchema = converter.convertObj(swagger);
}
let { paths, components } = customSchema;

const flattenedTypes = flattenPath(protobuf.nested);
Object.values(flattenedTypes).forEach((def) => {
if (/^Service /.test(def.toString())) {
paths = {
...service2Paths(def, formatServicePath),
...(paths || {}),
};
}
});

return {
openapi: '3.0.3',
info: {
title: protobuf.name || path.basename(files[0], '.proto'),
description: protobuf.comment,
verion: '1',
},
paths,
components: {
schemas: Object.assign(
processComponents(flattenedTypes),
(components && components.schemas) || {}
),
...(components || {}),
},
...customSchema,
};
}

function flattenPath(protobuf) {
let flattened = {};
Object.keys(protobuf).forEach((key) => {
const val = protobuf[key];
if (val.nested) {
Object.entries(flattenPath(val.nested)).forEach(([type, def]) => {
flattened[`${key}.${type}`] = def;
});
} else {
flattened[key] = val;
}
});
return flattened;
}

module.exports = convert;
File renamed without changes.
36 changes: 36 additions & 0 deletions lib/field2JSON.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const mapType = require('./mapType');
const bakeRef = require('./bakeRef');

function field2JSON({
type,
comment: description,
repeated,
typeDefault,
root,
}) {
const schema = {};
const mappedType = mapType(type);
if (mappedType) {
schema.type = mappedType;
schema.format = '';
schema.default = typeDefault;
} else {
const { lookupEnum, lookupType } = root;
try {
const enumDef = lookupEnum(type);
schema.enum = Object.values(enumDef);
} catch (e) {
schema.$ref = bakeRef(type);
}
}

return repeated
? {
type: 'array',
items: schema,
description,
}
: schema;
}

module.exports = field2JSON;
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./convert');
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions utils/processComponents.js → lib/processComponents.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const message2JSON = require('./message2JSON');
const enum2JSON = require('./enum2JSON');
const service2JSON = require('./service2Paths');

function processComponents(defs) {
return Object.keys(defs).reduce((ret, key) => {
Expand Down
File renamed without changes.
77 changes: 77 additions & 0 deletions lib/service2Paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const mapType = require('./mapType');
const bakeRef = require('./bakeRef');

function type2Parameters(type, paramIn = 'query') {
return type.fieldsArray.map((field) => {
const { name, comment, required, typeDefault, repeated } = field;
let ret = {
name,
description: comment,
paramIn,
required,
};
if (field.type === 'string') {
ret = {
...ret,
...field2JSON(type),
};
} else {
ret = {
...ret,
type: 'string',
};
}
return ret;
});
}

function service2Paths(def, formatServicePath) {
const schemas = {};
Object.values(def.methods).forEach((method) => {
const options = method.options || {};
let path = options.path || `/${def.fullName.slice(1)}/${method.name}`;
if (formatServicePath) {
path = formatServicePath(path);
}
let parameters = [];
let requestBody = {};
try {
const requestType = def.lookupType(method.requestType);
if (options.method === 'get') {
parameters = type2Parameters(requestType);
} else {
requestBody = {
description: requestType.comment,
content: {
'application/json': {
schema: {
$ref: bakeRef(requestType.fullName.slice(1)),
},
},
},
};
}
} catch (e) {}
schemas[path] = {
[options.method || 'post']: {
operationId: method.fullName.slice(1),
description: method.comment,
parameters,
requestBody,
responses: {
'200': {
content: {
'application/json': {
schema: {
$ref: bakeRef(method.responseType),
},
},
},
},
},
},
};
});
return schemas;
}
module.exports = service2Paths;
Loading

0 comments on commit 6a2d5c1

Please sign in to comment.