@apicircle/mock-server-core 1.0.9 → 1.1.2

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.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: { pathParams: [], queryParams: [], headers: [], cookies: [] },
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(path, upper, op, opts, warnings, endpointId++);
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 buildEndpointFromOp(path, method, op, opts, warnings, index) {
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 re = /([^.[\]]+)|\[([^\]]+)\]/g;
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
- const re = /([^.[\]]+)|\[([^\]]+)\]/g;
711
- let match;
712
- while ((match = re.exec(path)) !== null) {
713
- if (match[1] !== void 0) {
714
- if (FORBIDDEN_KEYS.has(match[1])) return [];
715
- out.push({ kind: "key", name: match[1] });
716
- } else if (match[2] !== void 0) {
717
- const n = Number(match[2]);
718
- if (Number.isInteger(n)) {
719
- out.push({ kind: "index", idx: n });
720
- } else {
721
- if (FORBIDDEN_KEYS.has(match[2])) return [];
722
- out.push({ kind: "key", name: match[2] });
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
- return path.replace(/\{([^}]+)\}/g, ":$1");
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", reject);
1181
+ server.on("error", wrapError);
974
1182
  } catch (err) {
975
- reject(err instanceof Error ? err : new Error(String(err)));
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,