Skip to content

Commit

Permalink
Merge pull request #323 from opticdev/scripts-experiment
Browse files Browse the repository at this point in the history
[Experimental] Added script capability to Optic
  • Loading branch information
acunniffe authored Aug 26, 2020
2 parents 7a9057b + 165e923 commit 48b2159
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 38 deletions.
9 changes: 9 additions & 0 deletions workspaces/cli-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export interface IOpticTask {
targetUrl?: string;
}

export interface IOpticScript {
command: string;
dependsOn?: string | string[];
install?: string;
}

export interface IOpticTaskAliases {
inboundUrl?: string;
}
Expand All @@ -45,6 +51,9 @@ export interface IApiCliConfig {
tasks: {
[key: string]: IOpticTask;
};
scripts?: {
[key: string]: string | IOpticScript;
};
ignoreRequests?: string[];
}

Expand Down
3 changes: 2 additions & 1 deletion workspaces/local-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"strip-ansi": "^6.0.0",
"tslib": "^1",
"url-join": "^4.0.1",
"uuid": "^8.0.0"
"uuid": "^8.0.0",
"which": "^2.0.2"
},
"devDependencies": {
"@oclif/dev-cli": "^1",
Expand Down
100 changes: 63 additions & 37 deletions workspaces/local-cli/src/commands/generate/oas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,77 @@ export default class GenerateOas extends Command {
static description = 'export an OpenAPI 3.0.1 spec';

static flags = {
json: flags.boolean({
default: true,
exclusive: ['yaml'],
}),
yaml: flags.boolean({
exclusive: ['json'],
}),
json: flags.boolean({}),
yaml: flags.boolean({}),
};

async run() {
const { flags } = this.parse(GenerateOas);
await generateOas(
flags.yaml || (!flags.json && !flags.yaml) /* make this default */,
flags.json
);
}
}

export async function generateOas(
flagYaml: boolean,
flagJson: boolean
): Promise<{ json: string | undefined; yaml: string | undefined } | undefined> {
try {
const paths = await getPathsRelativeToConfig();
const { specStorePath } = paths;
try {
const paths = await getPathsRelativeToConfig();
const { specStorePath } = paths;
try {
const eventsBuffer = await fs.readFile(specStorePath);
const eventsString = eventsBuffer.toString();
cli.action.start('Generating OAS file');
const parsedOas = OasProjectionHelper.fromEventString(eventsString);
const outputFile = await this.emit(paths, parsedOas);
cli.action.stop(
'\n' + fromOptic('Generated OAS file at ' + outputFile)
);
} catch (e) {
this.error(e);
}
const eventsBuffer = await fs.readFile(specStorePath);
const eventsString = eventsBuffer.toString();
cli.action.start('Generating OAS file');
const parsedOas = OasProjectionHelper.fromEventString(eventsString);
const outputFiles = await emit(paths, parsedOas, flagYaml, flagJson);
const filePaths = Object.values(outputFiles);
cli.action.stop(
'\n' +
fromOptic(
`Generated OAS file${filePaths.length > 1 && 's'} \n` +
filePaths.join('\n')
)
);

return outputFiles;
} catch (e) {
this.error(e);
console.error(e);
}
} catch (e) {
console.error(e);
}
}

async emit(paths: IPathMapping, parsedOas: object): Promise<string> {
const { flags } = this.parse(GenerateOas);
export async function emit(
paths: IPathMapping,
parsedOas: object,
flagYaml: boolean,
flagJson: boolean
): Promise<{ json: string | undefined; yaml: string | undefined }> {
const shouldOutputYaml = flagYaml;
const shouldOutputJson = flagJson;

const shouldOutputYaml = flags.yaml;

const outputPath = path.join(paths.basePath, 'generated');
await fs.ensureDir(outputPath);
if (shouldOutputYaml) {
const outputFile = path.join(outputPath, 'openapi.yaml');
await fs.writeFile(outputFile, yaml.safeDump(parsedOas, { indent: 1 }));
return outputFile;
} else {
const outputFile = path.join(outputPath, 'openapi.json');
await fs.writeJson(outputFile, parsedOas, { spaces: 2 });
return outputFile;
}
const outputPath = path.join(paths.basePath, 'generated');

let yamlPath, jsonPath;

await fs.ensureDir(outputPath);
if (shouldOutputYaml) {
const outputFile = path.join(outputPath, 'openapi.yaml');
await fs.writeFile(outputFile, yaml.safeDump(parsedOas, { indent: 1 }));
yamlPath = outputFile;
}
if (shouldOutputJson) {
const outputFile = path.join(outputPath, 'openapi.json');
await fs.writeJson(outputFile, parsedOas, { spaces: 2 });
jsonPath = outputFile;
}

return {
json: jsonPath,
yaml: yamlPath,
};
}
177 changes: 177 additions & 0 deletions workspaces/local-cli/src/commands/scripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Command, flags } from '@oclif/command';
import { verifyTask } from '../shared/verify';
// @ts-ignore
import {
getPathsRelativeToConfig,
IOpticTask,
readApiConfig,
TaskToStartConfig,
} from '@useoptic/cli-config';
//@ts-ignore
import niceTry from 'nice-try';
import { cli } from 'cli-ux';
//@ts-ignore
import which from 'which';
import colors from 'colors';
import { exec, spawn, SpawnOptions } from 'child_process';
import { IOpticScript } from '@useoptic/cli-config/build';
import { developerDebugLogger, fromOptic } from '@useoptic/cli-shared';
import GenerateOas, { generateOas } from './generate/oas';
export default class Scripts extends Command {
static description = 'Run one of the scripts in your optic.yml file';

static args = [
{
name: 'scriptName',
required: false,
},
];

static flags = {
install: flags.boolean({
required: false,
char: 'i',
}),
};

async run() {
const { args, flags } = this.parse(Scripts);
const scriptName: string | undefined = args.scriptName;

if (!scriptName) {
return console.log('list all scripts...');
}

const script: IOpticScript | undefined = await niceTry(async () => {
const paths = await getPathsRelativeToConfig();
const config = await readApiConfig(paths.configPath);
const foundScript = config.scripts?.[scriptName!];
if (foundScript) {
return normalizeScript(foundScript);
}
});

if (scriptName && script) {
this.log(fromOptic(`Found Script ${colors.bold(scriptName)}`));
const { found, missing } = await checkDependencies(script);
if (missing.length) {
const hasInstallScript = Boolean(script.install);
this.log(
fromOptic(
colors.red(
`Some bin dependencies are missing ${JSON.stringify(missing)}. ${
hasInstallScript &&
!flags.install &&
"Run the command again with the flag '--install' to install them"
}`
)
)
);
if (hasInstallScript && flags.install) {
const result = await tryInstall(script.install!);
if (!result) {
return this.log(
fromOptic(
colors.red(
'Install command failed. Please install the dependencies for this script manually'
)
)
);
} else {
return this.executeScript(script);
}
}
return;
} else {
return this.executeScript(script);
}
} else {
this.log(
fromOptic(colors.red(`No script ${scriptName} found in optic.yml`))
);
}
}

async executeScript(script: IOpticScript) {
const paths: any = await generateOas(true, true)!;
const env: any = {
//@ts-ignore
OPENAPI_JSON: paths.json,
//@ts-ignore
OPENAPI_YAML: paths.yaml,
};

console.log(`Running command: ${colors.grey(script.command)} `);
await spawnProcess(script.command, env);
}
}

function normalizeScript(scriptRaw: string | IOpticScript): IOpticScript {
if (typeof scriptRaw === 'string') {
return {
command: scriptRaw,
dependsOn: [],
};
} else {
const dependsOn =
(scriptRaw.dependsOn && typeof scriptRaw.dependsOn === 'string'
? [scriptRaw.dependsOn]
: scriptRaw.dependsOn) || [];
return { ...scriptRaw, dependsOn };
}
}

async function checkDependencies(
script: IOpticScript
): Promise<{ found: string[]; missing: string[] }> {
const dependencies = script.dependsOn as Array<string>;
cli.action.start(
`${colors.bold(`Checking bin dependencies`)} ${colors.grey(
'Requiring ' + JSON.stringify(dependencies)
)}`
);

const results: [string, string][] = [];
for (const bin of dependencies) {
const pathToBin = which.sync(bin, { nothrow: true });
results.push([bin, pathToBin]);
}

const found = results.filter((i) => Boolean(i[1])).map((i) => i[0]);
const missing = results.filter((i) => !Boolean(i[1])).map((i) => i[0]);

if (missing.length === 0) {
cli.action.stop(colors.green.bold('✓ All dependencies found'));
} else {
cli.action.stop(colors.red('Missing dependencies'));
}

return { found, missing };
}

async function tryInstall(installScript: string): Promise<boolean> {
cli.action.start(`Running install command: ${colors.grey(installScript)} `);
const status = await spawnProcess(installScript);
cli.action.stop('Success!');
return status;
}

async function spawnProcess(command: string, env: any = {}): Promise<boolean> {
const taskOptions: SpawnOptions = {
env: {
...process.env,
...env,
},
shell: true,
cwd: process.cwd(),
stdio: 'inherit',
};

const child = spawn(command, taskOptions);

return await new Promise((resolve) => {
child.on('exit', (code) => {
resolve(code === 0);
});
});
}

0 comments on commit 48b2159

Please sign in to comment.