Skip to content
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
9 changes: 8 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type GlobalOptions = {
force?: boolean;
'no-index'?: boolean;
'no-init'?: boolean;
'no-quote'?: boolean;
output?: string;
quiet?: boolean;
verbose?: boolean;
Expand Down Expand Up @@ -49,6 +50,12 @@ export const cliArguments: Record<string, CliArg> = {
force: { type: 'boolean', short: 'f', [description]: 'Overwrite existing files' },
'no-index': { type: 'boolean', short: 'I', [description]: 'Skip auto-creating index files, only models' },
'no-init': { type: 'boolean', short: 'N', [description]: 'Skip auto-creating init files' },
'no-quote': {
type: 'boolean',
short: 'Q',
[description]:
'Remove quotes around string values. Note that this is not guaranteed to output a valid value as YAML string rules are non-trivial, so use at your own discretion.',
},
output: { type: 'string', short: 'o', [argname]: 'OUTPUT_DIR', [description]: 'Location to output files to (defaults to current folder)' },
quiet: { type: 'boolean', short: 'q', [description]: 'Only output errors' },
verbose: { type: 'boolean', short: 'v', [description]: 'Print the contents of the files to be generated' },
Expand Down Expand Up @@ -260,7 +267,7 @@ export const cli = async (args: string[]): Promise<Record<string, GenerationTask
}

if (!globalOptions['no-init']) {
tasks.push({ contents: getIndex, filename: 'src/index.yml' }, { contents: getBoatsRc, filename: '.boatsrc' });
tasks.push({ contents: () => getIndex(globalOptions), filename: 'src/index.yml' }, { contents: getBoatsRc, filename: '.boatsrc' });
}

if (!tasks.length) {
Expand Down
16 changes: 11 additions & 5 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@ export const createFile = async (path: string, content: string, force = false):
return true;
};

export const toYaml = (object: Json, trailingNewline = true): string => {
/**
* YAML string rules are non-trivial - only set quoteProps=false when you know
* there is no chance of invalid unquoted strings.
*
* see https://stackoverflow.com/a/22235064 and the comments with it.
*/
export const toYaml = (object: Json, trailingNewline = true, quoteProps = true): string => {
const out: string[] = [];
toYamlRecurse(object, out, 0);
toYamlRecurse(object, out, 0, Array.isArray(object), quoteProps);

if (trailingNewline) {
out.push('');
Expand All @@ -65,7 +71,7 @@ export const toYaml = (object: Json, trailingNewline = true): string => {
return out.join('\n');
};

const toYamlRecurse = (object: Json | Json[string], out: string[], depth = 0, isArray = false): void => {
const toYamlRecurse = (object: Json | Json[string], out: string[], depth = 0, isArray = false, quoteProps = true): void => {
const prevIndent = depth ? ' '.repeat((depth - 1) * 2) : '';

if (typeof object === 'object' && object !== null) {
Expand All @@ -78,13 +84,13 @@ const toYamlRecurse = (object: Json | Json[string], out: string[], depth = 0, is
} else {
out.push(`${' '.repeat(depth * 2)}${key}:`);
}
toYamlRecurse((object as Json)[key], out, depth + 1, Array.isArray(object));
toYamlRecurse((object as Json)[key], out, depth + 1, Array.isArray(object), quoteProps);
}
return;
}

if (typeof object === 'string') {
object = `"${object.replace(/\"/g, '\\"')}"`;
object = quoteProps ? `"${object.replace(/\"/g, '\\"')}"` : object;
}

if (isArray) {
Expand Down
28 changes: 14 additions & 14 deletions src/subcommands/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getModel, getModels, getPaginationModel, getParam } from '../templates/

type ModelGenerationOptions = {
name: string;
globalOptions: GlobalOptions;
'no-model'?: boolean;
type?: 'query' | 'header' | 'path';
list?: boolean;
Expand All @@ -14,7 +15,6 @@ type ModelGenerationOptions = {
patch?: boolean;
post?: boolean;
put?: boolean;
rootRef?: string;
};

export const modelCliArguments: Record<string, CliArg> = {
Expand Down Expand Up @@ -105,7 +105,7 @@ export const parseModelCommand: SubcommandGenerator = (args: string[], globalOpt
return help(1, 'Error: --type argument must be query, header, or path');
}

return getModelTasks({ ...parsed.values, rootRef: globalOptions['root-ref'], name });
return getModelTasks({ globalOptions, ...parsed.values, name });
};

export const getModelTasks = (options: ModelGenerationOptions): GenerationTask[] => {
Expand All @@ -114,41 +114,41 @@ export const getModelTasks = (options: ModelGenerationOptions): GenerationTask[]

const tasks: GenerationTask[] = [];
tasks.push({
contents: getModel,
contents: () => getModel(options.globalOptions),
filename: `src/components/schemas/${dashName}/model.yml`,
generate: !options['no-model'] && !options.type,
});

const paginationRef = getRootRef('../pagination/model.yml', '#/components/schemas/PaginationModel', options.rootRef);
const paginationRef = getRootRef('../pagination/model.yml', '#/components/schemas/PaginationModel', options.globalOptions['root-ref']);
if (options.type) {
tasks.push({
contents: () => getParam(options.name, options.type as Exclude<(typeof options)['type'], undefined>),
contents: () => getParam(options.globalOptions, options.name, options.type as Exclude<(typeof options)['type'], undefined>),
filename: `src/components/parameters/${options.type}${capitalize(camelName)}.yml`,
});
}

if (options.list || options.crud) {
tasks.push(
{ contents: () => getModels(paginationRef), filename: `src/components/schemas/${dashName}/models.yml` },
{ contents: () => getParam('limit', 'query', 'integer'), filename: `src/components/parameters/queryLimit.yml` },
{ contents: () => getParam('offset', 'query', 'integer'), filename: `src/components/parameters/queryOffset.yml` },
{ contents: () => getPaginationModel(), filename: `src/components/schemas/pagination/model.yml` },
{ contents: () => getModels(options.globalOptions, paginationRef), filename: `src/components/schemas/${dashName}/models.yml` },
{ contents: () => getParam(options.globalOptions, 'limit', 'query', 'integer'), filename: `src/components/parameters/queryLimit.yml` },
{ contents: () => getParam(options.globalOptions, 'offset', 'query', 'integer'), filename: `src/components/parameters/queryOffset.yml` },
{ contents: () => getPaginationModel(options.globalOptions), filename: `src/components/schemas/pagination/model.yml` },
);
}
if (options.get || options.crud) {
tasks.push({ contents: getModel, filename: `src/components/schemas/${dashName}/get.yml` });
tasks.push({ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/get.yml` });
}
if (options.delete || options.crud) {
tasks.push({ contents: getModel, filename: `src/components/schemas/${dashName}/delete.yml` });
tasks.push({ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/delete.yml` });
}
if (options.patch || options.crud) {
tasks.push({ contents: getModel, filename: `src/components/schemas/${dashName}/patch.yml` });
tasks.push({ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/patch.yml` });
}
if (options.post || options.crud) {
tasks.push({ contents: getModel, filename: `src/components/schemas/${dashName}/post.yml` });
tasks.push({ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/post.yml` });
}
if (options.put) {
tasks.push({ contents: getModel, filename: `src/components/schemas/${dashName}/put.yml` });
tasks.push({ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/put.yml` });
}

return tasks;
Expand Down
81 changes: 45 additions & 36 deletions src/subcommands/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getCreate, getDelete, getList, getReplace, getShow, getUpdate } from '.

type PathGenerationOptions = {
name: string;
globalOptions: GlobalOptions;
'no-models'?: boolean;
model?: string;
type?: string;
Expand All @@ -16,7 +17,6 @@ type PathGenerationOptions = {
patch?: boolean;
post?: boolean;
put?: boolean;
rootRef?: string;
};

export const pathCliArguments: Record<string, CliArg> = {
Expand Down Expand Up @@ -127,7 +127,7 @@ export const parsePathCommand: SubcommandGenerator = (args: string[], globalOpti
return help(1, `invalid path arg "${name}"`);
}

const tasks = getPathTasks({ ...parsed.values, rootRef: globalOptions['root-ref'], name });
const tasks = getPathTasks({ ...parsed.values, globalOptions, name });

if (!tasks.length) {
return help(1, 'Error: Nothing to do. Aborting. Did you forget crud options (eg --get)?');
Expand All @@ -140,6 +140,7 @@ export const getPathTasks = (options: PathGenerationOptions): GenerationTask[] =
const pathParams: { rootRef: string; srcRef: string }[] = [];
let lastIsParam = false;
const tasks: GenerationTask[] = [];
const rootRef = options.globalOptions['root-ref'];

const parts = options.name.split('/').filter(Boolean);
for (let i = 0; i < parts.length; ++i) {
Expand All @@ -152,7 +153,7 @@ export const getPathTasks = (options: PathGenerationOptions): GenerationTask[] =
if (pathParam) {
lastIsParam = true;
tasks.push({
contents: () => getParam(pathParam, 'path'),
contents: () => getParam(options.globalOptions, pathParam, 'path'),
filename: `src/components/parameters/path${capitalize(pathParam)}.yml`,
generate: !options['no-models'],
});
Expand Down Expand Up @@ -181,32 +182,40 @@ export const getPathTasks = (options: PathGenerationOptions): GenerationTask[] =
const dashName = dashCase(singleModelName);
const singularName = camelCase(singleModelName);

const paginationRef = getRootRef('../pagination/model.yml', '#/components/schemas/PaginationModel', options.rootRef);
const paginationRef = getRootRef('../pagination/model.yml', '#/components/schemas/PaginationModel', rootRef);

if (options.list) {
const filename = `src/paths/${normalizedBaseFilepath}/get.yml`;
const listSchemaRef = getRootRef(
relative(`src/paths/${normalizedBaseFilepath}`, `src/components/schemas/${dashName}/models.yml`),
`#/components/schemas/${capitalize(singleModelName)}Models`,
options.rootRef,
rootRef,
);
const paramRefs = mapParamRefs(otherPathParams, dirname(filename), options.rootRef);
const paramRefs = mapParamRefs(otherPathParams, dirname(filename), rootRef);

tasks.push(
{
contents: () => getParam('limit', 'query', 'integer'),
contents: () => getParam(options.globalOptions, 'limit', 'query', 'integer'),
filename: `src/components/parameters/queryLimit.yml`,
generate: !options['no-models'],
},
{
contents: () => getParam('offset', 'query', 'integer'),
contents: () => getParam(options.globalOptions, 'offset', 'query', 'integer'),
filename: 'src/components/parameters/queryOffset.yml',
generate: !options['no-models'],
},
{ contents: getPaginationModel, filename: 'src/components/schemas/pagination/model.yml', generate: !options['no-models'] },
{ contents: getModel, filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getModels(paginationRef), filename: `src/components/schemas/${dashName}/models.yml`, generate: !options['no-models'] },
{ contents: () => getList(customModelName, listSchemaRef, paramRefs), filename },
{
contents: () => getPaginationModel(options.globalOptions),
filename: 'src/components/schemas/pagination/model.yml',
generate: !options['no-models'],
},
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{
contents: () => getModels(options.globalOptions, paginationRef),
filename: `src/components/schemas/${dashName}/models.yml`,
generate: !options['no-models'],
},
{ contents: () => getList(options.globalOptions, customModelName, listSchemaRef, paramRefs), filename },
);
}

Expand All @@ -215,19 +224,19 @@ export const getPathTasks = (options: PathGenerationOptions): GenerationTask[] =
const postRequestRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/post.yml`),
`#/components/schemas/${capitalize(singularName)}Post`,
options.rootRef,
rootRef,
);
const postResponseRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/model.yml`),
`#/components/schemas/${capitalize(singularName)}Model`,
options.rootRef,
rootRef,
);
const paramRefs = mapParamRefs(otherPathParams, dirname(filename), options.rootRef);
const paramRefs = mapParamRefs(otherPathParams, dirname(filename), rootRef);

tasks.push(
{ contents: getModel, filename: `src/components/schemas/${dashName}/post.yml`, generate: !options['no-models'] },
{ contents: getModel, filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getCreate(singularName, postRequestRef, postResponseRef, paramRefs), filename },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/post.yml`, generate: !options['no-models'] },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getCreate(options.globalOptions, singularName, postRequestRef, postResponseRef, paramRefs), filename },
);
}

Expand All @@ -236,41 +245,41 @@ export const getPathTasks = (options: PathGenerationOptions): GenerationTask[] =
const postResponseRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/model.yml`),
`#/components/schemas/${capitalize(singularName)}Model`,
options.rootRef,
rootRef,
);
const paramRefs = mapParamRefs(pathParams, dirname(filename), options.rootRef);
const paramRefs = mapParamRefs(pathParams, dirname(filename), rootRef);

tasks.push(
{ contents: getModel, filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getShow(singularName, postResponseRef, paramRefs), filename },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getShow(options.globalOptions, singularName, postResponseRef, paramRefs), filename },
);
}

if (options.delete) {
const filename = `src/paths/${normalizedFilepath}/delete.yml`;
const paramRefs = mapParamRefs(pathParams, dirname(filename), options.rootRef);
const paramRefs = mapParamRefs(pathParams, dirname(filename), rootRef);

tasks.push({ contents: () => getDelete(singularName, paramRefs), filename });
tasks.push({ contents: () => getDelete(options.globalOptions, singularName, paramRefs), filename });
}

if (options.patch) {
const filename = `src/paths/${normalizedFilepath}/patch.yml`;
const postRequestRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/patch.yml`),
`#/components/schemas/${capitalize(singularName)}Patch`,
options.rootRef,
rootRef,
);
const postResponseRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/model.yml`),
`#/components/schemas/${capitalize(singularName)}Model`,
options.rootRef,
rootRef,
);
const paramRefs = mapParamRefs(pathParams, dirname(filename), options.rootRef);
const paramRefs = mapParamRefs(pathParams, dirname(filename), rootRef);

tasks.push(
{ contents: getModel, filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: getModel, filename: `src/components/schemas/${dashName}/patch.yml`, generate: !options['no-models'] },
{ contents: () => getUpdate(singularName, postRequestRef, postResponseRef, paramRefs), filename },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/patch.yml`, generate: !options['no-models'] },
{ contents: () => getUpdate(options.globalOptions, singularName, postRequestRef, postResponseRef, paramRefs), filename },
);
}

Expand All @@ -279,19 +288,19 @@ export const getPathTasks = (options: PathGenerationOptions): GenerationTask[] =
const postRequestRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/put.yml`),
`#/components/schemas/${capitalize(singularName)}Put`,
options.rootRef,
rootRef,
);
const postResponseRef = getRootRef(
relative(dirname(filename), `src/components/schemas/${dashName}/model.yml`),
`#/components/schemas/${capitalize(singularName)}Model`,
options.rootRef,
rootRef,
);
const paramRefs = mapParamRefs(pathParams, dirname(filename), options.rootRef);
const paramRefs = mapParamRefs(pathParams, dirname(filename), rootRef);

tasks.push(
{ contents: getModel, filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: getModel, filename: `src/components/schemas/${dashName}/put.yml`, generate: !options['no-models'] },
{ contents: () => getReplace(singularName, postRequestRef, postResponseRef, paramRefs), filename },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/model.yml`, generate: !options['no-models'] },
{ contents: () => getModel(options.globalOptions), filename: `src/components/schemas/${dashName}/put.yml`, generate: !options['no-models'] },
{ contents: () => getReplace(options.globalOptions, singularName, postRequestRef, postResponseRef, paramRefs), filename },
);
}

Expand Down
37 changes: 21 additions & 16 deletions src/templates/init.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GlobalOptions } from '../cli';
import { toYaml } from '../lib';

const autoTagOpid = `
Expand All @@ -15,23 +16,27 @@ const autoTagOpid = `
])
}}`;

export const getIndex = (): string => {
const yaml = toYaml({
openapi: '3.1.0',
info: {
version: "{{ packageJson('version') }}",
title: "{{ packageJson('name') }}",
description: 'our sweet api',
contact: { name: 'acrontum', email: 'support@acrontum.de' },
license: { name: 'Apache 2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0.html' },
},
servers: [{ url: '/v1' }],
paths: { $ref: 'paths/index.yml' },
components: {
parameters: { $ref: 'components/parameters/index.yml' },
schemas: { $ref: 'components/schemas/index.yml' },
export const getIndex = (globalOptions: GlobalOptions): string => {
const yaml = toYaml(
{
openapi: '3.1.0',
info: {
version: "{{ packageJson('version') }}",
title: "{{ packageJson('name') }}",
description: 'our sweet api',
contact: { name: 'acrontum', email: 'support@acrontum.de' },
license: { name: 'Apache 2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0.html' },
},
servers: [{ url: '/v1' }],
paths: { $ref: 'paths/index.yml' },
components: {
parameters: { $ref: 'components/parameters/index.yml' },
schemas: { $ref: 'components/schemas/index.yml' },
},
},
});
true,
!globalOptions['no-quote'],
);

return `${yaml}${autoTagOpid}\n`;
};
Expand Down
Loading