openapi3-generator
Version:
Use your API OpenAPI 3 definition to generate code, documentation, and literally anything you need.
357 lines (315 loc) • 10.7 kB
JavaScript
/**
* This module generates a code skeleton for an API using OpenAPI.
* @module codegen
*/
const os = require('os');
const path = require('path');
const fs = require('fs');
const Handlebars = require('handlebars');
const _ = require('lodash');
const beautifier = require('./beautifier');
const xfs = require('fs.extra');
const randomName = require('project-name-generator');
const registerPartial = require('./register-partial');
const bundler = require('./bundler');
const yellow = text => `\x1b[33m${text}\x1b[0m`;
const findRemoveSync = require('find-remove');
const codegen = module.exports;
const HELPERS_DIRNAME = '.helpers';
const PARTIALS_DIRNAME = '.partials';
/**
* Deletes all matching subfolders in target directory
*
* @param {Object} config Configuration options
* @returns {Promise}
*/
const deleteFolders = config => new Promise((resolve, reject) => {
try {
if (config.deleteFolders) {
console.warn(yellow(`Deleting all subfolders named '${config.deleteFolders}' in ouput directory '${config.target_dir}'`));
findRemoveSync(config.target_dir, {dir: config.deleteFolders});
}
resolve();
} catch (e) {
reject(e);
}
});
/**
* Generates a file.
*
* @private
* @param {Object} options
* @param {String} options.templates_dir Directory where the templates live.
* @param {String} options.target_dir Directory where the file will be generated.
* @param {String} options.file_name Name of the generated file.
* @param {String} options.root Root directory.
* @param {Object} options.data Data to pass to the template.
* @return {Promise}
*/
const generateFile = options => new Promise((resolve, reject) => {
const templates_dir = options.templates_dir;
const target_dir = options.target_dir;
const file_name = options.file_name;
const root = options.root;
const data = options.data;
fs.readFile(path.resolve(root, file_name), 'utf8', (err, content) => {
if (err) return reject(err);
try {
const template = Handlebars.compile(content);
const parsed_content = template(data);
const template_path = path.relative(templates_dir, path.resolve(root, file_name));
const generated_path = path.resolve(target_dir, template_path).replace(/.hbs$/, '');
// WIP check here for existing?
const skipFile = data.skipExistingFiles && fs.existsSync(generated_path);
if (!skipFile) {
fs.writeFile(generated_path, parsed_content, 'utf8', (err) => {
if (err) return reject(err);
resolve();
});
}
else {
console.warn(yellow(`Skipping file: ${generated_path}`));
resolve();
}
} catch (e) {
reject(e);
}
});
});
/**
* Generates a file for every operation.
*
* @param config
* @param operation
* @param operation_name
* @returns {Promise}
*/
const generateOperationFile = (config, operation, operation_name) => new Promise((resolve, reject) => {
fs.readFile(path.join(config.root, config.file_name), 'utf8', (err, data) => {
if (err) return reject(err);
const subdir = config.root
.replace(new RegExp(`${config.templates_dir}[/]?`),'')
.replace("$$path$$", _.kebabCase(operation_name));
const new_filename = config.file_name.replace('$$path$$', operation_name).replace(/.hbs$/, '');
const target_file = path.resolve(config.target_dir, subdir, new_filename);
const template = Handlebars.compile(data.toString());
const content = template({
openbrace: '{',
closebrace: '}' ,
operation_name: operation_name.replace(/[}{]/g, ''),
operation,
openapi: config.data.openapi
});
xfs.mkdirpSync(path.dirname(target_file));
fs.writeFile(target_file, content, 'utf8', (err) => {
if (err) return reject(err);
resolve();
});
});
});
/**
* Generates all the files for each operation by iterating over the operations.
*
* @param {Object} config Configuration options
* @returns {Promise}
*/
const generateOperationFiles = config => new Promise((resolve, reject) => {
const files = {};
_.each(config.data.openapi.paths, (path, path_name) => {
const operation_name = path.endpointName;
if (files[operation_name] === undefined) {
files[operation_name] = [];
}
path_name = path_name.replace(/}/g, '').replace(/{/g, ':');
files[operation_name].push({
path_name,
path,
subresource: (path_name.substring(operation_name.length+1) || '/').replace(/}/g, '').replace(/{/g, ':')
});
Promise.all(
_.map(files, (operation, operation_name) => generateOperationFile(config, operation, operation_name))
).then(resolve).catch(reject);
resolve();
});
});
/**
* Generates the directory structure.
*
* @private
* @param {Object} config Configuration options
* @param {Object|String} config.openapi OpenAPI JSON or a string pointing to a OpenAPI file.
* @param {String} config.target_dir Absolute path to the directory where the files will be generated.
* @param {String} config.templates Absolute path to the templates that should be used.
* @return {Promise}
*/
const generateDirectoryStructure = config => new Promise((resolve, reject) => {
const target_dir = config.target_dir;
const templates_dir = config.templates;
xfs.mkdirpSync(target_dir);
const walker = xfs.walk(templates_dir, {
followLinks: false
});
walker.on('file', async (root, stats, next) => {
try {
if (stats.name.includes('$$path$$') || root.includes("$$path$$")) {
// this file should be handled for each in openapi.paths
await generateOperationFiles({
root,
templates_dir,
target_dir,
data: config,
file_name: stats.name
});
const template_path = path.relative(templates_dir, path.resolve(root, stats.name));
fs.unlink(path.resolve(target_dir, template_path), next);
} else {
const file_path = path.relative(templates_dir, path.resolve(root, stats.name));
if (!file_path.startsWith(`${PARTIALS_DIRNAME}${path.sep}`) && !file_path.startsWith(`${HELPERS_DIRNAME}${path.sep}`)) {
// this file should only exist once.
await generateFile({
root,
templates_dir,
target_dir,
data: config,
file_name: stats.name
});
}
next();
}
} catch (e) {
reject(e);
}
});
walker.on('directory', async (root, stats, next) => {
try {
const dir_path = path.resolve(target_dir, path.relative(templates_dir, path.resolve(root, stats.name)));
if (
stats.name !== PARTIALS_DIRNAME &&
stats.name !== HELPERS_DIRNAME &&
!stats.name.includes("$$path$$")
) {
xfs.mkdirpSync(dir_path);
}
next();
} catch (e) {
reject(e);
}
});
walker.on('errors', (root, nodeStatsArray) => {
reject(nodeStatsArray);
});
walker.on('end', async () => {
resolve();
});
});
/**
* Register the template partials
*
* @private
* @param {Object} config Configuration options
* @param {String} config.templates Absolute path to the templates that should be used.
* @return {Promise}
*/
const registerHelpers = config => new Promise((resolve, reject) => {
const helpers_dir = path.resolve(config.templates, HELPERS_DIRNAME);
if (!fs.existsSync(helpers_dir)) return resolve();
const walker = xfs.walk(helpers_dir, {
followLinks: false
});
walker.on('file', async (root, stats, next) => {
try {
const file_path = path.resolve(config.templates, path.resolve(root, stats.name));
// If it's a module constructor, inject dependencies to ensure consistent usage in remote templates in other projects or plain directories.
const mod = require(file_path);
if (typeof mod === 'function') mod(Handlebars, _);
next();
} catch (e) {
reject(e);
}
});
walker.on('errors', (root, nodeStatsArray) => {
reject(nodeStatsArray);
});
walker.on('end', async () => {
resolve();
});
});
/**
* Register the template helpers
*
* @private
* @param {Object} config Configuration options
* @param {String} config.templates Absolute path to the templates that should be used.
* @return {Promise}
*/
const registerPartials = config => new Promise((resolve, reject) => {
const partials_dir = path.resolve(config.templates, PARTIALS_DIRNAME);
if (!fs.existsSync(partials_dir)) return resolve();
const walker = xfs.walk(partials_dir, {
followLinks: false
});
walker.on('file', async (root, stats, next) => {
try {
const file_path = path.resolve(config.templates, path.resolve(root, stats.name));
await registerPartial(file_path);
next();
} catch (e) {
reject(e);
}
});
walker.on('errors', (root, nodeStatsArray) => {
reject(nodeStatsArray);
});
walker.on('end', () => {
resolve();
});
});
const bundle = async (openapi, baseDir) => {
if (typeof openapi === 'string') {
try {
return await bundler(openapi, baseDir);
} catch (e) {
throw e;
}
} else if (typeof openapi !== 'object') {
throw new Error(`Could not find a valid OpenAPI definition: ${openapi}`);
}
};
/**
* Generates a code skeleton for an API given an OpenAPI file.
*
* @module codegen.generate
* @param {Object} config Configuration options
* @param {Object|String} config.openapi OpenAPI JSON or a string pointing to an OpenAPI file.
* @param {String} config.target_dir Path to the directory where the files will be generated.
* @return {Promise}
*/
codegen.generate = config => new Promise((resolve, reject) => {
bundle(config.openapi, config.base_dir)
.catch(reject)
.then((openapi) => {
openapi = beautifier(openapi, config);
const randomTitle = randomName().dashed;
config.openapi = openapi;
_.defaultsDeep(config, {
openapi: {
info: {
title: randomTitle
}
},
package: {
name: _.kebabCase(_.result(config, 'openapi.info.title', randomTitle))
},
target_dir: path.resolve(os.tmpdir(), 'openapi-generated'),
templates: path.resolve(__dirname, '../templates')
});
config.templates = `${config.templates}/${config.template}`;
async function start () {
await deleteFolders(config);
await registerHelpers(config);
await registerPartials(config);
await generateDirectoryStructure(config);
}
start().then(resolve).catch(reject);
});
});