-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(spec2cdk): fix cli not working for single services (#27892)
Fixes an issue with the CLI version of `spec2cdk` not working for single services anymore. This used to be implicitly tested by being included in the hot path. However after a refactor, we were not using it anymore and thus didn't notice the breaking. Added a test to ensure future functionality. Replaced to custom options parser with Node's built-in parser for increased usability and support for list options (calling the same option multiple time). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Showing
9 changed files
with
335 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.