mock-fried 1.0.7 → 1.1.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/README.md +5 -2
- package/dist/module.json +1 -1
- package/dist/runtime/components/ApiExplorer.vue +2 -0
- package/dist/runtime/components/EndpointCard.vue +1 -0
- package/dist/runtime/components/ResponseViewer.vue +1 -0
- package/dist/runtime/components/RpcMethodCard.vue +1 -0
- package/dist/runtime/server/handlers/rpc.js +145 -8
- package/dist/runtime/server/handlers/schema.js +32 -3
- package/dist/runtime/server/utils/client-parser.js +41 -25
- package/dist/runtime/server/utils/mock/proto-generator.d.ts +15 -1
- package/dist/runtime/server/utils/mock/proto-generator.js +47 -4
- package/dist/runtime/server/utils/mock/providers/proto-item-provider.js +3 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -346,8 +346,11 @@ yarn lint:fix
|
|
|
346
346
|
yarn format
|
|
347
347
|
|
|
348
348
|
# Run tests
|
|
349
|
-
yarn test
|
|
350
|
-
yarn test:
|
|
349
|
+
yarn test # All tests
|
|
350
|
+
yarn test:unit # Unit tests only
|
|
351
|
+
yarn test:e2e # E2E tests only
|
|
352
|
+
yarn test:coverage # With coverage report
|
|
353
|
+
yarn test:watch # Watch mode
|
|
351
354
|
|
|
352
355
|
# Type check
|
|
353
356
|
yarn test:types
|
package/dist/module.json
CHANGED
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
</template>
|
|
89
89
|
|
|
90
90
|
<script setup>
|
|
91
|
+
import { ref, onMounted } from "vue";
|
|
92
|
+
import { useNuxtApp } from "#imports";
|
|
91
93
|
import EndpointCard from "./EndpointCard.vue";
|
|
92
94
|
import RpcMethodCard from "./RpcMethodCard.vue";
|
|
93
95
|
import ResponseViewer from "./ResponseViewer.vue";
|
|
@@ -85,12 +85,148 @@ export function clearProtoCache() {
|
|
|
85
85
|
protoCursorManager = null;
|
|
86
86
|
protoPageManager = null;
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const PROTO_TYPE_MAP = {
|
|
89
|
+
// 숫자 키 (legacy/fallback)
|
|
90
|
+
1: "double",
|
|
91
|
+
2: "float",
|
|
92
|
+
3: "int64",
|
|
93
|
+
4: "uint64",
|
|
94
|
+
5: "int32",
|
|
95
|
+
6: "fixed64",
|
|
96
|
+
7: "fixed32",
|
|
97
|
+
8: "bool",
|
|
98
|
+
9: "string",
|
|
99
|
+
10: "group",
|
|
100
|
+
11: "message",
|
|
101
|
+
12: "bytes",
|
|
102
|
+
13: "uint32",
|
|
103
|
+
14: "enum",
|
|
104
|
+
15: "sfixed32",
|
|
105
|
+
16: "sfixed64",
|
|
106
|
+
17: "sint32",
|
|
107
|
+
18: "sint64",
|
|
108
|
+
// 문자열 키 (proto-loader 실제 반환값)
|
|
109
|
+
TYPE_DOUBLE: "double",
|
|
110
|
+
TYPE_FLOAT: "float",
|
|
111
|
+
TYPE_INT64: "int64",
|
|
112
|
+
TYPE_UINT64: "uint64",
|
|
113
|
+
TYPE_INT32: "int32",
|
|
114
|
+
TYPE_FIXED64: "fixed64",
|
|
115
|
+
TYPE_FIXED32: "fixed32",
|
|
116
|
+
TYPE_BOOL: "bool",
|
|
117
|
+
TYPE_STRING: "string",
|
|
118
|
+
TYPE_GROUP: "group",
|
|
119
|
+
TYPE_MESSAGE: "message",
|
|
120
|
+
TYPE_BYTES: "bytes",
|
|
121
|
+
TYPE_UINT32: "uint32",
|
|
122
|
+
TYPE_ENUM: "enum",
|
|
123
|
+
TYPE_SFIXED32: "sfixed32",
|
|
124
|
+
TYPE_SFIXED64: "sfixed64",
|
|
125
|
+
TYPE_SINT32: "sint32",
|
|
126
|
+
TYPE_SINT64: "sint64"
|
|
127
|
+
};
|
|
128
|
+
function findTypeInPackageDefinition(typeName, packageDefinition) {
|
|
129
|
+
const cleanName = typeName.startsWith(".") ? typeName.slice(1) : typeName;
|
|
130
|
+
const directMatch = packageDefinition[cleanName];
|
|
131
|
+
if (directMatch?.type) {
|
|
132
|
+
return directMatch.type;
|
|
92
133
|
}
|
|
93
|
-
|
|
134
|
+
if (!cleanName.includes(".")) {
|
|
135
|
+
for (const key of Object.keys(packageDefinition)) {
|
|
136
|
+
if (key.endsWith(`.${cleanName}`)) {
|
|
137
|
+
const matchedType = packageDefinition[key];
|
|
138
|
+
if (matchedType?.type) {
|
|
139
|
+
return matchedType.type;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function convertFieldDescriptor(fieldDesc, protoType, context) {
|
|
147
|
+
const typeName = PROTO_TYPE_MAP[fieldDesc.type] || "string";
|
|
148
|
+
const isRepeated = fieldDesc.label === 3 || fieldDesc.label === "LABEL_REPEATED";
|
|
149
|
+
const result = {
|
|
150
|
+
type: typeName,
|
|
151
|
+
rule: isRepeated ? "repeated" : void 0
|
|
152
|
+
};
|
|
153
|
+
if (fieldDesc.typeName) {
|
|
154
|
+
const shortName = fieldDesc.typeName.split(".").pop() || fieldDesc.typeName;
|
|
155
|
+
result.typeName = shortName;
|
|
156
|
+
if (context.depth >= context.maxDepth) {
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
const enumType = protoType.enumType?.find(
|
|
160
|
+
(e) => e.name === shortName
|
|
161
|
+
);
|
|
162
|
+
if (enumType) {
|
|
163
|
+
result.resolvedType = {
|
|
164
|
+
values: enumType.value?.reduce((acc, v) => {
|
|
165
|
+
acc[v.name] = v.number;
|
|
166
|
+
return acc;
|
|
167
|
+
}, {})
|
|
168
|
+
};
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
const nestedType = protoType.nestedType?.find(
|
|
172
|
+
(t) => t.name === shortName
|
|
173
|
+
);
|
|
174
|
+
if (nestedType) {
|
|
175
|
+
const nestedContext = {
|
|
176
|
+
...context,
|
|
177
|
+
depth: context.depth + 1,
|
|
178
|
+
visitedTypes: /* @__PURE__ */ new Set([...context.visitedTypes, fieldDesc.typeName])
|
|
179
|
+
};
|
|
180
|
+
result.resolvedType = resolveProtoType(nestedType, nestedContext);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
const externalType = findTypeInPackageDefinition(fieldDesc.typeName, context.packageDefinition);
|
|
184
|
+
if (externalType) {
|
|
185
|
+
const externalContext = {
|
|
186
|
+
...context,
|
|
187
|
+
depth: context.depth + 1,
|
|
188
|
+
visitedTypes: /* @__PURE__ */ new Set([...context.visitedTypes, fieldDesc.typeName])
|
|
189
|
+
};
|
|
190
|
+
result.resolvedType = resolveProtoType(externalType, externalContext);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
function resolveProtoType(protoType, context) {
|
|
196
|
+
if (!protoType.field) {
|
|
197
|
+
return { fields: {}, name: protoType.name };
|
|
198
|
+
}
|
|
199
|
+
const fields = {};
|
|
200
|
+
const oneofGroups = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const fieldDesc of protoType.field) {
|
|
202
|
+
if (fieldDesc.oneofIndex !== void 0 && !fieldDesc.proto3Optional) {
|
|
203
|
+
const group = oneofGroups.get(fieldDesc.oneofIndex) || [];
|
|
204
|
+
group.push(fieldDesc.name);
|
|
205
|
+
oneofGroups.set(fieldDesc.oneofIndex, group);
|
|
206
|
+
}
|
|
207
|
+
fields[fieldDesc.name] = convertFieldDescriptor(fieldDesc, protoType, context);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
fields,
|
|
211
|
+
name: protoType.name,
|
|
212
|
+
// oneof 그룹 정보 (첫 번째 필드만 생성하도록)
|
|
213
|
+
oneofGroups: oneofGroups.size > 0 ? Object.fromEntries(oneofGroups) : void 0
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function getResponseTypeInfo(methodDef, packageDefinition) {
|
|
217
|
+
const methodDefWithResponseType = methodDef;
|
|
218
|
+
const responseType = methodDefWithResponseType.responseType;
|
|
219
|
+
if (!responseType?.type?.field) {
|
|
220
|
+
return { name: "unknown" };
|
|
221
|
+
}
|
|
222
|
+
const context = {
|
|
223
|
+
packageDefinition,
|
|
224
|
+
visitedTypes: /* @__PURE__ */ new Set(),
|
|
225
|
+
depth: 0,
|
|
226
|
+
maxDepth: 5
|
|
227
|
+
// 최대 재귀 깊이
|
|
228
|
+
};
|
|
229
|
+
return resolveProtoType(responseType.type, context);
|
|
94
230
|
}
|
|
95
231
|
export default defineEventHandler(async (event) => {
|
|
96
232
|
const config = useRuntimeConfig(event);
|
|
@@ -147,8 +283,9 @@ export default defineEventHandler(async (event) => {
|
|
|
147
283
|
} catch {
|
|
148
284
|
requestBody = {};
|
|
149
285
|
}
|
|
150
|
-
const responseTypeInfo = getResponseTypeInfo(methodDef);
|
|
151
|
-
const
|
|
286
|
+
const responseTypeInfo = getResponseTypeInfo(methodDef, cache.packageDefinition);
|
|
287
|
+
const seedNum = deriveSeedFromRequest(requestBody);
|
|
288
|
+
const seed = String(seedNum);
|
|
152
289
|
const paginationInfo = analyzeProtoPagination(responseTypeInfo);
|
|
153
290
|
let mockResponse;
|
|
154
291
|
if (paginationInfo) {
|
|
@@ -217,7 +354,7 @@ export default defineEventHandler(async (event) => {
|
|
|
217
354
|
mockResponse = responseData;
|
|
218
355
|
}
|
|
219
356
|
} else {
|
|
220
|
-
mockResponse = generateMockMessage(responseTypeInfo,
|
|
357
|
+
mockResponse = generateMockMessage(responseTypeInfo, seedNum);
|
|
221
358
|
}
|
|
222
359
|
return {
|
|
223
360
|
success: true,
|
|
@@ -2,7 +2,8 @@ import { defineEventHandler, createError } from "h3";
|
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
import { consola } from "consola";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join, extname } from "pathe";
|
|
6
7
|
import yaml from "js-yaml";
|
|
7
8
|
import { getClientPackage } from "../utils/client-parser.js";
|
|
8
9
|
const logger = consola.withTag("mock-fried");
|
|
@@ -180,18 +181,46 @@ function parseOpenApiSpec(specPath) {
|
|
|
180
181
|
return void 0;
|
|
181
182
|
}
|
|
182
183
|
}
|
|
184
|
+
function findProtoFiles(dirPath) {
|
|
185
|
+
const files = [];
|
|
186
|
+
const stat = statSync(dirPath);
|
|
187
|
+
if (stat.isFile() && extname(dirPath) === ".proto") {
|
|
188
|
+
return [dirPath];
|
|
189
|
+
}
|
|
190
|
+
if (stat.isDirectory()) {
|
|
191
|
+
const entries = readdirSync(dirPath);
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const fullPath = join(dirPath, entry);
|
|
194
|
+
const entryStat = statSync(fullPath);
|
|
195
|
+
if (entryStat.isFile() && extname(entry) === ".proto") {
|
|
196
|
+
files.push(fullPath);
|
|
197
|
+
} else if (entryStat.isDirectory()) {
|
|
198
|
+
files.push(...findProtoFiles(fullPath));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return files;
|
|
203
|
+
}
|
|
183
204
|
async function parseProtoSpec(protoPath) {
|
|
184
205
|
if (!existsSync(protoPath)) {
|
|
185
206
|
return void 0;
|
|
186
207
|
}
|
|
187
208
|
try {
|
|
188
209
|
const protoLoader = await import("@grpc/proto-loader");
|
|
189
|
-
const
|
|
210
|
+
const { dirname } = await import("pathe");
|
|
211
|
+
const stat = statSync(protoPath);
|
|
212
|
+
const protoFiles = stat.isDirectory() ? findProtoFiles(protoPath) : [protoPath];
|
|
213
|
+
const includeDir = stat.isDirectory() ? protoPath : dirname(protoPath);
|
|
214
|
+
if (protoFiles.length === 0) {
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
const packageDefinition = await protoLoader.load(protoFiles, {
|
|
190
218
|
keepCase: false,
|
|
191
219
|
longs: String,
|
|
192
220
|
enums: String,
|
|
193
221
|
defaults: true,
|
|
194
|
-
oneofs: true
|
|
222
|
+
oneofs: true,
|
|
223
|
+
includeDirs: [includeDir]
|
|
195
224
|
});
|
|
196
225
|
const services = [];
|
|
197
226
|
let packageName;
|
|
@@ -12,19 +12,22 @@ function parseApiFile(filePath, fileName) {
|
|
|
12
12
|
while ((match = methodBodyRegex.exec(content)) !== null) {
|
|
13
13
|
const methodName = match[1];
|
|
14
14
|
const body = match[2];
|
|
15
|
-
|
|
15
|
+
if (methodName && body) {
|
|
16
|
+
methodBodies.set(methodName, body);
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
19
|
rawMethodRegex.lastIndex = 0;
|
|
18
20
|
while ((match = rawMethodRegex.exec(content)) !== null) {
|
|
19
21
|
const jsdocContent = match[1];
|
|
20
22
|
const operationId = match[2];
|
|
21
23
|
const responseType = match[3];
|
|
24
|
+
if (!jsdocContent || !operationId || !responseType) continue;
|
|
22
25
|
const jsdocLines = jsdocContent.split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("@"));
|
|
23
26
|
const summary = jsdocLines[jsdocLines.length - 1] || jsdocLines[0] || operationId;
|
|
24
27
|
const body = methodBodies.get(operationId);
|
|
25
28
|
if (!body) continue;
|
|
26
29
|
const pathMatch = body.match(/let\s+urlPath\s*=\s*`([^`]+)`/);
|
|
27
|
-
if (!pathMatch) continue;
|
|
30
|
+
if (!pathMatch?.[1]) continue;
|
|
28
31
|
let path = pathMatch[1];
|
|
29
32
|
path = path.replace(/\$\{[^}]+\}/g, (match2) => {
|
|
30
33
|
const paramName = match2.match(/requestParameters(?:\.(\w+)|\["?(\w+)"?\])/)?.[1] || match2.match(/requestParameters(?:\.(\w+)|\["?(\w+)"?\])/)?.[2];
|
|
@@ -38,25 +41,31 @@ function parseApiFile(filePath, fileName) {
|
|
|
38
41
|
const pathParams = [];
|
|
39
42
|
const pathParamMatches = path.matchAll(/\{(\w+)\}/g);
|
|
40
43
|
for (const pm of pathParamMatches) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const paramName = pm[1];
|
|
45
|
+
if (paramName) {
|
|
46
|
+
pathParams.push({
|
|
47
|
+
name: paramName,
|
|
48
|
+
type: "string",
|
|
49
|
+
required: true
|
|
50
|
+
});
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
const queryParams = [];
|
|
48
54
|
const queryParamRegex = /if\s*\(requestParameters\[?['"]?(\w+)['"]?\]?\s*!==?\s*(?:undefined|null)\)\s*\{\s*queryParameters\[['"](\w+)['"]\]/g;
|
|
49
55
|
let qpm;
|
|
50
56
|
while ((qpm = queryParamRegex.exec(body)) !== null) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const paramName = qpm[2];
|
|
58
|
+
if (paramName) {
|
|
59
|
+
queryParams.push({
|
|
60
|
+
name: paramName,
|
|
61
|
+
type: "string",
|
|
62
|
+
required: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
let requestBodyType;
|
|
58
67
|
const bodyMatch = body.match(/body:\s*(\w+)ToJSON\(requestParameters\.(\w+)\)/);
|
|
59
|
-
if (bodyMatch) {
|
|
68
|
+
if (bodyMatch?.[1]) {
|
|
60
69
|
requestBodyType = bodyMatch[1];
|
|
61
70
|
}
|
|
62
71
|
endpoints.push({
|
|
@@ -79,7 +88,7 @@ function analyzeType(rawType) {
|
|
|
79
88
|
let type = rawType;
|
|
80
89
|
if (isArray) {
|
|
81
90
|
const arrayTypeMatch = rawType.match(/Array<(.+)>/) || rawType.match(/(.+)\[\]/);
|
|
82
|
-
if (arrayTypeMatch) {
|
|
91
|
+
if (arrayTypeMatch?.[1]) {
|
|
83
92
|
type = arrayTypeMatch[1].trim();
|
|
84
93
|
}
|
|
85
94
|
}
|
|
@@ -96,11 +105,13 @@ function parseModelFile(filePath, fileName) {
|
|
|
96
105
|
if (!hasInterface) {
|
|
97
106
|
const enumRegex = new RegExp(`export\\s+const\\s+(${modelName})\\s*=\\s*\\{([^}]+)\\}\\s*as\\s*const`);
|
|
98
107
|
const enumMatch = content.match(enumRegex);
|
|
99
|
-
if (enumMatch) {
|
|
108
|
+
if (enumMatch?.[2]) {
|
|
100
109
|
const enumValues = [];
|
|
101
110
|
const valueMatches = enumMatch[2].matchAll(/(\w+):\s*['"]([^'"]+)['"]/g);
|
|
102
111
|
for (const vm of valueMatches) {
|
|
103
|
-
|
|
112
|
+
if (vm[2]) {
|
|
113
|
+
enumValues.push(vm[2]);
|
|
114
|
+
}
|
|
104
115
|
}
|
|
105
116
|
if (enumValues.length > 0) {
|
|
106
117
|
return {
|
|
@@ -114,11 +125,12 @@ function parseModelFile(filePath, fileName) {
|
|
|
114
125
|
const fields = [];
|
|
115
126
|
const jsonKeyMap = /* @__PURE__ */ new Map();
|
|
116
127
|
const toJsonMatch = content.match(/export\s+function\s+\w+ToJSON[^{]*\{[\s\S]*?return\s*\{([\s\S]*?)\};?\s*\}/);
|
|
117
|
-
if (toJsonMatch) {
|
|
128
|
+
if (toJsonMatch?.[1]) {
|
|
118
129
|
const returnBody = toJsonMatch[1];
|
|
119
130
|
const fieldMatches = returnBody.matchAll(/['"](\w+)['"]\s*:\s*(?:value\.(\w+)|[^,\n]*?value\.(\w+))/g);
|
|
120
131
|
for (const fm of fieldMatches) {
|
|
121
132
|
const jsonKey = fm[1];
|
|
133
|
+
if (!jsonKey) continue;
|
|
122
134
|
const propertyName = fm[2] || fm[3];
|
|
123
135
|
if (!propertyName) continue;
|
|
124
136
|
jsonKeyMap.set(propertyName, jsonKey);
|
|
@@ -134,29 +146,31 @@ function parseModelFile(filePath, fileName) {
|
|
|
134
146
|
}
|
|
135
147
|
}
|
|
136
148
|
const fromJsonMatch = content.match(/export\s+function\s+\w+FromJSONTyped[^{]*\{[\s\S]*?return\s*\{([\s\S]*?)\};?\s*\}/);
|
|
137
|
-
if (fromJsonMatch) {
|
|
149
|
+
if (fromJsonMatch?.[1]) {
|
|
138
150
|
const returnBody = fromJsonMatch[1];
|
|
139
151
|
const dateFields = /* @__PURE__ */ new Set();
|
|
140
152
|
const dateMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?new\s+Date\(/g);
|
|
141
153
|
for (const dm of dateMatches) {
|
|
142
|
-
dateFields.add(dm[1]);
|
|
154
|
+
if (dm[1]) dateFields.add(dm[1]);
|
|
143
155
|
}
|
|
144
156
|
const arrayFields = /* @__PURE__ */ new Set();
|
|
145
157
|
const arrayMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?\(json\[['"](\w+)['"]\]\s*as\s*Array/g);
|
|
146
158
|
for (const am of arrayMatches) {
|
|
147
|
-
arrayFields.add(am[1]);
|
|
159
|
+
if (am[1]) arrayFields.add(am[1]);
|
|
148
160
|
}
|
|
149
161
|
const refTypeMap = /* @__PURE__ */ new Map();
|
|
150
162
|
const refMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?(\w+)FromJSON\(json\[/g);
|
|
151
163
|
for (const rm of refMatches) {
|
|
152
|
-
if (!rm[2].includes("Array")) {
|
|
164
|
+
if (rm[1] && rm[2] && !rm[2].includes("Array")) {
|
|
153
165
|
refTypeMap.set(rm[1], rm[2]);
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
const arrayRefMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?\.map\((\w+)FromJSON\)/g);
|
|
157
169
|
for (const arm of arrayRefMatches) {
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
if (arm[1] && arm[2]) {
|
|
171
|
+
refTypeMap.set(arm[1], arm[2]);
|
|
172
|
+
arrayFields.add(arm[1]);
|
|
173
|
+
}
|
|
160
174
|
}
|
|
161
175
|
for (const field of fields) {
|
|
162
176
|
if (dateFields.has(field.name)) {
|
|
@@ -173,14 +187,16 @@ function parseModelFile(filePath, fileName) {
|
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
189
|
const interfaceMatch = content.match(/export\s+interface\s+(\w+)(?:\s+extends\s+[\w,\s]+)?\s*\{([\s\S]*?)\n\}/);
|
|
176
|
-
if (interfaceMatch) {
|
|
190
|
+
if (interfaceMatch?.[2]) {
|
|
177
191
|
const interfaceBody = interfaceMatch[2];
|
|
178
192
|
const simpleFieldRegex = /^\s+(\w+)(\?)?:\s*([^;]+);/gm;
|
|
179
193
|
let sfm;
|
|
180
194
|
while ((sfm = simpleFieldRegex.exec(interfaceBody)) !== null) {
|
|
181
195
|
const name = sfm[1];
|
|
196
|
+
const rawTypeStr = sfm[3];
|
|
197
|
+
if (!name || !rawTypeStr) continue;
|
|
182
198
|
const optional = sfm[2] === "?";
|
|
183
|
-
const rawType =
|
|
199
|
+
const rawType = rawTypeStr.trim();
|
|
184
200
|
const { type, isArray, refType } = analyzeType(rawType);
|
|
185
201
|
const existingField = fields.find((f) => f.name === name);
|
|
186
202
|
if (existingField) {
|
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
* Proto 필드 타입에 따른 mock 값 생성
|
|
3
3
|
*/
|
|
4
4
|
export declare function generateMockValueForProtoField(fieldName: string, fieldType: string, seed?: number): unknown;
|
|
5
|
+
/**
|
|
6
|
+
* 재귀 생성 옵션
|
|
7
|
+
*/
|
|
8
|
+
export interface MockGenerationOptions {
|
|
9
|
+
/** 현재 재귀 깊이 */
|
|
10
|
+
depth?: number;
|
|
11
|
+
/** 최대 재귀 깊이 (기본: 3) */
|
|
12
|
+
maxDepth?: number;
|
|
13
|
+
/** 방문한 타입 이름 Set (간접 재귀 감지) */
|
|
14
|
+
visitedTypes?: Set<string>;
|
|
15
|
+
/** 현재 타입 이름 */
|
|
16
|
+
typeName?: string;
|
|
17
|
+
}
|
|
5
18
|
/**
|
|
6
19
|
* Proto 메시지 타입에서 mock 객체 생성
|
|
20
|
+
* 재귀적 타입 지원 (깊이 제한으로 무한 루프 방지)
|
|
7
21
|
*/
|
|
8
|
-
export declare function generateMockMessage(messageType: Record<string, unknown>, seed?: number): Record<string, unknown>;
|
|
22
|
+
export declare function generateMockMessage(messageType: Record<string, unknown>, seed?: number, options?: MockGenerationOptions): Record<string, unknown>;
|
|
9
23
|
/**
|
|
10
24
|
* 요청 객체에서 seed 추출
|
|
11
25
|
*/
|
|
@@ -30,7 +30,23 @@ export function generateMockValueForProtoField(fieldName, fieldType, seed = 1) {
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
export function generateMockMessage(messageType, seed = 1) {
|
|
33
|
+
export function generateMockMessage(messageType, seed = 1, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
depth = 0,
|
|
36
|
+
maxDepth = 3,
|
|
37
|
+
visitedTypes = /* @__PURE__ */ new Set(),
|
|
38
|
+
typeName
|
|
39
|
+
} = options;
|
|
40
|
+
if (depth >= maxDepth) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
if (typeName && visitedTypes.has(typeName)) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
const newVisitedTypes = new Set(visitedTypes);
|
|
47
|
+
if (typeName) {
|
|
48
|
+
newVisitedTypes.add(typeName);
|
|
49
|
+
}
|
|
34
50
|
const result = {};
|
|
35
51
|
const fields = messageType.fields || {};
|
|
36
52
|
let currentSeed = seed;
|
|
@@ -41,15 +57,42 @@ export function generateMockMessage(messageType, seed = 1) {
|
|
|
41
57
|
const resolvedType = field.resolvedType;
|
|
42
58
|
if (resolvedType.values) {
|
|
43
59
|
const enumValues = Object.keys(resolvedType.values);
|
|
44
|
-
|
|
60
|
+
const random = seededRandom(currentSeed);
|
|
61
|
+
const index = Math.floor(random() * enumValues.length);
|
|
62
|
+
value = enumValues[index] || "UNKNOWN";
|
|
45
63
|
} else if (resolvedType.fields) {
|
|
46
|
-
|
|
64
|
+
const nestedTypeName = resolvedType.name || field.typeName || fieldName;
|
|
65
|
+
value = generateMockMessage(resolvedType, currentSeed, {
|
|
66
|
+
depth: depth + 1,
|
|
67
|
+
maxDepth,
|
|
68
|
+
visitedTypes: newVisitedTypes,
|
|
69
|
+
typeName: nestedTypeName
|
|
70
|
+
});
|
|
47
71
|
}
|
|
48
72
|
} else {
|
|
49
73
|
value = generateMockValueForProtoField(fieldName, field.type || "string", currentSeed);
|
|
50
74
|
}
|
|
51
75
|
if (field.rule === "repeated") {
|
|
52
|
-
value
|
|
76
|
+
if (depth < maxDepth - 1 && value && typeof value === "object") {
|
|
77
|
+
const itemCount = Math.max(1, 3 - depth);
|
|
78
|
+
const items = [value];
|
|
79
|
+
for (let i = 1; i < itemCount; i++) {
|
|
80
|
+
if (field.resolvedType) {
|
|
81
|
+
const resolvedType = field.resolvedType;
|
|
82
|
+
if (resolvedType.fields) {
|
|
83
|
+
items.push(generateMockMessage(resolvedType, currentSeed + i * 100, {
|
|
84
|
+
depth: depth + 1,
|
|
85
|
+
maxDepth,
|
|
86
|
+
visitedTypes: newVisitedTypes,
|
|
87
|
+
typeName: resolvedType.name || field.typeName || fieldName
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
value = items;
|
|
93
|
+
} else {
|
|
94
|
+
value = value !== null && value !== void 0 ? [value] : [];
|
|
95
|
+
}
|
|
53
96
|
}
|
|
54
97
|
if (field.keyType) {
|
|
55
98
|
const keyValue = field.keyType === "string" ? `key_${currentSeed}` : currentSeed;
|
|
@@ -90,6 +90,9 @@ export function analyzeProtoPagination(messageType) {
|
|
|
90
90
|
const foundCursorFields = cursorFieldPatterns.filter((p) => fieldNames.includes(p.toLowerCase()));
|
|
91
91
|
const isPageBased = foundPageFields.length >= 2;
|
|
92
92
|
const isCursorBased = foundCursorFields.length >= 1;
|
|
93
|
+
if (!isPageBased && !isCursorBased) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
93
96
|
return {
|
|
94
97
|
itemsFieldName,
|
|
95
98
|
itemMessageType,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mock-fried",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Nuxt3 Mock API Module - OpenAPI & Protobuf RPC Mock Server",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -52,12 +52,14 @@
|
|
|
52
52
|
"format:check": "prettier --check .",
|
|
53
53
|
"test": "vitest run",
|
|
54
54
|
"test:watch": "vitest watch",
|
|
55
|
+
"test:coverage": "vitest run --coverage",
|
|
55
56
|
"test:unit": "vitest run --exclude 'test/e2e/**'",
|
|
56
57
|
"test:e2e": "vitest run test/e2e/",
|
|
57
58
|
"test:e2e:openapi": "vitest run test/e2e/playground-openapi.e2e.test.ts",
|
|
58
59
|
"test:e2e:openapi-client": "vitest run test/e2e/playground-openapi-client.e2e.test.ts",
|
|
59
|
-
"test:e2e:proto": "vitest run test/e2e/playground-proto.e2e.test.ts",
|
|
60
|
-
"test:
|
|
60
|
+
"test:e2e:proto": "vitest run test/e2e/playground-proto.e2e.test.ts test/e2e/playground-proto-advanced.e2e.test.ts",
|
|
61
|
+
"test:e2e:proto-advanced": "vitest run test/e2e/playground-proto-advanced.e2e.test.ts",
|
|
62
|
+
"test:types": "vue-tsc --noEmit"
|
|
61
63
|
},
|
|
62
64
|
"dependencies": {
|
|
63
65
|
"@grpc/grpc-js": "^1.12.0",
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"@nuxt/test-utils": "^3.21.0",
|
|
78
80
|
"@types/js-yaml": "^4.0.9",
|
|
79
81
|
"@types/node": "latest",
|
|
82
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
80
83
|
"changelogen": "^0.6.2",
|
|
81
84
|
"eslint": "^9.39.2",
|
|
82
85
|
"nuxt": "^4.2.2",
|