@apicircle/mock-server-core 1.0.9 → 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 CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ MockServerStartError: () => MockServerStartError,
33
34
  buildRouter: () => buildRouter,
34
35
  createMockApp: () => createMockApp,
35
36
  getFreePort: () => getFreePort,
@@ -48,6 +49,7 @@ module.exports = __toCommonJS(index_exports);
48
49
  // src/parsers/openapi.ts
49
50
  var import_swagger_parser = __toESM(require("@apidevtools/swagger-parser"), 1);
50
51
  var import_js_yaml = __toESM(require("js-yaml"), 1);
52
+ var import_shared2 = require("@apicircle/shared");
51
53
 
52
54
  // src/faker/schemaToExample.ts
53
55
  var FORMAT_DEFAULTS = {
@@ -110,6 +112,17 @@ function pickType(type) {
110
112
  }
111
113
 
112
114
  // src/parsers/buildEndpoint.ts
115
+ var import_shared = require("@apicircle/shared");
116
+ function paramDef(name, opts) {
117
+ return {
118
+ id: (0, import_shared.generateId)(),
119
+ name,
120
+ typeHint: opts?.typeHint,
121
+ required: opts?.required,
122
+ description: opts?.description,
123
+ example: opts?.example
124
+ };
125
+ }
113
126
  function bodyTypeForContentType(contentType) {
114
127
  if (!contentType) return "json";
115
128
  const main = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
@@ -150,7 +163,7 @@ function buildMockEndpoint(input) {
150
163
  method: input.method,
151
164
  pathPattern: input.pathPattern,
152
165
  description: input.description,
153
- requestSchema: { pathParams: [], queryParams: [], headers: [], cookies: [] },
166
+ requestSchema: input.requestSchema ?? (0, import_shared.makeDefaultRequestSchema)(),
154
167
  requestValidation: [],
155
168
  responseRules: [],
156
169
  defaultResponse: buildMockResponse(input.response),
@@ -189,18 +202,64 @@ async function parseOpenApiToEndpoints(source, format = "json", opts = {}) {
189
202
  let endpointId = 0;
190
203
  for (const [path, ops] of Object.entries(paths)) {
191
204
  if (!ops || typeof ops !== "object") continue;
205
+ const pathItemParams = ops.parameters ?? [];
192
206
  for (const method of Object.keys(ops)) {
193
207
  const upper = method.toUpperCase();
194
208
  if (!SUPPORTED_METHODS.includes(upper)) continue;
195
209
  const op = ops[method];
196
210
  if (!op || typeof op !== "object") continue;
197
- const built = buildEndpointFromOp(path, upper, op, opts, warnings, endpointId++);
211
+ const built = buildEndpointFromOp(
212
+ path,
213
+ upper,
214
+ op,
215
+ pathItemParams,
216
+ opts,
217
+ warnings,
218
+ endpointId++
219
+ );
198
220
  if (built) endpoints.push(built);
199
221
  }
200
222
  }
201
223
  return { endpoints, warnings };
202
224
  }
203
- function buildEndpointFromOp(path, method, op, opts, warnings, index) {
225
+ function buildRequestSchema(pathItemParams, op) {
226
+ const merged = /* @__PURE__ */ new Map();
227
+ for (const p of [...pathItemParams, ...op.parameters ?? []]) {
228
+ if (p && typeof p === "object" && typeof p.name === "string") {
229
+ merged.set(`${p.in ?? "query"}:${p.name}`, p);
230
+ }
231
+ }
232
+ const schema = (0, import_shared2.makeDefaultRequestSchema)();
233
+ for (const p of merged.values()) {
234
+ const rawType = p.schema?.type ?? p.type;
235
+ const typeHint = p.schema?.format ?? (Array.isArray(rawType) ? rawType[0] : rawType);
236
+ const exampleVal = p.example ?? p.schema?.example;
237
+ const def = paramDef(p.name, {
238
+ typeHint: typeof typeHint === "string" ? typeHint : void 0,
239
+ required: typeof p.required === "boolean" ? p.required : void 0,
240
+ description: typeof p.description === "string" ? p.description : void 0,
241
+ example: exampleVal === void 0 ? void 0 : typeof exampleVal === "string" ? exampleVal : JSON.stringify(exampleVal)
242
+ });
243
+ switch (p.in) {
244
+ case "path":
245
+ schema.pathParams.push(def);
246
+ break;
247
+ case "query":
248
+ schema.queryParams.push(def);
249
+ break;
250
+ case "header":
251
+ schema.headers.push(def);
252
+ break;
253
+ case "cookie":
254
+ schema.cookies.push(def);
255
+ break;
256
+ default:
257
+ break;
258
+ }
259
+ }
260
+ return schema;
261
+ }
262
+ function buildEndpointFromOp(path, method, op, pathItemParams, opts, warnings, index) {
204
263
  const responses = op.responses ?? {};
205
264
  const candidates = Object.keys(responses).filter((code) => /^2\d\d$/.test(code)).map((code) => Number(code));
206
265
  if (candidates.length === 0) {
@@ -220,6 +279,7 @@ function buildEndpointFromOp(path, method, op, opts, warnings, index) {
220
279
  method,
221
280
  pathPattern: path,
222
281
  example: exampleName,
282
+ requestSchema: buildRequestSchema(pathItemParams, op),
223
283
  response: { status, headers, body }
224
284
  });
225
285
  }
@@ -322,6 +382,26 @@ function safeYamlLoad(s) {
322
382
  }
323
383
 
324
384
  // src/parsers/postman.ts
385
+ var import_shared3 = require("@apicircle/shared");
386
+ function postmanRequestSchema(req) {
387
+ const schema = (0, import_shared3.makeDefaultRequestSchema)();
388
+ const url = req.url;
389
+ if (url && typeof url === "object") {
390
+ for (const v of url.variable ?? []) {
391
+ if (v?.key)
392
+ schema.pathParams.push(paramDef(v.key, { example: v.value, description: v.description }));
393
+ }
394
+ for (const q of url.query ?? []) {
395
+ if (q?.key && !q.disabled)
396
+ schema.queryParams.push(paramDef(q.key, { example: q.value, description: q.description }));
397
+ }
398
+ }
399
+ for (const h of req.header ?? []) {
400
+ if (h?.key && !h.disabled)
401
+ schema.headers.push(paramDef(h.key, { example: h.value, description: h.description }));
402
+ }
403
+ return schema;
404
+ }
325
405
  var SUPPORTED_METHODS2 = [
326
406
  "GET",
327
407
  "POST",
@@ -353,6 +433,7 @@ function parsePostmanToEndpoints(source) {
353
433
  warnings.push(`Skipping request with no extractable path: ${item.name ?? "(unnamed)"}`);
354
434
  return;
355
435
  }
436
+ const requestSchema = postmanRequestSchema(item.request);
356
437
  const example = item.response?.[0];
357
438
  if (example) {
358
439
  endpoints.push(
@@ -362,6 +443,7 @@ function parsePostmanToEndpoints(source) {
362
443
  method,
363
444
  pathPattern: path,
364
445
  example: example.name,
446
+ requestSchema,
365
447
  response: {
366
448
  // Postman's `code` is the canonical numeric status; `status` is a
367
449
  // human-readable label that *sometimes* parses as a number. Try
@@ -379,6 +461,7 @@ function parsePostmanToEndpoints(source) {
379
461
  name: item.name,
380
462
  method,
381
463
  pathPattern: path,
464
+ requestSchema,
382
465
  response: {
383
466
  status: 200,
384
467
  headers: [{ key: "Content-Type", value: "application/json" }],
@@ -422,6 +505,26 @@ function slug2(s) {
422
505
  }
423
506
 
424
507
  // src/parsers/insomnia.ts
508
+ var import_shared4 = require("@apicircle/shared");
509
+ function extractPathSlots(path) {
510
+ const out = /* @__PURE__ */ new Set();
511
+ for (const m of path.matchAll(/[:{]([A-Za-z0-9_]+)\}?/g)) out.add(m[1]);
512
+ return [...out];
513
+ }
514
+ function insomniaRequestSchema(r, path) {
515
+ const schema = (0, import_shared4.makeDefaultRequestSchema)();
516
+ for (const slot of extractPathSlots(path))
517
+ schema.pathParams.push(paramDef(slot, { required: true }));
518
+ for (const p of r.parameters ?? []) {
519
+ if (p?.name && !p.disabled)
520
+ schema.queryParams.push(paramDef(p.name, { example: p.value, description: p.description }));
521
+ }
522
+ for (const h of r.headers ?? []) {
523
+ if (h?.name && !h.disabled)
524
+ schema.headers.push(paramDef(h.name, { example: h.value, description: h.description }));
525
+ }
526
+ return schema;
527
+ }
425
528
  var SUPPORTED_METHODS3 = [
426
529
  "GET",
427
530
  "POST",
@@ -459,6 +562,7 @@ function parseInsomniaToEndpoints(source) {
459
562
  name: r.name,
460
563
  method,
461
564
  pathPattern: path,
565
+ requestSchema: insomniaRequestSchema(r, path),
462
566
  response: {
463
567
  status: 200,
464
568
  headers: [{ key: "Content-Type", value: "application/json" }],
@@ -658,12 +762,8 @@ function resolveJsonPath(body, jsonPath) {
658
762
  if (path.startsWith(".")) path = path.slice(1);
659
763
  if (path === "") return body;
660
764
  let cursor = body;
661
- const re = /([^.[\]]+)|\[([^\]]+)\]/g;
662
- let match;
663
- while ((match = re.exec(path)) !== null) {
765
+ for (const key of iterJsonPathTokens(path)) {
664
766
  if (cursor === void 0 || cursor === null) return void 0;
665
- const key = match[1] ?? match[2];
666
- if (key === void 0) return void 0;
667
767
  if (Array.isArray(cursor)) {
668
768
  const idx = Number(key);
669
769
  if (!Number.isInteger(idx)) return void 0;
@@ -678,6 +778,32 @@ function resolveJsonPath(body, jsonPath) {
678
778
  }
679
779
  return cursor;
680
780
  }
781
+ function* iterJsonPathTokens(path) {
782
+ let i = 0;
783
+ while (i < path.length) {
784
+ const ch = path[i];
785
+ if (ch === "." || ch === "]") {
786
+ i++;
787
+ continue;
788
+ }
789
+ if (ch === "[") {
790
+ const close = path.indexOf("]", i + 1);
791
+ if (close === -1) return;
792
+ const inner = path.slice(i + 1, close);
793
+ if (inner.length > 0) yield inner;
794
+ i = close + 1;
795
+ continue;
796
+ }
797
+ let j = i + 1;
798
+ while (j < path.length) {
799
+ const c = path[j];
800
+ if (c === "." || c === "[" || c === "]") break;
801
+ j++;
802
+ }
803
+ yield path.slice(i, j);
804
+ i = j;
805
+ }
806
+ }
681
807
 
682
808
  // src/response/applyMultipliers.ts
683
809
  function applyMultipliers(response, ctx) {
@@ -754,21 +880,39 @@ function parsePathSegments(jsonPath) {
754
880
  if (path.startsWith(".")) path = path.slice(1);
755
881
  if (path === "") return [];
756
882
  const out = [];
757
- const re = /([^.[\]]+)|\[([^\]]+)\]/g;
758
- let match;
759
- while ((match = re.exec(path)) !== null) {
760
- if (match[1] !== void 0) {
761
- if (FORBIDDEN_KEYS.has(match[1])) return [];
762
- out.push({ kind: "key", name: match[1] });
763
- } else if (match[2] !== void 0) {
764
- const n = Number(match[2]);
765
- if (Number.isInteger(n)) {
766
- out.push({ kind: "index", idx: n });
767
- } else {
768
- if (FORBIDDEN_KEYS.has(match[2])) return [];
769
- out.push({ kind: "key", name: match[2] });
883
+ let i = 0;
884
+ while (i < path.length) {
885
+ const ch = path[i];
886
+ if (ch === "." || ch === "]") {
887
+ i++;
888
+ continue;
889
+ }
890
+ if (ch === "[") {
891
+ const close = path.indexOf("]", i + 1);
892
+ if (close === -1) break;
893
+ const inner = path.slice(i + 1, close);
894
+ if (inner.length > 0) {
895
+ const n = Number(inner);
896
+ if (Number.isInteger(n)) {
897
+ out.push({ kind: "index", idx: n });
898
+ } else {
899
+ if (FORBIDDEN_KEYS.has(inner)) return [];
900
+ out.push({ kind: "key", name: inner });
901
+ }
770
902
  }
903
+ i = close + 1;
904
+ continue;
905
+ }
906
+ let j = i + 1;
907
+ while (j < path.length) {
908
+ const c = path[j];
909
+ if (c === "." || c === "[" || c === "]") break;
910
+ j++;
771
911
  }
912
+ const name = path.slice(i, j);
913
+ if (FORBIDDEN_KEYS.has(name)) return [];
914
+ out.push({ kind: "key", name });
915
+ i = j;
772
916
  }
773
917
  return out;
774
918
  }
@@ -965,7 +1109,23 @@ function defaultContentTypeFor(bodyType) {
965
1109
  }
966
1110
  }
967
1111
  function openApiPathToHono(path) {
968
- return path.replace(/\{([^}]+)\}/g, ":$1");
1112
+ let out = "";
1113
+ let i = 0;
1114
+ while (i < path.length) {
1115
+ if (path[i] === "{") {
1116
+ const close = path.indexOf("}", i + 1);
1117
+ if (close === -1) {
1118
+ out += path.slice(i);
1119
+ break;
1120
+ }
1121
+ out += ":" + path.slice(i + 1, close);
1122
+ i = close + 1;
1123
+ } else {
1124
+ out += path[i];
1125
+ i++;
1126
+ }
1127
+ }
1128
+ return out;
969
1129
  }
970
1130
 
971
1131
  // src/runtime/nodeAdapter.ts
@@ -1003,11 +1163,60 @@ async function isPortFree(port) {
1003
1163
 
1004
1164
  // src/runtime/nodeAdapter.ts
1005
1165
  var CLOSE_TIMEOUT_MS = 3e3;
1166
+ var MockServerStartError = class extends Error {
1167
+ code;
1168
+ port;
1169
+ host;
1170
+ constructor(opts) {
1171
+ super(opts.message);
1172
+ this.name = "MockServerStartError";
1173
+ this.code = opts.code;
1174
+ this.port = opts.port;
1175
+ this.host = opts.host;
1176
+ }
1177
+ };
1178
+ function explainBindError(code, port, host) {
1179
+ switch (code) {
1180
+ case "EADDRINUSE":
1181
+ return `Port ${port} on ${host} is already in use. Stop the other process or pick a different port.`;
1182
+ case "EACCES":
1183
+ return `Permission denied binding to port ${port} on ${host}. Ports below 1024 usually require elevated privileges \u2014 pick a port in 1024\u201365535.`;
1184
+ case "EADDRNOTAVAIL":
1185
+ return `Cannot bind to ${host}:${port} \u2014 that address is not available on this machine.`;
1186
+ default:
1187
+ return `Failed to bind ${host}:${port} (${code}).`;
1188
+ }
1189
+ }
1006
1190
  async function serveOnNode(app, opts = {}) {
1007
- const port = opts.port && opts.port > 0 ? opts.port : await getFreePort();
1008
1191
  const host = opts.host ?? "127.0.0.1";
1192
+ let port;
1193
+ if (opts.port === void 0 || opts.port === 0) {
1194
+ port = await getFreePort();
1195
+ } else {
1196
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
1197
+ throw new MockServerStartError({
1198
+ code: "INVALID_PORT",
1199
+ port: opts.port,
1200
+ host,
1201
+ message: `Invalid port ${String(opts.port)} \u2014 must be an integer between 1 and 65535.`
1202
+ });
1203
+ }
1204
+ port = opts.port;
1205
+ }
1009
1206
  let server = null;
1010
1207
  await new Promise((resolve, reject) => {
1208
+ const wrapError = (err) => {
1209
+ const candidate = err && typeof err === "object" && "code" in err ? err.code : void 0;
1210
+ const code = typeof candidate === "string" ? candidate : "UNKNOWN";
1211
+ reject(
1212
+ new MockServerStartError({
1213
+ code,
1214
+ port,
1215
+ host,
1216
+ message: explainBindError(code, port, host)
1217
+ })
1218
+ );
1219
+ };
1011
1220
  try {
1012
1221
  server = (0, import_node_server.serve)(
1013
1222
  {
@@ -1017,9 +1226,9 @@ async function serveOnNode(app, opts = {}) {
1017
1226
  },
1018
1227
  () => resolve()
1019
1228
  );
1020
- server.on("error", reject);
1229
+ server.on("error", wrapError);
1021
1230
  } catch (err) {
1022
- reject(err instanceof Error ? err : new Error(String(err)));
1231
+ wrapError(err);
1023
1232
  }
1024
1233
  });
1025
1234
  return {
@@ -1075,6 +1284,7 @@ async function stopMockServer(handle) {
1075
1284
  }
1076
1285
  // Annotate the CommonJS export names for ESM import in node:
1077
1286
  0 && (module.exports = {
1287
+ MockServerStartError,
1078
1288
  buildRouter,
1079
1289
  createMockApp,
1080
1290
  getFreePort,