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

chore(spec2cdk): fix cli not working for single services #27892

Merged
merged 1 commit into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 52 additions & 30 deletions tools/@aws-cdk/spec2cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,70 @@

Generates AWS CDK L1s in TypeScript from `@aws-cdk/aws-service-spec`.

```console
Usage:
## Usage

spec2cdk OUTPUT-PATH [--option=value]
```ts
import { generateAll } from '@aws-cdk/spec2cdk';

declare const outputDir: string;

Options:
// Generate all modules
await generateAll(outputPath, { outputPath });

Note: Passing values to non-boolean options MUST use the = sign: --option=value

--augmentations [string] [default: "%moduleName%/%serviceShortName%-augmentations.generated.ts"]
File and path pattern for generated augmentations files
--augmentations-support [boolean] [default: false]
Generates additional files required for augmentation files to compile. Use for testing only.
--clear-output [boolean] [default: false]
Completely delete the output path before generating new files
--debug [boolean] [default: false]
Show additional debug output
--metrics [string] [default: "%moduleName%/%serviceShortName%-canned-metrics.generated.ts"]
File and path pattern for generated canned metrics files
--pattern [string] [default: "%moduleName%/%serviceShortName%.generated.ts"]
File and path pattern for generated files
--service [string] [default: all services]
Generate files only for a specific service, e.g. aws-lambda

Path patterns can use the following variables:
// Generate modules with specific instructions
await generate({
'aws-lambda': { services: ['AWS::Lambda'] },
'aws-s3': { services: ['AWS::S3'] },
}, { outputPath });
```

%moduleName% The name of the module, e.g. aws-lambda
%serviceName% The full name of the service, e.g. aws-lambda
%serviceShortName% The short name of the service, e.g. lambda
Refer to code autocompletion for all options.

Note that %moduleName% and %serviceName% can be different if multiple services are generated into a single module.
### Use as @aws-cdk/cfn2ts replacement

```
The package provides a binary that can be used as a drop-in replacement of the legacy `@aws-cdk/cfn2ts` package.
At a code level, import `@aws-cdk/spec2cdk/lib/cfn2ts` for a drop-in replacement.

## Temporary Schemas

You can import additional, temporary CloudFormation Registry Schemas to test new functionality that is not yet published in `@aws-cdk/aws-service-spec`.
To do this, drop the schema file into `temporary-schemas/us-east-1` and it will be imported on top of the default model.

## Use as @aws-cdk/cfn2ts replacement
## CLI

You can use the `cfn2ts` binary as a drop-in replacement for the existing `@aws-cdk/cfn2ts` command.
A CLI is available for testing and ad-hoc usage.
However its API is limited and you should use the programmatic interface for implementations.

At a code level, import `@aws-cdk/spec2cdk/lib/cfn2ts` for a drop-in replacement.
```console
Usage:
spec2cdk <OUTPUT-PATH> [--option=value]

Arguments:
OUTPUT-PATH The directory the generated code will be written to

Options:
--augmentations [string] [default: %moduleName%/%serviceShortName%-augmentations.generated.ts]
File and path pattern for generated augmentations files
--augmentations-support [boolean]
Generates additional files required for augmentation files to compile. Use for testing only
--clear-output [boolean]
Completely delete the output path before generating new files
--debug [boolean]
Show additional debug output
-h, --help [boolean]
Show this help
--metrics [string] [default: %moduleName%/%serviceShortName%-canned-metrics.generated.ts]
File and path pattern for generated canned metrics files
--pattern [string] [default: %moduleName%/%serviceShortName%.generated.ts]
File and path pattern for generated files
-s, --service [array]
Generate files only for a specific service, e.g. AWS::S3

Path patterns can use the following variables:

%moduleName% The name of the module, e.g. aws-lambda
%serviceName% The full name of the service, e.g. aws-lambda
%serviceShortName% The short name of the service, e.g. lambda

Note that %moduleName% and %serviceName% can be different if multiple services are generated into a single module.
```
44 changes: 0 additions & 44 deletions tools/@aws-cdk/spec2cdk/lib/cli/args.ts

This file was deleted.

145 changes: 145 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as path from 'node:path';
import { parseArgs } from 'node:util';
import { PositionalArg, showHelp } from './help';
import { GenerateModuleMap, PatternKeys, generate, generateAll } from '../generate';
import { log, parsePattern } from '../util';

const command = 'spec2cdk';
const args: PositionalArg[] = [{
name: 'output-path',
required: true,
description: 'The directory the generated code will be written to',
}];
const config = {
'help': {
short: 'h',
type: 'boolean',
description: 'Show this help',
},
'debug': {
type: 'boolean',
description: 'Show additional debug output',
},
'pattern': {
type: 'string',
default: '%moduleName%/%serviceShortName%.generated.ts',
description: 'File and path pattern for generated files',
},
'augmentations': {
type: 'string',
default: '%moduleName%/%serviceShortName%-augmentations.generated.ts',
description: 'File and path pattern for generated augmentations files',
},
'metrics': {
type: 'string',
default: '%moduleName%/%serviceShortName%-canned-metrics.generated.ts',
description: 'File and path pattern for generated canned metrics files ',
},
'service': {
short: 's',
type: 'string',
description: 'Generate files only for a specific service, e.g. AWS::S3',
multiple: true,
},
'clear-output': {
type: 'boolean',
default: false,
description: 'Completely delete the output path before generating new files',
},
'augmentations-support': {
type: 'boolean',
default: false,
description: 'Generates additional files required for augmentation files to compile. Use for testing only',
},
} as const;

