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.
Files changed (68) hide show
  1. package/README.md +182 -0
  2. package/dist/cli/commands/config.d.ts +2 -0
  3. package/dist/cli/commands/config.d.ts.map +1 -0
  4. package/dist/cli/commands/config.js +34 -0
  5. package/dist/cli/commands/config.js.map +1 -0
  6. package/dist/cli/commands/generate.d.ts +9 -0
  7. package/dist/cli/commands/generate.d.ts.map +1 -0
  8. package/dist/cli/commands/generate.js +74 -0
  9. package/dist/cli/commands/generate.js.map +1 -0
  10. package/dist/cli/commands/init.d.ts +4 -0
  11. package/dist/cli/commands/init.d.ts.map +1 -0
  12. package/dist/cli/commands/init.js +104 -0
  13. package/dist/cli/commands/init.js.map +1 -0
  14. package/dist/cli/commands/list.d.ts +6 -0
  15. package/dist/cli/commands/list.d.ts.map +1 -0
  16. package/dist/cli/commands/list.js +26 -0
  17. package/dist/cli/commands/list.js.map +1 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +40 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +71 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lib/codeGenerator.d.ts +6 -0
  27. package/dist/lib/codeGenerator.d.ts.map +1 -0
  28. package/dist/lib/codeGenerator.js +331 -0
  29. package/dist/lib/codeGenerator.js.map +1 -0
  30. package/dist/lib/configManager.d.ts +8 -0
  31. package/dist/lib/configManager.d.ts.map +1 -0
  32. package/dist/lib/configManager.js +59 -0
  33. package/dist/lib/configManager.js.map +1 -0
  34. package/dist/lib/detectStructure.d.ts +3 -0
  35. package/dist/lib/detectStructure.d.ts.map +1 -0
  36. package/dist/lib/detectStructure.js +65 -0
  37. package/dist/lib/detectStructure.js.map +1 -0
  38. package/dist/lib/fetchSwagger.d.ts +5 -0
  39. package/dist/lib/fetchSwagger.d.ts.map +1 -0
  40. package/dist/lib/fetchSwagger.js +57 -0
  41. package/dist/lib/fetchSwagger.js.map +1 -0
  42. package/dist/lib/types.d.ts +101 -0
  43. package/dist/lib/types.d.ts.map +1 -0
  44. package/dist/lib/types.js +3 -0
  45. package/dist/lib/types.js.map +1 -0
  46. package/dist/tools/configUpdate.d.ts +2 -0
  47. package/dist/tools/configUpdate.d.ts.map +1 -0
  48. package/dist/tools/configUpdate.js +25 -0
  49. package/dist/tools/configUpdate.js.map +1 -0
  50. package/dist/tools/setup.d.ts +2 -0
  51. package/dist/tools/setup.d.ts.map +1 -0
  52. package/dist/tools/setup.js +63 -0
  53. package/dist/tools/setup.js.map +1 -0
  54. package/dist/tools/sync.d.ts +2 -0
  55. package/dist/tools/sync.d.ts.map +1 -0
  56. package/dist/tools/sync.js +87 -0
  57. package/dist/tools/sync.js.map +1 -0
  58. package/package.json +39 -0
  59. package/src/cli/commands/config.ts +33 -0
  60. package/src/cli/commands/generate.ts +90 -0
  61. package/src/cli/commands/init.ts +76 -0
  62. package/src/cli/commands/list.ts +31 -0
  63. package/src/cli/index.ts +44 -0
  64. package/src/lib/codeGenerator.ts +376 -0
  65. package/src/lib/configManager.ts +54 -0
  66. package/src/lib/detectStructure.ts +66 -0
  67. package/src/lib/fetchSwagger.ts +52 -0
  68. 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
+ }
@@ -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
+ }