Skip to content

Commit

Permalink
feat: adding support for generators with additional options
Browse files Browse the repository at this point in the history
  • Loading branch information
ColinEberhardt committed Feb 2, 2023
1 parent 0b6ce91 commit bf7ea54
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 28 deletions.
7 changes: 1 addition & 6 deletions src/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,13 @@ function getFilesInFolders(basePath, partialPath = "") {
});
}

async function generate(schemaPathOrUrl, generatorPathOrUrl, options) {
async function generate(schemaPathOrUrl, generatorPath, options) {
log.setLogLevel(options.logLevel);
log.logTitle();
let exception = null;
let numberOfDiscoveredModels = 0;
let numberOfDiscoveredEndpoints = 0;
try {
log.standard(`Loading generator from '${generatorPathOrUrl}'`);

let generatorPath = generatorResolver.getGenerator(generatorPathOrUrl);

log.standard("Validating generator");
validateGenerator(generatorPath);
Expand Down Expand Up @@ -269,8 +266,6 @@ async function generate(schemaPathOrUrl, generatorPathOrUrl, options) {
}
} catch (e) {
exception = e;
} finally {
generatorResolver.cleanup();
}

if (exception === null) {
Expand Down
62 changes: 62 additions & 0 deletions src/generatorOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require("fs");
const path = require("path");

const { Option, Command } = require("commander");

const generatorResolver = require("./generatorResolver");

const generatorOptionsPrefix = "generator.";

async function generatorOptions(generatorPathOrUrl) {
let optionsHelp = "";
const generatorPath = generatorResolver.getGenerator(generatorPathOrUrl);
const configFile = path.join(generatorPath, "config.json");

if (!fs.existsSync(configFile)) {
optionsHelp += "The generator has no additional options";
} else {
const config = JSON.parse(fs.readFileSync(configFile, "utf8"));
const options = configToCommanderOptions(config);

// we use commander to create the formatted help for these options
const command = new Command();
options.forEach((option) => command.addOption(option));
const commanderHelp = command.helpInformation();

// extract the parts we are interested in
const lines = commanderHelp.split("\n");
lines.splice(0, 2);
lines.splice(lines.length - 2, 2);

optionsHelp +=
"This generator has a number of additional options which can be supplied when executing the 'forge' command.\n\n";
optionsHelp += lines.join("\n");
}

generatorResolver.cleanup();

return optionsHelp;
}

// we use the commander library to parse the command line arguments and provide
// help text. This function converts the config.json file into a set of options
function configToCommanderOptions(config) {
return Object.keys(config).map((optionName) => {
const option = config[optionName];
const commanderOption = new Option(`--${generatorOptionsPrefix}${optionName} <value>`);
if (option.description) {
commanderOption.description = option.description;
}
if (option.choices) {
commanderOption.choices(option.choices);
commanderOption.default(option.choices[0]);
}
return commanderOption;
});
}

module.exports = {
generatorOptions,
configToCommanderOptions,
generatorOptionsPrefix
};
110 changes: 89 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#! /usr/bin/env node

const path = require("path");
const fs = require("fs");
const { Command, Option } = require("commander");
const generate = require("./generate");
const generatorResolver = require("./generatorResolver");
const {
configToCommanderOptions,
generatorOptions,
generatorOptionsPrefix
} = require("./generatorOptions");
const packageJson = require("../package.json");
const testGenerators = require("./testGenerators");
const program = new Command();
Expand All @@ -10,34 +18,94 @@ program.name("openapi-forge");

program.version(packageJson.version);

program
.command("forge")
function addArguments(program) {
program
.argument("<schema>", "An OpenAPI schema, either a URL or a file path")
.argument(
"<generator>",
"Git URL, file path or npm package of a language-specific generator"
);
}

function addOptions(program) {
program
.option(
"-e, --exclude <glob>",
"A glob pattern that excludes files from the generator in the output",
""
)
.option(
"-o, --output <path>",
"The path where the generated client API will be written",
"."
)
.option("-s, --skipValidation", "Skip schema validation")
.option(
"-l, --logLevel <level>",
"Sets the logging level, options are: quiet ('quiet', 'q' or '0'), standard (default) ('standard', 's' or '1'), verbose ('verbose', 'v' or '2')",
"1"
);
}

const forgeCommand = new Command("forge")
.description(
"Forge the API client from an OpenAPI specification. This command takes an OpenAPI schema, and uses the given generator to create a client library."
)
.argument("<schema>", "An OpenAPI schema, either a URL or a file path")
.allowUnknownOption()
.action(async (schema, generatorPathOrUrl) => {
try {
const generatorPath = generatorResolver.getGenerator(generatorPathOrUrl);
const configFile = path.join(generatorPath, "config.json");

// create an additional command to parse the additional options from the generator's config.json file
const argsParser = new Command();
addArguments(argsParser);
addOptions(argsParser);
argsParser.action(async (_, __, options) => {

// set the additional options as environment variables
const generatorOptions = Object.keys(options).filter((key) =>
key.startsWith(generatorOptionsPrefix)
);
generatorOptions.forEach((option) => {
const optionName = option.substring(generatorOptionsPrefix.length);
process.env[optionName] = options[option];
});

generate(schema, generatorPath, options);
});

// add the additional options from the generator's config.json file
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, "utf8"));
configToCommanderOptions(config).forEach((option) => {
argsParser.addOption(option);
});
}

// parse the command line arguments, and perform generation (on success)
argsParser.parse(process.argv);
} finally {
generatorResolver.cleanup();
}
});