const helpText = `Path patterns can use the following variables:

%moduleName% The name of the module, e.g. aws-lambda
%serviceName% The full name of the service, e.g. aws-lambda
%serviceShortName% The short name of the service, e.g. lambda

Note that %moduleName% and %serviceName% can be different if multiple services are generated into a single module.`;

const help = () => showHelp(command, args, config, helpText);
export const shortHelp = () => showHelp(command, args);

export async function main(argv: string[]) {
const {
positionals,
values: options,
} = parseArgs({
args: argv,
allowPositionals: true,
options: config,
});

if (options.help) {
help();
return;
}

if (options.debug) {
process.env.DEBUG = '1';
}
log.debug('CLI args', positionals, options);

const outputDir = positionals[0];
if (!outputDir) {
throw new EvalError('Please specify the output-path');
}

const pss: Record<PatternKeys, true> = { moduleName: true, serviceName: true, serviceShortName: true };

const outputPath = outputDir ?? path.join(__dirname, '..', 'services');
const resourceFilePattern = parsePattern(
stringOr(options.pattern, path.join('%moduleName%', '%serviceShortName%.generated.ts')),
pss,
);

const augmentationsFilePattern = parsePattern(
stringOr(options.augmentations, path.join('%moduleName%', '%serviceShortName%-augmentations.generated.ts')),
pss,
);

const cannedMetricsFilePattern = parsePattern(
stringOr(options.metrics, path.join('%moduleName%', '%serviceShortName%-canned-metrics.generated.ts')),
pss,
);

const generatorOptions = {
outputPath,
filePatterns: {
resources: resourceFilePattern,
augmentations: augmentationsFilePattern,
cannedMetrics: cannedMetricsFilePattern,
},
clearOutput: options['clear-output'],
augmentationsSupport: options['augmentations-support'],
debug: options.debug as boolean,
};

if (options.service?.length) {
const moduleMap: GenerateModuleMap = {};
for (const service of options.service) {
if (!service.includes('::')) {
throw new EvalError(`Each service must be in the form <Partition>::<Service>, e.g. AWS::S3. Got: ${service}`);
}
moduleMap[service.toLocaleLowerCase().split('::').join('-')] = { services: [service] };
}
await generate(moduleMap, generatorOptions);
return;
}

await generateAll(generatorOptions);
}

function stringOr(pat: unknown, def: string) {
if (!pat) {
return def;
}
if (typeof pat !== 'string') {
throw new Error(`Expected string, got: ${JSON.stringify(pat)}`);
}
return pat;
}
83 changes: 83 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/cli/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable no-console */

export interface PositionalArg {
name: string;
description?: string;
required?: boolean
}

export interface Option {
type: 'string' | 'boolean',
short?: string;
default?: string | boolean;
multiple?: boolean;
description?: string;
}

const TAB = ' '.repeat(4);

export function showHelp(command: string, args: PositionalArg[] = [], options: {
[longOption: string]: Option
} = {}, text?: string) {
console.log('Usage:');
console.log(`${TAB}${command} ${renderArgsList(args)} [--option=value]`);

const leftColSize = 6 + longest([
...args.map(a => a.name),
...Object.entries(options).map(([name, def]) => renderOptionName(name, def.short)),
]);

if (args.length) {
console.log('\nArguments:');
for (const arg of args) {
console.log(`${TAB}${arg.name.toLocaleUpperCase().padEnd(leftColSize)}\t${arg.description}`);
}
}

if (Object.keys(options).length) {
console.log('\nOptions:');
const ordered = Object.entries(options).sort(([a], [b]) => a.localeCompare(b));
for (const [option, def] of ordered) {
console.log(`${TAB}${renderOptionName(option, def.short).padEnd(leftColSize)}\t${renderOptionText(def)}`);
}
}
console.log();

if (text) {
console.log(text + '\n');
}
}

function renderArgsList(args: PositionalArg[] = []) {
return args.map(arg => {
const brackets = arg.required ? ['<', '>'] : ['[', ']'];
return `${brackets[0]}${arg.name.toLocaleUpperCase()}${brackets[1]}`;
}).join(' ');
}

function renderOptionName(option: string, short?: string): string {
if (short) {
return `-${short}, --${option}`;
}

return `${' '.repeat(4)}--${option}`;
}

function renderOptionText(def: Option): string {
const out = new Array<string>;

out.push(`[${def.multiple ? 'array' : def.type}]`);

if (def.default) {
out.push(` [default: ${def.default}]`);
}
if (def.description) {
out.push(`\n${TAB.repeat(2)} ${def.description}`);
}

return out.join('');
}

function longest(xs: string[]): number {
return xs.sort((a, b) => b.length - a.length).at(0)?.length ?? 0;
}
Loading
Loading