@apicircle/mock-server-core 1.0.8 → 1.1.0
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/dist/index.cjs +235 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +234 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/parsers/openapi.ts
|
|
2
2
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
|
+
import { makeDefaultRequestSchema as makeDefaultRequestSchema2 } from "@apicircle/shared";
|
|
4
5
|
|
|
5
6
|
// src/faker/schemaToExample.ts
|
|
6
7
|
var FORMAT_DEFAULTS = {
|
|
@@ -63,6 +64,17 @@ function pickType(type) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// src/parsers/buildEndpoint.ts
|
|
67
|
+
import { generateId, makeDefaultRequestSchema } from "@apicircle/shared";
|
|
68
|
+
function paramDef(name, opts) {
|
|
69
|
+
return {
|
|
70
|
+
id: generateId(),
|
|
71
|
+
name,
|
|
72
|
+
typeHint: opts?.typeHint,
|
|
73
|
+
required: opts?.required,
|
|
74
|
+
description: opts?.description,
|
|
75
|
+
example: opts?.example
|
|
76
|
+
};
|
|
77
|
+
}
|
|
66
78
|
function bodyTypeForContentType(contentType) {
|
|
67
79
|
if (!contentType) return "json";
|
|
68
80
|
const main = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
@@ -103,7 +115,7 @@ function buildMockEndpoint(input) {
|
|
|
103
115
|
method: input.method,
|
|
104
116
|
pathPattern: input.pathPattern,
|
|
105
117
|
description: input.description,
|
|
106
|
-
requestSchema:
|
|
118
|
+
requestSchema: input.requestSchema ?? makeDefaultRequestSchema(),
|
|
107
119
|
requestValidation: [],
|
|
108
120
|
responseRules: [],
|
|
109
121
|
defaultResponse: buildMockResponse(input.response),
|
|
@@ -142,18 +154,64 @@ async function parseOpenApiToEndpoints(source, format = "json", opts = {}) {
|
|
|
142
154
|
let endpointId = 0;
|
|
143
155
|
for (const [path, ops] of Object.entries(paths)) {
|
|
144
156
|
if (!ops || typeof ops !== "object") continue;
|
|
157
|
+
const pathItemParams = ops.parameters ?? [];
|
|
145
158
|
for (const method of Object.keys(ops)) {
|
|
146
159
|
const upper = method.toUpperCase();
|
|
147
160
|
if (!SUPPORTED_METHODS.includes(upper)) continue;
|
|
148
161
|
const op = ops[method];
|
|
149
162
|
if (!op || typeof op !== "object") continue;
|
|
150
|
-
const built = buildEndpointFromOp(
|
|
163
|
+
const built = buildEndpointFromOp(
|
|
164
|
+
path,
|
|
165
|
+
upper,
|
|
166
|
+
op,
|
|
167
|
+
pathItemParams,
|
|
168
|
+
opts,
|
|
169
|
+
warnings,
|
|
170
|
+
endpointId++
|
|
171
|
+
);
|
|
151
172
|
if (built) endpoints.push(built);
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
return { endpoints, warnings };
|
|
155
176
|
}
|
|
156
|
-
function
|
|
177
|
+
function buildRequestSchema(pathItemParams, op) {
|
|
178
|
+
const merged = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const p of [...pathItemParams, ...op.parameters ?? []]) {
|
|
180
|
+
if (p && typeof p === "object" && typeof p.name === "string") {
|
|
181
|
+
merged.set(`${p.in ?? "query"}:${p.name}`, p);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const schema = makeDefaultRequestSchema2();
|
|
185
|
+
for (const p of merged.values()) {
|
|
186
|
+
const rawType = p.schema?.type ?? p.type;
|
|
187
|
+
const typeHint = p.schema?.format ?? (Array.isArray(rawType) ? rawType[0] : rawType);
|
|
188
|
+
const exampleVal = p.example ?? p.schema?.example;
|
|
189
|
+
const def = paramDef(p.name, {
|
|
190
|
+
typeHint: typeof typeHint === "string" ? typeHint : void 0,
|
|
191
|
+
required: typeof p.required === "boolean" ? p.required : void 0,
|
|
192
|
+
description: typeof p.description === "string" ? p.description : void 0,
|
|
193
|
+
example: exampleVal === void 0 ? void 0 : typeof exampleVal === "string" ? exampleVal : JSON.stringify(exampleVal)
|
|
194
|
+
});
|
|
195
|
+
switch (p.in) {
|
|
196
|
+
case "path":
|
|
197
|
+
schema.pathParams.push(def);
|
|
198
|
+
break;
|
|
199
|
+
case "query":
|
|
200
|
+
schema.queryParams.push(def);
|
|
201
|
+
break;
|
|
202
|
+
case "header":
|
|
203
|
+
schema.headers.push(def);
|
|
204
|
+
break;
|
|
205
|
+
case "cookie":
|
|
206
|
+
schema.cookies.push(def);
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return schema;
|
|
213
|
+
}
|
|
214
|
+
function buildEndpointFromOp(path, method, op, pathItemParams, opts, warnings, index) {
|
|
157
215
|
const responses = op.responses ?? {};
|
|
158
216
|
const candidates = Object.keys(responses).filter((code) => /^2\d\d$/.test(code)).map((code) => Number(code));
|
|
159
217
|
if (candidates.length === 0) {
|
|
@@ -173,6 +231,7 @@ function buildEndpointFromOp(path, method, op, opts, warnings, index) {
|
|
|
173
231
|
method,
|
|
174
232
|
pathPattern: path,
|
|
175
233
|
example: exampleName,
|
|
234
|
+
requestSchema: buildRequestSchema(pathItemParams, op),
|
|
176
235
|
response: { status, headers, body }
|
|
177
236
|
});
|
|
178
237
|
}
|
|
@@ -275,6 +334,26 @@ function safeYamlLoad(s) {
|
|
|
275
334
|
}
|
|
276
335
|
|
|
277
336
|
// src/parsers/postman.ts
|
|
337
|
+
import { makeDefaultRequestSchema as makeDefaultRequestSchema3 } from "@apicircle/shared";
|
|
338
|
+
function postmanRequestSchema(req) {
|
|
339
|
+
const schema = makeDefaultRequestSchema3();
|
|
340
|
+
const url = req.url;
|
|
341
|
+
if (url && typeof url === "object") {
|
|
342
|
+
for (const v of url.variable ?? []) {
|
|
343
|
+
if (v?.key)
|
|
344
|
+
schema.pathParams.push(paramDef(v.key, { example: v.value, description: v.description }));
|
|
345
|
+
}
|
|
346
|
+
for (const q of url.query ?? []) {
|
|
347
|
+
if (q?.key && !q.disabled)
|
|
348
|
+
schema.queryParams.push(paramDef(q.key, { example: q.value, description: q.description }));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const h of req.header ?? []) {
|
|
352
|
+
if (h?.key && !h.disabled)
|
|
353
|
+
schema.headers.push(paramDef(h.key, { example: h.value, description: h.description }));
|
|
354
|
+
}
|
|
355
|
+
return schema;
|
|
356
|
+
}
|
|
278
357
|
var SUPPORTED_METHODS2 = [
|
|
279
358
|
"GET",
|
|
280
359
|
"POST",
|
|
@@ -306,6 +385,7 @@ function parsePostmanToEndpoints(source) {
|
|
|
306
385
|
warnings.push(`Skipping request with no extractable path: ${item.name ?? "(unnamed)"}`);
|
|
307
386
|
return;
|
|
308
387
|
}
|
|
388
|
+
const requestSchema = postmanRequestSchema(item.request);
|
|
309
389
|
const example = item.response?.[0];
|
|
310
390
|
if (example) {
|
|
311
391
|
endpoints.push(
|
|
@@ -315,6 +395,7 @@ function parsePostmanToEndpoints(source) {
|
|
|
315
395
|
method,
|
|
316
396
|
pathPattern: path,
|
|
317
397
|
example: example.name,
|
|
398
|
+
requestSchema,
|
|
318
399
|
response: {
|
|
319
400
|
// Postman's `code` is the canonical numeric status; `status` is a
|
|
320
401
|
// human-readable label that *sometimes* parses as a number. Try
|
|
@@ -332,6 +413,7 @@ function parsePostmanToEndpoints(source) {
|
|
|
332
413
|
name: item.name,
|
|
333
414
|
method,
|
|
334
415
|
pathPattern: path,
|
|
416
|
+
requestSchema,
|
|
335
417
|
response: {
|
|
336
418
|
status: 200,
|
|
337
419
|
headers: [{ key: "Content-Type", value: "application/json" }],
|
|
@@ -375,6 +457,26 @@ function slug2(s) {
|
|
|
375
457
|
}
|
|
376
458
|
|
|
377
459
|
// src/parsers/insomnia.ts
|
|
460
|
+
import { makeDefaultRequestSchema as makeDefaultRequestSchema4 } from "@apicircle/shared";
|
|
461
|
+
function extractPathSlots(path) {
|
|
462
|
+
const out = /* @__PURE__ */ new Set();
|
|
463
|
+
for (const m of path.matchAll(/[:{]([A-Za-z0-9_]+)\}?/g)) out.add(m[1]);
|
|
464
|
+
return [...out];
|
|
465
|
+
}
|
|
466
|
+
function insomniaRequestSchema(r, path) {
|
|
467
|
+
const schema = makeDefaultRequestSchema4();
|
|
468
|
+
for (const slot of extractPathSlots(path))
|
|
469
|
+
schema.pathParams.push(paramDef(slot, { required: true }));
|
|
470
|
+
for (const p of r.parameters ?? []) {
|
|
471
|
+
if (p?.name && !p.disabled)
|
|
472
|
+
schema.queryParams.push(paramDef(p.name, { example: p.value, description: p.description }));
|
|
473
|
+
}
|
|
474
|
+
for (const h of r.headers ?? []) {
|
|
475
|
+
if (h?.name && !h.disabled)
|
|
476
|
+
schema.headers.push(paramDef(h.name, { example: h.value, description: h.description }));
|
|
477
|
+
}
|
|
478
|
+
return schema;
|
|
479
|
+
}
|
|
378
480
|
var SUPPORTED_METHODS3 = [
|
|
379
481
|
"GET",
|
|
380
482
|
"POST",
|
|
@@ -412,6 +514,7 @@ function parseInsomniaToEndpoints(source) {
|
|
|
412
514
|
name: r.name,
|
|
413
515
|
method,
|
|
414
516
|
pathPattern: path,
|
|
517
|
+
requestSchema: insomniaRequestSchema(r, path),
|
|
415
518
|
response: {
|
|
416
519
|
status: 200,
|
|
417
520
|
headers: [{ key: "Content-Type", value: "application/json" }],
|
|
@@ -611,12 +714,8 @@ function resolveJsonPath(body, jsonPath) {
|
|
|
611
714
|
if (path.startsWith(".")) path = path.slice(1);
|
|
612
715
|
if (path === "") return body;
|
|
613
716
|
let cursor = body;
|
|
614
|
-
const
|
|
615
|
-
let match;
|
|
616
|
-
while ((match = re.exec(path)) !== null) {
|
|
717
|
+
for (const key of iterJsonPathTokens(path)) {
|
|
617
718
|
if (cursor === void 0 || cursor === null) return void 0;
|
|
618
|
-
const key = match[1] ?? match[2];
|
|
619
|
-
if (key === void 0) return void 0;
|
|
620
719
|
if (Array.isArray(cursor)) {
|
|
621
720
|
const idx = Number(key);
|
|
622
721
|
if (!Number.isInteger(idx)) return void 0;
|
|
@@ -631,6 +730,32 @@ function resolveJsonPath(body, jsonPath) {
|
|
|
631
730
|
}
|
|
632
731
|
return cursor;
|
|
633
732
|
}
|
|
733
|
+
function* iterJsonPathTokens(path) {
|
|
734
|
+
let i = 0;
|
|
735
|
+
while (i < path.length) {
|
|
736
|
+
const ch = path[i];
|
|
737
|
+
if (ch === "." || ch === "]") {
|
|
738
|
+
i++;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (ch === "[") {
|
|
742
|
+
const close = path.indexOf("]", i + 1);
|
|
743
|
+
if (close === -1) return;
|
|
744
|
+
const inner = path.slice(i + 1, close);
|
|
745
|
+
if (inner.length > 0) yield inner;
|
|
746
|
+
i = close + 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
let j = i + 1;
|
|
750
|
+
while (j < path.length) {
|
|
751
|
+
const c = path[j];
|
|
752
|
+
if (c === "." || c === "[" || c === "]") break;
|
|
753
|
+
j++;
|
|
754
|
+
}
|
|
755
|
+
yield path.slice(i, j);
|
|
756
|
+
i = j;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
634
759
|
|
|
635
760
|
// src/response/applyMultipliers.ts
|
|
636
761
|
function applyMultipliers(response, ctx) {
|
|
@@ -707,21 +832,39 @@ function parsePathSegments(jsonPath) {
|
|
|
707
832
|
if (path.startsWith(".")) path = path.slice(1);
|
|
708
833
|
if (path === "") return [];
|
|
709
834
|
const out = [];
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
835
|
+
let i = 0;
|
|
836
|
+
while (i < path.length) {
|
|
837
|
+
const ch = path[i];
|
|
838
|
+
if (ch === "." || ch === "]") {
|
|
839
|
+
i++;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (ch === "[") {
|
|
843
|
+
const close = path.indexOf("]", i + 1);
|
|
844
|
+
if (close === -1) break;
|
|
845
|
+
const inner = path.slice(i + 1, close);
|
|
846
|
+
if (inner.length > 0) {
|
|
847
|
+
const n = Number(inner);
|
|
848
|
+
if (Number.isInteger(n)) {
|
|
849
|
+
out.push({ kind: "index", idx: n });
|
|
850
|
+
} else {
|
|
851
|
+
if (FORBIDDEN_KEYS.has(inner)) return [];
|
|
852
|
+
out.push({ kind: "key", name: inner });
|
|
853
|
+
}
|
|
723
854
|
}
|
|
855
|
+
i = close + 1;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
let j = i + 1;
|
|
859
|
+
while (j < path.length) {
|
|
860
|
+
const c = path[j];
|
|
861
|
+
if (c === "." || c === "[" || c === "]") break;
|
|
862
|
+
j++;
|
|
724
863
|
}
|
|
864
|
+
const name = path.slice(i, j);
|
|
865
|
+
if (FORBIDDEN_KEYS.has(name)) return [];
|
|
866
|
+
out.push({ kind: "key", name });
|
|
867
|
+
i = j;
|
|
725
868
|
}
|
|
726
869
|
return out;
|
|
727
870
|
}
|
|
@@ -918,7 +1061,23 @@ function defaultContentTypeFor(bodyType) {
|
|
|
918
1061
|
}
|
|
919
1062
|
}
|
|
920
1063
|
function openApiPathToHono(path) {
|
|
921
|
-
|
|
1064
|
+
let out = "";
|
|
1065
|
+
let i = 0;
|
|
1066
|
+
while (i < path.length) {
|
|
1067
|
+
if (path[i] === "{") {
|
|
1068
|
+
const close = path.indexOf("}", i + 1);
|
|
1069
|
+
if (close === -1) {
|
|
1070
|
+
out += path.slice(i);
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
out += ":" + path.slice(i + 1, close);
|
|
1074
|
+
i = close + 1;
|
|
1075
|
+
} else {
|
|
1076
|
+
out += path[i];
|
|
1077
|
+
i++;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return out;
|
|
922
1081
|
}
|
|
923
1082
|
|
|
924
1083
|
// src/runtime/nodeAdapter.ts
|
|
@@ -956,11 +1115,60 @@ async function isPortFree(port) {
|
|
|
956
1115
|
|
|
957
1116
|
// src/runtime/nodeAdapter.ts
|
|
958
1117
|
var CLOSE_TIMEOUT_MS = 3e3;
|
|
1118
|
+
var MockServerStartError = class extends Error {
|
|
1119
|
+
code;
|
|
1120
|
+
port;
|
|
1121
|
+
host;
|
|
1122
|
+
constructor(opts) {
|
|
1123
|
+
super(opts.message);
|
|
1124
|
+
this.name = "MockServerStartError";
|
|
1125
|
+
this.code = opts.code;
|
|
1126
|
+
this.port = opts.port;
|
|
1127
|
+
this.host = opts.host;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
function explainBindError(code, port, host) {
|
|
1131
|
+
switch (code) {
|
|
1132
|
+
case "EADDRINUSE":
|
|
1133
|
+
return `Port ${port} on ${host} is already in use. Stop the other process or pick a different port.`;
|
|
1134
|
+
case "EACCES":
|
|
1135
|
+
return `Permission denied binding to port ${port} on ${host}. Ports below 1024 usually require elevated privileges \u2014 pick a port in 1024\u201365535.`;
|
|
1136
|
+
case "EADDRNOTAVAIL":
|
|
1137
|
+
return `Cannot bind to ${host}:${port} \u2014 that address is not available on this machine.`;
|
|
1138
|
+
default:
|
|
1139
|
+
return `Failed to bind ${host}:${port} (${code}).`;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
959
1142
|
async function serveOnNode(app, opts = {}) {
|
|
960
|
-
const port = opts.port && opts.port > 0 ? opts.port : await getFreePort();
|
|
961
1143
|
const host = opts.host ?? "127.0.0.1";
|
|
1144
|
+
let port;
|
|
1145
|
+
if (opts.port === void 0 || opts.port === 0) {
|
|
1146
|
+
port = await getFreePort();
|
|
1147
|
+
} else {
|
|
1148
|
+
if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
|
|
1149
|
+
throw new MockServerStartError({
|
|
1150
|
+
code: "INVALID_PORT",
|
|
1151
|
+
port: opts.port,
|
|
1152
|
+
host,
|
|
1153
|
+
message: `Invalid port ${String(opts.port)} \u2014 must be an integer between 1 and 65535.`
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
port = opts.port;
|
|
1157
|
+
}
|
|
962
1158
|
let server = null;
|
|
963
1159
|
await new Promise((resolve, reject) => {
|
|
1160
|
+
const wrapError = (err) => {
|
|
1161
|
+
const candidate = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
1162
|
+
const code = typeof candidate === "string" ? candidate : "UNKNOWN";
|
|
1163
|
+
reject(
|
|
1164
|
+
new MockServerStartError({
|
|
1165
|
+
code,
|
|
1166
|
+
port,
|
|
1167
|
+
host,
|
|
1168
|
+
message: explainBindError(code, port, host)
|
|
1169
|
+
})
|
|
1170
|
+
);
|
|
1171
|
+
};
|
|
964
1172
|
try {
|
|
965
1173
|
server = serve(
|
|
966
1174
|
{
|
|
@@ -970,9 +1178,9 @@ async function serveOnNode(app, opts = {}) {
|
|
|
970
1178
|
},
|
|
971
1179
|
() => resolve()
|
|
972
1180
|
);
|
|
973
|
-
server.on("error",
|
|
1181
|
+
server.on("error", wrapError);
|
|
974
1182
|
} catch (err) {
|
|
975
|
-
|
|
1183
|
+
wrapError(err);
|
|
976
1184
|
}
|
|
977
1185
|
});
|
|
978
1186
|
return {
|
|
@@ -1027,6 +1235,7 @@ async function stopMockServer(handle) {
|
|
|
1027
1235
|
return handle.close();
|
|
1028
1236
|
}
|
|
1029
1237
|
export {
|
|
1238
|
+
MockServerStartError,
|
|
1030
1239
|
buildRouter,
|
|
1031
1240
|
createMockApp,
|
|
1032
1241
|
getFreePort,
|