swagshot 0.1.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.
- package/README.md +182 -0
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +34 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +9 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +74 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +6 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +26 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +40 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/codeGenerator.d.ts +6 -0
- package/dist/lib/codeGenerator.d.ts.map +1 -0
- package/dist/lib/codeGenerator.js +331 -0
- package/dist/lib/codeGenerator.js.map +1 -0
- package/dist/lib/configManager.d.ts +8 -0
- package/dist/lib/configManager.d.ts.map +1 -0
- package/dist/lib/configManager.js +59 -0
- package/dist/lib/configManager.js.map +1 -0
- package/dist/lib/detectStructure.d.ts +3 -0
- package/dist/lib/detectStructure.d.ts.map +1 -0
- package/dist/lib/detectStructure.js +65 -0
- package/dist/lib/detectStructure.js.map +1 -0
- package/dist/lib/fetchSwagger.d.ts +5 -0
- package/dist/lib/fetchSwagger.d.ts.map +1 -0
- package/dist/lib/fetchSwagger.js +57 -0
- package/dist/lib/fetchSwagger.js.map +1 -0
- package/dist/lib/types.d.ts +101 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +3 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/tools/configUpdate.d.ts +2 -0
- package/dist/tools/configUpdate.d.ts.map +1 -0
- package/dist/tools/configUpdate.js +25 -0
- package/dist/tools/configUpdate.js.map +1 -0
- package/dist/tools/setup.d.ts +2 -0
- package/dist/tools/setup.d.ts.map +1 -0
- package/dist/tools/setup.js +63 -0
- package/dist/tools/setup.js.map +1 -0
- package/dist/tools/sync.d.ts +2 -0
- package/dist/tools/sync.d.ts.map +1 -0
- package/dist/tools/sync.js +87 -0
- package/dist/tools/sync.js.map +1 -0
- package/package.json +39 -0
- package/src/cli/commands/config.ts +33 -0
- package/src/cli/commands/generate.ts +90 -0
- package/src/cli/commands/init.ts +76 -0
- package/src/cli/commands/list.ts +31 -0
- package/src/cli/index.ts +44 -0
- package/src/lib/codeGenerator.ts +376 -0
- package/src/lib/configManager.ts +54 -0
- package/src/lib/detectStructure.ts +66 -0
- package/src/lib/fetchSwagger.ts +52 -0
- package/src/lib/types.ts +102 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { SwaggerSpec, SchemaObject, Operation, Parameter } from "./types.js";
|
|
2
|
+
import { SwagShotConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// ── Schema → TypeScript type ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function schemaToTS(
|
|
7
|
+
schema: SchemaObject,
|
|
8
|
+
schemas: Record<string, SchemaObject> = {},
|
|
9
|
+
indent = 0
|
|
10
|
+
): string {
|
|
11
|
+
if (schema.$ref) {
|
|
12
|
+
const name = schema.$ref.split("/").pop()!;
|
|
13
|
+
return toPascalCase(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (schema.allOf) {
|
|
17
|
+
return schema.allOf.map((s) => schemaToTS(s, schemas, indent)).join(" & ");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (schema.oneOf || schema.anyOf) {
|
|
21
|
+
const arr = schema.oneOf ?? schema.anyOf!;
|
|
22
|
+
return arr.map((s) => schemaToTS(s, schemas, indent)).join(" | ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (schema.enum) {
|
|
26
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
switch (schema.type) {
|
|
30
|
+
case "string":
|
|
31
|
+
return "string";
|
|
32
|
+
case "number":
|
|
33
|
+
case "integer":
|
|
34
|
+
return "number";
|
|
35
|
+
case "boolean":
|
|
36
|
+
return "boolean";
|
|
37
|
+
case "array":
|
|
38
|
+
return schema.items ? `${schemaToTS(schema.items, schemas, indent)}[]` : "unknown[]";
|
|
39
|
+
case "object": {
|
|
40
|
+
if (!schema.properties) {
|
|
41
|
+
if (schema.additionalProperties) {
|
|
42
|
+
const valType =
|
|
43
|
+
typeof schema.additionalProperties === "object"
|
|
44
|
+
? schemaToTS(schema.additionalProperties, schemas, indent)
|
|
45
|
+
: "unknown";
|
|
46
|
+
return `Record<string, ${valType}>`;
|
|
47
|
+
}
|
|
48
|
+
return "Record<string, unknown>";
|
|
49
|
+
}
|
|
50
|
+
const pad = " ".repeat(indent + 1);
|
|
51
|
+
const required = new Set(schema.required ?? []);
|
|
52
|
+
const props = Object.entries(schema.properties)
|
|
53
|
+
.map(([k, v]) => {
|
|
54
|
+
const optional = !required.has(k) ? "?" : "";
|
|
55
|
+
const nullable = v.nullable ? " | null" : "";
|
|
56
|
+
return `${pad}${k}${optional}: ${schemaToTS(v, schemas, indent + 1)}${nullable};`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
return `{\n${props}\n${" ".repeat(indent)}}`;
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Swagger 2.0은 definitions, OpenAPI 3.0은 components.schemas
|
|
67
|
+
function getSchemas(spec: SwaggerSpec): Record<string, SchemaObject> {
|
|
68
|
+
return spec.components?.schemas ?? spec.definitions ?? {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function generateTypes(spec: SwaggerSpec, tag: string): string {
|
|
72
|
+
const schemas = getSchemas(spec);
|
|
73
|
+
const lines: string[] = [
|
|
74
|
+
`// Auto-generated by swagshot`,
|
|
75
|
+
`// Tag: ${tag}`,
|
|
76
|
+
`// Do not edit manually`,
|
|
77
|
+
"",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Collect referenced schemas from this tag's paths
|
|
81
|
+
const referencedSchemas = collectReferencedSchemas(spec, tag);
|
|
82
|
+
|
|
83
|
+
for (const schemaName of referencedSchemas) {
|
|
84
|
+
const schema = schemas[schemaName];
|
|
85
|
+
if (!schema) continue;
|
|
86
|
+
|
|
87
|
+
lines.push(`export interface ${toPascalCase(schemaName)} ${schemaToTS(schema, schemas)}`);
|
|
88
|
+
lines.push("");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Generate request/response types per operation
|
|
92
|
+
for (const [, pathItem] of Object.entries(spec.paths)) {
|
|
93
|
+
for (const [, op] of Object.entries(pathItem)) {
|
|
94
|
+
if (!op?.tags?.includes(tag) || !op.operationId) continue;
|
|
95
|
+
|
|
96
|
+
// Query params type
|
|
97
|
+
const queryParams = (op.parameters ?? []).filter((p: Parameter) => p.in === "query");
|
|
98
|
+
if (queryParams.length > 0) {
|
|
99
|
+
const typeName = `${toPascalCase(op.operationId)}Params`;
|
|
100
|
+
const props = queryParams
|
|
101
|
+
.map((p: Parameter) => {
|
|
102
|
+
const optional = !p.required ? "?" : "";
|
|
103
|
+
const tsType = schemaToTS(paramToSchema(p), schemas);
|
|
104
|
+
return ` ${p.name}${optional}: ${tsType};`;
|
|
105
|
+
})
|
|
106
|
+
.join("\n");
|
|
107
|
+
lines.push(`export interface ${typeName} {`);
|
|
108
|
+
lines.push(props);
|
|
109
|
+
lines.push("}");
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Inline response type (응답 스키마가 $ref 없는 inline object일 때)
|
|
114
|
+
const okRes = op.responses["200"] ?? op.responses["201"];
|
|
115
|
+
if (okRes) {
|
|
116
|
+
const resSchema = getResponseSchema(okRes);
|
|
117
|
+
if (resSchema && !resSchema.$ref) {
|
|
118
|
+
const typeName = `${toPascalCase(op.operationId)}Response`;
|
|
119
|
+
lines.push(`export interface ${typeName} ${schemaToTS(resSchema, schemas)}`);
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Inline request body type (Swagger 2.0 body param 또는 OpenAPI 3.0 inline requestBody)
|
|
125
|
+
const bodyType = extractBodyType(op, schemas);
|
|
126
|
+
if (bodyType && bodyType.startsWith("{")) {
|
|
127
|
+
// inline object → named interface
|
|
128
|
+
const typeName = `${toPascalCase(op.operationId)}Request`;
|
|
129
|
+
lines.push(`export interface ${typeName} ${bodyType}`);
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── API function generator ────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export function generateApiFile(
|
|
141
|
+
spec: SwaggerSpec,
|
|
142
|
+
tag: string,
|
|
143
|
+
config: SwagShotConfig
|
|
144
|
+
): string {
|
|
145
|
+
const schemas = getSchemas(spec);
|
|
146
|
+
const { httpClient, axiosInstance, queryLibrary } = config.style;
|
|
147
|
+
const tagCamel = toCamelCase(tag.replace(/-controller$/, "").replace(/-/g, " "));
|
|
148
|
+
const typesImportPath = `../types/${tagCamel}`;
|
|
149
|
+
|
|
150
|
+
// Suppress unused variable warning for queryLibrary
|
|
151
|
+
void queryLibrary;
|
|
152
|
+
|
|
153
|
+
const lines: string[] = [
|
|
154
|
+
`// Auto-generated by swagshot`,
|
|
155
|
+
`// Tag: ${tag}`,
|
|
156
|
+
`// Do not edit manually`,
|
|
157
|
+
"",
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
// Imports
|
|
161
|
+
if (httpClient === "axios" && axiosInstance) {
|
|
162
|
+
lines.push(`import api from '${axiosInstance.replace(/\.ts$/, "")}';`);
|
|
163
|
+
} else if (httpClient === "axios") {
|
|
164
|
+
lines.push(`import axios from 'axios';`);
|
|
165
|
+
lines.push(`const api = axios.create({ baseURL: '' });`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Collect type imports
|
|
169
|
+
const typeImports = new Set<string>();
|
|
170
|
+
for (const [, pathItem] of Object.entries(spec.paths)) {
|
|
171
|
+
for (const [, op] of Object.entries(pathItem)) {
|
|
172
|
+
if (!op?.tags?.includes(tag) || !op.operationId) continue;
|
|
173
|
+
const queryParams = (op.parameters ?? []).filter((p: Parameter) => p.in === "query");
|
|
174
|
+
if (queryParams.length > 0) {
|
|
175
|
+
typeImports.add(`${toPascalCase(op.operationId)}Params`);
|
|
176
|
+
}
|
|
177
|
+
// Response type
|
|
178
|
+
const responseType = extractResponseType(op, schemas);
|
|
179
|
+
if (responseType && !isPrimitive(responseType)) {
|
|
180
|
+
typeImports.add(responseType);
|
|
181
|
+
}
|
|
182
|
+
// Request body type
|
|
183
|
+
const bodyType = extractBodyType(op, schemas);
|
|
184
|
+
if (bodyType && !isPrimitive(bodyType)) {
|
|
185
|
+
typeImports.add(bodyType);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeImports.size > 0) {
|
|
191
|
+
lines.push(`import type { ${Array.from(typeImports).join(", ")} } from '${typesImportPath}';`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
lines.push("");
|
|
195
|
+
|
|
196
|
+
// Generate functions
|
|
197
|
+
for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
|
|
198
|
+
for (const [method, op] of Object.entries(pathItem)) {
|
|
199
|
+
if (!op?.tags?.includes(tag) || !op.operationId) continue;
|
|
200
|
+
|
|
201
|
+
const fnName = toCamelCase(op.operationId);
|
|
202
|
+
const pathParams = (op.parameters ?? []).filter((p: Parameter) => p.in === "path");
|
|
203
|
+
const queryParams = (op.parameters ?? []).filter((p: Parameter) => p.in === "query");
|
|
204
|
+
const bodyType = extractBodyType(op, schemas);
|
|
205
|
+
const responseType = extractResponseType(op, schemas) ?? "void";
|
|
206
|
+
|
|
207
|
+
// Build function signature
|
|
208
|
+
const params: string[] = [];
|
|
209
|
+
for (const p of pathParams) {
|
|
210
|
+
params.push(`${p.name}: ${p.schema ? schemaToTS(p.schema, schemas) : "string"}`);
|
|
211
|
+
}
|
|
212
|
+
if (queryParams.length > 0) {
|
|
213
|
+
const paramsType = `${toPascalCase(op.operationId)}Params`;
|
|
214
|
+
params.push(`params: ${paramsType}`);
|
|
215
|
+
}
|
|
216
|
+
if (bodyType && ["post", "put", "patch"].includes(method)) {
|
|
217
|
+
params.push(`data: ${bodyType}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (op.summary) {
|
|
221
|
+
lines.push(`/** ${op.summary} */`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const url = pathKey.replace(/{(\w+)}/g, "${$1}");
|
|
225
|
+
const hasQuery = queryParams.length > 0;
|
|
226
|
+
|
|
227
|
+
if (httpClient === "axios") {
|
|
228
|
+
lines.push(
|
|
229
|
+
`export const ${fnName} = (${params.join(", ")}) =>`,
|
|
230
|
+
` api.${method}<${responseType}>(\`${url}\`${
|
|
231
|
+
hasQuery ? ", { params }" : bodyType ? ", data" : ""
|
|
232
|
+
});`
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
// fetch
|
|
236
|
+
lines.push(
|
|
237
|
+
`export const ${fnName} = async (${params.join(", ")}): Promise<${responseType}> => {`,
|
|
238
|
+
` const url = new URL(\`${url}\`, window.location.origin);`
|
|
239
|
+
);
|
|
240
|
+
if (hasQuery) {
|
|
241
|
+
lines.push(
|
|
242
|
+
` Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, String(v)));`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
lines.push(
|
|
246
|
+
` const res = await fetch(url.toString()${
|
|
247
|
+
bodyType ? `, { method: '${method.toUpperCase()}', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }` : ""
|
|
248
|
+
});`,
|
|
249
|
+
` return res.json();`,
|
|
250
|
+
`};`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
lines.push("");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/** Swagger 2.0 파라미터는 schema 없이 type/items를 직접 가짐 → 통일된 SchemaObject로 변환 */
|
|
264
|
+
function paramToSchema(p: Parameter): SchemaObject {
|
|
265
|
+
if (p.schema) return p.schema; // OpenAPI 3.0
|
|
266
|
+
return {
|
|
267
|
+
type: p.type,
|
|
268
|
+
items: p.items,
|
|
269
|
+
enum: p.enum,
|
|
270
|
+
format: p.format,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Swagger 2.0 / OpenAPI 3.0 응답 스키마 추출 */
|
|
275
|
+
function getResponseSchema(res: { schema?: SchemaObject; content?: Record<string, { schema?: SchemaObject }> }): SchemaObject | null {
|
|
276
|
+
if (res.content) return res.content["application/json"]?.schema ?? null; // OpenAPI 3.0
|
|
277
|
+
return res.schema ?? null; // Swagger 2.0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function toPascalCase(str: string): string {
|
|
281
|
+
return str
|
|
282
|
+
.replace(/[-_\s]+(.)/g, (_, c: string) => c.toUpperCase())
|
|
283
|
+
.replace(/^(.)/, (c: string) => c.toUpperCase());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function toCamelCase(str: string): string {
|
|
287
|
+
const pascal = toPascalCase(str);
|
|
288
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isPrimitive(type: string): boolean {
|
|
292
|
+
return ["string", "number", "boolean", "void", "unknown"].includes(type);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function extractResponseType(op: Operation, schemas: Record<string, SchemaObject>): string | null {
|
|
296
|
+
const ok = op.responses["200"] ?? op.responses["201"];
|
|
297
|
+
if (!ok) return null;
|
|
298
|
+
const schema = getResponseSchema(ok);
|
|
299
|
+
if (!schema) return null;
|
|
300
|
+
if (schema.$ref) return toPascalCase(schema.$ref.split("/").pop()!);
|
|
301
|
+
if (schema.type === "array" && schema.items?.$ref) {
|
|
302
|
+
return `${toPascalCase(schema.items.$ref.split("/").pop()!)}[]`;
|
|
303
|
+
}
|
|
304
|
+
if (schema.type === "object" || schema.properties) {
|
|
305
|
+
// inline object → use named response type
|
|
306
|
+
return `${toPascalCase(op.operationId ?? "unknown")}Response`;
|
|
307
|
+
}
|
|
308
|
+
return schemaToTS(schema, schemas);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function extractBodyType(op: Operation, schemas: Record<string, SchemaObject>): string | null {
|
|
312
|
+
// OpenAPI 3.0
|
|
313
|
+
if (op.requestBody?.content) {
|
|
314
|
+
const schema = op.requestBody.content["application/json"]?.schema;
|
|
315
|
+
if (!schema) return null;
|
|
316
|
+
if (schema.$ref) return toPascalCase(schema.$ref.split("/").pop()!);
|
|
317
|
+
return schemaToTS(schema, schemas);
|
|
318
|
+
}
|
|
319
|
+
// Swagger 2.0: body parameter
|
|
320
|
+
const bodyParam = (op.parameters ?? []).find((p: Parameter) => p.in === "body");
|
|
321
|
+
if (bodyParam?.schema) {
|
|
322
|
+
if (bodyParam.schema.$ref) return toPascalCase(bodyParam.schema.$ref.split("/").pop()!);
|
|
323
|
+
return schemaToTS(bodyParam.schema, schemas);
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function collectReferencedSchemas(spec: SwaggerSpec, tag: string): string[] {
|
|
329
|
+
const refs = new Set<string>();
|
|
330
|
+
const allSchemas = getSchemas(spec);
|
|
331
|
+
|
|
332
|
+
function collectFromSchema(schema: SchemaObject): void {
|
|
333
|
+
if (schema.$ref) {
|
|
334
|
+
const name = schema.$ref.split("/").pop()!;
|
|
335
|
+
if (refs.has(name)) return; // 순환 참조 방지
|
|
336
|
+
refs.add(name);
|
|
337
|
+
// Recurse into the referenced schema
|
|
338
|
+
const nested = allSchemas[name];
|
|
339
|
+
if (nested) collectFromSchema(nested);
|
|
340
|
+
}
|
|
341
|
+
if (schema.properties) {
|
|
342
|
+
Object.values(schema.properties).forEach(collectFromSchema);
|
|
343
|
+
}
|
|
344
|
+
if (schema.items) collectFromSchema(schema.items);
|
|
345
|
+
if (schema.allOf) schema.allOf.forEach(collectFromSchema);
|
|
346
|
+
if (schema.oneOf) schema.oneOf.forEach(collectFromSchema);
|
|
347
|
+
if (schema.anyOf) schema.anyOf.forEach(collectFromSchema);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const [, pathItem] of Object.entries(spec.paths)) {
|
|
351
|
+
for (const [, op] of Object.entries(pathItem)) {
|
|
352
|
+
if (!op?.tags?.includes(tag)) continue;
|
|
353
|
+
|
|
354
|
+
// Check parameters (OpenAPI 3.0: schema / Swagger 2.0: direct fields)
|
|
355
|
+
for (const p of op.parameters ?? []) {
|
|
356
|
+
collectFromSchema(paramToSchema(p));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Check request body (OpenAPI 3.0)
|
|
360
|
+
if (op.requestBody?.content) {
|
|
361
|
+
for (const mediaType of Object.values(op.requestBody.content)) {
|
|
362
|
+
const mt = mediaType as { schema?: SchemaObject };
|
|
363
|
+
if (mt.schema) collectFromSchema(mt.schema);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Check responses (OpenAPI 3.0 + Swagger 2.0)
|
|
368
|
+
for (const responseValue of Object.values(op.responses)) {
|
|
369
|
+
const schema = getResponseSchema(responseValue as { schema?: SchemaObject; content?: Record<string, { schema?: SchemaObject }> });
|
|
370
|
+
if (schema) collectFromSchema(schema);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return Array.from(refs);
|
|
376
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { SwagShotConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_FILE = ".swagshot.json";
|
|
6
|
+
|
|
7
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(filePath);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function dirExists(dirPath: string): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
const stat = await fs.stat(dirPath);
|
|
19
|
+
return stat.isDirectory();
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readConfig(root: string): Promise<SwagShotConfig | null> {
|
|
26
|
+
const configPath = path.join(root, CONFIG_FILE);
|
|
27
|
+
if (!(await fileExists(configPath))) return null;
|
|
28
|
+
const raw = await fs.readFile(configPath, "utf-8");
|
|
29
|
+
return JSON.parse(raw) as SwagShotConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function writeConfig(root: string, config: SwagShotConfig): Promise<void> {
|
|
33
|
+
const configPath = path.join(root, CONFIG_FILE);
|
|
34
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function updateConfig(
|
|
38
|
+
root: string,
|
|
39
|
+
updates: Record<string, string | null>
|
|
40
|
+
): Promise<SwagShotConfig> {
|
|
41
|
+
const config = await readConfig(root);
|
|
42
|
+
if (!config) throw new Error("설정 파일이 없습니다. swagger_setup을 먼저 실행하세요.");
|
|
43
|
+
|
|
44
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
45
|
+
if (key in config.project) {
|
|
46
|
+
(config.project as Record<string, unknown>)[key] = value;
|
|
47
|
+
} else if (key in config.style) {
|
|
48
|
+
(config.style as Record<string, unknown>)[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await writeConfig(root, config);
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DetectedStructure } from "./types.js";
|
|
4
|
+
import { dirExists, fileExists } from "./configManager.js";
|
|
5
|
+
|
|
6
|
+
export async function detectProjectStructure(root: string): Promise<DetectedStructure> {
|
|
7
|
+
const candidates = {
|
|
8
|
+
apiDir: ["api", "apis", "src/api", "src/apis", "src/services", "src/lib/api", "src/fetchers"],
|
|
9
|
+
typesDir: ["types", "types/api", "src/types/api", "src/types", "src/@types", "src/models", "src/interfaces"],
|
|
10
|
+
hooksDir: ["hooks", "hooks/api", "src/hooks/api", "src/hooks/queries", "src/hooks", "src/queries"],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const detected: DetectedStructure = {
|
|
14
|
+
apiDir: null,
|
|
15
|
+
typesDir: null,
|
|
16
|
+
hooksDir: null,
|
|
17
|
+
httpClient: null,
|
|
18
|
+
queryLibrary: null,
|
|
19
|
+
axiosInstance: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (const [key, paths] of Object.entries(candidates)) {
|
|
23
|
+
for (const p of paths) {
|
|
24
|
+
if (await dirExists(path.join(root, p))) {
|
|
25
|
+
(detected as unknown as Record<string, unknown>)[key] = p;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Detect from package.json
|
|
32
|
+
const pkgPath = path.join(root, "package.json");
|
|
33
|
+
if (await fileExists(pkgPath)) {
|
|
34
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
35
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
36
|
+
|
|
37
|
+
detected.httpClient = "axios" in deps ? "axios" : "fetch";
|
|
38
|
+
detected.queryLibrary =
|
|
39
|
+
"@tanstack/react-query" in deps
|
|
40
|
+
? "react-query"
|
|
41
|
+
: "react-query" in deps
|
|
42
|
+
? "react-query"
|
|
43
|
+
: "swr" in deps
|
|
44
|
+
? "swr"
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try to find axios instance file
|
|
49
|
+
if (detected.httpClient === "axios") {
|
|
50
|
+
const axiosCandidates = [
|
|
51
|
+
"src/lib/axios.ts",
|
|
52
|
+
"src/lib/api.ts",
|
|
53
|
+
"src/utils/axios.ts",
|
|
54
|
+
"src/api/client.ts",
|
|
55
|
+
"src/config/axios.ts",
|
|
56
|
+
];
|
|
57
|
+
for (const p of axiosCandidates) {
|
|
58
|
+
if (await fileExists(path.join(root, p))) {
|
|
59
|
+
detected.axiosInstance = p;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return detected;
|
|
66
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { fileExists } from "./configManager.js";
|
|
4
|
+
import { SwaggerSpec } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export async function fetchSwaggerSpec(input: string): Promise<SwaggerSpec> {
|
|
7
|
+
// Local file
|
|
8
|
+
if (!input.startsWith("http")) {
|
|
9
|
+
if (!(await fileExists(input))) {
|
|
10
|
+
throw new Error(`파일을 찾을 수 없습니다: ${input}`);
|
|
11
|
+
}
|
|
12
|
+
const raw = await fs.readFile(input, "utf-8");
|
|
13
|
+
return JSON.parse(raw) as SwaggerSpec;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Remote URL
|
|
17
|
+
const response = await axios.get<SwaggerSpec>(input, {
|
|
18
|
+
headers: { Accept: "application/json" },
|
|
19
|
+
timeout: 10000,
|
|
20
|
+
});
|
|
21
|
+
return response.data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getControllerTags(spec: SwaggerSpec): string[] {
|
|
25
|
+
const tags = new Set<string>();
|
|
26
|
+
for (const pathItem of Object.values(spec.paths)) {
|
|
27
|
+
for (const op of Object.values(pathItem)) {
|
|
28
|
+
if (op?.tags) {
|
|
29
|
+
for (const tag of op.tags) tags.add(tag);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Array.from(tags);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function filterByTag(spec: SwaggerSpec, tag: string, includeDeprecated = false): SwaggerSpec {
|
|
37
|
+
const filteredPaths: SwaggerSpec["paths"] = {};
|
|
38
|
+
|
|
39
|
+
for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
|
|
40
|
+
const filteredItem: typeof pathItem = {};
|
|
41
|
+
for (const [method, op] of Object.entries(pathItem)) {
|
|
42
|
+
if (!op?.tags?.includes(tag)) continue;
|
|
43
|
+
if (!includeDeprecated && op.deprecated) continue;
|
|
44
|
+
(filteredItem as Record<string, unknown>)[method] = op;
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(filteredItem).length > 0) {
|
|
47
|
+
filteredPaths[pathKey] = filteredItem;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { ...spec, paths: filteredPaths };
|
|
52
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export interface SwagShotConfig {
|
|
2
|
+
version: string;
|
|
3
|
+
project: {
|
|
4
|
+
root: string;
|
|
5
|
+
apiDir: string;
|
|
6
|
+
typesDir: string;
|
|
7
|
+
hooksDir: string | null;
|
|
8
|
+
outputDir: string;
|
|
9
|
+
};
|
|
10
|
+
style: {
|
|
11
|
+
httpClient: "axios" | "fetch";
|
|
12
|
+
axiosInstance: string | null;
|
|
13
|
+
queryLibrary: "react-query" | "swr" | null;
|
|
14
|
+
namingConvention: "camelCase" | "snake_case";
|
|
15
|
+
};
|
|
16
|
+
swagger?: {
|
|
17
|
+
url: string | null;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DetectedStructure {
|
|
22
|
+
apiDir: string | null;
|
|
23
|
+
typesDir: string | null;
|
|
24
|
+
hooksDir: string | null;
|
|
25
|
+
httpClient: "axios" | "fetch" | null;
|
|
26
|
+
queryLibrary: "react-query" | "swr" | null;
|
|
27
|
+
axiosInstance: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SwaggerSpec {
|
|
31
|
+
openapi?: string;
|
|
32
|
+
swagger?: string;
|
|
33
|
+
info: { title: string; version: string };
|
|
34
|
+
paths: Record<string, PathItem>;
|
|
35
|
+
// OpenAPI 3.0
|
|
36
|
+
components?: {
|
|
37
|
+
schemas?: Record<string, SchemaObject>;
|
|
38
|
+
};
|
|
39
|
+
// Swagger 2.0 (Spring Boot / springfox)
|
|
40
|
+
definitions?: Record<string, SchemaObject>;
|
|
41
|
+
tags?: Array<{ name: string; description?: string }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PathItem {
|
|
45
|
+
get?: Operation;
|
|
46
|
+
post?: Operation;
|
|
47
|
+
put?: Operation;
|
|
48
|
+
patch?: Operation;
|
|
49
|
+
delete?: Operation;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Operation {
|
|
53
|
+
tags?: string[];
|
|
54
|
+
operationId?: string;
|
|
55
|
+
summary?: string;
|
|
56
|
+
deprecated?: boolean;
|
|
57
|
+
parameters?: Parameter[];
|
|
58
|
+
requestBody?: RequestBody;
|
|
59
|
+
responses: Record<string, Response>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Parameter {
|
|
63
|
+
name: string;
|
|
64
|
+
in: "query" | "path" | "header" | "cookie" | "body" | "formData";
|
|
65
|
+
required?: boolean;
|
|
66
|
+
description?: string;
|
|
67
|
+
// OpenAPI 3.0
|
|
68
|
+
schema?: SchemaObject;
|
|
69
|
+
// Swagger 2.0 direct fields (no nested schema)
|
|
70
|
+
type?: string;
|
|
71
|
+
format?: string;
|
|
72
|
+
items?: SchemaObject;
|
|
73
|
+
enum?: unknown[];
|
|
74
|
+
collectionFormat?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RequestBody {
|
|
78
|
+
required?: boolean;
|
|
79
|
+
content?: Record<string, { schema?: SchemaObject }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Response {
|
|
83
|
+
description?: string;
|
|
84
|
+
schema?: SchemaObject; // Swagger 2.0
|
|
85
|
+
content?: Record<string, { schema?: SchemaObject }>; // OpenAPI 3.0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SchemaObject {
|
|
89
|
+
type?: string;
|
|
90
|
+
format?: string;
|
|
91
|
+
$ref?: string;
|
|
92
|
+
items?: SchemaObject;
|
|
93
|
+
properties?: Record<string, SchemaObject>;
|
|
94
|
+
required?: string[];
|
|
95
|
+
enum?: unknown[];
|
|
96
|
+
description?: string;
|
|
97
|
+
nullable?: boolean;
|
|
98
|
+
allOf?: SchemaObject[];
|
|
99
|
+
oneOf?: SchemaObject[];
|
|
100
|
+
anyOf?: SchemaObject[];
|
|
101
|
+
additionalProperties?: boolean | SchemaObject;
|
|
102
|
+
}
|