@gomjellie/lazyapi 0.0.1
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/LICENSE +22 -0
- package/README.md +162 -0
- package/dist/App.d.ts +9 -0
- package/dist/App.js +65 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +39 -0
- package/dist/components/common/ErrorMessage.d.ts +10 -0
- package/dist/components/common/ErrorMessage.js +17 -0
- package/dist/components/common/KeybindingFooter.d.ts +18 -0
- package/dist/components/common/KeybindingFooter.js +20 -0
- package/dist/components/common/LoadingSpinner.d.ts +9 -0
- package/dist/components/common/LoadingSpinner.js +15 -0
- package/dist/components/common/SearchInput.d.ts +14 -0
- package/dist/components/common/SearchInput.js +72 -0
- package/dist/components/common/StatusBar.d.ts +14 -0
- package/dist/components/common/StatusBar.js +36 -0
- package/dist/components/detail-view/DetailView.d.ts +5 -0
- package/dist/components/detail-view/DetailView.js +266 -0
- package/dist/components/detail-view/LeftPanel.d.ts +19 -0
- package/dist/components/detail-view/LeftPanel.js +363 -0
- package/dist/components/detail-view/ParameterForm.d.ts +17 -0
- package/dist/components/detail-view/ParameterForm.js +77 -0
- package/dist/components/detail-view/RightPanel.d.ts +22 -0
- package/dist/components/detail-view/RightPanel.js +251 -0
- package/dist/components/list-view/EndpointList.d.ts +12 -0
- package/dist/components/list-view/EndpointList.js +72 -0
- package/dist/components/list-view/Explorer.d.ts +13 -0
- package/dist/components/list-view/Explorer.js +83 -0
- package/dist/components/list-view/FilterBar.d.ts +12 -0
- package/dist/components/list-view/FilterBar.js +33 -0
- package/dist/components/list-view/ListView.d.ts +5 -0
- package/dist/components/list-view/ListView.js +304 -0
- package/dist/components/list-view/SearchBar.d.ts +11 -0
- package/dist/components/list-view/SearchBar.js +14 -0
- package/dist/hooks/useApiClient.d.ts +11 -0
- package/dist/hooks/useApiClient.js +34 -0
- package/dist/hooks/useKeyPress.d.ts +26 -0
- package/dist/hooks/useKeyPress.js +57 -0
- package/dist/hooks/useNavigation.d.ts +10 -0
- package/dist/hooks/useNavigation.js +33 -0
- package/dist/services/http-client.d.ts +21 -0
- package/dist/services/http-client.js +173 -0
- package/dist/services/openapi-parser.d.ts +47 -0
- package/dist/services/openapi-parser.js +318 -0
- package/dist/store/appSlice.d.ts +14 -0
- package/dist/store/appSlice.js +144 -0
- package/dist/store/hooks.d.ts +9 -0
- package/dist/store/hooks.js +7 -0
- package/dist/store/index.d.ts +12 -0
- package/dist/store/index.js +12 -0
- package/dist/types/app-state.d.ts +126 -0
- package/dist/types/app-state.js +4 -0
- package/dist/types/openapi.d.ts +86 -0
- package/dist/types/openapi.js +115 -0
- package/dist/utils/clipboard.d.ts +11 -0
- package/dist/utils/clipboard.js +26 -0
- package/dist/utils/formatters.d.ts +35 -0
- package/dist/utils/formatters.js +93 -0
- package/dist/utils/schema-formatter.d.ts +21 -0
- package/dist/utils/schema-formatter.js +301 -0
- package/dist/utils/validators.d.ts +16 -0
- package/dist/utils/validators.js +158 -0
- package/package.json +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema를 읽기 쉬운 형태로 포맷팅하는 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import type { SchemaObject, OpenAPIDocument, ReferenceObject } from '../types/openapi.js';
|
|
5
|
+
interface FormatOptions {
|
|
6
|
+
indent?: number;
|
|
7
|
+
maxDepth?: number;
|
|
8
|
+
currentDepth?: number;
|
|
9
|
+
document: OpenAPIDocument;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Schema를 TypeScript 인터페이스 스타일로 포맷팅
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatSchema(schema: SchemaObject | ReferenceObject, options: FormatOptions): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Schema 전체를 포맷팅 (최상위 레벨)
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatSchemaFull(schema: SchemaObject | {
|
|
19
|
+
$ref: string;
|
|
20
|
+
}, document: OpenAPIDocument, maxDepth?: number): string[];
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema를 읽기 쉬운 형태로 포맷팅하는 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import { isReferenceObject as checkIsRef, resolveRef as resolve } from '../types/openapi.js';
|
|
5
|
+
/**
|
|
6
|
+
* Schema를 TypeScript 인터페이스 스타일로 포맷팅
|
|
7
|
+
*/
|
|
8
|
+
export function formatSchema(schema, options) {
|
|
9
|
+
const { indent = 0, maxDepth = 3, currentDepth = 0, document } = options;
|
|
10
|
+
const lines = [];
|
|
11
|
+
const indentStr = ' '.repeat(indent);
|
|
12
|
+
// $ref인 경우
|
|
13
|
+
if (checkIsRef(schema)) {
|
|
14
|
+
const refName = schema.$ref.split('/').pop() || schema.$ref;
|
|
15
|
+
// 최대 깊이 도달 시 참조만 표시
|
|
16
|
+
if (currentDepth >= maxDepth) {
|
|
17
|
+
lines.push(`${indentStr}${refName} { ... }`);
|
|
18
|
+
return lines;
|
|
19
|
+
}
|
|
20
|
+
const resolved = resolve(schema.$ref, document);
|
|
21
|
+
if (resolved) {
|
|
22
|
+
lines.push(`${indentStr}${refName} {`);
|
|
23
|
+
const innerLines = formatSchemaObject(resolved, {
|
|
24
|
+
...options,
|
|
25
|
+
indent: indent + 1,
|
|
26
|
+
currentDepth: currentDepth + 1,
|
|
27
|
+
});
|
|
28
|
+
lines.push(...innerLines);
|
|
29
|
+
lines.push(`${indentStr}}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
lines.push(`${indentStr}${refName} { ... }`);
|
|
33
|
+
}
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
// 일반 schema
|
|
37
|
+
return formatSchemaObject(schema, options);
|
|
38
|
+
}
|
|
39
|
+
function formatSchemaObject(schema, options) {
|
|
40
|
+
const { indent = 0, document, currentDepth = 0, maxDepth = 3 } = options;
|
|
41
|
+
const lines = [];
|
|
42
|
+
const indentStr = ' '.repeat(indent);
|
|
43
|
+
// Object 타입
|
|
44
|
+
if (schema.type === 'object' || schema.properties) {
|
|
45
|
+
const required = schema.required || [];
|
|
46
|
+
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
47
|
+
const isRequired = required.includes(propName);
|
|
48
|
+
const propNameStr = isRequired ? `${propName}*` : propName;
|
|
49
|
+
if (checkIsRef(propSchema)) {
|
|
50
|
+
// $ref 속성
|
|
51
|
+
const refName = propSchema.$ref.split('/').pop() || propSchema.$ref;
|
|
52
|
+
const resolved = resolve(propSchema.$ref, document);
|
|
53
|
+
if (resolved && (resolved.type === 'object' || resolved.properties)) {
|
|
54
|
+
// 중첩 객체 처리
|
|
55
|
+
if (currentDepth + 1 >= maxDepth) {
|
|
56
|
+
// maxDepth 도달 시 축약
|
|
57
|
+
lines.push(`${indentStr}${propNameStr}: ${refName} { ... }`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// maxDepth 미도달 시 펼치기
|
|
61
|
+
lines.push(`${indentStr}${propNameStr}: ${refName} {`);
|
|
62
|
+
const innerLines = formatSchemaObject(resolved, {
|
|
63
|
+
...options,
|
|
64
|
+
indent: indent + 1,
|
|
65
|
+
currentDepth: currentDepth + 1,
|
|
66
|
+
});
|
|
67
|
+
lines.push(...innerLines);
|
|
68
|
+
lines.push(`${indentStr}}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (resolved) {
|
|
72
|
+
// 단순 타입은 확장
|
|
73
|
+
const typeStr = formatType(resolved);
|
|
74
|
+
lines.push(`${indentStr}${propNameStr}: ${refName}(${typeStr})`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
lines.push(`${indentStr}${propNameStr}: ${refName}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (propSchema.type === 'array' && propSchema.items && checkIsRef(propSchema.items)) {
|
|
81
|
+
// Array with $ref items - 펼쳐서 표시
|
|
82
|
+
const refName = propSchema.items.$ref.split('/').pop() || propSchema.items.$ref;
|
|
83
|
+
const resolved = resolve(propSchema.items.$ref, document);
|
|
84
|
+
if (resolved && (resolved.type === 'object' || resolved.properties)) {
|
|
85
|
+
// 객체 배열 - 펼쳐서 표시
|
|
86
|
+
if (currentDepth + 1 >= maxDepth) {
|
|
87
|
+
lines.push(`${indentStr}${propNameStr}: ${refName} { ... } []`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push(`${indentStr}${propNameStr}: ${refName} {`);
|
|
91
|
+
const innerLines = formatSchemaObject(resolved, {
|
|
92
|
+
...options,
|
|
93
|
+
indent: indent + 1,
|
|
94
|
+
currentDepth: currentDepth + 1,
|
|
95
|
+
});
|
|
96
|
+
lines.push(...innerLines);
|
|
97
|
+
lines.push(`${indentStr}} []`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// 단순 타입 배열
|
|
102
|
+
lines.push(`${indentStr}${propNameStr}: ${refName} []`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// 일반 속성
|
|
107
|
+
const typeStr = formatType(propSchema);
|
|
108
|
+
const exampleStr = propSchema.example
|
|
109
|
+
? ` (example: ${JSON.stringify(propSchema.example)})`
|
|
110
|
+
: '';
|
|
111
|
+
const descStr = propSchema.description ? ` // ${propSchema.description}` : '';
|
|
112
|
+
lines.push(`${indentStr}${propNameStr}: ${typeStr}${exampleStr}${descStr}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
// Array 타입
|
|
118
|
+
if (schema.type === 'array' && schema.items) {
|
|
119
|
+
const itemsSchema = schema.items;
|
|
120
|
+
if (checkIsRef(itemsSchema)) {
|
|
121
|
+
const refName = itemsSchema.$ref.split('/').pop() || itemsSchema.$ref;
|
|
122
|
+
const resolved = resolve(itemsSchema.$ref, document);
|
|
123
|
+
if (resolved && (resolved.type === 'object' || resolved.properties)) {
|
|
124
|
+
// 객체 배열 - 펼쳐서 표시
|
|
125
|
+
if (currentDepth >= maxDepth) {
|
|
126
|
+
lines.push(`${indentStr}${refName} { ... } []`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
lines.push(`${indentStr}${refName} {`);
|
|
130
|
+
const innerLines = formatSchemaObject(resolved, {
|
|
131
|
+
...options,
|
|
132
|
+
indent: indent + 1,
|
|
133
|
+
currentDepth: currentDepth + 1,
|
|
134
|
+
});
|
|
135
|
+
lines.push(...innerLines);
|
|
136
|
+
lines.push(`${indentStr}} []`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// 단순 타입 배열
|
|
141
|
+
lines.push(`${indentStr}${refName} []`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const itemType = formatType(itemsSchema);
|
|
146
|
+
lines.push(`${indentStr}Array<${itemType}>`);
|
|
147
|
+
}
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
// 단순 타입
|
|
151
|
+
const typeStr = formatType(schema);
|
|
152
|
+
lines.push(`${indentStr}${typeStr}`);
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Schema 타입을 문자열로 포맷팅
|
|
157
|
+
*/
|
|
158
|
+
function formatType(schema) {
|
|
159
|
+
// Enum 타입
|
|
160
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
161
|
+
const enumValues = schema.enum.map((v) => `'${v}'`).join(' | ');
|
|
162
|
+
return `Enum(${enumValues})`;
|
|
163
|
+
}
|
|
164
|
+
// Array 타입
|
|
165
|
+
if (schema.type === 'array' && schema.items) {
|
|
166
|
+
if (checkIsRef(schema.items)) {
|
|
167
|
+
const refName = schema.items.$ref.split('/').pop() || schema.items.$ref;
|
|
168
|
+
return `Array<${refName}>`;
|
|
169
|
+
}
|
|
170
|
+
const itemType = formatType(schema.items);
|
|
171
|
+
return `Array<${itemType}>`;
|
|
172
|
+
}
|
|
173
|
+
// 기본 타입
|
|
174
|
+
const schemaType = schema.type;
|
|
175
|
+
let typeStr;
|
|
176
|
+
if (Array.isArray(schemaType)) {
|
|
177
|
+
typeStr = schemaType.join(' | ');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
typeStr = schemaType || 'any';
|
|
181
|
+
}
|
|
182
|
+
// Format 추가
|
|
183
|
+
if (schema.format) {
|
|
184
|
+
typeStr = `${typeStr}($${schema.format})`;
|
|
185
|
+
}
|
|
186
|
+
return typeStr;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 복합 타입(oneOf, anyOf, allOf) 포맷팅
|
|
190
|
+
*/
|
|
191
|
+
function formatComplexType(schema, options) {
|
|
192
|
+
const { document } = options;
|
|
193
|
+
const lines = [];
|
|
194
|
+
// oneOf / anyOf
|
|
195
|
+
const variants = schema.oneOf || schema.anyOf;
|
|
196
|
+
if (variants) {
|
|
197
|
+
const types = variants.map(variant => {
|
|
198
|
+
if (checkIsRef(variant)) {
|
|
199
|
+
return variant.$ref.split('/').pop() || 'unknown';
|
|
200
|
+
}
|
|
201
|
+
return formatType(variant);
|
|
202
|
+
});
|
|
203
|
+
lines.push(types.join(' | '));
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
// allOf
|
|
207
|
+
if (schema.allOf) {
|
|
208
|
+
// allOf는 객체들의 합집합이므로, 각 스키마의 속성을 병합해서 표시하는 것이 좋음
|
|
209
|
+
lines.push('{');
|
|
210
|
+
for (const subSchema of schema.allOf) {
|
|
211
|
+
if (checkIsRef(subSchema)) {
|
|
212
|
+
const refName = subSchema.$ref.split('/').pop() || subSchema.$ref;
|
|
213
|
+
const resolved = resolve(subSchema.$ref, document);
|
|
214
|
+
if (resolved) {
|
|
215
|
+
const innerLines = formatSchemaObject(resolved, { ...options, indent: 1 });
|
|
216
|
+
lines.push(...innerLines);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
lines.push(` ...${refName}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const innerLines = formatSchemaObject(subSchema, { ...options, indent: 1 });
|
|
224
|
+
lines.push(...innerLines);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
lines.push('}');
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Schema 전체를 포맷팅 (최상위 레벨)
|
|
234
|
+
*/
|
|
235
|
+
export function formatSchemaFull(schema, document, maxDepth = 3) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
// $ref인 경우 먼저 resolve
|
|
238
|
+
let actualSchema;
|
|
239
|
+
let typeName = '';
|
|
240
|
+
if (checkIsRef(schema)) {
|
|
241
|
+
typeName = schema.$ref.split('/').pop() || '';
|
|
242
|
+
const resolved = resolve(schema.$ref, document);
|
|
243
|
+
if (!resolved) {
|
|
244
|
+
return [`${typeName} { ... }`];
|
|
245
|
+
}
|
|
246
|
+
actualSchema = resolved;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
actualSchema = schema;
|
|
250
|
+
}
|
|
251
|
+
// Array 타입 먼저 체크
|
|
252
|
+
if (actualSchema.type === 'array' && actualSchema.items) {
|
|
253
|
+
const innerLines = formatSchemaObject(actualSchema, {
|
|
254
|
+
indent: 0,
|
|
255
|
+
maxDepth,
|
|
256
|
+
currentDepth: 0,
|
|
257
|
+
document,
|
|
258
|
+
});
|
|
259
|
+
lines.push(...innerLines);
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
262
|
+
// Complex types (oneOf, anyOf, allOf)
|
|
263
|
+
if (actualSchema.oneOf || actualSchema.anyOf || actualSchema.allOf) {
|
|
264
|
+
const complexLines = formatComplexType(actualSchema, {
|
|
265
|
+
indent: 0,
|
|
266
|
+
maxDepth,
|
|
267
|
+
currentDepth: 0,
|
|
268
|
+
document,
|
|
269
|
+
});
|
|
270
|
+
lines.push(...complexLines);
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
// Object 타입
|
|
274
|
+
if (actualSchema.type === 'object' || actualSchema.properties) {
|
|
275
|
+
if (typeName) {
|
|
276
|
+
lines.push(`${typeName} {`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
lines.push('{');
|
|
280
|
+
}
|
|
281
|
+
const innerLines = formatSchema(actualSchema, {
|
|
282
|
+
indent: 1,
|
|
283
|
+
maxDepth,
|
|
284
|
+
currentDepth: 0,
|
|
285
|
+
document,
|
|
286
|
+
});
|
|
287
|
+
lines.push(...innerLines);
|
|
288
|
+
lines.push('}');
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// 단순 타입
|
|
292
|
+
const typeStr = formatType(actualSchema);
|
|
293
|
+
if (typeName) {
|
|
294
|
+
lines.push(`${typeName}: ${typeStr}`);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
lines.push(typeStr);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return lines;
|
|
301
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 검증 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import { NormalizedParameter } from '../types/openapi.js';
|
|
5
|
+
/**
|
|
6
|
+
* 파라미터 값 검증
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateParameter(parameter: NormalizedParameter, value: string): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* JSON 문자열 검증
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateJSON(json: string): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* URL 검증
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateURL(url: string): string | null;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 검증 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 파라미터 값 검증
|
|
6
|
+
*/
|
|
7
|
+
export function validateParameter(parameter, value) {
|
|
8
|
+
// 필수 파라미터 검증
|
|
9
|
+
if (parameter.required && !value.trim()) {
|
|
10
|
+
return `${parameter.name}은(는) 필수 항목입니다`;
|
|
11
|
+
}
|
|
12
|
+
// 값이 없으면 추가 검증 스킵
|
|
13
|
+
if (!value.trim()) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
// 스키마 기반 검증
|
|
17
|
+
if (parameter.schema && typeof parameter.schema === 'object' && !('$ref' in parameter.schema)) {
|
|
18
|
+
const schema = parameter.schema;
|
|
19
|
+
// 타입 검증
|
|
20
|
+
if (schema.type) {
|
|
21
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
22
|
+
let hasValidType = false;
|
|
23
|
+
for (const type of types) {
|
|
24
|
+
if (type === 'null' && !value.trim()) {
|
|
25
|
+
hasValidType = true;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const error = validateType(type, value);
|
|
29
|
+
if (!error) {
|
|
30
|
+
hasValidType = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!hasValidType && types.length === 1) {
|
|
35
|
+
const error = validateType(types[0], value);
|
|
36
|
+
if (error) {
|
|
37
|
+
return `${parameter.name}: ${error}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// enum 검증
|
|
42
|
+
if (schema.enum && Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
43
|
+
return `${parameter.name}은(는) 다음 값 중 하나여야 합니다: ${schema.enum.map(String).join(', ')}`;
|
|
44
|
+
}
|
|
45
|
+
// 포맷 검증
|
|
46
|
+
if (schema.format) {
|
|
47
|
+
const error = validateFormat(schema.format, value);
|
|
48
|
+
if (error) {
|
|
49
|
+
return `${parameter.name}: ${error}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 타입 검증
|
|
57
|
+
*/
|
|
58
|
+
function validateType(type, value) {
|
|
59
|
+
switch (type) {
|
|
60
|
+
case 'integer':
|
|
61
|
+
case 'number':
|
|
62
|
+
if (isNaN(Number(value))) {
|
|
63
|
+
return '숫자여야 합니다';
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case 'boolean':
|
|
67
|
+
if (value !== 'true' && value !== 'false') {
|
|
68
|
+
return 'true 또는 false여야 합니다';
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case 'array':
|
|
72
|
+
// 간단한 배열 검증 (쉼표로 구분)
|
|
73
|
+
// 실제로는 더 복잡한 검증이 필요할 수 있음
|
|
74
|
+
break;
|
|
75
|
+
case 'object':
|
|
76
|
+
try {
|
|
77
|
+
JSON.parse(value);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return '유효한 JSON 객체여야 합니다';
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 포맷 검증
|
|
88
|
+
*/
|
|
89
|
+
function validateFormat(format, value) {
|
|
90
|
+
switch (format) {
|
|
91
|
+
case 'email':
|
|
92
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
93
|
+
return '유효한 이메일 주소여야 합니다';
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case 'uri':
|
|
97
|
+
case 'url':
|
|
98
|
+
try {
|
|
99
|
+
new URL(value);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return '유효한 URL이어야 합니다';
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case 'uuid':
|
|
106
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
107
|
+
return '유효한 UUID여야 합니다';
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'date':
|
|
111
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
112
|
+
return '유효한 날짜 형식(YYYY-MM-DD)이어야 합니다';
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case 'date-time':
|
|
116
|
+
try {
|
|
117
|
+
new Date(value).toISOString();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return '유효한 날짜-시간 형식이어야 합니다';
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* JSON 문자열 검증
|
|
128
|
+
*/
|
|
129
|
+
export function validateJSON(json) {
|
|
130
|
+
if (!json.trim()) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
JSON.parse(json);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
return `유효하지 않은 JSON: ${error.message}`;
|
|
140
|
+
}
|
|
141
|
+
return '유효하지 않은 JSON';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* URL 검증
|
|
146
|
+
*/
|
|
147
|
+
export function validateURL(url) {
|
|
148
|
+
if (!url.trim()) {
|
|
149
|
+
return 'URL을 입력해주세요';
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
new URL(url);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return '유효한 URL을 입력해주세요';
|
|
157
|
+
}
|
|
158
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gomjellie/lazyapi",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A terminal-based Swagger/OpenAPI explorer with Vim-style keyboard navigation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"lazyapi": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx src/cli.tsx",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"test:ui": "vitest --ui",
|
|
17
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
18
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
19
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepush": "pnpm lint && pnpm typecheck && pnpm test",
|
|
22
|
+
"prepare": "node scripts/install-hooks.mjs || true",
|
|
23
|
+
"prepublishOnly": "pnpm build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"swagger",
|
|
27
|
+
"openapi",
|
|
28
|
+
"cli",
|
|
29
|
+
"tui",
|
|
30
|
+
"terminal",
|
|
31
|
+
"vim",
|
|
32
|
+
"api",
|
|
33
|
+
"rest"
|
|
34
|
+
],
|
|
35
|
+
"author": "gomjellie",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@apidevtools/swagger-parser": "^10.1.0",
|
|
39
|
+
"@reduxjs/toolkit": "^2.11.2",
|
|
40
|
+
"axios": "^1.6.2",
|
|
41
|
+
"clipboardy": "^4.0.0",
|
|
42
|
+
"ink": "^6.0.0",
|
|
43
|
+
"ink-spinner": "^5.0.0",
|
|
44
|
+
"ink-text-input": "^6.0.0",
|
|
45
|
+
"react": "^19.2.3",
|
|
46
|
+
"react-redux": "^9.2.0",
|
|
47
|
+
"yaml": "^2.3.4"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@testing-library/react": "^16.3.1",
|
|
51
|
+
"@total-typescript/ts-reset": "^0.6.1",
|
|
52
|
+
"@types/node": "^22.12.0",
|
|
53
|
+
"@types/react": "^19.0.10",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.23.0",
|
|
56
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
57
|
+
"@vitest/ui": "^4.0.16",
|
|
58
|
+
"eslint": "^9.23.0",
|
|
59
|
+
"eslint-config-prettier": "^10.1.8",
|
|
60
|
+
"eslint-plugin-react": "^7.37.5",
|
|
61
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
62
|
+
"husky": "^9.1.7",
|
|
63
|
+
"ink-testing-library": "^4.0.0",
|
|
64
|
+
"jsdom": "^27.3.0",
|
|
65
|
+
"openapi-types": "^12.1.3",
|
|
66
|
+
"prettier": "^3.7.4",
|
|
67
|
+
"tsx": "^4.21.0",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vitest": "^4.0.16"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=20"
|
|
73
|
+
},
|
|
74
|
+
"files": [
|
|
75
|
+
"dist",
|
|
76
|
+
"README.md",
|
|
77
|
+
"LICENSE"
|
|
78
|
+
],
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "https://github.com/gomjellie/lazyapi.git"
|
|
82
|
+
},
|
|
83
|
+
"pnpm": {
|
|
84
|
+
"onlyBuiltDependencies": [
|
|
85
|
+
"esbuild"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|