Skip to content

Commit

Permalink
feat: split toOpenAPI method
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Aug 16, 2024
1 parent b8b0691 commit 21e9d52
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 118 deletions.
2 changes: 1 addition & 1 deletion src/getConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AspidaConfig } from 'aspida/dist/cjs/commands';
import { getConfigs } from 'aspida/dist/cjs/commands';

export type Config = { input: string; baseURL: string; output: string };
export type Config = { input: string; baseURL?: string; output: string };

export type ConfigFile = AspidaConfig & { openapi?: { outputFile?: string } };

Expand Down
241 changes: 124 additions & 117 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,139 @@ import * as TJS from 'typescript-json-schema';
import type { PartialConfig } from './getConfig';
import { getConfig } from './getConfig';

export default (configs?: PartialConfig) =>
getConfig(configs).forEach((config) => {
const tree = getDirentTree(config.input);

const createFilePaths = (tree: DirentTree): string[] => {
return tree.children.flatMap((child) =>
child.isDir ? createFilePaths(child.tree) : tree.path,
);
};
export const toOpenAPI = (params: {
input: string;
template?: OpenAPIV3_1.Document | string;
}): string => {
const tree = getDirentTree(params.input);

const createFilePaths = (tree: DirentTree): string[] => {
return tree.children.flatMap((child) =>
child.isDir ? createFilePaths(child.tree) : tree.path,
);
};

const paths = createFilePaths(tree);
const paths = createFilePaths(tree);

const typeFile = `${paths
.map(
(p, i) => `import type { Methods as Methods${i} } from '${p.replace(config.input, '.')}'`,
)
.join('\n')}
const typeFile = `${paths
.map((p, i) => `import type { Methods as Methods${i} } from '${p.replace(params.input, '.')}'`)
.join('\n')}
type AllMethods = [${paths.map((_, i) => `Methods${i}`).join(', ')}]`;

const typeFilePath = join(config.input, '@tmp-type.ts');
const typeFilePath = join(params.input, `@openapi-${Date.now()}.ts`);

writeFileSync(typeFilePath, typeFile, 'utf8');

const compilerOptions: TJS.CompilerOptions = {
strictNullChecks: true,
rootDir: process.cwd(),
baseUrl: process.cwd(),
// @ts-expect-error dont match ScriptTarget
target: 'ES2022',
};

const program = TJS.getProgramFromFiles([typeFilePath], compilerOptions);
const schema = TJS.generateSchema(program, 'AllMethods', { required: true });
const doc: OpenAPIV3_1.Document = {
...(typeof params.template === 'string'
? JSON.parse(readFileSync(params.template, 'utf8'))
: params.template),
paths: {},
components: { schemas: schema?.definitions as any },
};

unlinkSync(typeFilePath);

(schema?.items as TJS.Definition[])?.forEach((def, i) => {
const parameters: { name: string; in: 'path' | 'query'; required: boolean; schema: any }[] = [];

let path = paths[i];

if (path.includes('/_')) {
parameters.push(
...path
.split('/')
.filter((p) => p.startsWith('_'))
.map((p) => ({
name: p.slice(1).split('@')[0],
in: 'path' as const,
required: true,
schema: ['number', 'string'].includes(p.slice(1).split('@')[1])
? { type: p.slice(1).split('@')[1] }
: { anyOf: [{ type: 'number' }, { type: 'string' }] },
})),
);

writeFileSync(typeFilePath, typeFile, 'utf8');
path = path.replace(/\/_([^/@]+)(@[^/]+)?/g, '/{$1}');
}

path = path.replace(params.input, '') || '/';

doc.paths![path] = Object.entries(def.properties!).reduce((dict, [method, val]) => {
const params = [...parameters];
// const required = ((val as TJS.Definition).required ?? []) as (keyof AspidaMethodParams)[];
const props = (val as TJS.Definition).properties as {
[Key in keyof AspidaMethodParams]: TJS.Definition;
};

if (props.query) {
const def = (props.query.properties ??
schema?.definitions?.[props.query.$ref!.split('/').at(-1)!]) as TJS.Definition;

params.push(
...Object.entries(def).map(([name, value]) => ({
name,
in: 'query' as const,
required: props.query?.required?.includes(name) ?? false,
schema: value,
})),
);
}

const compilerOptions: TJS.CompilerOptions = {
strictNullChecks: true,
rootDir: process.cwd(),
baseUrl: process.cwd(),
// @ts-expect-error dont match ScriptTarget
target: 'ES2022',
};
const reqFormat = props.reqFormat?.$ref;
const reqContentType =
((props.reqHeaders?.properties?.['content-type'] as TJS.Definition)?.const ??
reqFormat?.includes('FormData'))
? 'multipart/form-data'
: reqFormat?.includes('URLSearchParams')
? 'application/x-www-form-urlencoded'
: 'application/json';
const resContentType =
((props.resHeaders?.properties?.['content-type'] as TJS.Definition)?.const as string) ??
'application/json';

return {
...dict,
[method]: {
tags: path === '/' ? undefined : path.split('/{')[0].replace(/^\//, '').split('/'),
parameters: params,
requestBody:
props.reqBody === undefined
? undefined
: { content: { [reqContentType]: { schema: props.reqBody } } },
responses:
props.resBody === undefined
? undefined
: {
[(props.status?.const as string) ?? '2XX']: {
content: { [resContentType]: { schema: props.resBody } },
},
},
},
};
}, {});
});

return JSON.stringify(doc, null, 2).replaceAll('#/definitions', '#/components/schemas');
};

const program = TJS.getProgramFromFiles([typeFilePath], compilerOptions);
const schema = TJS.generateSchema(program, 'AllMethods', { required: true });
export default (configs?: PartialConfig) =>
getConfig(configs).forEach((config) => {
const existingDoc: OpenAPIV3_1.Document | undefined = existsSync(config.output)
? JSON.parse(readFileSync(config.output, 'utf8'))
: undefined;

const doc: OpenAPIV3_1.Document = {
const template: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: {
title: `${config.output.split('/').at(-1)?.replace('.json', '')} api`,
Expand All @@ -55,95 +149,8 @@ type AllMethods = [${paths.map((_, i) => `Methods${i}`).join(', ')}]`;
servers: config.baseURL ? [{ url: config.baseURL }] : undefined,
...existingDoc,
paths: {},
components: { schemas: schema?.definitions as any },
components: {},
};

unlinkSync(typeFilePath);

(schema?.items as TJS.Definition[])?.forEach((def, i) => {
const parameters: { name: string; in: 'path' | 'query'; required: boolean; schema: any }[] =
[];

let path = paths[i];

if (path.includes('/_')) {
parameters.push(
...path
.split('/')
.filter((p) => p.startsWith('_'))
.map((p) => ({
name: p.slice(1).split('@')[0],
in: 'path' as const,
required: true,
schema: ['number', 'string'].includes(p.slice(1).split('@')[1])
? { type: p.slice(1).split('@')[1] }
: { anyOf: [{ type: 'number' }, { type: 'string' }] },
})),
);

path = path.replace(/\/_([^/@]+)(@[^/]+)?/g, '/{$1}');
}

path = path.replace(config.input, '') || '/';

doc.paths![path] = Object.entries(def.properties!).reduce((dict, [method, val]) => {
const params = [...parameters];
// const required = ((val as TJS.Definition).required ?? []) as (keyof AspidaMethodParams)[];
const props = (val as TJS.Definition).properties as {
[Key in keyof AspidaMethodParams]: TJS.Definition;
};

if (props.query) {
const def = (props.query.properties ??
schema?.definitions?.[props.query.$ref!.split('/').at(-1)!]) as TJS.Definition;

params.push(
...Object.entries(def).map(([name, value]) => ({
name,
in: 'query' as const,
required: props.query?.required?.includes(name) ?? false,
schema: value,
})),
);
}

const reqFormat = props.reqFormat?.$ref;
const reqContentType =
((props.reqHeaders?.properties?.['content-type'] as TJS.Definition)?.const ??
reqFormat?.includes('FormData'))
? 'multipart/form-data'
: reqFormat?.includes('URLSearchParams')
? 'application/x-www-form-urlencoded'
: 'application/json';
const resContentType =
((props.resHeaders?.properties?.['content-type'] as TJS.Definition)?.const as string) ??
'application/json';

return {
...dict,
[method]: {
tags: path === '/' ? undefined : path.split('/{')[0].replace(/^\//, '').split('/'),
parameters: params,
requestBody:
props.reqBody === undefined
? undefined
: { content: { [reqContentType]: { schema: props.reqBody } } },
responses:
props.resBody === undefined
? undefined
: {
[(props.status?.const as string) ?? '2XX']: {
content: { [resContentType]: { schema: props.resBody } },
},
},
},
};
}, {});
});

writeFileSync(
config.output,
JSON.stringify(doc, null, 2).replaceAll('#/definitions', '#/components/schemas'),
'utf8',
);
writeFileSync(config.output, toOpenAPI({ input: config.input, template }), 'utf8');
});

0 comments on commit 21e9d52

Please sign in to comment.