addArguments(forgeCommand);
addOptions(forgeCommand);

program.addCommand(forgeCommand);

program
.command("generator-options")
.description(
"List the options available for a generator. Some generators take additional options that configure their output, this command lists and describes the options available."
)
.argument(
"<generator>",
"Git URL, file path or npm package of a language-specific generator"
)
.option(
"-e, --exclude <glob>",
"A glob pattern that excludes files from the generator in the output",
""
)
.option(
"-o, --output <path>",
"The path where the generated client API will be written",
"."
)
.option("-s, --skipValidation", "Skip schema validation")
.option(
"-l, --logLevel <level>",
"Sets the logging level, options are: quiet ('quiet', 'q' or '0'), standard (default) ('standard', 's' or '1'), verbose ('verbose', 'v' or '2')",
"1"
)
.action(async (schema, template, options) => {
generate(schema, template, options);
.action(async (generator) => {
console.log(await generatorOptions(generator));
});

program
Expand Down
1 change: 0 additions & 1 deletion test/generate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe("generate", () => {
beforeAll(() => {
// For these tests, we don't really care about the responses for these:
path.resolve.mockImplementation((path) => path);
generatorResolver.getGenerator.mockImplementation((path) => path);
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue(fakeSchema);
generatorResolver.isUrl.mockReturnValue(false);
Expand Down
84 changes: 84 additions & 0 deletions test/generatorOptions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const fs = require("fs");

const { generatorOptions } = require("../src/generatorOptions");
const generatorResolver = require("../src/generatorResolver");

jest.mock("fs");
jest.mock("../src/generatorResolver");

describe("generate", () => {
beforeAll(() => {
generatorResolver.getGenerator.mockImplementation((path) => path);
});

beforeEach(() => {
jest.clearAllMocks();
fs.existsSync.mockReturnValue(true);
});

it("should indicate if the generator doesn't have any additional options", async () => {
fs.existsSync.mockReturnValue(false);
const optionsHelp = await generatorOptions("some/path");
expect(optionsHelp).toEqual("The generator has no additional options");
});

it("should include the description in the help output", async () => {
const config = {
moduleFormat: {
description: "The module format to use for the generated code.",
},
};
fs.readFileSync.mockReturnValue(JSON.stringify(config));
const optionsHelp = await generatorOptions("some/path");
expect(optionsHelp).toEqual(
expect.stringMatching(
/moduleFormat.*The module format to use for the generated code./
)
);
});

it("should treat the description as optional", async () => {
const config = {
moduleFormat: {},
};
fs.readFileSync.mockReturnValue(JSON.stringify(config));
const optionsHelp = await generatorOptions("some/path");
expect(optionsHelp).toEqual(expect.stringMatching(/moduleFormat/));
});

it("should handle multiple options", async () => {
const config = {
optionOne: {
description: "description One.",
},
optionTwo: {
description: "description Two.",
},
};

fs.readFileSync.mockReturnValue(JSON.stringify(config));
const optionsHelp = await generatorOptions("some/path");

expect(optionsHelp).toEqual(
expect.stringMatching(/optionOne.*description One./)
);
expect(optionsHelp).toEqual(
expect.stringMatching(/optionTwo.*description Two./)
);
});

it("should output choices", async () => {
const config = {
moduleFormat: {
choices: ["cjs", "esm"],
},
};
fs.readFileSync.mockReturnValue(JSON.stringify(config));
const optionsHelp = await generatorOptions("some/path");
expect(optionsHelp).toEqual(
expect.stringMatching(
/moduleFormat.*(choices: "cjs", "esm", default: "cjs")/
)
);
});
});

0 comments on commit bf7ea54

Please sign in to comment.