From 23f03d5a7c802a9149dd08ab86e8bee847994458 Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Sat, 4 Oct 2025 19:45:46 +0900 Subject: [PATCH 1/4] fix: resolve syntax error in generated openapi json --- examples/express/expected/oas.json | 4 +-- lib/dsl/generator/OpenAPIGenerator.ts | 36 ++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/examples/express/expected/oas.json b/examples/express/expected/oas.json index c98ba29..22475a0 100644 --- a/examples/express/expected/oas.json +++ b/examples/express/expected/oas.json @@ -89,7 +89,7 @@ } } }, - "/users/:userId": { + "/users/{userId}": { "get": { "summary": "사용자 조회 API", "tags": ["User"], @@ -383,7 +383,7 @@ } } }, - "/users/:userId/friends/:friendName": { + "/users/{userId}/friends/{friendName}": { "delete": { "summary": "특정 사용자의 친구를 삭제합니다.", "tags": ["User"], diff --git a/lib/dsl/generator/OpenAPIGenerator.ts b/lib/dsl/generator/OpenAPIGenerator.ts index 1a2a38b..a983495 100644 --- a/lib/dsl/generator/OpenAPIGenerator.ts +++ b/lib/dsl/generator/OpenAPIGenerator.ts @@ -130,10 +130,15 @@ export class OpenAPIGenerator implements IOpenAPIGenerator { const paths: Record> = {} for (const [path, methods] of groupedResults) { - paths[path] = {} + const normalizedPath = this.normalizePathTemplate(path) + paths[normalizedPath] = {} for (const [method, statusCodes] of methods) { - paths[path][method] = this.generateOperationObject(path, method, statusCodes) + paths[normalizedPath][method] = this.generateOperationObject( + normalizedPath, + method, + statusCodes, + ) } } @@ -533,8 +538,7 @@ export class OpenAPIGenerator implements IOpenAPIGenerator { */ private validatePathParameters(paths: Record>): void { for (const [path, pathItem] of Object.entries(paths)) { - const pathParamMatches = path.match(/:([^/]+)/g) || [] - const pathParams = pathParamMatches.map((param) => param.slice(1)) + const pathParams = this.extractPathParameterNames(path) if (pathParams.length === 0) continue @@ -571,6 +575,30 @@ export class OpenAPIGenerator implements IOpenAPIGenerator { } } + /** + * Converts colon-prefixed Express parameters to OpenAPI-compatible templates. + * @param {string} path Raw route path + * @returns {string} Normalized OpenAPI path + */ + private normalizePathTemplate(path: string): string { + return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}") + } + + /** + * Extracts parameter names from a normalized or raw path template. + * @param {string} path Path string potentially containing parameters + * @returns {string[]} Parameter names + */ + private extractPathParameterNames(path: string): string[] { + const braceMatches = path.match(/\{([^}]+)\}/g) || [] + if (braceMatches.length > 0) { + return braceMatches.map((param) => param.slice(1, -1)) + } + + const colonMatches = path.match(/:([^/]+)/g) || [] + return colonMatches.map((param) => param.slice(1)) + } + /** * Normalizes examples according to the schema. * @param {any} example Example value From 3998752e85e120624033da2e3e690eef493e8655 Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Sun, 5 Oct 2025 17:01:33 +0900 Subject: [PATCH 2/4] apply review : display warn when has duplicated header key --- lib/dsl/test-builders/RequestBuilder.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/dsl/test-builders/RequestBuilder.ts b/lib/dsl/test-builders/RequestBuilder.ts index 2a89b40..8b4ae50 100644 --- a/lib/dsl/test-builders/RequestBuilder.ts +++ b/lib/dsl/test-builders/RequestBuilder.ts @@ -19,6 +19,7 @@ import { DSLField } from "../interface" import { ResponseBuilder } from "./ResponseBuilder" import { FIELD_TYPES } from "../interface/field" import { AbstractTestBuilder } from "./AbstractTestBuilder" +import logger from "../../config/logger" /** * Builder class for setting API request information. @@ -31,9 +32,18 @@ export class RequestBuilder extends AbstractTestBuilder { */ public header(headers: Record>): this { const normalizedHeaders: Record> = {} + const seen = new Set() Object.entries(headers).forEach(([headerName, headerValue]) => { - normalizedHeaders[headerName.toLowerCase()] = headerValue + const normalized = headerName.toLowerCase() + + if (seen.has(normalized)) { + logger.warn(`Duplicate header detected: "${headerName}" (already set)`) + return + } + + seen.add(normalized) + normalizedHeaders[normalized] = headerValue }) this.config.requestHeaders = normalizedHeaders From 5cae1f6337d015d9c5a0bef7f4432e3c2fb37ad3 Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Sun, 5 Oct 2025 17:16:37 +0900 Subject: [PATCH 3/4] apply review : support hypen path-variable and add some tc about it --- .../unit/dsl/OpenAPIGenerator.test.ts | 20 +++++++++++++++++++ lib/dsl/generator/OpenAPIGenerator.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts b/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts index 26860f4..afa1f4f 100644 --- a/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts +++ b/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts @@ -231,4 +231,24 @@ describe("OpenAPIGenerator", () => { assert.isUndefined(spec.paths["/test/no-body-responses"].get.responses["400"].content) }) }) + + describe("normalizePathTemplate", () => { + it("should convert Express-style params to OpenAPI format", () => { + const generator = OpenAPIGenerator.getInstance() + const normalized = generator["normalizePathTemplate"]("/users/:userId/posts/:postId") + assert.strictEqual(normalized, "/users/{userId}/posts/{postId}") + }) + + it("should handle already normalized paths", () => { + const generator = OpenAPIGenerator.getInstance() + const normalized = generator["normalizePathTemplate"]("/users/{userId}") + assert.strictEqual(normalized, "/users/{userId}") + }) + + it("should handle already normalized paths", () => { + const generator = OpenAPIGenerator.getInstance() + const normalized = generator["normalizePathTemplate"]("/files/:file-name") + assert.strictEqual(normalized, "/files/{file-name}") + }) + }) }) diff --git a/lib/dsl/generator/OpenAPIGenerator.ts b/lib/dsl/generator/OpenAPIGenerator.ts index a983495..88a0afa 100644 --- a/lib/dsl/generator/OpenAPIGenerator.ts +++ b/lib/dsl/generator/OpenAPIGenerator.ts @@ -581,7 +581,7 @@ export class OpenAPIGenerator implements IOpenAPIGenerator { * @returns {string} Normalized OpenAPI path */ private normalizePathTemplate(path: string): string { - return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}") + return path.replace(/:([A-Za-z0-9_-]+)/g, "{$1}") } /** From f1dd5c824f2e54f339a10b506dffc9e989f2c0fc Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Sun, 5 Oct 2025 17:34:48 +0900 Subject: [PATCH 4/4] apply review : refactor tc --- .../unit/dsl/OpenAPIGenerator.test.ts | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts b/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts index afa1f4f..53d5c9f 100644 --- a/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts +++ b/lib/__tests__/unit/dsl/OpenAPIGenerator.test.ts @@ -233,19 +233,37 @@ describe("OpenAPIGenerator", () => { }) describe("normalizePathTemplate", () => { - it("should convert Express-style params to OpenAPI format", () => { + it("should handle paths without parameters", () => { const generator = OpenAPIGenerator.getInstance() - const normalized = generator["normalizePathTemplate"]("/users/:userId/posts/:postId") + const normalized = generator["normalizePathTemplate"]("/users") + assert.strictEqual(normalized, "/users") + }) + + it("should handle mixed format paths", () => { + const generator = OpenAPIGenerator.getInstance() + const normalized = generator["normalizePathTemplate"]("/users/{userId}/posts/:postId") assert.strictEqual(normalized, "/users/{userId}/posts/{postId}") }) - it("should handle already normalized paths", () => { + it("should handle parameters with underscores", () => { + const generator = OpenAPIGenerator.getInstance() + const normalized = generator["normalizePathTemplate"]("/items/:item_id") + assert.strictEqual(normalized, "/items/{item_id}") + }) + + it("should handle empty path", () => { + const generator = OpenAPIGenerator.getInstance() + const normalized = generator["normalizePathTemplate"]("") + assert.strictEqual(normalized, "") + }) + + it("should handle root path", () => { const generator = OpenAPIGenerator.getInstance() - const normalized = generator["normalizePathTemplate"]("/users/{userId}") - assert.strictEqual(normalized, "/users/{userId}") + const normalized = generator["normalizePathTemplate"]("/") + assert.strictEqual(normalized, "/") }) - it("should handle already normalized paths", () => { + it("should handle hyphenated parameter names", () => { const generator = OpenAPIGenerator.getInstance() const normalized = generator["normalizePathTemplate"]("/files/:file-name") assert.strictEqual(normalized, "/files/{file-name}")