Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generator options - merge to master #160

Merged
merged 10 commits into from
Feb 2, 2023
2 changes: 1 addition & 1 deletion CICD/compareTestResults.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require("fs");

const log = require("../src/log.js");
const log = require("../src/common/log");

log.setLogLevel(log.logLevels.verbose);

Expand Down
2 changes: 1 addition & 1 deletion CICD/updateWebpage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require("fs");

const log = require("../src/log.js");
const log = require("../src/common/log");

// This regex extracts the generator table from README.md.
const tableRegex =
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ Options:
-h, --help Display help for command
```

Individual generators may have their own options. Try it out:

```
% openapi-forge generator-options https://github.com/ScottLogic/openapi-forge-javascript.git
Usage: openapi-forge generator-options <generator>

This generator has a number of additional options which can be supplied when executing the 'forge' command.

Options:
--generator.moduleFormat <value> The module format to use for the generated
code. (choices: "commonjs", "esmodule",
default: "commonjs")
```

and then

```
% openapi-forge forge https://petstore3.swagger.io/api/v3/openapi.json https://github.com/ScottLogic/openapi-forge-javascript.git --generator.moduleFormat "esmodule"
```

## Client generation

In order to generate a client you need a suitable API specification, this can be supplied as a URL or a local file and can be in JSON or YML format. For this tutorial, we’ll use the Swagger Petstore API:
Expand Down
File renamed without changes.
File renamed without changes.
71 changes: 71 additions & 0 deletions src/forge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const path = require("path");
const fs = require("fs");

const generate = require("../generate");
const generatorResolver = require("../common/generatorResolver");
const {
configToCommanderOptions,
generatorOptionsPrefix,
} = require("../generatorOptions/generatorOptions");

const forgeCommand = function (program) {
const command = program
.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")
.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"
)
.allowUnknownOption()
.action(async (schema, generatorPathOrUrl) => {
const generatorPath = generatorResolver.getGenerator(generatorPathOrUrl);
const configFile = path.join(generatorPath, "config.json");

// re-configure the command to generate the API
command.allowUnknownOption(false).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];
});

await generate(schema, generatorPath, options);
generatorResolver.cleanup();
});

// 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) => {
command.addOption(option);
});
}

// parse the command line arguments, and perform generation (on success)
command.parse(process.argv);
});
};

module.exports = forgeCommand;
17 changes: 8 additions & 9 deletions src/generate.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// IMPORTANT: This file is used in the generators, so you will need to change them if this file is moved.
// See issue https://github.com/ScottLogic/openapi-forge/issues/158

const fs = require("fs");

const path = require("path");
Expand All @@ -7,9 +10,9 @@ const minimatch = require("minimatch");
const fetch = require("node-fetch");
const { parse } = require("yaml");

const generatorResolver = require("./generatorResolver");
const generatorResolver = require("./common/generatorResolver");
const helpers = require("./helpers");
const log = require("./log");
const log = require("./common/log");
const transformers = require("./transformers");
const SwaggerParser = require("@apidevtools/swagger-parser");
const converter = require("swagger2openapi");
Expand Down Expand Up @@ -153,17 +156,15 @@ function getFilesInFolders(basePath, partialPath = "") {
});
}

async function generate(schemaPathOrUrl, generatorPathOrUrl, options) {
// IMPORTANT: This function is used in the generators, so be careful when modifying!
// See issue https://github.com/ScottLogic/openapi-forge/issues/158
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 @@ -265,8 +266,6 @@ async function generate(schemaPathOrUrl, generatorPathOrUrl, options) {
}
} catch (e) {
exception = e;
} finally {
generatorResolver.cleanup();
}

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

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

const generatorResolver = require("../common/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,
};
18 changes: 18 additions & 0 deletions src/generatorOptions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { generatorOptions } = require("./generatorOptions");

const generatorOptionsCommand = function (program) {
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"
)
.action(async (generator) => {
console.log(await generatorOptions(generator));
});
};

module.exports = generatorOptionsCommand;
61 changes: 8 additions & 53 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,19 @@
#! /usr/bin/env node

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

const packageJson = require("../package.json");
const testGenerators = require("./testGenerators");
const forgeCommand = require("./forge");
const generatorOptionsCommand = require("./generatorOptions");
const testGeneratorsCommand = require("./testGenerators");
const program = new Command();

program.name("openapi-forge");

program.version(packageJson.version);

program
.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")
.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);
});

program
.command("test-generators")
.description("Test language specific generators.")
.option(
"-g, --generators <gens...>",
"Generators to test, e.g. openapi-forge-typescript"
)
.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"
)
.addOption(
new Option("-f, --format <format>", "Output format")
.choices(["table", "json"])
.default("table")
)
.action(async (options) => {
testGenerators.testGenerators(options);
});
forgeCommand(program);
generatorOptionsCommand(program);
testGeneratorsCommand(program);

program.parse();
27 changes: 27 additions & 0 deletions src/testGenerators/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { Option } = require("commander");
const testGenerators = require("./testGenerators");

const testGeneratorsCommand = function (program) {
program
.command("test-generators")
.description("Test language specific generators.")
.option(
"-g, --generators <gens...>",
"Generators to test, e.g. openapi-forge-typescript"
)
.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"
)
.addOption(
new Option("-f, --format <format>", "Output format")
.choices(["table", "json"])
.default("table")
)
.action(async (options) => {
testGenerators.testGenerators(options);
});
};

module.exports = testGeneratorsCommand;
15 changes: 13 additions & 2 deletions src/testGenerators.js → src/testGenerators/testGenerators.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const path = require("path");
const shell = require("shelljs");

const log = require("./log");
const log = require("../common/log");

function isJson(str) {
try {
Expand Down Expand Up @@ -52,7 +52,18 @@ function testGenerators(options) {
options.generators.forEach((generator) => {
try {
const generatorPath = path.resolve(
path.join(__dirname, "..", "..", generator)
// Assuming the file structure:
// - openapi-forge
// |- src
// |- testGenerators
// |- testGenerators.js
// - openapi-forge-javascript
// - openapi-forge-...
//
// The generators must be in the above location, otherwise this test-generators command will not work.
// If you move this file, the test-generators command will not work unless you fix the line below.
// We need to go up three directories to get to the root of openapi-forge:
path.join(__dirname, "..", "..", "..", generator)
);

log.standard(`Starting tests for generator ${generator}`);
Expand Down
Loading