openapi-ts-mock-generator
Version:
typescript mock data generator based openapi
1 lines • 121 kB
Source Map (JSON)
{"version":3,"sources":["../src/core/types.ts","../src/core/config.ts","../src/core/options.ts","../src/core/index.ts","../src/utils/string-utils.ts","../src/utils/code-utils.ts","../src/utils/array-utils.ts","../src/utils/file-utils.ts","../src/utils/validation.ts","../src/utils/index.ts","../src/parsers/openapi-parser.ts","../src/parsers/schema-parser.ts","../src/parsers/faker-parser.ts","../src/parsers/index.ts","../src/generators/api-generator.ts","../src/generators/schema-generator.ts","../src/generators/response-generator.ts","../src/generators/handler-generator.ts","../src/generators/faker-generator.ts","../src/generators/index.ts","../src/index.ts","../node_modules/cac/dist/index.mjs","../src/cli.ts"],"sourcesContent":["import { OpenAPIV3_1 } from \"openapi-types\"\n\n/**\n * 메인 옵션 타입\n * CLI 및 프로그래매틱 API에서 사용되는 모든 설정을 포함\n */\nexport type Options = {\n path: string\n arrayMinLength: number\n arrayMaxLength: number\n includeCodes: number[] | undefined\n baseDir: string\n specialPath: string | undefined\n handlerUrl: string\n fakerLocale: string\n generateTarget: string\n clear: boolean\n} & TypeScriptCodeOptions\n\n/**\n * 스키마 출력 기본 타입\n */\nexport type SchemaOutputType = string | number | boolean | null | undefined | Date\n\n/**\n * 중첩된 스키마 출력 타입 (재귀적)\n */\ntype NestedSchemaOutputType<T> =\n | {\n [K in keyof T]: T[K] extends object ? NestedSchemaOutputType<T[K]> : T[K]\n }\n | SchemaOutputType\n | {}\n\n/**\n * 파싱된 스키마 타입\n */\nexport type ParseSchemaType = NestedSchemaOutputType<string>\n\n/**\n * HTTP 메서드 열거형\n */\nexport enum HttpMethods {\n GET = \"get\",\n PUT = \"put\",\n POST = \"post\",\n DELETE = \"delete\",\n OPTIONS = \"options\",\n HEAD = \"head\",\n PATCH = \"patch\",\n TRACE = \"trace\",\n}\n\n/**\n * 응답 스키마 타입 정의\n */\nexport type ResponseSchemaType =\n | {\n type: \"anyOf\" | \"oneOf\" | \"array\"\n value: OpenAPIV3_1.SchemaObject\n }\n | {\n type: \"ref\"\n value: OpenAPIV3_1.ReferenceObject\n }\n | undefined\n\n/**\n * 정규화된 경로 타입\n * OpenAPI 경로 정보를 내부에서 사용하기 쉬운 형태로 변환한 타입\n */\nexport type PathNormalizedType = {\n pathname: string\n operationId: string\n summary: string\n method: HttpMethods\n responses: {\n statusCode: number\n description: string\n schema: ResponseSchemaType\n }[]\n tags: string[]\n}\n\n/**\n * 코드 생성 관련 공통 옵션\n */\nexport type CodeFormatOptions = {\n depth?: number\n isStatic?: boolean\n singleLine?: boolean\n}\n\n/**\n * TypeScript 코드 생성 옵션\n */\nexport type TypeScriptCodeOptions = {\n depth?: number\n isStatic: boolean\n isOptional: boolean\n}\n\n/**\n * null 또는 undefined가 아닌 값인지 확인하는 타입 가드\n */\nexport const isNotNullish = <TValue>(value: TValue | null | undefined): value is TValue => {\n return value !== null && value !== undefined\n}\n","import { Faker, ko } from \"@faker-js/faker\"\nimport { Options } from \"./types\"\n\n/**\n * 기본 옵션 설정\n */\nexport const defaultOptions: Options = {\n path: \"\",\n arrayMinLength: 1,\n arrayMaxLength: 3,\n includeCodes: undefined,\n baseDir: \"./\",\n specialPath: undefined,\n handlerUrl: \"*\",\n fakerLocale: \"ko\",\n generateTarget: \"api,schema\",\n clear: false,\n // TypeScriptCodeOptions\n isStatic: false,\n isOptional: false,\n}\n\n/**\n * 배열 길이 관련 상수\n */\nexport const ARRAY_MIN_LENGTH = 1\nexport const ARRAY_MAX_LENGTH = 3\n\n/**\n * 문자열 길이 관련 상수\n */\nexport const MIN_STRING_LENGTH = 3\nexport const MAX_STRING_LENGTH = 20\n\n/**\n * 정수 범위 관련 상수\n */\nexport const MIN_INTEGER = 1\nexport const MAX_INTEGER = 100000\n\n/**\n * 소수 범위 관련 상수\n */\nexport const MIN_NUMBER = 0\nexport const MAX_NUMBER = 100\n\n/**\n * 단어 길이 관련 상수\n */\nexport const MIN_WORD_LENGTH = 0\nexport const MAX_WORD_LENGTH = 3\n\n/**\n * Faker 시드값 (일관된 결과를 위함)\n */\nconst FAKER_SEED = 1\n\n/**\n * 전역 Faker 인스턴스\n * 한국어 로케일을 기본으로 설정하고 시드를 고정하여 일관된 결과 제공\n */\nexport const faker = new Faker({\n locale: [ko],\n})\nfaker.seed(FAKER_SEED)\n\n/**\n * 생성된 파일의 상단에 추가되는 주석\n */\nexport const GEN_COMMENT =\n \"/* Do not edit this file. */\\n/* This file generated by openapi-ts-mock-generator. */\\n\\n\"\n","import { Options } from \"./types\"\nimport { defaultOptions } from \"./config\"\n\n/**\n * 사용자 옵션을 기본 옵션과 병합하여 완전한 옵션 객체를 생성\n */\nexport const mergeOptions = (userOptions: Partial<Options>): Options => {\n return {\n ...defaultOptions,\n ...userOptions,\n }\n}\n\n/**\n * CLI에서 받은 원시 옵션을 내부 옵션 형태로 변환\n */\nexport const transformCliOptions = (rawOptions: any): Options => {\n return {\n path: rawOptions.path || defaultOptions.path,\n baseDir: rawOptions.baseDir || defaultOptions.baseDir,\n arrayMinLength: parseInt(rawOptions.arrayMinLength) || defaultOptions.arrayMinLength,\n arrayMaxLength: parseInt(rawOptions.arrayMaxLength) || defaultOptions.arrayMaxLength,\n handlerUrl: rawOptions.handlerUrl || defaultOptions.handlerUrl,\n fakerLocale: rawOptions.locales || defaultOptions.fakerLocale,\n generateTarget: rawOptions.generateTarget || defaultOptions.generateTarget,\n specialPath: rawOptions.specialPath || defaultOptions.specialPath,\n clear: rawOptions.clear || defaultOptions.clear,\n includeCodes: rawOptions.includeCodes\n ? rawOptions.includeCodes\n .toString()\n .split(\",\")\n .map((code: string) => parseInt(code))\n : undefined,\n // TypeScriptCodeOptions\n isStatic: rawOptions.static || defaultOptions.isStatic,\n isOptional: rawOptions.optional || defaultOptions.isOptional,\n }\n}\n\n/**\n * 옵션 유효성 검증\n */\nexport const validateOptions = (options: Options): string[] => {\n const errors: string[] = []\n\n if (!options.path) {\n errors.push(\"path is required\")\n }\n\n if (\n options.arrayMinLength &&\n options.arrayMaxLength &&\n options.arrayMinLength > options.arrayMaxLength\n ) {\n errors.push(\"arrayMinLength should not be greater than arrayMaxLength\")\n }\n\n if (\n options.generateTarget &&\n !options.generateTarget.split(\",\").every((target) => [\"api\", \"schema\"].includes(target.trim()))\n ) {\n errors.push(\"generateTarget should contain only 'api' and/or 'schema'\")\n }\n\n return errors\n}\n","/**\n * Core 모듈 - 타입, 설정, 옵션 처리를 담당\n */\n\n// Types\nexport * from \"./types\"\n\n// Configuration\nexport * from \"./config\"\n\n// Options handling\nexport * from \"./options\"\n","/**\n * 문자열 처리 관련 유틸리티 함수들\n */\n\n/**\n * UUID를 Base64 형태로 변환\n * URL-safe Base64 인코딩을 사용하여 패딩 제거\n */\nexport const uuidToB64 = (uuid: string): string => {\n const uuidBuffer = Buffer.from(uuid.replace(/-/g, \"\"), \"hex\")\n const base64Uuid = uuidBuffer\n .toString(\"base64\")\n .replace(/\\+/g, \"-\") // + 를 - 로\n .replace(/\\//g, \"_\") // / 를 _ 로\n .replace(/=/g, \"\") // 패딩 제거\n return base64Uuid\n}\n\n/**\n * camelCase 문자열을 kebab-case로 변환\n */\nexport const camelToKebab = (str: string): string => {\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase()\n}\n\n/**\n * 문자열이 URL인지 확인\n */\nexport const isUrl = (str: string): boolean => {\n return str.startsWith(\"http://\") || str.startsWith(\"https://\")\n}\n\n/**\n * 파일 확장자 추출\n */\nexport const getFileExtension = (filename: string): string => {\n return filename.split(\".\").pop() || \"\"\n}\n","/**\n * 코드 생성 관련 유틸리티 함수들\n */\n\nimport { ParseSchemaType, TypeScriptCodeOptions, CodeFormatOptions } from \"../core\"\n\n/**\n * 객체를 TypeScript 코드로 변환 (nullable 타입 확장 지원)\n * 동적 모킹에서 optional 필드를 지원하는 코드를 생성\n */\nexport const toTypeScriptCode = (\n param: ParseSchemaType,\n options: TypeScriptCodeOptions\n): string => {\n const { depth = 0, isStatic } = options\n\n const prefixSpace = \" \".repeat(depth * 2) // 들여쓰기용\n\n if (param === null) {\n return \"null\"\n }\n\n if (Array.isArray(param)) {\n const results = param\n .map((elem) => toTypeScriptCode(elem, { ...options, depth: depth + 1 }))\n .join(\",\\n\" + prefixSpace)\n return [\"[\", results, \"]\"].join(\"\\n\" + prefixSpace)\n }\n\n if (typeof param === \"object\") {\n const results = Object.entries(param)\n .map(([key, value]) => {\n return generateObjectProperty(key, value, options, prefixSpace)\n })\n .join(\"\\n\" + prefixSpace)\n\n return [\"{\", `${results}`, \"}\"].join(\"\\n\" + prefixSpace)\n }\n\n // 문자열 처리\n if (typeof param === \"string\") {\n // 동적 faker 호출은 그대로 유지\n if (\n isStatic === false &&\n (param.startsWith(\"faker\") || param.startsWith(\"Buffer.from(faker\"))\n ) {\n return param\n }\n\n // \" as const\" 분리하여 처리\n if (param.endsWith(\" as const\")) {\n return `\"${param.slice(0, -\" as const\".length)}\" as const`\n }\n }\n\n return JSON.stringify(param)\n}\n\n/**\n * nullable 타입 확장 여부 확인\n */\nconst shouldApplyNullableExtension = (value: any, isOptional: boolean): boolean => {\n if (!isOptional) return false\n\n // 값이 null인 경우\n if (value === null) return true\n\n // 문자열 및 null을 포함한 경우\n if (typeof value === \"string\" && value.includes(\",null\")) {\n return true\n }\n\n return false\n}\n\n/**\n * 객체 프로퍼티를 생성 (nullable 타입 확장 포함)\n */\nconst generateObjectProperty = (\n key: string,\n value: any,\n options: TypeScriptCodeOptions,\n prefixSpace: string\n): string => {\n const { isOptional, depth = 0 } = options\n\n // nullable 타입 확장 로직\n const shouldApplyNullable = shouldApplyNullableExtension(value, isOptional)\n const nullableTypeExtensionStart = shouldApplyNullable\n ? `...(faker.datatype.boolean() ? {\\n${prefixSpace}`\n : \"\"\n const nullableTypeExtensionEnd = shouldApplyNullable ? `\\n${prefixSpace}} : {})` : \"\"\n\n const propertyValue = toTypeScriptCode(value, {\n ...options,\n depth: depth + 1,\n })\n\n return `${nullableTypeExtensionStart}${prefixSpace}${key}: ${propertyValue}${nullableTypeExtensionEnd},`\n}\n\n/**\n * 멀티라인 코드를 한 줄로 압축\n */\nexport const compressCode = (code: string): string => {\n return code\n .replace(/\\n/g, \" \") // 줄바꿈을 공백으로\n .replace(/\\s+/g, \" \") // 연속된 공백을 하나로\n .replace(/\\s\\./g, \".\") // 공백 + 점을 점으로\n .trim()\n}\n\n/**\n * TypeScript 인터페이스 코드 생성\n */\nexport const generateInterface = (name: string, properties: Record<string, string>): string => {\n const props = Object.entries(properties)\n .map(([key, type]) => ` ${key}: ${type}`)\n .join(\"\\n\")\n\n return `interface ${name} {\\n${props}\\n}`\n}\n\n/**\n * TypeScript 타입 별칭 코드 생성\n */\nexport const generateTypeAlias = (name: string, type: string): string => {\n return `type ${name} = ${type}`\n}\n","/**\n * 배열 처리 관련 유틸리티 함수들\n */\n\nimport { faker } from \"../core\"\n\n/**\n * 지정된 범위 내에서 랜덤한 길이의 배열을 생성\n * 배열의 각 요소는 인덱스 값으로 초기화됨\n */\nexport const getRandomLengthArray = (min: number = 1, max: number = 3): number[] => {\n const length = faker.number.int({ min, max })\n return Array.from({ length }, (_, i) => i)\n}\n\n/**\n * 배열을 지정된 크기로 청크 단위로 분할\n */\nexport const chunkArray = <T>(array: T[], size: number): T[][] => {\n const chunks: T[][] = []\n for (let i = 0; i < array.length; i += size) {\n chunks.push(array.slice(i, i + size))\n }\n return chunks\n}\n\n/**\n * 배열에서 중복 제거 (primitive 타입용)\n */\nexport const unique = <T>(array: T[]): T[] => {\n return [...new Set(array)]\n}\n\n/**\n * 배열에서 중복 제거 (객체용, 키 기준)\n */\nexport const uniqueBy = <T>(array: T[], keyFn: (item: T) => any): T[] => {\n const seen = new Set()\n return array.filter((item) => {\n const key = keyFn(item)\n if (seen.has(key)) {\n return false\n }\n seen.add(key)\n return true\n })\n}\n","/**\n * 파일 시스템 관련 유틸리티 함수들\n */\n\nimport { existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, readFileSync } from \"fs\"\nimport * as path from \"path\"\n\n/**\n * 디렉토리가 존재하지 않으면 생성\n */\nexport const ensureDir = (dirPath: string): void => {\n if (!existsSync(dirPath)) {\n mkdirSync(dirPath, { recursive: true })\n }\n}\n\n/**\n * 디렉토리를 비우고 정리\n */\nexport const clearDirectory = (dirPath: string): void => {\n if (existsSync(dirPath)) {\n readdirSync(dirPath).forEach((file) => {\n rmSync(path.join(dirPath, file))\n })\n }\n}\n\n/**\n * 안전하게 파일 쓰기 (디렉토리 자동 생성)\n */\nexport const safeWriteFile = (filePath: string, content: string): void => {\n const dir = path.dirname(filePath)\n ensureDir(dir)\n writeFileSync(filePath, content)\n}\n\n/**\n * JSON 파일 읽기 (파일이 없으면 기본값 반환)\n */\nexport const readJsonFile = <T>(filePath: string, defaultValue: T): T => {\n if (!existsSync(filePath)) {\n return defaultValue\n }\n\n try {\n const content = readFileSync(filePath, \"utf-8\")\n return JSON.parse(content)\n } catch (error) {\n console.warn(`Failed to read JSON file ${filePath}:`, error)\n return defaultValue\n }\n}\n\n/**\n * 파일 경로가 URL인지 로컬 파일인지 확인하여 절대 경로 반환\n */\nexport const resolveFilePath = (inputPath: string, baseDir?: string): string => {\n if (inputPath.startsWith(\"http\")) {\n return inputPath\n }\n\n if (baseDir) {\n return path.join(baseDir, inputPath)\n }\n\n return inputPath\n}\n\n/**\n * 여러 파일명 조합하여 고유한 파일명 생성\n */\nexport const createUniqueFileName = (baseName: string, extension: string): string => {\n const timestamp = Date.now()\n return `${baseName}-${timestamp}.${extension}`\n}\n","/**\n * 검증 관련 유틸리티 함수들\n */\n\n/**\n * 값이 null이나 undefined가 아닌지 확인하는 타입 가드\n * core/types.ts의 isNotNullish와 동일하지만 utils에서 독립적으로 사용\n */\nexport const isNotEmpty = <T>(value: T | null | undefined): value is T => {\n return value !== null && value !== undefined\n}\n\n/**\n * 문자열이 비어있지 않은지 확인\n */\nexport const isNonEmptyString = (value: any): value is string => {\n return typeof value === \"string\" && value.trim().length > 0\n}\n\n/**\n * 숫자가 유효한 범위 내에 있는지 확인\n */\nexport const isInRange = (value: number, min: number, max: number): boolean => {\n return value >= min && value <= max\n}\n\n/**\n * 배열이 비어있지 않은지 확인\n */\nexport const isNonEmptyArray = <T>(value: any): value is T[] => {\n return Array.isArray(value) && value.length > 0\n}\n\n/**\n * 객체가 비어있지 않은지 확인\n */\nexport const isNonEmptyObject = (value: any): value is Record<string, any> => {\n return typeof value === \"object\" && value !== null && Object.keys(value).length > 0\n}\n\n/**\n * HTTP 상태 코드가 유효한지 확인\n */\nexport const isValidStatusCode = (code: number): boolean => {\n return isInRange(code, 100, 599)\n}\n\n/**\n * 파일 확장자가 허용된 목록에 있는지 확인\n */\nexport const hasValidExtension = (filename: string, allowedExtensions: string[]): boolean => {\n const extension = filename.split(\".\").pop()?.toLowerCase()\n return extension ? allowedExtensions.includes(extension) : false\n}\n\n/**\n * URL 형식이 올바른지 간단히 확인\n */\nexport const isValidUrl = (url: string): boolean => {\n try {\n new URL(url)\n return true\n } catch {\n return false\n }\n}\n","/**\n * Utils 모듈 - 공통 유틸리티 함수들\n */\n\n// String utilities\nexport * from \"./string-utils\"\n\n// Code generation utilities\nexport * from \"./code-utils\"\n\n// Array utilities\nexport * from \"./array-utils\"\n\n// File system utilities\nexport * from \"./file-utils\"\n\n// Validation utilities\nexport * from \"./validation\"\n","/**\n * OpenAPI 문서 파싱 관련 유틸리티\n */\n\nimport SwaggerParser from \"@apidevtools/swagger-parser\"\nimport { OpenAPIV3_1 } from \"openapi-types\"\n\n/**\n * OpenAPI 문서를 역참조(dereference)하여 로드\n * 모든 $ref를 실제 스키마로 해석하여 반환\n */\nexport const getOpenAPIDocsDeref = async (\n path: string\n): Promise<OpenAPIV3_1.Document | undefined> => {\n const doc = await SwaggerParser.dereference(path)\n const isOpenApiV3 = \"openapi\" in doc && doc.openapi.startsWith(\"3\")\n if (isOpenApiV3) return doc as OpenAPIV3_1.Document\n return undefined\n}\n\n/**\n * OpenAPI 문서를 번들(bundle)하여 로드\n * 외부 참조를 포함하여 하나의 문서로 통합\n */\nexport const getOpenAPIDocsBundle = async (\n path: string\n): Promise<OpenAPIV3_1.Document | undefined> => {\n const doc = await SwaggerParser.bundle(path)\n const isOpenApiV3 = \"openapi\" in doc && doc.openapi.startsWith(\"3\")\n if (isOpenApiV3) return doc as OpenAPIV3_1.Document\n return undefined\n}\n\n/**\n * OpenAPI 문서의 유효성을 검증\n */\nexport const validateOpenAPIDoc = async (path: string): Promise<boolean> => {\n try {\n await SwaggerParser.validate(path)\n return true\n } catch (error) {\n console.error(\"OpenAPI validation failed:\", error)\n return false\n }\n}\n\n/**\n * OpenAPI 문서에서 스키마 컴포넌트 추출\n */\nexport const extractSchemas = (\n doc: OpenAPIV3_1.Document\n): Record<string, OpenAPIV3_1.SchemaObject> => {\n return doc.components?.schemas || {}\n}\n\n/**\n * OpenAPI 문서에서 경로 정보 추출\n */\nexport const extractPaths = (doc: OpenAPIV3_1.Document): OpenAPIV3_1.PathsObject => {\n return doc.paths || {}\n}\n\n/**\n * OpenAPI 버전 확인\n */\nexport const getOpenAPIVersion = (doc: any): string => {\n return doc.openapi || doc.swagger || \"unknown\"\n}\n","/**\n * OpenAPI 스키마 파싱 및 값 생성 로직\n */\n\nimport {\n Options,\n ParseSchemaType,\n SchemaOutputType,\n faker,\n MIN_STRING_LENGTH,\n MAX_STRING_LENGTH,\n MIN_INTEGER,\n MAX_INTEGER,\n MIN_NUMBER,\n MAX_NUMBER,\n MIN_WORD_LENGTH,\n MAX_WORD_LENGTH,\n TypeScriptCodeOptions,\n} from \"../core\"\nimport { compressCode, uuidToB64, getRandomLengthArray, toTypeScriptCode } from \"../utils\"\nimport SwaggerParser from \"@apidevtools/swagger-parser\"\nimport { pascalCase } from \"change-case-all\"\nimport { SchemaObject, isReference } from \"oazapfts/generate\"\nimport { OpenAPIV3_1 } from \"openapi-types\"\n\n/**\n * OpenAPI 스키마를 파싱하여 실제 값을 생성\n */\nexport const parseSchema = (\n schemaValue: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject,\n specialSchema: {\n titleSpecial: Record<string, SchemaOutputType>\n descriptionSpecial: Record<string, SchemaOutputType>\n },\n options: Options,\n outputSchema: ParseSchemaType = {}\n): ParseSchemaType => {\n if (isReference(schemaValue)) {\n console.warn(\"can't parse reference schema\", schemaValue, schemaValue.$ref)\n return\n }\n\n if (schemaValue.type === \"object\") {\n if (schemaValue.properties === undefined) return {}\n return Object.entries(schemaValue.properties).reduce((acc, [key, field]) => {\n acc[key] = parseSchema(field, specialSchema, options, outputSchema) as SchemaOutputType\n return acc\n }, {} as Record<string, SchemaOutputType>)\n } else if (schemaValue.enum !== undefined) {\n // enum value\n const enumValue = options.isStatic\n ? faker.helpers.arrayElement(schemaValue.enum)\n : `faker.helpers.arrayElement<${schemaValue.enum\n .map((item) => `\"${item}\"`)\n .join(\" | \")}>(${toTypeScriptCode(schemaValue.enum, {\n depth: 0,\n ...options,\n })})`\n if (options.isStatic && typeof enumValue === \"string\") return enumValue + \" as const\"\n return enumValue\n } else if (schemaValue.allOf !== undefined) {\n // allOf value, sub model\n const allOfValue = schemaValue.allOf\n return faker.helpers.arrayElement(\n allOfValue.map((field) => {\n return parseSchema(field, specialSchema, options, outputSchema)\n })\n )\n } else if (schemaValue.anyOf !== undefined) {\n // anyOf value, select one or more. ex) string or null\n const anyOfValue = schemaValue.anyOf\n return options.isStatic\n ? faker.helpers.arrayElement(\n anyOfValue.map((field) => {\n return parseSchema(field, specialSchema, options, outputSchema)\n })\n )\n : compressCode(\n `\n faker.helpers.arrayElement([\n ${anyOfValue.map((field) =>\n toTypeScriptCode(parseSchema(field, specialSchema, options, {}), {\n depth: 0,\n ...options,\n })\n )}\n ])\n `\n )\n } else if (schemaValue.oneOf !== undefined) {\n // oneOf value, exactly one. Can't find example\n const oneOfValue = schemaValue.oneOf\n return options.isStatic\n ? faker.helpers.arrayElement(\n oneOfValue.map((field) => {\n return parseSchema(field, specialSchema, options, outputSchema)\n })\n )\n : compressCode(\n `\n faker.helpers.arrayElement([\n ${oneOfValue.map((field) =>\n toTypeScriptCode(parseSchema(field, specialSchema, options, {}), {\n depth: 0,\n ...options,\n })\n )}\n ])\n `\n )\n } else if (schemaValue.type === \"array\") {\n if (\"prefixItems\" in schemaValue) {\n const length = faker.number.int({\n min: schemaValue.minItems,\n max: schemaValue.maxItems,\n })\n\n return (schemaValue.prefixItems as Array<SchemaObject>)\n .slice(0, length)\n .map((field) => parseSchema(field, specialSchema, options, outputSchema)) as (\n | SchemaOutputType\n | Record<string, SchemaOutputType>\n )[]\n }\n // array\n const arrayValue = schemaValue.items\n return getRandomLengthArray(options.arrayMinLength, options.arrayMaxLength).map(() =>\n parseSchema(arrayValue, specialSchema, options, outputSchema)\n ) as (SchemaOutputType | Record<string, SchemaOutputType>)[]\n }\n return valueGenerator(schemaValue, specialSchema, options)\n}\n\n/**\n * 참조 스키마를 파싱하여 이름과 값을 반환\n */\nexport const refSchemaParser = (ref: string, refs: SwaggerParser[\"$refs\"]) => {\n const schemaName = pascalCase(ref.replace(\"#/components/schemas/\", \"\"))\n const schemaValue = refs.get(ref) as OpenAPIV3_1.SchemaObject\n return { name: schemaName, value: schemaValue }\n}\n\n/**\n * 스키마 타입별로 실제 값을 생성하는 함수\n */\nexport const valueGenerator = (\n schemaValue: OpenAPIV3_1.SchemaObject,\n specialSchema: {\n titleSpecial: Record<string, SchemaOutputType>\n descriptionSpecial: Record<string, SchemaOutputType>\n },\n options: TypeScriptCodeOptions\n): ParseSchemaType => {\n const { isStatic } = options\n // if title or description in special keys\n // return special faker data\n const { titleSpecial, descriptionSpecial } = specialSchema\n if (schemaValue.title && titleSpecial[schemaValue.title]) {\n return titleSpecial[schemaValue.title]\n } else if (schemaValue.description && descriptionSpecial[schemaValue.description]) {\n return descriptionSpecial[schemaValue.description]\n }\n\n if (schemaValue.type === \"string\" && schemaValue.format === \"date-time\") {\n // date-time, 2017-07-21T17:32:28Z\n return isStatic\n ? faker.date\n .between({\n from: \"2020-01-01T00:00:00.000Z\",\n to: \"2030-12-31T23:59:59.999Z\",\n })\n .toISOString()\n : compressCode(\n `\n faker.date.between({\n from: \"2020-01-01T00:00:00.000Z\",\n to: \"2030-12-31T23:59:59.999Z\",\n })\n .toISOString()\n `\n )\n } else if (schemaValue.type === \"string\" && schemaValue.format === \"date\") {\n // date, 2017-07-21\n return isStatic\n ? faker.date\n .between({\n from: \"2020-01-01T00:00:00.000Z\",\n to: \"2030-12-31T23:59:59.999Z\",\n })\n .toISOString()\n .split(\"T\")[0]\n : compressCode(\n `\n faker.date.between({\n from: \"2020-01-01T00:00:00.000Z\",\n to: \"2030-12-31T23:59:59.999Z\",\n })\n .toISOString()\n .split(\"T\")[0]\n `\n )\n } else if (schemaValue.type === \"string\" && schemaValue.pattern) {\n return isStatic\n ? faker.helpers.fromRegExp(schemaValue.pattern)\n : `faker.helpers.fromRegExp(/${schemaValue.pattern}/)`\n } else if (schemaValue.type === \"string\" && schemaValue.title?.toLowerCase() === \"b64uuid\") {\n // generate base 64 uuid\n const baseUuid = faker.string.uuid()\n return isStatic\n ? uuidToB64(baseUuid)\n : compressCode(\n `\n Buffer.from(faker.string.uuid().replace(/-/g, \"\"), \"hex\")\n .toString(\"base64\")\n .replace(/\\\\+/g, \"-\")\n .replace(/\\\\//g, \"_\")\n .replace(/=/g, \"\")\n `\n )\n } else if (schemaValue.type === \"string\") {\n const minLength =\n schemaValue.minLength ??\n Math.min(MIN_STRING_LENGTH, schemaValue.maxLength ?? MAX_STRING_LENGTH)\n const maxLength =\n schemaValue.maxLength ??\n Math.max(MAX_STRING_LENGTH, schemaValue.minLength ?? MIN_STRING_LENGTH)\n\n return isStatic\n ? faker.string.alphanumeric({\n length: { min: minLength, max: maxLength },\n })\n : compressCode(\n `\n faker.string.alphanumeric({\n length: { min: ${minLength}, max: ${maxLength} },\n })\n `\n )\n } else if (schemaValue.type === \"integer\") {\n return isStatic\n ? faker.number.int({ min: MIN_INTEGER, max: MAX_INTEGER })\n : compressCode(\n `\n faker.number.int({ min: ${MIN_INTEGER}, max: ${MAX_INTEGER} })\n `\n )\n } else if (schemaValue.type === \"number\") {\n const minNumber = schemaValue.minimum ?? Math.min(MIN_NUMBER, schemaValue.maximum ?? MAX_NUMBER)\n const maxNumber = schemaValue.maximum ?? Math.max(MAX_NUMBER, schemaValue.minimum ?? MIN_NUMBER)\n return isStatic\n ? faker.number.float({\n min: minNumber,\n max: maxNumber,\n fractionDigits: 2,\n })\n : compressCode(\n `\n faker.number.float({\n min: ${minNumber},\n max: ${maxNumber},\n fractionDigits: 2,\n })\n `\n )\n } else if (schemaValue.type === \"boolean\") {\n return isStatic ? faker.datatype.boolean() : \"faker.datatype.boolean()\"\n } else if (schemaValue.type === \"null\") {\n return null\n } else if (Object.keys(schemaValue).length === 0) {\n // array any. ex) blank=True list\n return isStatic\n ? faker.word.words({\n count: {\n min: MIN_WORD_LENGTH,\n max: MAX_WORD_LENGTH,\n },\n })\n : compressCode(\n `\n faker.word.words({\n count: {\n min: ${MIN_WORD_LENGTH},\n max: ${MAX_WORD_LENGTH},\n },\n })\n `\n )\n }\n\n return isStatic ? faker.word.adjective() : \"faker.word.adjective()\"\n}\n","/**\n * Faker 관련 파싱 및 특별한 데이터 생성 로직\n */\n\nimport { Options, SchemaOutputType, faker } from \"../core\"\nimport { toTypeScriptCode, readJsonFile } from \"../utils\"\nimport { join } from \"path\"\n\n/**\n * 특별한 Faker 규칙을 파싱하여 적용\n * titles.json과 descriptions.json에서 사용자 정의 faker 규칙을 로드\n */\nexport const specialFakerParser = (options: Options) => {\n if (options.specialPath === undefined)\n return {\n titleSpecial: {},\n descriptionSpecial: {},\n }\n\n const titlePath = join(options.baseDir ?? \"\", options.specialPath, \"titles.json\")\n const descPath = join(options.baseDir ?? \"\", options.specialPath, \"descriptions.json\")\n const titleSpecialKey: Record<string, object> = readJsonFile(titlePath, {})\n const descriptionSpecialKey: Record<string, object> = readJsonFile(descPath, {})\n\n const titleSpecial = Object.entries(titleSpecialKey).reduce((acc, [key, value]) => {\n const fakerValue = getFakerValue(value, options)\n acc[key] = fakerValue\n return acc\n }, {} as Record<string, SchemaOutputType>)\n\n const descriptionSpecial = Object.entries(descriptionSpecialKey).reduce((acc, [key, value]) => {\n const fakerValue = getFakerValue(value, options)\n acc[key] = fakerValue\n return acc\n }, {} as Record<string, SchemaOutputType>)\n\n return { titleSpecial, descriptionSpecial }\n}\n\n/**\n * 특별한 Faker 값 정의에서 실제 값을 생성\n */\nconst getFakerValue = (value: object, options: Options): SchemaOutputType => {\n if (\"value\" in value) {\n // value type, use directly\n return value.value as SchemaOutputType\n }\n\n if (\"module\" in value && \"type\" in value) {\n // dynamic faker\n if (options.isStatic === false) {\n const fakerOption =\n \"options\" in value\n ? toTypeScriptCode(value.options, {\n depth: 0,\n ...options,\n })\n : \"\"\n return `faker.${value.module}.${value.type}(${fakerOption})`\n }\n\n // faker type, make faker\n const fakerModule = faker[value.module as keyof typeof faker]\n if (fakerModule === undefined) {\n console.warn(\"can't find faker module\", fakerModule)\n return undefined\n }\n\n const fakerFunc = fakerModule[value.type as keyof typeof fakerModule] as Function\n if (fakerFunc === undefined || typeof fakerFunc !== \"function\") {\n console.warn(\"can't find faker function\", fakerFunc)\n return undefined\n }\n\n return \"options\" in value ? fakerFunc(value.options) : fakerFunc()\n }\n\n return undefined\n}\n\n/**\n * Faker 모듈의 유효성을 검증\n */\nexport const validateFakerModule = (moduleName: string): boolean => {\n return moduleName in faker\n}\n\n/**\n * Faker 함수의 유효성을 검증\n */\nexport const validateFakerFunction = (moduleName: string, functionName: string): boolean => {\n try {\n const fakerModule = faker[moduleName as keyof typeof faker]\n return typeof fakerModule?.[functionName as keyof typeof fakerModule] === \"function\"\n } catch {\n return false\n }\n}\n\n/**\n * 사용 가능한 Faker 모듈 목록 반환\n */\nexport const getAvailableFakerModules = (): string[] => {\n return Object.keys(faker).filter((key) => typeof faker[key as keyof typeof faker] === \"object\")\n}\n\n/**\n * 특정 Faker 모듈의 사용 가능한 함수 목록 반환\n */\nexport const getAvailableFakerFunctions = (moduleName: string): string[] => {\n try {\n const fakerModule = faker[moduleName as keyof typeof faker]\n if (!fakerModule) return []\n\n return Object.keys(fakerModule).filter(\n (key) => typeof fakerModule[key as keyof typeof fakerModule] === \"function\"\n )\n } catch {\n return []\n }\n}\n","/**\n * Parsers 모듈 - OpenAPI 파싱 관련 모든 기능\n */\n\n// OpenAPI document parsing\nexport * from \"./openapi-parser\"\n\n// Schema parsing\nexport * from \"./schema-parser\"\n\n// Faker parsing\nexport * from \"./faker-parser\"\n","/**\n * API 경로 정보 생성 로직\n * OpenAPI 문서에서 경로 정보를 추출하고 정규화하여 반환\n */\n\nimport {\n HttpMethods,\n Options,\n PathNormalizedType,\n ResponseSchemaType,\n SchemaOutputType,\n isNotNullish,\n} from \"../core\"\nimport { resolveFilePath } from \"../utils\"\nimport { getOpenAPIDocsBundle, parseSchema, specialFakerParser } from \"../parsers\"\nimport { isReference } from \"oazapfts/generate\"\n\n/**\n * OpenAPI 문서에서 API 경로 정보를 생성\n * 모든 경로와 메서드를 분석하여 정규화된 형태로 반환\n */\nexport const generateAPI = async (options: Options): Promise<PathNormalizedType[] | undefined> => {\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n const doc = await getOpenAPIDocsBundle(openapiPath)\n\n const samplePaths = doc?.paths\n if (samplePaths === undefined) {\n console.warn(\"No paths found\")\n return undefined\n }\n\n const specialFakers = specialFakerParser(options)\n const normalizedPaths = Object.entries(samplePaths).reduce((acc, [apiName, api]) => {\n if (api === undefined) return acc\n\n const paths = Object.values(HttpMethods)\n .map((method) => {\n if (api[method] === undefined) return undefined\n\n const responses = Object.entries(api[method]?.responses ?? [])\n .map(([statusCode, response]) => {\n if (isReference(response)) return undefined\n if (options.includeCodes && !options.includeCodes.includes(parseInt(statusCode)))\n return undefined\n\n const schema = response.content?.[\"application/json\"]?.schema ?? {}\n const compositeSchema = createCompositeSchema(schema, specialFakers, options)\n\n return {\n statusCode: parseInt(statusCode),\n description: response.description,\n schema: compositeSchema,\n }\n })\n .filter(isNotNullish)\n\n return {\n pathname: apiName.replace(/{/g, \":\").replace(/}/g, \"\"),\n operationId: api[method]?.operationId ?? \"\",\n summary: api[method]?.summary ?? \"\",\n tags: api[method]?.tags ?? [\"default\"],\n method,\n responses,\n } as PathNormalizedType\n })\n .filter(isNotNullish)\n\n return [...acc, ...paths]\n }, [] as PathNormalizedType[])\n\n return normalizedPaths\n}\n\n/**\n * 스키마에서 복합 스키마 타입을 생성\n */\nconst createCompositeSchema = (\n schema: any,\n specialFakers: ReturnType<typeof specialFakerParser>,\n options: Options\n): ResponseSchemaType => {\n if (\"oneOf\" in schema) {\n return {\n type: \"oneOf\",\n value: schema,\n } as ResponseSchemaType\n }\n\n if (\"anyOf\" in schema) {\n return {\n type: \"anyOf\",\n value: schema,\n } as ResponseSchemaType\n }\n\n if (\"type\" in schema && \"items\" in schema && schema.type === \"array\") {\n return {\n type: \"array\",\n value: schema.items,\n } as ResponseSchemaType\n }\n\n // Todo: can't find sample data\n // if (\"allOf\" in schema) {\n // return parseSchema(schema, {})\n // }\n\n if (isReference(schema)) {\n return { type: \"ref\", value: schema } as ResponseSchemaType\n }\n\n if (Object.keys(schema).length === 0) {\n // empty object return undefined\n return undefined\n }\n\n return parseSchema(schema ?? {}, specialFakers, options, {}) as ResponseSchemaType\n}\n\n/**\n * API 경로에서 고유한 태그 목록 추출\n */\nexport const extractUniqueTags = (paths: PathNormalizedType[]): string[] => {\n return Array.from(new Set(paths.map((path) => path.tags[0])))\n}\n\n/**\n * 특정 태그에 해당하는 경로들만 필터링\n */\nexport const filterPathsByTag = (\n paths: PathNormalizedType[],\n tag: string\n): PathNormalizedType[] => {\n return paths.filter((path) => path.tags.includes(tag))\n}\n\n/**\n * HTTP 메서드별로 경로들을 그룹화\n */\nexport const groupPathsByMethod = (\n paths: PathNormalizedType[]\n): Record<HttpMethods, PathNormalizedType[]> => {\n return paths.reduce((acc, path) => {\n if (!acc[path.method]) {\n acc[path.method] = []\n }\n acc[path.method].push(path)\n return acc\n }, {} as Record<HttpMethods, PathNormalizedType[]>)\n}\n\n/**\n * 경로에서 사용된 모든 스키마 참조 추출\n */\nexport const extractSchemaReferences = (paths: PathNormalizedType[]): string[] => {\n const refs = new Set<string>()\n\n paths.forEach((path) => {\n path.responses.forEach((response) => {\n if (response.schema?.type === \"ref\") {\n refs.add(response.schema.value.$ref)\n }\n })\n })\n\n return Array.from(refs)\n}\n","/**\n * 스키마 모킹 데이터 생성 로직\n * OpenAPI 스키마 컴포넌트에서 모킹 데이터를 생성\n */\n\nimport { Options, SchemaOutputType } from \"../core\"\nimport { resolveFilePath } from \"../utils\"\nimport { getOpenAPIDocsDeref, parseSchema, specialFakerParser } from \"../parsers\"\n\n/**\n * OpenAPI 문서에서 스키마 모킹 데이터를 생성\n * 모든 스키마 컴포넌트를 분석하여 실제 모킹 데이터로 변환\n */\nexport const generateSchema = async (\n options: Options\n): Promise<Record<string, SchemaOutputType> | undefined> => {\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n const doc = await getOpenAPIDocsDeref(openapiPath)\n\n const sampleSchemas = doc?.components?.schemas\n if (sampleSchemas === undefined) {\n console.warn(\"No schemas found\")\n return undefined\n }\n\n const specialFakers = specialFakerParser(options)\n return Object.entries(sampleSchemas).reduce((acc, [schemaName, schema]) => {\n acc[schemaName] = parseSchema(schema, specialFakers, options, {}) as SchemaOutputType\n return acc\n }, {} as Record<string, SchemaOutputType>)\n}\n\n/**\n * 특정 스키마만 선택적으로 생성\n */\nexport const generateSpecificSchemas = async (\n options: Options,\n schemaNames: string[]\n): Promise<Record<string, SchemaOutputType> | undefined> => {\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n const doc = await getOpenAPIDocsDeref(openapiPath)\n\n const sampleSchemas = doc?.components?.schemas\n if (sampleSchemas === undefined) {\n console.warn(\"No schemas found\")\n return undefined\n }\n\n const specialFakers = specialFakerParser(options)\n const filteredSchemas = Object.entries(sampleSchemas).filter(([schemaName]) =>\n schemaNames.includes(schemaName)\n )\n\n return filteredSchemas.reduce((acc, [schemaName, schema]) => {\n acc[schemaName] = parseSchema(schema, specialFakers, options, {}) as SchemaOutputType\n return acc\n }, {} as Record<string, SchemaOutputType>)\n}\n\n/**\n * 스키마 의존성 분석\n * 어떤 스키마가 다른 스키마를 참조하는지 분석\n */\nexport const analyzeSchemaDependencies = async (\n options: Options\n): Promise<Record<string, string[]>> => {\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n const doc = await getOpenAPIDocsDeref(openapiPath)\n\n const sampleSchemas = doc?.components?.schemas\n if (sampleSchemas === undefined) {\n return {}\n }\n\n const dependencies: Record<string, string[]> = {}\n\n Object.entries(sampleSchemas).forEach(([schemaName, schema]) => {\n dependencies[schemaName] = extractSchemaReferences(schema)\n })\n\n return dependencies\n}\n\n/**\n * 스키마에서 참조하는 다른 스키마들을 추출\n */\nconst extractSchemaReferences = (schema: any): string[] => {\n const refs: string[] = []\n\n const extractFromObject = (obj: any) => {\n if (typeof obj !== \"object\" || obj === null) return\n\n if (obj.$ref && typeof obj.$ref === \"string\") {\n const refName = obj.$ref.replace(\"#/components/schemas/\", \"\")\n refs.push(refName)\n return\n }\n\n Object.values(obj).forEach((value) => {\n if (Array.isArray(value)) {\n value.forEach(extractFromObject)\n } else {\n extractFromObject(value)\n }\n })\n }\n\n extractFromObject(schema)\n return refs\n}\n\n/**\n * 사용 가능한 모든 스키마 이름 목록 반환\n */\nexport const getAvailableSchemas = async (options: Options): Promise<string[]> => {\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n const doc = await getOpenAPIDocsDeref(openapiPath)\n\n const sampleSchemas = doc?.components?.schemas\n if (sampleSchemas === undefined) {\n return []\n }\n\n return Object.keys(sampleSchemas)\n}\n\n/**\n * 스키마 크기 정보 (프로퍼티 개수)\n */\nexport const getSchemaStats = async (\n options: Options\n): Promise<Record<string, { properties: number; required: number }>> => {\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n const doc = await getOpenAPIDocsDeref(openapiPath)\n\n const sampleSchemas = doc?.components?.schemas\n if (sampleSchemas === undefined) {\n return {}\n }\n\n const stats: Record<string, { properties: number; required: number }> = {}\n\n Object.entries(sampleSchemas).forEach(([schemaName, schema]) => {\n if (typeof schema === \"object\" && schema.type === \"object\") {\n stats[schemaName] = {\n properties: schema.properties ? Object.keys(schema.properties).length : 0,\n required: schema.required ? schema.required.length : 0,\n }\n }\n })\n\n return stats\n}\n","/**\n * 응답 모킹 함수 생성 로직\n * MSW에서 사용할 응답 모킹 함수들을 생성\n */\n\nimport { Options, PathNormalizedType } from \"../core\"\nimport {\n toTypeScriptCode,\n ensureDir,\n clearDirectory,\n resolveFilePath,\n safeWriteFile,\n getRandomLengthArray,\n} from \"../utils\"\nimport { parseSchema, refSchemaParser, specialFakerParser } from \"../parsers\"\nimport SwaggerParser from \"@apidevtools/swagger-parser\"\nimport { pascalCase } from \"change-case-all\"\nimport * as path from \"path\"\nimport { isReference } from \"oazapfts/generate\"\n\n/**\n * 응답 모킹 함수들을 생성하고 파일로 출력\n */\nexport const generateResponses = async (\n paths: PathNormalizedType[],\n options: Options\n): Promise<void> => {\n const parser = new SwaggerParser()\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n await parser.dereference(openapiPath)\n const refs = parser.$refs\n\n const firstTags = Array.from(new Set(paths.map((path) => path.tags[0])))\n // create records with tag as key\n const codeBasePerTag = firstTags.reduce((acc, tag) => {\n acc[tag] = []\n return acc\n }, {} as Record<string, string[]>)\n\n const specialFakers = specialFakerParser(options)\n\n paths.forEach((path) => {\n const pathResponses = path.responses.map((res) => {\n return generateSingleResponse(path, res, refs, specialFakers, options)\n })\n\n const pathResponsesWithComment = `// ${path.operationId}\\n` + pathResponses.join(\"\\n\\n\")\n codeBasePerTag[path.tags[0]].push(pathResponsesWithComment)\n })\n\n await writeResponseFiles(codeBasePerTag, options)\n}\n\n/**\n * 단일 응답 모킹 함수 생성\n */\nconst generateSingleResponse = (\n path: PathNormalizedType,\n res: { statusCode: number; description: string; schema: any },\n refs: SwaggerParser[\"$refs\"],\n specialFakers: ReturnType<typeof specialFakerParser>,\n options: Options\n): string => {\n const codeBaseArray = [\n `export const get${pascalCase(path.operationId)}${res.statusCode} = () => {`,\n ]\n\n if (res.schema?.type === \"ref\") {\n const { name, value } = refSchemaParser(res.schema.value.$ref, refs)\n const outputSchema = parseSchema(value, specialFakers, options)\n codeBaseArray.push(` // Schema is ${name}`)\n codeBaseArray.push(\n ` return ${toTypeScriptCode(outputSchema, {\n depth: 1,\n ...options,\n })}`\n )\n } else if (res.schema?.type === \"array\") {\n if (isReference(res.schema.value)) {\n const { name, value } = refSchemaParser(res.schema.value.$ref, refs)\n const outputSchema = getRandomLengthArray(options.arrayMinLength, options.arrayMaxLength).map(\n () => parseSchema(value, specialFakers, options)\n )\n codeBaseArray.push(` // Schema is ${name} array`)\n codeBaseArray.push(\n ` return ${toTypeScriptCode(outputSchema, {\n depth: 1,\n ...options,\n })}`\n )\n } else {\n const outputSchema = getRandomLengthArray(options.arrayMinLength, options.arrayMaxLength).map(\n () => res.schema && parseSchema(res.schema.value, specialFakers, options)\n )\n codeBaseArray.push(\n ` return ${toTypeScriptCode(outputSchema, {\n depth: 1,\n ...options,\n })}`\n )\n }\n } else if (res.schema?.type === \"anyOf\") {\n const firstSchema = res.schema.value.anyOf?.[0]\n if (isReference(firstSchema)) {\n const { name, value } = refSchemaParser(firstSchema.$ref, refs)\n const outputSchema = parseSchema(value, specialFakers, options)\n codeBaseArray.push(` // Schema is ${name}`)\n codeBaseArray.push(\n ` return ${toTypeScriptCode(outputSchema, {\n depth: 1,\n ...options,\n })}`\n )\n } else {\n codeBaseArray.push(` return ${res.schema.value}`)\n }\n } else {\n codeBaseArray.push(` return ${res.schema?.value}`)\n }\n\n return [...codeBaseArray, `}`].join(\"\\n\")\n}\n\n/**\n * 응답 파일들을 실제로 디스크에 작성\n */\nconst writeResponseFiles = async (\n codeBasePerTag: Record<string, string[]>,\n options: Options\n): Promise<void> => {\n const directory = path.join(options.baseDir, \"response\")\n ensureDir(directory)\n if (options.clear) {\n clearDirectory(directory)\n }\n\n Object.entries(codeBasePerTag).forEach(([tag, responses]) => {\n const needImportFaker = responses.some((res) => res.includes(\"faker.\"))\n const importFaker =\n options.isStatic || !needImportFaker ? \"\" : 'import { faker } from \"../fakers\"\\n\\n'\n\n const fileName = `${directory}/${tag}.ts`\n const content = generateResponseFileContent(importFaker, responses)\n safeWriteFile(fileName, content)\n console.log(`Generated ${fileName}`)\n })\n\n // make index.ts for merge all responses\n const indexContent = generateResponseIndexFile(codeBasePerTag)\n const indexFileName = `${directory}/index.ts`\n safeWriteFile(indexFileName, indexContent)\n console.log(`Generated ${indexFileName}`)\n}\n\n/**\n * 응답 파일의 내용 생성\n */\nconst generateResponseFileContent = (importFaker: string, responses: string[]): string => {\n const { GEN_COMMENT } = require(\"../core\")\n return GEN_COMMENT + importFaker + responses.join(\"\\n\\n\")\n}\n\n/**\n * 응답 인덱스 파일 생성\n */\nconst generateResponseIndexFile = (codeBasePerTag: Record<string, string[]>): string => {\n const { GEN_COMMENT } = require(\"../core\")\n\n const importResponses = Object.entries(codeBasePerTag).map(([tag, responses]) => {\n const responseNames = responses\n .reduce((acc, handler) => {\n const matched = handler.match(/get[A-Z]\\w+/g)\n if (matched === null) return acc\n return [...acc, ...matched]\n }, [] as string[])\n .join(\",\\n \")\n return [\"export {\", \" \" + responseNames, '} from \"./' + tag + '\"'].join(\"\\n\")\n })\n\n return GEN_COMMENT + importResponses.join(\"\\n\")\n}\n\n/**\n * 응답 함수명 생성\n */\nexport const generateResponseFunctionName = (operationId: string, statusCode: number): string => {\n return `get${pascalCase(operationId)}${statusCode}`\n}\n\n/**\n * 특정 태그의 응답만 생성\n */\nexport const generateResponsesForTag = async (\n paths: PathNormalizedType[],\n tag: string,\n options: Options\n): Promise<string[]> => {\n const filteredPaths = paths.filter((path) => path.tags.includes(tag))\n\n const parser = new SwaggerParser()\n const openapiPath = resolveFilePath(options.path, options.baseDir)\n await parser.dereference(openapiPath)\n const refs = parser.$refs\n const specialFakers = specialFakerParser(options)\n\n return filteredPaths.flatMap((path) =>\n path.responses.map((res) => generateSingleResponse(path, res, refs, specialFakers, options))\n )\n}\n","/**\n * MSW 핸들러 코드 생성 로직\n * MSW에서 사용할 HTTP 핸들러들을 생성\n */\n\nimport { GEN_COMMENT, Options, PathNormalizedType } from \"../core\"\nimport { ensureDir, clearDirectory, safeWriteFile } from \"../utils\"\nimport { camelCase, pascalCase } from \"change-case-all\"\nimport * as path from \"path\"\n\n/**\n * MSW 핸들러 코드를 생성하고 파일로 출력\n */\nexport const generateHandlers = (paths: PathNormalizedType[], options: Options): void => {\n const firstTags = Array.from(new Set(paths.map((path) => path.tags[0])))\n // create records with tag as key\n const handlersPerTag = firstTags.reduce((acc, tag) => {\n acc[tag] = []\n return acc\n }, {} as Record<string, string[]>)\n\n paths.forEach((path) => {\n const handler = generateSingleHandler(path, options)\n handlersPerTag[path.tags[0]].push(handler)\n })\n\n writeHandlerFiles(handlersPerTag, options)\n}\n\n/**\n * 단일 경로에 대한 핸들러 생성\n */\nconst generateSingleHandler = (path: PathNormalizedType, options: Options): string => {\n const codeBaseArray = [` http.${path.method}(\\`\\${handlerUrl}${path.pathname}\\`, () => {`]\n\n if (path.responses.length === 1) {\n // single response\n const res = path.responses[0]\n if (res.schema?.type === \"ref\") {\n const schemaName = pascalCase(res.schema.value.$ref.replace(\"#/components/schemas/\", \"\"))\n codeBaseArray.push(` // Schema is ${schemaName}`)\n }\n const outputResName = `get${pascalCase(path.operationId)}${res.statusCode}`\n codeBaseArray.push(` return HttpResponse.json(${outputResName}(), {`)\n codeBaseArray.push(` status: ${res.statusCode},`)\n codeBaseArray.push(` })`)\n } else if (path.responses.length > 1) {\n // multiple responses\n // random select response\n codeBaseArray.push(` const responses = [`)\n path.responses.forEach((res) => {\n const schemaName =\n res.schema?.type === \"ref\"\n ? pascalCase(res.schema.value.$ref.replace(\"#/components/schemas/\", \"\"))\n : \"\"\n const schemaComment = schemaName ? ` // Schema is ${schemaName}` : \"\"\n const outputResName = `get${pascalCase(path.operationId)}${res.statusCode}`\n codeBaseArray.push(\n ` [${outputResName}(), { status: ${res.statusCode} }],${schemaComment}`\n )\n })\n codeBaseArray.push(` ]`)\n codeBaseArray.push(` const randomIndex = Math.floor(Math.random() * responses.length)`)\n codeBaseArray.push(` return HttpResponse.json(...responses[randomIndex])`)\n } else {\n // empty responses\n codeBaseArray.push(` return HttpResponse.json()`)\n }\n\n codeBaseArray.push(` }),`)\n return codeBaseArray.join(\"\\n\")\n}\n\n/**\n * 핸들러 파일들을 실제로 디스크에 작성\n */\nconst writeHandlerFiles = (handlersPerTag: Record<string, string[]>, options: Options): void => {\n const directory = path.join(options.baseDir, \"handlers\")\n ensureDir(directory)\n if (options.clear) {\n clearDirectory(directory)\n }\n\n Object.entries(handlersPerTag).forEach(([tag, handlers]) => {\n const content = generateHandlerFileContent(tag, handlers, options)\n\n const fileName = path.join(directory, `${tag}.ts`)\n safeWriteFile(fileName, content)\n console.log(`Generated Handler ${fileName}`)\n })\n\n // make mockHandlers.ts for merge all handlers\n const mockHandlersContent = generateMockHandlersFile(handlersPerTag, options)\n const fileName = path.join(options.baseDir ?? \"\", \"mockHandlers.ts\")\n safeWriteFile(fileName, mockHandlersContent)\n console.log(`Generated mock handlers ${fileName}`)\n}\n\n/**\n * 핸들러 파일의 내용 생성\n */\nconst generateHandlerFileContent = (tag: string, handlers: string[], options: Options): string => {\n const importMSW = `import { http, HttpResponse } from 'msw'`\n const responseNames = handlers\n .reduce((acc, handler) => {\n const matched = handler.match(/get[A-Z]\\w+/g)\n if (matched === null) return acc\n return [...acc, ...matched]\n }, [] as string[])\n .join(\", \")\n const importResponses =\n responseNames.length > 0