Skip to content

Commit abb168c

Browse files
committed
fix: 빈 타입 생성 누락 문제 수정
- parsed.docs가 있지만 JSON 추출 실패 시에도 void 타입 생성 - 빈 객체 {}인 경우 Record<string, never> 타입 생성 - typeGenerator에서 빈 인터페이스를 Record<string, never>로 변환 - 빈 타입 생성 케이스에 대한 테스트 추가
1 parent aa20c5a commit abb168c

File tree

11 files changed

+287
-8
lines changed

11 files changed

+287
-8
lines changed

src/generator/apiFactoryGenerator.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ import { ParsedBrunoFile, extractJsonFromDocs } from '../parser/bruParser';
77
import { ApiFunction } from './apiClientGenerator';
88
import { generateTypeScriptInterface, toCamelCase, functionNameToTypeName } from './typeGenerator';
99

10+
/**
11+
* 빈 인터페이스를 Record<string, never> 타입으로 변환
12+
*/
13+
function convertEmptyInterfaceToType(content: string, typeName: string): string {
14+
// 빈 인터페이스 패턴: export interface TypeName { }
15+
const emptyInterfacePattern = new RegExp(`export interface ${typeName}\\s*\\{\\s*\\}`);
16+
if (emptyInterfacePattern.test(content)) {
17+
return `export type ${typeName} = Record<string, never>;`;
18+
}
19+
return content;
20+
}
21+
1022
/**
1123
* 팩토리용 API 함수 코드 생성 (객체 속성으로 사용)
1224
*/
@@ -91,14 +103,35 @@ export function generateApiFactory(
91103
// Response 타입 생성
92104
if (parsed.docs) {
93105
const jsonData = extractJsonFromDocs(parsed.docs);
94-
if (jsonData) {
95-
const typeDefs = generateTypeScriptInterface(jsonData, responseType);
96-
for (const typeDef of typeDefs) {
97-
if (!typeDefinitions.has(typeDef.content)) {
98-
typeDefinitions.add(typeDef.content);
99-
lines.push(typeDef.content);
106+
if (jsonData !== null && jsonData !== undefined) {
107+
// 빈 객체 체크
108+
if (typeof jsonData === 'object' && !Array.isArray(jsonData) && Object.keys(jsonData).length === 0) {
109+
// 빈 객체인 경우 Record<string, never> 타입 생성
110+
const emptyType = `export type ${responseType} = Record<string, never>;`;
111+
if (!typeDefinitions.has(emptyType)) {
112+
typeDefinitions.add(emptyType);
113+
lines.push(emptyType);
100114
lines.push('');
101115
}
116+
} else {
117+
const typeDefs = generateTypeScriptInterface(jsonData, responseType);
118+
for (const typeDef of typeDefs) {
119+
// 빈 인터페이스 체크 및 변환
120+
const processedContent = convertEmptyInterfaceToType(typeDef.content, responseType);
121+
if (!typeDefinitions.has(processedContent)) {
122+
typeDefinitions.add(processedContent);
123+
lines.push(processedContent);
124+
lines.push('');
125+
}
126+
}
127+
}
128+
} else {
129+
// JSON 추출 실패 시 void 타입 생성
130+
const defaultType = `export type ${responseType} = void;`;
131+
if (!typeDefinitions.has(defaultType)) {
132+
typeDefinitions.add(defaultType);
133+
lines.push(defaultType);
134+
lines.push('');
102135
}
103136
}
104137
} else {

src/generator/typeGenerator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,14 @@ export function generateTypeScriptInterface(
9999
properties.push(` ${key}: ${type};`);
100100
}
101101

102-
const mainInterface = `export interface ${interfaceName} {\n${properties.join('\n')}\n}`;
103-
definitions.push({ name: interfaceName, content: mainInterface });
102+
// 빈 인터페이스인 경우 Record<string, never> 타입으로 생성
103+
if (properties.length === 0) {
104+
const emptyType = `export type ${interfaceName} = Record<string, never>;`;
105+
definitions.push({ name: interfaceName, content: emptyType });
106+
} else {
107+
const mainInterface = `export interface ${interfaceName} {\n${properties.join('\n')}\n}`;
108+
definitions.push({ name: interfaceName, content: mainInterface });
109+
}
104110

105111
return definitions;
106112
}

tests/cli.test.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,5 +453,159 @@ docs {
453453
});
454454
});
455455

456+
describe('빈 타입 생성 테스트', () => {
457+
test('빈 객체 {}인 경우 Record<string, never> 타입 생성', () => {
458+
const emptyObjectFixtureDir = join(FIXTURES_DIR, 'bruno-empty-object');
459+
mkdirSync(emptyObjectFixtureDir, { recursive: true });
460+
461+
// 폴더 구조 생성
462+
const testFolder = join(emptyObjectFixtureDir, 'test');
463+
mkdirSync(testFolder, { recursive: true });
464+
465+
const emptyObjectFile = join(testFolder, 'empty-response.bru');
466+
const emptyObjectContent = `meta {
467+
name: Empty Response Test
468+
type: http
469+
}
470+
471+
get /test/empty
472+
473+
docs {
474+
\`\`\`json
475+
{}
476+
\`\`\`
477+
}
478+
`;
479+
require('fs').writeFileSync(emptyObjectFile, emptyObjectContent);
480+
481+
const outputDir = join(TEST_OUTPUT_DIR, 'empty-object-output');
482+
execSync(`node dist/cli/index.js generate-hooks -i ${emptyObjectFixtureDir} -o ${outputDir}`, {
483+
cwd: join(__dirname, '..'),
484+
});
485+
486+
// api.ts 파일 확인 (도메인은 폴더명 'test'가 됨)
487+
const apiFile = join(outputDir, 'test', 'api.ts');
488+
assert.ok(existsSync(apiFile), 'api.ts 파일이 생성되어야 함');
489+
490+
const apiContent = readFileSync(apiFile, 'utf-8');
491+
assert.ok(apiContent.includes('Record<string, never>'), '빈 객체는 Record<string, never> 타입이어야 함');
492+
assert.ok(!apiContent.includes('export interface'), '빈 인터페이스는 생성되지 않아야 함');
493+
494+
console.log('✅ 빈 객체 Record<string, never> 타입 생성 테스트 통과');
495+
});
496+
497+
test('parsed.docs가 있지만 JSON 추출 실패 시 void 타입 생성', () => {
498+
const invalidJsonFixtureDir = join(FIXTURES_DIR, 'bruno-invalid-json');
499+
mkdirSync(invalidJsonFixtureDir, { recursive: true });
500+
501+
// 폴더 구조 생성
502+
const testFolder = join(invalidJsonFixtureDir, 'test');
503+
mkdirSync(testFolder, { recursive: true });
504+
505+
const invalidJsonFile = join(testFolder, 'invalid-json.bru');
506+
const invalidJsonContent = `meta {
507+
name: Invalid JSON Test
508+
type: http
509+
}
510+
511+
get /test/invalid
512+
513+
docs {
514+
이것은 유효하지 않은 JSON입니다
515+
{ invalid json }
516+
}
517+
`;
518+
require('fs').writeFileSync(invalidJsonFile, invalidJsonContent);
519+
520+
const outputDir = join(TEST_OUTPUT_DIR, 'invalid-json-output');
521+
execSync(`node dist/cli/index.js generate-hooks -i ${invalidJsonFixtureDir} -o ${outputDir}`, {
522+
cwd: join(__dirname, '..'),
523+
});
524+
525+
// api.ts 파일 확인 (도메인은 폴더명 'test'가 됨)
526+
const apiFile = join(outputDir, 'test', 'api.ts');
527+
assert.ok(existsSync(apiFile), 'api.ts 파일이 생성되어야 함');
528+
529+
const apiContent = readFileSync(apiFile, 'utf-8');
530+
assert.ok(apiContent.includes('void'), 'JSON 추출 실패 시 void 타입이 생성되어야 함');
531+
532+
console.log('✅ JSON 추출 실패 시 void 타입 생성 테스트 통과');
533+
});
534+
535+
test('빈 배열 []인 경우 any[] 타입 생성', () => {
536+
const emptyArrayFixtureDir = join(FIXTURES_DIR, 'bruno-empty-array');
537+
mkdirSync(emptyArrayFixtureDir, { recursive: true });
538+
539+
// 폴더 구조 생성
540+
const testFolder = join(emptyArrayFixtureDir, 'test');
541+
mkdirSync(testFolder, { recursive: true });
542+
543+
const emptyArrayFile = join(testFolder, 'empty-array.bru');
544+
const emptyArrayContent = `meta {
545+
name: Empty Array Test
546+
type: http
547+
}
548+
549+
get /test/empty-array
550+
551+
docs {
552+
\`\`\`json
553+
{
554+
"items": []
555+
}
556+
\`\`\`
557+
}
558+
`;
559+
require('fs').writeFileSync(emptyArrayFile, emptyArrayContent);
560+
561+
const outputDir = join(TEST_OUTPUT_DIR, 'empty-array-output');
562+
execSync(`node dist/cli/index.js generate-hooks -i ${emptyArrayFixtureDir} -o ${outputDir}`, {
563+
cwd: join(__dirname, '..'),
564+
});
565+
566+
// api.ts 파일 확인 (도메인은 폴더명 'test'가 됨)
567+
const apiFile = join(outputDir, 'test', 'api.ts');
568+
assert.ok(existsSync(apiFile), 'api.ts 파일이 생성되어야 함');
569+
570+
const apiContent = readFileSync(apiFile, 'utf-8');
571+
assert.ok(apiContent.includes('any[]'), '빈 배열은 any[] 타입이어야 함');
572+
573+
console.log('✅ 빈 배열 any[] 타입 생성 테스트 통과');
574+
});
575+
576+
test('parsed.docs가 없는 경우 void 타입 생성', () => {
577+
const noDocsFixtureDir = join(FIXTURES_DIR, 'bruno-no-docs');
578+
mkdirSync(noDocsFixtureDir, { recursive: true });
579+
580+
// 폴더 구조 생성
581+
const testFolder = join(noDocsFixtureDir, 'test');
582+
mkdirSync(testFolder, { recursive: true });
583+
584+
const noDocsFile = join(testFolder, 'no-docs.bru');
585+
const noDocsContent = `meta {
586+
name: No Docs Test
587+
type: http
588+
}
589+
590+
get /test/no-docs
591+
`;
592+
require('fs').writeFileSync(noDocsFile, noDocsContent);
593+
594+
const outputDir = join(TEST_OUTPUT_DIR, 'no-docs-output');
595+
execSync(`node dist/cli/index.js generate-hooks -i ${noDocsFixtureDir} -o ${outputDir}`, {
596+
cwd: join(__dirname, '..'),
597+
});
598+
599+
// api.ts 파일 확인 (도메인은 폴더명 'test'가 됨)
600+
const apiFile = join(outputDir, 'test', 'api.ts');
601+
assert.ok(existsSync(apiFile), 'api.ts 파일이 생성되어야 함');
602+
603+
const apiContent = readFileSync(apiFile, 'utf-8');
604+
assert.ok(apiContent.includes('void'), 'docs가 없으면 void 타입이 생성되어야 함');
605+
606+
console.log('✅ docs 없을 때 void 타입 생성 테스트 통과');
607+
});
608+
});
609+
456610
console.log('\n🎉 모든 테스트 완료!');
457611

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
meta {
2+
name: Empty Array Test
3+
type: http
4+
}
5+
6+
get /test/empty-array
7+
8+
docs {
9+
```json
10+
{
11+
"items": []
12+
}
13+
```
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
meta {
2+
name: Empty Array Test
3+
type: http
4+
}
5+
6+
get /test/empty-array
7+
8+
docs {
9+
```json
10+
{
11+
"items": []
12+
}
13+
```
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
meta {
2+
name: Empty Response Test
3+
type: http
4+
}
5+
6+
get /test/empty
7+
8+
docs {
9+
```json
10+
{}
11+
```
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
meta {
2+
name: Empty Response Test
3+
type: http
4+
}
5+
6+
get /test/empty
7+
8+
docs {
9+
```json
10+
{}
11+
```
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
meta {
2+
name: Invalid JSON Test
3+
type: http
4+
}
5+
6+
get /test/invalid
7+
8+
docs {
9+
이것은 유효하지 않은 JSON입니다
10+
{ invalid json }
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
meta {
2+
name: Invalid JSON Test
3+
type: http
4+
}
5+
6+
get /test/invalid
7+
8+
docs {
9+
이것은 유효하지 않은 JSON입니다
10+
{ invalid json }
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
meta {
2+
name: No Docs Test
3+
type: http
4+
}
5+
6+
get /test/no-docs

0 commit comments

Comments
 (0)