next-openapi-gen 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,250 +1,295 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { parse } from "@babel/parser";
4
- import traverse from "@babel/traverse";
5
- import * as t from "@babel/types";
6
- export class SchemaProcessor {
7
- schemaDir;
8
- typeDefinitions = {};
9
- openapiDefinitions = {};
10
- contentType = "";
11
- constructor(schemaDir) {
12
- this.schemaDir = path.resolve(schemaDir);
13
- }
14
- findSchemaDefinition(schemaName, contentType) {
15
- let schemaNode = null;
16
- // assign type that is actually processed
17
- this.contentType = contentType;
18
- this.scanSchemaDir(this.schemaDir, schemaName);
19
- return schemaNode;
20
- }
21
- scanSchemaDir(dir, schemaName) {
22
- const files = fs.readdirSync(dir);
23
- files.forEach((file) => {
24
- const filePath = path.join(dir, file);
25
- const stat = fs.statSync(filePath);
26
- if (stat.isDirectory()) {
27
- this.scanSchemaDir(filePath, schemaName);
28
- }
29
- else if (file.endsWith(".ts")) {
30
- this.processSchemaFile(filePath, schemaName);
31
- }
32
- });
33
- }
34
- collectTypeDefinitions(ast, schemaName) {
35
- traverse.default(ast, {
36
- VariableDeclarator: (path) => {
37
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
38
- const name = path.node.id.name;
39
- this.typeDefinitions[name] = path.node.init || path.node;
40
- }
41
- },
42
- TSTypeAliasDeclaration: (path) => {
43
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
44
- const name = path.node.id.name;
45
- this.typeDefinitions[name] = path.node.typeAnnotation;
46
- }
47
- },
48
- TSInterfaceDeclaration: (path) => {
49
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
50
- const name = path.node.id.name;
51
- this.typeDefinitions[name] = path.node;
52
- }
53
- },
54
- TSEnumDeclaration: (path) => {
55
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
56
- const name = path.node.id.name;
57
- this.typeDefinitions[name] = path.node;
58
- }
59
- },
60
- });
61
- }
62
- resolveType(typeName) {
63
- const typeNode = this.typeDefinitions[typeName.toString()];
64
- if (!typeNode)
65
- return {};
66
- if (t.isTSEnumDeclaration(typeNode)) {
67
- const enumValues = this.processEnum(typeNode);
68
- return enumValues;
69
- }
70
- if (t.isTSTypeLiteral(typeNode) || t.isTSInterfaceBody(typeNode)) {
71
- const properties = {};
72
- if ("members" in typeNode) {
73
- (typeNode.members || []).forEach((member) => {
74
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
75
- const propName = member.key.name;
76
- const options = this.getPropertyOptions(member);
77
- const property = {
78
- ...this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation),
79
- ...options,
80
- };
81
- properties[propName] = property;
82
- }
83
- });
84
- }
85
- return { type: "object", properties };
86
- }
87
- if (t.isTSArrayType(typeNode)) {
88
- return {
89
- type: "array",
90
- items: this.resolveTSNodeType(typeNode.elementType),
91
- };
92
- }
93
- return {};
94
- }
95
- resolveTSNodeType(node) {
96
- if (t.isTSStringKeyword(node))
97
- return { type: "string" };
98
- if (t.isTSNumberKeyword(node))
99
- return { type: "number" };
100
- if (t.isTSBooleanKeyword(node))
101
- return { type: "boolean" };
102
- if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
103
- const typeName = node.typeName.name;
104
- // Find type definition
105
- this.findSchemaDefinition(typeName, this.contentType);
106
- return this.resolveType(node.typeName.name);
107
- }
108
- if (t.isTSArrayType(node)) {
109
- return {
110
- type: "array",
111
- items: this.resolveTSNodeType(node.elementType),
112
- };
113
- }
114
- if (t.isTSTypeLiteral(node)) {
115
- const properties = {};
116
- node.members.forEach((member) => {
117
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
118
- const propName = member.key.name;
119
- properties[propName] = this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation);
120
- }
121
- });
122
- return { type: "object", properties };
123
- }
124
- if (t.isTSUnionType(node)) {
125
- return {
126
- anyOf: node.types.map((subNode) => this.resolveTSNodeType(subNode)),
127
- };
128
- }
129
- // case where a type is a reference to another defined type
130
- if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
131
- return { $ref: `#/components/schemas/${node.typeName.name}` };
132
- }
133
- console.warn("Unrecognized TypeScript type node:", node);
134
- return {};
135
- }
136
- processSchemaFile(filePath, schemaName) {
137
- // Recognizes different elements of TS like variable, type, interface, enum
138
- const content = fs.readFileSync(filePath, "utf-8");
139
- const ast = parse(content, {
140
- sourceType: "module",
141
- plugins: ["typescript", "decorators-legacy"],
142
- });
143
- this.collectTypeDefinitions(ast, schemaName);
144
- const definition = this.resolveType(schemaName);
145
- this.openapiDefinitions[schemaName] = definition;
146
- return definition;
147
- }
148
- processEnum(enumNode) {
149
- // Initialization OpenAPI enum object
150
- const enumSchema = {
151
- type: "string",
152
- enum: [],
153
- };
154
- // Iterate throught enum members
155
- enumNode.members.forEach((member) => {
156
- if (t.isTSEnumMember(member)) {
157
- // @ts-ignore
158
- const name = member.id?.name;
159
- // @ts-ignore
160
- const value = member.initializer?.value;
161
- let type = member.initializer?.type;
162
- if (type === "NumericLiteral") {
163
- enumSchema.type = "number";
164
- }
165
- const targetValue = value || name;
166
- enumSchema.enum.push(targetValue);
167
- }
168
- });
169
- return enumSchema;
170
- }
171
- getPropertyOptions(node) {
172
- const key = node.key.name;
173
- const isOptional = !!node.optional; // check if property is optional
174
- const typeName = node.typeAnnotation?.typeAnnotation?.typeName?.name;
175
- let description = null;
176
- // get comments for field
177
- if (node.trailingComments && node.trailingComments.length) {
178
- description = node.trailingComments[0].value.trim(); // get first comment
179
- }
180
- const options = {};
181
- if (description) {
182
- options.description = description;
183
- }
184
- if (this.contentType === "params") {
185
- options.required = !isOptional;
186
- }
187
- else if (this.contentType === "body") {
188
- options.nullable = isOptional;
189
- }
190
- return options;
191
- }
192
- createRequestParamsSchema(params) {
193
- const queryParams = [];
194
- if (params.properties) {
195
- for (let [name, value] of Object.entries(params.properties)) {
196
- const param = {
197
- in: "query",
198
- name,
199
- schema: {
200
- type: value.type,
201
- },
202
- required: value.required,
203
- };
204
- if (value.enum) {
205
- param.schema.enum = value.enum;
206
- }
207
- if (value.description) {
208
- param.description = value.description;
209
- param.schema.description = value.description;
210
- }
211
- queryParams.push(param);
212
- }
213
- }
214
- return queryParams;
215
- }
216
- createRequestBodySchema(body) {
217
- return {
218
- content: {
219
- "application/json": {
220
- schema: body,
221
- },
222
- },
223
- };
224
- }
225
- createResponseSchema(responses) {
226
- return {
227
- 200: {
228
- description: "Successful response",
229
- content: {
230
- "application/json": {
231
- schema: responses,
232
- },
233
- },
234
- },
235
- };
236
- }
237
- getSchemaContent({ paramsType, bodyType, responseType }) {
238
- this.findSchemaDefinition(paramsType, "params");
239
- this.findSchemaDefinition(bodyType, "body");
240
- this.findSchemaDefinition(responseType, "response");
241
- const params = this.openapiDefinitions[paramsType];
242
- const body = this.openapiDefinitions[bodyType];
243
- const responses = this.openapiDefinitions[responseType];
244
- return {
245
- params,
246
- body,
247
- responses,
248
- };
249
- }
250
- }
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { parse } from "@babel/parser";
4
+ import traverse from "@babel/traverse";
5
+ import * as t from "@babel/types";
6
+ export class SchemaProcessor {
7
+ schemaDir;
8
+ typeDefinitions = {};
9
+ openapiDefinitions = {};
10
+ contentType = "";
11
+ directoryCache = {};
12
+ statCache = {};
13
+ processSchemaTracker = {};
14
+ constructor(schemaDir) {
15
+ this.schemaDir = path.resolve(schemaDir);
16
+ }
17
+ findSchemaDefinition(schemaName, contentType) {
18
+ let schemaNode = null;
19
+ // assign type that is actually processed
20
+ this.contentType = contentType;
21
+ this.scanSchemaDir(this.schemaDir, schemaName);
22
+ return schemaNode;
23
+ }
24
+ scanSchemaDir(dir, schemaName) {
25
+ let files = this.directoryCache[dir];
26
+ if (typeof files === "undefined") {
27
+ files = fs.readdirSync(dir);
28
+ this.directoryCache[dir] = files;
29
+ }
30
+ files.forEach((file) => {
31
+ const filePath = path.join(dir, file);
32
+ let stat = this.statCache[filePath];
33
+ if (typeof stat === "undefined") {
34
+ stat = fs.statSync(filePath);
35
+ this.statCache[filePath] = stat;
36
+ }
37
+ if (stat.isDirectory()) {
38
+ this.scanSchemaDir(filePath, schemaName);
39
+ }
40
+ else if (file.endsWith(".ts")) {
41
+ this.processSchemaFile(filePath, schemaName);
42
+ }
43
+ });
44
+ }
45
+ collectTypeDefinitions(ast, schemaName) {
46
+ traverse.default(ast, {
47
+ VariableDeclarator: (path) => {
48
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
49
+ const name = path.node.id.name;
50
+ this.typeDefinitions[name] = path.node.init || path.node;
51
+ }
52
+ },
53
+ TSTypeAliasDeclaration: (path) => {
54
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
55
+ const name = path.node.id.name;
56
+ this.typeDefinitions[name] = path.node.typeAnnotation;
57
+ }
58
+ },
59
+ TSInterfaceDeclaration: (path) => {
60
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
61
+ const name = path.node.id.name;
62
+ this.typeDefinitions[name] = path.node;
63
+ }
64
+ },
65
+ TSEnumDeclaration: (path) => {
66
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
67
+ const name = path.node.id.name;
68
+ this.typeDefinitions[name] = path.node;
69
+ }
70
+ },
71
+ });
72
+ }
73
+ resolveType(typeName) {
74
+ const typeNode = this.typeDefinitions[typeName.toString()];
75
+ if (!typeNode)
76
+ return {};
77
+ if (t.isTSEnumDeclaration(typeNode)) {
78
+ const enumValues = this.processEnum(typeNode);
79
+ return enumValues;
80
+ }
81
+ if (t.isTSTypeLiteral(typeNode) || t.isTSInterfaceBody(typeNode)) {
82
+ const properties = {};
83
+ if ("members" in typeNode) {
84
+ (typeNode.members || []).forEach((member) => {
85
+ if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
86
+ const propName = member.key.name;
87
+ const options = this.getPropertyOptions(member);
88
+ const property = {
89
+ ...this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation),
90
+ ...options,
91
+ };
92
+ properties[propName] = property;
93
+ }
94
+ });
95
+ }
96
+ return { type: "object", properties };
97
+ }
98
+ if (t.isTSArrayType(typeNode)) {
99
+ return {
100
+ type: "array",
101
+ items: this.resolveTSNodeType(typeNode.elementType),
102
+ };
103
+ }
104
+ return {};
105
+ }
106
+ isDateString(node) {
107
+ if (t.isStringLiteral(node)) {
108
+ const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z)?$/;
109
+ return dateRegex.test(node.value);
110
+ }
111
+ return false;
112
+ }
113
+ isDateObject(node) {
114
+ return t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" });
115
+ }
116
+ isDateNode(node) {
117
+ return this.isDateString(node) || this.isDateObject(node);
118
+ }
119
+ resolveTSNodeType(node) {
120
+ if (t.isTSStringKeyword(node))
121
+ return { type: "string" };
122
+ if (t.isTSNumberKeyword(node))
123
+ return { type: "number" };
124
+ if (t.isTSBooleanKeyword(node))
125
+ return { type: "boolean" };
126
+ if (this.isDateNode(node))
127
+ return { type: "Date" };
128
+ if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
129
+ const typeName = node.typeName.name;
130
+ // Find type definition
131
+ this.findSchemaDefinition(typeName, this.contentType);
132
+ return this.resolveType(node.typeName.name);
133
+ }
134
+ if (t.isTSArrayType(node)) {
135
+ return {
136
+ type: "array",
137
+ items: this.resolveTSNodeType(node.elementType),
138
+ };
139
+ }
140
+ if (t.isTSTypeLiteral(node)) {
141
+ const properties = {};
142
+ node.members.forEach((member) => {
143
+ if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
144
+ const propName = member.key.name;
145
+ properties[propName] = this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation);
146
+ }
147
+ });
148
+ return { type: "object", properties };
149
+ }
150
+ if (t.isTSUnionType(node)) {
151
+ return {
152
+ anyOf: node.types.map((subNode) => this.resolveTSNodeType(subNode)),
153
+ };
154
+ }
155
+ // case where a type is a reference to another defined type
156
+ if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
157
+ return { $ref: `#/components/schemas/${node.typeName.name}` };
158
+ }
159
+ console.warn("Unrecognized TypeScript type node:", node);
160
+ return {};
161
+ }
162
+ processSchemaFile(filePath, schemaName) {
163
+ // Check if the file has already been processed
164
+ if (this.processSchemaTracker[`${filePath}-${schemaName}`])
165
+ return;
166
+ // Recognizes different elements of TS like variable, type, interface, enum
167
+ const content = fs.readFileSync(filePath, "utf-8");
168
+ const ast = parse(content, {
169
+ sourceType: "module",
170
+ plugins: ["typescript", "decorators-legacy"],
171
+ });
172
+ this.collectTypeDefinitions(ast, schemaName);
173
+ const definition = this.resolveType(schemaName);
174
+ this.openapiDefinitions[schemaName] = definition;
175
+ this.processSchemaTracker[`${filePath}-${schemaName}`] = true;
176
+ return definition;
177
+ }
178
+ processEnum(enumNode) {
179
+ // Initialization OpenAPI enum object
180
+ const enumSchema = {
181
+ type: "string",
182
+ enum: [],
183
+ };
184
+ // Iterate throught enum members
185
+ enumNode.members.forEach((member) => {
186
+ if (t.isTSEnumMember(member)) {
187
+ // @ts-ignore
188
+ const name = member.id?.name;
189
+ // @ts-ignore
190
+ const value = member.initializer?.value;
191
+ let type = member.initializer?.type;
192
+ if (type === "NumericLiteral") {
193
+ enumSchema.type = "number";
194
+ }
195
+ const targetValue = value || name;
196
+ enumSchema.enum.push(targetValue);
197
+ }
198
+ });
199
+ return enumSchema;
200
+ }
201
+ getPropertyOptions(node) {
202
+ const key = node.key.name;
203
+ const isOptional = !!node.optional; // check if property is optional
204
+ const typeName = node.typeAnnotation?.typeAnnotation?.typeName?.name;
205
+ let description = null;
206
+ // get comments for field
207
+ if (node.trailingComments && node.trailingComments.length) {
208
+ description = node.trailingComments[0].value.trim(); // get first comment
209
+ }
210
+ const options = {};
211
+ if (description) {
212
+ options.description = description;
213
+ }
214
+ if (this.contentType === "params") {
215
+ options.required = !isOptional;
216
+ }
217
+ else if (this.contentType === "body") {
218
+ options.nullable = isOptional;
219
+ }
220
+ return options;
221
+ }
222
+ createRequestParamsSchema(params, isPathParam = false) {
223
+ const queryParams = [];
224
+ if (params.properties) {
225
+ for (let [name, value] of Object.entries(params.properties)) {
226
+ const param = {
227
+ in: isPathParam ? "path" : "query",
228
+ name,
229
+ schema: {
230
+ type: value.type,
231
+ },
232
+ required: value.required,
233
+ };
234
+ if (value.enum) {
235
+ param.schema.enum = value.enum;
236
+ }
237
+ if (value.description) {
238
+ param.description = value.description;
239
+ param.schema.description = value.description;
240
+ }
241
+ queryParams.push(param);
242
+ }
243
+ }
244
+ return queryParams;
245
+ }
246
+ createRequestBodySchema(body) {
247
+ return {
248
+ content: {
249
+ "application/json": {
250
+ schema: body,
251
+ },
252
+ },
253
+ };
254
+ }
255
+ createResponseSchema(responses) {
256
+ return {
257
+ 200: {
258
+ description: "Successful response",
259
+ content: {
260
+ "application/json": {
261
+ schema: responses,
262
+ },
263
+ },
264
+ },
265
+ };
266
+ }
267
+ getSchemaContent({ paramsType, pathParamsType, bodyType, responseType }) {
268
+ let params = this.openapiDefinitions[paramsType];
269
+ let pathParams = this.openapiDefinitions[pathParamsType];
270
+ let body = this.openapiDefinitions[bodyType];
271
+ let responses = this.openapiDefinitions[responseType];
272
+ if (paramsType && !params) {
273
+ this.findSchemaDefinition(paramsType, "params");
274
+ params = this.openapiDefinitions[paramsType];
275
+ }
276
+ if (pathParamsType && !pathParams) {
277
+ this.findSchemaDefinition(pathParamsType, "pathParams");
278
+ pathParams = this.openapiDefinitions[pathParamsType];
279
+ }
280
+ if (bodyType && !body) {
281
+ this.findSchemaDefinition(bodyType, "body");
282
+ body = this.openapiDefinitions[bodyType];
283
+ }
284
+ if (responseType && !responses) {
285
+ this.findSchemaDefinition(responseType, "response");
286
+ responses = this.openapiDefinitions[responseType];
287
+ }
288
+ return {
289
+ params,
290
+ pathParams,
291
+ body,
292
+ responses,
293
+ };
294
+ }
295
+ }