Skip to content

Commit 21e9d52

Browse files
committed
feat: split toOpenAPI method
1 parent b8b0691 commit 21e9d52

File tree

2 files changed

+125
-118
lines changed

2 files changed

+125
-118
lines changed

src/getConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AspidaConfig } from 'aspida/dist/cjs/commands';
22
import { getConfigs } from 'aspida/dist/cjs/commands';
33

4-
export type Config = { input: string; baseURL: string; output: string };
4+
export type Config = { input: string; baseURL?: string; output: string };
55

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

src/index.ts

Lines changed: 124 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,139 @@ import * as TJS from 'typescript-json-schema';
88
import type { PartialConfig } from './getConfig';
99
import { getConfig } from './getConfig';
1010

11-
export default (configs?: PartialConfig) =>
12-
getConfig(configs).forEach((config) => {
13-
const tree = getDirentTree(config.input);
14-
15-
const createFilePaths = (tree: DirentTree): string[] => {
16-
return tree.children.flatMap((child) =>
17-
child.isDir ? createFilePaths(child.tree) : tree.path,
18-
);
19-
};
11+
export const toOpenAPI = (params: {
12+
input: string;
13+
template?: OpenAPIV3_1.Document | string;
14+
}): string => {
15+
const tree = getDirentTree(params.input);
16+
17+
const createFilePaths = (tree: DirentTree): string[] => {
18+
return tree.children.flatMap((child) =>
19+
child.isDir ? createFilePaths(child.tree) : tree.path,
20+
);
21+
};
2022

21-
const paths = createFilePaths(tree);
23+
const paths = createFilePaths(tree);
2224

23-
const typeFile = `${paths
24-
.map(
25-
(p, i) => `import type { Methods as Methods${i} } from '${p.replace(config.input, '.')}'`,
26-
)
27-
.join('\n')}
25+
const typeFile = `${paths
26+
.map((p, i) => `import type { Methods as Methods${i} } from '${p.replace(params.input, '.')}'`)
27+
.join('\n')}
2828
2929
type AllMethods = [${paths.map((_, i) => `Methods${i}`).join(', ')}]`;
3030

31-
const typeFilePath = join(config.input, '@tmp-type.ts');
31+
const typeFilePath = join(params.input, `@openapi-${Date.now()}.ts`);
32+
33+
writeFileSync(typeFilePath, typeFile, 'utf8');
34+
35+
const compilerOptions: TJS.CompilerOptions = {
36+
strictNullChecks: true,
37+
rootDir: process.cwd(),
38+
baseUrl: process.cwd(),
39+
// @ts-expect-error dont match ScriptTarget
40+
target: 'ES2022',
41+
};
42+
43+
const program = TJS.getProgramFromFiles([typeFilePath], compilerOptions);
44+
const schema = TJS.generateSchema(program, 'AllMethods', { required: true });
45+
const doc: OpenAPIV3_1.Document = {
46+
...(typeof params.template === 'string'
47+
? JSON.parse(readFileSync(params.template, 'utf8'))
48+
: params.template),
49+
paths: {},
50+
components: { schemas: schema?.definitions as any },
51+
};
52+
53+
unlinkSync(typeFilePath);
54+
55+
(schema?.items as TJS.Definition[])?.forEach((def, i) => {
56+
const parameters: { name: string; in: 'path' | 'query'; required: boolean; schema: any }[] = [];
57+
58+
let path = paths[i];
59+
60+
if (path.includes('/_')) {
61+
parameters.push(
62+
...path
63+
.split('/')
64+
.filter((p) => p.startsWith('_'))
65+
.map((p) => ({
66+
name: p.slice(1).split('@')[0],
67+
in: 'path' as const,
68+
required: true,
69+
schema: ['number', 'string'].includes(p.slice(1).split('@')[1])
70+
? { type: p.slice(1).split('@')[1] }
71+
: { anyOf: [{ type: 'number' }, { type: 'string' }] },
72+
})),
73+
);
3274

33-
writeFileSync(typeFilePath, typeFile, 'utf8');
75+
path = path.replace(/\/_([^/@]+)(@[^/]+)?/g, '/{$1}');
76+
}
77+
78+
path = path.replace(params.input, '') || '/';
79+
80+
doc.paths![path] = Object.entries(def.properties!).reduce((dict, [method, val]) => {
81+
const params = [...parameters];
82+
// const required = ((val as TJS.Definition).required ?? []) as (keyof AspidaMethodParams)[];
83+
const props = (val as TJS.Definition).properties as {
84+
[Key in keyof AspidaMethodParams]: TJS.Definition;
85+
};
86+
87+
if (props.query) {
88+
const def = (props.query.properties ??
89+
schema?.definitions?.[props.query.$ref!.split('/').at(-1)!]) as TJS.Definition;
90+
91+
params.push(
92+
...Object.entries(def).map(([name, value]) => ({
93+
name,
94+
in: 'query' as const,
95+
required: props.query?.required?.includes(name) ?? false,
96+
schema: value,
97+
})),
98+
);
99+
}
34100

35-
const compilerOptions: TJS.CompilerOptions = {
36-
strictNullChecks: true,
37-
rootDir: process.cwd(),
38-
baseUrl: process.cwd(),
39-
// @ts-expect-error dont match ScriptTarget
40-
target: 'ES2022',
41-
};
101+
const reqFormat = props.reqFormat?.$ref;
102+
const reqContentType =
103+
((props.reqHeaders?.properties?.['content-type'] as TJS.Definition)?.const ??
104+
reqFormat?.includes('FormData'))
105+
? 'multipart/form-data'
106+
: reqFormat?.includes('URLSearchParams')
107+
? 'application/x-www-form-urlencoded'
108+
: 'application/json';
109+
const resContentType =
110+
((props.resHeaders?.properties?.['content-type'] as TJS.Definition)?.const as string) ??
111+
'application/json';
112+
113+
return {
114+
...dict,
115+
[method]: {
116+
tags: path === '/' ? undefined : path.split('/{')[0].replace(/^\//, '').split('/'),
117+
parameters: params,
118+
requestBody:
119+
props.reqBody === undefined
120+
? undefined
121+
: { content: { [reqContentType]: { schema: props.reqBody } } },
122+
responses:
123+
props.resBody === undefined
124+
? undefined
125+
: {
126+
[(props.status?.const as string) ?? '2XX']: {
127+
content: { [resContentType]: { schema: props.resBody } },
128+
},
129+
},
130+
},
131+
};
132+
}, {});
133+
});
134+
135+
return JSON.stringify(doc, null, 2).replaceAll('#/definitions', '#/components/schemas');
136+
};
42137

43-
const program = TJS.getProgramFromFiles([typeFilePath], compilerOptions);
44-
const schema = TJS.generateSchema(program, 'AllMethods', { required: true });
138+
export default (configs?: PartialConfig) =>
139+
getConfig(configs).forEach((config) => {
45140
const existingDoc: OpenAPIV3_1.Document | undefined = existsSync(config.output)
46141
? JSON.parse(readFileSync(config.output, 'utf8'))
47142
: undefined;
48-
49-
const doc: OpenAPIV3_1.Document = {
143+
const template: OpenAPIV3_1.Document = {
50144
openapi: '3.1.0',
51145
info: {
52146
title: `${config.output.split('/').at(-1)?.replace('.json', '')} api`,
@@ -55,95 +149,8 @@ type AllMethods = [${paths.map((_, i) => `Methods${i}`).join(', ')}]`;
55149
servers: config.baseURL ? [{ url: config.baseURL }] : undefined,
56150
...existingDoc,
57151
paths: {},
58-
components: { schemas: schema?.definitions as any },
152+
components: {},
59153
};
60154

61-
unlinkSync(typeFilePath);
62-
63-
(schema?.items as TJS.Definition[])?.forEach((def, i) => {
64-
const parameters: { name: string; in: 'path' | 'query'; required: boolean; schema: any }[] =
65-
[];
66-
67-
let path = paths[i];
68-
69-
if (path.includes('/_')) {
70-
parameters.push(
71-
...path
72-
.split('/')
73-
.filter((p) => p.startsWith('_'))
74-
.map((p) => ({
75-
name: p.slice(1).split('@')[0],
76-
in: 'path' as const,
77-
required: true,
78-
schema: ['number', 'string'].includes(p.slice(1).split('@')[1])
79-
? { type: p.slice(1).split('@')[1] }
80-
: { anyOf: [{ type: 'number' }, { type: 'string' }] },
81-
})),
82-
);
83-
84-
path = path.replace(/\/_([^/@]+)(@[^/]+)?/g, '/{$1}');
85-
}
86-
87-
path = path.replace(config.input, '') || '/';
88-
89-
doc.paths![path] = Object.entries(def.properties!).reduce((dict, [method, val]) => {
90-
const params = [...parameters];
91-
// const required = ((val as TJS.Definition).required ?? []) as (keyof AspidaMethodParams)[];
92-
const props = (val as TJS.Definition).properties as {
93-
[Key in keyof AspidaMethodParams]: TJS.Definition;
94-
};
95-
96-
if (props.query) {
97-
const def = (props.query.properties ??
98-
schema?.definitions?.[props.query.$ref!.split('/').at(-1)!]) as TJS.Definition;
99-
100-
params.push(
101-
...Object.entries(def).map(([name, value]) => ({
102-
name,
103-
in: 'query' as const,
104-
required: props.query?.required?.includes(name) ?? false,
105-
schema: value,
106-
})),
107-
);
108-
}
109-
110-
const reqFormat = props.reqFormat?.$ref;
111-
const reqContentType =
112-
((props.reqHeaders?.properties?.['content-type'] as TJS.Definition)?.const ??
113-
reqFormat?.includes('FormData'))
114-
? 'multipart/form-data'
115-
: reqFormat?.includes('URLSearchParams')
116-
? 'application/x-www-form-urlencoded'
117-
: 'application/json';
118-
const resContentType =
119-
((props.resHeaders?.properties?.['content-type'] as TJS.Definition)?.const as string) ??
120-
'application/json';
121-
122-
return {
123-
...dict,
124-
[method]: {
125-
tags: path === '/' ? undefined : path.split('/{')[0].replace(/^\//, '').split('/'),
126-
parameters: params,
127-
requestBody:
128-
props.reqBody === undefined
129-
? undefined
130-
: { content: { [reqContentType]: { schema: props.reqBody } } },
131-
responses:
132-
props.resBody === undefined
133-
? undefined
134-
: {
135-
[(props.status?.const as string) ?? '2XX']: {
136-
content: { [resContentType]: { schema: props.resBody } },
137-
},
138-
},
139-
},
140-
};
141-
}, {});
142-
});
143-
144-
writeFileSync(
145-
config.output,
146-
JSON.stringify(doc, null, 2).replaceAll('#/definitions', '#/components/schemas'),
147-
'utf8',
148-
);
155+
writeFileSync(config.output, toOpenAPI({ input: config.input, template }), 'utf8');
149156
});

0 commit comments

Comments
 (0)