openapi-typesafe-fetch 1.0.0-rc.2 → 1.0.0-rc.4

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
@@ -3,13 +3,15 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  const PATH_PARAM_RE = /\{[^{}]+\}/g;
6
- const DOT_SEGMENT_RE = /(^|\/)\.\.?(\/|$)/;
6
+ const DOT_SEGMENT_RE = /^\.\.?$/;
7
7
  const QUERY_OR_FRAGMENT_RE = /[?#]/;
8
- const JSON_MEDIA_TYPE_RE = /json/i;
8
+ const JSON_MEDIA_TYPE_RE = /^[^/]+\/([^;]*\+)?json\s*(;|$)/i;
9
+ const TEXT_MEDIA_TYPE_RE = /^text\//i;
9
10
  const FORM_MEDIA_TYPE_RE = /^application\/x-www-form-urlencoded(;|$)/i;
10
11
  const MULTIPART_MEDIA_TYPE_RE = /^multipart\/form-data(;|$)/i;
11
12
  const NESTED_FORM_VALUE_MESSAGE = "Nested form values need your own `bodySerializer()`.";
12
13
  const BOUNDARY_PARAMETER_RE = /;\s*boundary=/i;
14
+ const CLIENT_BUILT_FORM = Symbol.for("openapi-typesafe-fetch/form");
13
15
  const supportsRequestInitExt = () => {
14
16
  return typeof process === "object" && process.versions?.undici;
15
17
  };
@@ -80,12 +82,14 @@ function createClient(clientOptions) {
80
82
  headers,
81
83
  params.header
82
84
  );
83
- if (serializedBody instanceof FormData && !BOUNDARY_PARAMETER_RE.test(finalHeaders.get("Content-Type"))) {
84
- finalHeaders.delete("Content-Type");
85
+ if (isPlatformInstance(serializedBody, "FormData")) {
86
+ dropFormContentType(finalHeaders, serializedBody[CLIENT_BUILT_FORM] === true);
85
87
  }
86
88
  const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
87
89
  const requestInit = {
88
90
  redirect: "follow",
91
+ // The Fetch standard requires `duplex: "half"` (its only value) for a stream body.
92
+ duplex: isStreamBody(serializedBody) ? "half" : void 0,
89
93
  ...baseOptions,
90
94
  ...init,
91
95
  body: serializedBody,
@@ -145,8 +149,8 @@ function createClient(clientOptions) {
145
149
  if (!response) {
146
150
  try {
147
151
  response = await fetch(request, requestInitExt);
148
- } catch (error2) {
149
- let errorAfterMiddleware = error2;
152
+ } catch (error) {
153
+ let errorAfterMiddleware = error;
150
154
  if (finalMiddlewares.length) {
151
155
  for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
152
156
  const m = finalMiddlewares[i];
@@ -178,6 +182,9 @@ function createClient(clientOptions) {
178
182
  throw errorAfterMiddleware;
179
183
  }
180
184
  }
185
+ if (!isResponse(response)) {
186
+ throw new Error("fetch: must return new Response()");
187
+ }
181
188
  if (finalMiddlewares.length) {
182
189
  for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
183
190
  const m = finalMiddlewares[i];
@@ -207,28 +214,9 @@ function createClient(clientOptions) {
207
214
  return response.ok ? { data: void 0, response } : { error: void 0, response };
208
215
  }
209
216
  if (response.ok) {
210
- const getResponseData = async () => {
211
- if (parseAs === "stream") {
212
- return response.body;
213
- }
214
- if (parseAs === "json" && !contentLength) {
215
- const raw2 = await response.text();
216
- return raw2 ? JSON.parse(raw2) : void 0;
217
- }
218
- return await response[parseAs]();
219
- };
220
- return { data: await getResponseData(), response };
221
- }
222
- const raw = await response.text();
223
- if (!raw) {
224
- return { error: void 0, response };
225
- }
226
- let error = raw;
227
- try {
228
- error = JSON.parse(raw);
229
- } catch {
217
+ return { data: await readSuccessBody(response, parseAs), response };
230
218
  }
231
- return { error, response };
219
+ return { error: await readErrorBody(response), response };
232
220
  }
233
221
  return {
234
222
  request(method, url, init) {
@@ -352,6 +340,11 @@ function wrapAsPathBasedClient(coreClient) {
352
340
  function createPathBasedClient(clientOptions) {
353
341
  return wrapAsPathBasedClient(createClient(clientOptions));
354
342
  }
343
+ function dropFormContentType(headers, isClientBuilt) {
344
+ if (isClientBuilt || !BOUNDARY_PARAMETER_RE.test(headers.get("Content-Type"))) {
345
+ headers.delete("Content-Type");
346
+ }
347
+ }
355
348
  function isHeaders(value) {
356
349
  return typeof value?.get === "function";
357
350
  }
@@ -359,142 +352,183 @@ function isRequest(value) {
359
352
  return typeof value?.method === "string" && typeof value.url === "string" && isHeaders(value.headers);
360
353
  }
361
354
  function isResponse(value) {
362
- return typeof value?.status === "number" && isHeaders(value.headers);
355
+ return typeof value?.ok === "boolean" && typeof value.status === "number" && typeof value.text === "function" && isHeaders(value.headers);
356
+ }
357
+ function parseJSON(raw, response) {
358
+ try {
359
+ return JSON.parse(raw);
360
+ } catch {
361
+ const contentType = response.headers.get("Content-Type");
362
+ const hint = TEXT_MEDIA_TYPE_RE.test(contentType) ? 'set `parseAs: "text"`' : "set `parseAs`";
363
+ const subject = `Response ${response.status} (${contentType ?? "no Content-Type"})`;
364
+ throw new SyntaxError(`${subject} is not JSON (${raw.length} characters); ${hint}`);
365
+ }
366
+ }
367
+ async function readSuccessBody(response, parseAs) {
368
+ if (parseAs === "stream") {
369
+ return response.body;
370
+ }
371
+ if (parseAs !== "json") {
372
+ return response[parseAs]();
373
+ }
374
+ const raw = await response.text();
375
+ return raw ? parseJSON(raw, response) : void 0;
376
+ }
377
+ async function readErrorBody(response) {
378
+ const raw = await response.text();
379
+ if (!raw) {
380
+ return void 0;
381
+ }
382
+ try {
383
+ return JSON.parse(raw);
384
+ } catch {
385
+ return raw;
386
+ }
363
387
  }
364
388
  const KEPT_NAME_ESCAPES = /%(?:5B|5D|24|3A|40|2C)/g;
365
389
  function encodeName(name) {
366
390
  return encodeURIComponent(name).replace(KEPT_NAME_ESCAPES, decodeURIComponent);
367
391
  }
392
+ function isDate(value) {
393
+ return typeof value?.toISOString === "function";
394
+ }
395
+ function valueText(value) {
396
+ return isDate(value) ? value.toISOString() : value;
397
+ }
398
+ function encodeValue(value, allowReserved) {
399
+ const text = valueText(value);
400
+ return allowReserved === true ? text : encodeURIComponent(text);
401
+ }
368
402
  function serializePrimitiveParam(name, value, options) {
369
403
  if (value === void 0 || value === null) {
370
404
  return "";
371
405
  }
372
- if (typeof value === "object") {
406
+ if (typeof value === "object" && !isDate(value)) {
373
407
  throw new Error(
374
408
  "Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."
375
409
  );
376
410
  }
377
- return `${name}=${options?.allowReserved === true ? value : encodeURIComponent(value)}`;
411
+ return `${name}=${encodeValue(value, options?.allowReserved)}`;
412
+ }
413
+ function joinExploded(values, style) {
414
+ const joiner = { simple: ",", label: ".", matrix: ";" }[style] || "&";
415
+ const final = values.join(joiner);
416
+ return style === "label" || style === "matrix" ? `${joiner}${final}` : final;
417
+ }
418
+ function serializeUnexplodedObject(name, value, options) {
419
+ const values = [];
420
+ for (const k in value) {
421
+ values.push(encodeValue(k), encodeValue(value[k], options.allowReserved));
422
+ }
423
+ const final = values.join(",");
424
+ switch (options.style) {
425
+ case "form": {
426
+ return `${name}=${final}`;
427
+ }
428
+ case "label": {
429
+ return `.${final}`;
430
+ }
431
+ case "matrix": {
432
+ return `;${name}=${final}`;
433
+ }
434
+ default: {
435
+ return final;
436
+ }
437
+ }
378
438
  }
379
439
  function serializeObjectParam(name, value, options) {
380
- if (!value || typeof value !== "object") {
440
+ if (!value || typeof value !== "object" || !Object.keys(value).length) {
381
441
  return "";
382
442
  }
383
- const values = [];
384
- const joiner = {
385
- simple: ",",
386
- label: ".",
387
- matrix: ";"
388
- }[options.style] || "&";
389
443
  if (options.style !== "deepObject" && options.explode === false) {
390
- for (const k in value) {
391
- values.push(
392
- encodeName(k),
393
- options.allowReserved === true ? value[k] : encodeURIComponent(value[k])
394
- );
395
- }
396
- const final2 = values.join(",");
397
- switch (options.style) {
398
- case "form": {
399
- return `${name}=${final2}`;
400
- }
401
- case "label": {
402
- return `.${final2}`;
403
- }
404
- case "matrix": {
405
- return `;${name}=${final2}`;
406
- }
407
- default: {
408
- return final2;
409
- }
410
- }
444
+ return serializeUnexplodedObject(name, value, options);
411
445
  }
446
+ const values = [];
412
447
  for (const k in value) {
413
- const finalName = options.style === "deepObject" ? `${name}[${encodeName(k)}]` : encodeName(k);
414
- values.push(serializePrimitiveParam(finalName, value[k], options));
448
+ const key = encodeValue(k);
449
+ const finalName = options.style === "deepObject" ? `${name}[${key}]` : key;
450
+ if (value[k] != null) {
451
+ values.push(serializePrimitiveParam(finalName, value[k], options));
452
+ }
453
+ }
454
+ return values.length ? joinExploded(values, options.style) : "";
455
+ }
456
+ function serializeUnexplodedArray(name, value, options) {
457
+ const joiner = { form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || ",";
458
+ const final = value.map((v) => encodeValue(v, options.allowReserved)).join(joiner);
459
+ switch (options.style) {
460
+ case "simple": {
461
+ return final;
462
+ }
463
+ case "label": {
464
+ return `.${final}`;
465
+ }
466
+ case "matrix": {
467
+ return `;${name}=${final}`;
468
+ }
469
+ default: {
470
+ return `${name}=${final}`;
471
+ }
415
472
  }
416
- const final = values.join(joiner);
417
- return options.style === "label" || options.style === "matrix" ? `${joiner}${final}` : final;
418
473
  }
419
474
  function serializeArrayParam(name, value, options) {
420
- if (!Array.isArray(value)) {
475
+ if (!Array.isArray(value) || !value.length) {
421
476
  return "";
422
477
  }
423
478
  if (options.explode === false) {
424
- const joiner2 = { form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || ",";
425
- const final = (options.allowReserved === true ? value : value.map((v) => encodeURIComponent(v))).join(joiner2);
426
- switch (options.style) {
427
- case "simple": {
428
- return final;
429
- }
430
- case "label": {
431
- return `.${final}`;
432
- }
433
- case "matrix": {
434
- return `;${name}=${final}`;
435
- }
436
- // case "spaceDelimited":
437
- // case "pipeDelimited":
438
- default: {
439
- return `${name}=${final}`;
440
- }
441
- }
479
+ return serializeUnexplodedArray(name, value, options);
442
480
  }
443
- const joiner = { simple: ",", label: ".", matrix: ";" }[options.style] || "&";
444
481
  const values = [];
445
482
  for (const v of value) {
446
483
  if (options.style === "simple" || options.style === "label") {
447
- values.push(options.allowReserved === true ? v : encodeURIComponent(v));
448
- } else {
484
+ values.push(encodeValue(v, options.allowReserved));
485
+ } else if (v != null) {
449
486
  values.push(serializePrimitiveParam(name, v, options));
450
487
  }
451
488
  }
452
- return options.style === "label" || options.style === "matrix" ? `${joiner}${values.join(joiner)}` : values.join(joiner);
489
+ return values.length ? joinExploded(values, options.style) : "";
490
+ }
491
+ function serializeQueryParam(name, value, options) {
492
+ const allowReserved = options?.allowReserved || false;
493
+ if (Array.isArray(value)) {
494
+ return serializeArrayParam(name, value, {
495
+ style: "form",
496
+ explode: true,
497
+ ...options?.array,
498
+ allowReserved
499
+ });
500
+ }
501
+ if (value && typeof value === "object" && !isDate(value)) {
502
+ return serializeObjectParam(name, value, {
503
+ style: "deepObject",
504
+ explode: true,
505
+ ...options?.object,
506
+ allowReserved
507
+ });
508
+ }
509
+ return serializePrimitiveParam(name, value, options);
453
510
  }
454
511
  function createQuerySerializer(options) {
455
512
  return function querySerializer(queryParams) {
456
513
  const search = [];
457
514
  if (queryParams && typeof queryParams === "object") {
458
515
  for (const key in queryParams) {
459
- const value = queryParams[key];
460
- const name = encodeName(key);
461
- if (value === void 0 || value === null) {
462
- continue;
463
- }
464
- if (Array.isArray(value)) {
465
- if (value.length === 0) {
466
- continue;
467
- }
468
- search.push(
469
- serializeArrayParam(name, value, {
470
- style: "form",
471
- explode: true,
472
- ...options?.array,
473
- allowReserved: options?.allowReserved || false
474
- })
475
- );
476
- continue;
477
- }
478
- if (typeof value === "object") {
479
- search.push(
480
- serializeObjectParam(name, value, {
481
- style: "deepObject",
482
- explode: true,
483
- ...options?.object,
484
- allowReserved: options?.allowReserved || false
485
- })
486
- );
487
- continue;
516
+ const part = serializeQueryParam(encodeName(key), queryParams[key], options);
517
+ if (part) {
518
+ search.push(part);
488
519
  }
489
- search.push(serializePrimitiveParam(name, value, options));
490
520
  }
491
521
  }
492
522
  return search.join("&");
493
523
  };
494
524
  }
495
525
  function defaultPathSerializer(pathname, pathParams) {
526
+ const matches = pathname.match(PATH_PARAM_RE);
527
+ if (!matches) {
528
+ return pathname;
529
+ }
496
530
  let nextURL = pathname;
497
- for (const match of pathname.match(PATH_PARAM_RE) ?? []) {
531
+ for (const match of matches) {
498
532
  let name = match.substring(1, match.length - 1);
499
533
  let explode = false;
500
534
  let style = "simple";
@@ -516,26 +550,49 @@ function defaultPathSerializer(pathname, pathParams) {
516
550
  let serialized;
517
551
  if (Array.isArray(value)) {
518
552
  serialized = serializeArrayParam(name, value, { style, explode });
519
- } else if (typeof value === "object") {
553
+ } else if (typeof value === "object" && !isDate(value)) {
520
554
  serialized = serializeObjectParam(name, value, { style, explode });
521
555
  } else if (style === "matrix") {
522
556
  serialized = `;${serializePrimitiveParam(name, value)}`;
523
557
  } else {
524
- serialized = style === "label" ? `.${encodeURIComponent(value)}` : encodeURIComponent(value);
558
+ serialized = style === "label" ? `.${encodeValue(value)}` : encodeValue(value);
525
559
  }
526
560
  nextURL = nextURL.replace(match, () => serialized);
527
561
  }
528
- const path = nextURL.split(QUERY_OR_FRAGMENT_RE, 1)[0];
529
- if (DOT_SEGMENT_RE.test(path)) {
530
- throw new Error(`Dot path segment in ${nextURL}`);
562
+ checkSerializedPath(pathname, nextURL);
563
+ return nextURL;
564
+ }
565
+ function checkSerializedPath(template, url) {
566
+ const fixed = template.slice(0, template.search(PATH_PARAM_RE));
567
+ const path = url.split(QUERY_OR_FRAGMENT_RE, 1)[0];
568
+ const first = fixed.split("/").length - 1;
569
+ let index = path.split("/").findIndex((segment, at) => at >= first && DOT_SEGMENT_RE.test(segment));
570
+ let problem = "Dot";
571
+ if (index === -1 && path.startsWith("//") && !fixed.startsWith("//")) {
572
+ [index, problem] = [1, "Empty first"];
531
573
  }
532
- if (path.startsWith("//")) {
533
- throw new Error(`Empty first path segment in ${nextURL}`);
574
+ if (index !== -1) {
575
+ const name = template.split(QUERY_OR_FRAGMENT_RE, 1)[0].split("/")[index];
576
+ throw new Error(`${problem} path segment in "${name}"`);
534
577
  }
535
- return nextURL;
578
+ }
579
+ function isPlatformInstance(value, name) {
580
+ const type = globalThis[name];
581
+ return typeof type === "function" && value instanceof type;
582
+ }
583
+ function isStreamBody(body) {
584
+ return isPlatformInstance(body, "ReadableStream") || typeof body?.[Symbol.asyncIterator] === "function";
536
585
  }
537
586
  function isRawBody(body) {
538
- return body instanceof FormData || body instanceof Blob || body instanceof URLSearchParams || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
587
+ return ["FormData", "Blob", "URLSearchParams", "ArrayBuffer"].some(
588
+ (name) => isPlatformInstance(body, name)
589
+ ) || ArrayBuffer.isView(body) || isStreamBody(body);
590
+ }
591
+ function isNestedFormValue(value) {
592
+ return value && typeof value === "object" && !isDate(value);
593
+ }
594
+ function formItems(value) {
595
+ return Array.isArray(value) ? Array.from(value) : [value];
539
596
  }
540
597
  function formBody(body) {
541
598
  if (!body || typeof body !== "object" || Array.isArray(body)) {
@@ -543,25 +600,27 @@ function formBody(body) {
543
600
  }
544
601
  const form = new URLSearchParams();
545
602
  for (const [name, value] of Object.entries(body)) {
546
- if (value && typeof value === "object" && !Array.isArray(value)) {
603
+ const items = formItems(value);
604
+ if (items.some(isNestedFormValue)) {
547
605
  throw new Error(NESTED_FORM_VALUE_MESSAGE);
548
606
  }
549
607
  if (value != null) {
550
- form.append(name, value);
608
+ form.append(name, items.map(valueText).join(","));
551
609
  }
552
610
  }
553
611
  return form.toString();
554
612
  }
555
613
  function multipartBody(body) {
556
614
  const formData = new FormData();
615
+ formData[CLIENT_BUILT_FORM] = true;
557
616
  for (const [name, value] of Object.entries(body)) {
558
- for (const item of [value].flat()) {
559
- if (item instanceof Blob) {
617
+ for (const item of formItems(value)) {
618
+ if (isPlatformInstance(item, "Blob")) {
560
619
  formData.append(name, item);
561
- } else if (item && typeof item === "object") {
620
+ } else if (isNestedFormValue(item)) {
562
621
  throw new Error(NESTED_FORM_VALUE_MESSAGE);
563
622
  } else if (item != null) {
564
- formData.append(name, String(item));
623
+ formData.append(name, String(valueText(item)));
565
624
  }
566
625
  }
567
626
  }