decoders 2.3.0 → 2.4.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/decoders.svg)](https://www.npmjs.com/package/decoders)
4
4
  [![Build Status](https://github.com/nvie/decoders/workflows/test/badge.svg)](https://github.com/nvie/decoders/actions)
5
- [![Minified Size](https://badgen.net/bundlephobia/minzip/decoders)](https://bundlephobia.com/result?p=decoders)
5
+ [![Bundle size for decoders](https://pkg-size.dev/badge/bundle/4200)](https://pkg-size.dev/decoders)
6
6
 
7
7
  Elegant and battle-tested validation library for type-safe input data for
8
8
  [TypeScript](https://www.typescriptlang.org/).
package/dist/index.cjs CHANGED
@@ -66,7 +66,8 @@ function annotateArray(arr, text, seen) {
66
66
  function annotateObject(obj, text, seen) {
67
67
  seen.add(obj);
68
68
  const fields = /* @__PURE__ */ new Map();
69
- for (const [key, value] of Object.entries(obj)) {
69
+ for (const key of Object.keys(obj)) {
70
+ const value = obj[key];
70
71
  fields.set(key, annotate(value, void 0, seen));
71
72
  }
72
73
  return makeObjectAnn(fields, text);
@@ -116,6 +117,10 @@ function indent(s, prefix = INDENT) {
116
117
  return `${prefix}${s}`;
117
118
  }
118
119
  }
120
+ var quotePattern = /'/g;
121
+ function quote(value) {
122
+ return typeof value === "string" ? "'" + value.replace(quotePattern, "\\'") + "'" : JSON.stringify(value);
123
+ }
119
124
 
120
125
  // src/core/format.ts
121
126
  function summarize(ann, keypath = []) {
@@ -144,9 +149,9 @@ function summarize(ann, keypath = []) {
144
149
  if (keypath.length === 0) {
145
150
  prefix = "";
146
151
  } else if (keypath.length === 1) {
147
- prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${JSON.stringify(keypath[0])}: `;
152
+ prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${quote(keypath[0])}: `;
148
153
  } else {
149
- prefix = `Value at keypath ${keypath.map(String).join(".")}: `;
154
+ prefix = `Value at keypath ${quote(keypath.map(String).join("."))}: `;
150
155
  }
151
156
  return [...result, `${prefix}${text}`];
152
157
  }
@@ -202,7 +207,7 @@ function serializeValue(value) {
202
207
  return "undefined";
203
208
  } else {
204
209
  if (isDate(value)) {
205
- return `new Date(${JSON.stringify(value.toISOString())})`;
210
+ return `new Date(${quote(value.toISOString())})`;
206
211
  } else if (value instanceof Date) {
207
212
  return "(Invalid Date)";
208
213
  } else {
@@ -311,10 +316,16 @@ function define(fn) {
311
316
  }
312
317
  function then(next) {
313
318
  return define((blob, ok2, err2) => {
314
- const result = decode(blob);
315
- return result.ok ? next(result.value, ok2, err2) : result;
319
+ const r1 = decode(blob);
320
+ if (!r1.ok)
321
+ return r1;
322
+ const r2 = isDecoder(next) ? next : next(r1.value, ok2, err2);
323
+ return isDecoder(r2) ? r2.decode(r1.value) : r2;
316
324
  });
317
325
  }
326
+ function pipe(next) {
327
+ return then(next);
328
+ }
318
329
  function reject(rejectFn) {
319
330
  return then((blob, ok2, err2) => {
320
331
  const errmsg = rejectFn(blob);
@@ -331,13 +342,7 @@ function define(fn) {
331
342
  }
332
343
  });
333
344
  }
334
- function peek(next) {
335
- return define((blob, ok2, err2) => {
336
- const result = decode(blob);
337
- return result.ok ? next([blob, result.value], ok2, err2) : result;
338
- });
339
- }
340
- return Object.freeze({
345
+ return brand2({
341
346
  verify,
342
347
  value,
343
348
  decode,
@@ -346,9 +351,17 @@ function define(fn) {
346
351
  reject,
347
352
  describe,
348
353
  then,
349
- peek
354
+ pipe
350
355
  });
351
356
  }
357
+ var _register2 = /* @__PURE__ */ new WeakSet();
358
+ function brand2(decoder) {
359
+ _register2.add(decoder);
360
+ return decoder;
361
+ }
362
+ function isDecoder(thing) {
363
+ return _register2.has(thing);
364
+ }
352
365
 
353
366
  // src/arrays.ts
354
367
  var poja = define((blob, ok2, err2) => {
@@ -489,7 +502,7 @@ function object(decoders) {
489
502
  objAnn = merge(objAnn, errors);
490
503
  }
491
504
  if (missingKeys.size > 0) {
492
- const errMsg = Array.from(missingKeys).map((key) => `"${key}"`).join(", ");
505
+ const errMsg = Array.from(missingKeys).map(quote).join(", ");
493
506
  const pluralized = missingKeys.size > 1 ? "keys" : "key";
494
507
  objAnn = updateText(objAnn, `Missing ${pluralized}: ${errMsg}`);
495
508
  }
@@ -503,17 +516,14 @@ function exact(decoders) {
503
516
  const checked = pojo.reject((plainObj) => {
504
517
  const actualKeys = new Set(Object.keys(plainObj));
505
518
  const extraKeys = difference(actualKeys, allowedKeys);
506
- return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).join(", ")}` : (
507
- // Don't reject
508
- null
509
- );
519
+ return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).map(quote).join(", ")}` : null;
510
520
  });
511
- return checked.then(object(decoders).decode);
521
+ return checked.pipe(object(decoders));
512
522
  }
513
523
  function inexact(decoders) {
514
- return pojo.then((plainObj) => {
524
+ return pojo.pipe((plainObj) => {
515
525
  const allkeys = new Set(Object.keys(plainObj));
516
- const decoder = object(decoders).transform((safepart) => {
526
+ return object(decoders).transform((safepart) => {
517
527
  const safekeys = new Set(Object.keys(decoders));
518
528
  for (const k of safekeys)
519
529
  allkeys.add(k);
@@ -530,7 +540,6 @@ function inexact(decoders) {
530
540
  }
531
541
  return rv;
532
542
  });
533
- return decoder.decode(plainObj);
534
543
  });
535
544
  }
536
545
 
@@ -566,9 +575,7 @@ function oneOf(constants) {
566
575
  if (winner !== void 0) {
567
576
  return ok2(winner);
568
577
  }
569
- return err2(
570
- `Must be one of ${constants.map((value) => JSON.stringify(value)).join(", ")}`
571
- );
578
+ return err2(`Must be one of ${constants.map((value) => quote(value)).join(", ")}`);
572
579
  });
573
580
  }
574
581
  function enum_(enumObj) {
@@ -594,9 +601,9 @@ function taggedUnion(field, mapping2) {
594
601
  );
595
602
  }
596
603
  function select(scout, selectFn) {
597
- return scout.peek(([blob, peekResult]) => {
598
- const decoder = selectFn(peekResult);
599
- return decoder.decode(blob);
604
+ return define((blob) => {
605
+ const result = scout.decode(blob);
606
+ return result.ok ? selectFn(result.value).decode(blob) : result;
600
607
  });
601
608
  }
602
609
 
@@ -627,9 +634,7 @@ function nullish(decoder, defaultValue) {
627
634
  }
628
635
  function constant(value) {
629
636
  return define(
630
- (blob, ok2, err2) => blob === value ? ok2(value) : err2(
631
- `Must be ${typeof value === "symbol" ? String(value) : JSON.stringify(value)}`
632
- )
637
+ (blob, ok2, err2) => blob === value ? ok2(value) : err2(`Must be ${typeof value === "symbol" ? String(value) : quote(value)}`)
633
638
  );
634
639
  }
635
640
  function always(value) {
@@ -645,36 +650,11 @@ var hardcoded = always;
645
650
  var unknown = define((blob, ok2, _) => ok2(blob));
646
651
  var mixed = unknown;
647
652
 
648
- // src/numbers.ts
649
- var anyNumber = define(
650
- (blob, ok2, err2) => isNumber(blob) ? ok2(blob) : err2("Must be number")
651
- );
652
- var number = anyNumber.refine(
653
- (n) => Number.isFinite(n),
654
- "Number must be finite"
655
- );
656
- var integer = number.refine(
657
- (n) => Number.isInteger(n),
658
- "Number must be an integer"
659
- );
660
- var positiveNumber = number.refine(
661
- (n) => n >= 0 && !Object.is(n, -0),
662
- "Number must be positive"
663
- );
664
- var positiveInteger = integer.refine(
665
- (n) => n >= 0 && !Object.is(n, -0),
666
- "Number must be positive"
667
- );
668
- var bigint = define(
669
- (blob, ok2, err2) => isBigInt(blob) ? ok2(blob) : err2("Must be bigint")
670
- );
671
-
672
653
  // src/booleans.ts
673
654
  var boolean = define((blob, ok2, err2) => {
674
655
  return typeof blob === "boolean" ? ok2(blob) : err2("Must be boolean");
675
656
  });
676
657
  var truthy = define((blob, ok2, _) => ok2(!!blob));
677
- var numericBoolean = number.transform((n) => !!n);
678
658
 
679
659
  // src/collections.ts
680
660
  function record(fst, snd) {
@@ -683,14 +663,12 @@ function record(fst, snd) {
683
663
  return pojo.then((input, ok2, err2) => {
684
664
  let rv = {};
685
665
  const errors = /* @__PURE__ */ new Map();
686
- for (const [key, value] of Object.entries(input)) {
666
+ for (const key of Object.keys(input)) {
667
+ const value = input[key];
687
668
  const keyResult = _optionalChain([keyDecoder, 'optionalAccess', _2 => _2.decode, 'call', _3 => _3(key)]);
688
669
  if (_optionalChain([keyResult, 'optionalAccess', _4 => _4.ok]) === false) {
689
670
  return err2(
690
- public_annotate(
691
- input,
692
- `Invalid key ${JSON.stringify(key)}: ${formatShort(keyResult.error)}`
693
- )
671
+ public_annotate(input, `Invalid key ${quote(key)}: ${formatShort(keyResult.error)}`)
694
672
  );
695
673
  }
696
674
  const k = _nullishCoalesce(_optionalChain([keyResult, 'optionalAccess', _5 => _5.value]), () => ( key));
@@ -720,6 +698,18 @@ function mapping(decoder) {
720
698
  return record(decoder).transform((obj) => new Map(Object.entries(obj)));
721
699
  }
722
700
 
701
+ // src/lib/size-options.ts
702
+ function bySizeOptions(options) {
703
+ const size = _optionalChain([options, 'optionalAccess', _6 => _6.size]);
704
+ const min = _nullishCoalesce(size, () => ( _optionalChain([options, 'optionalAccess', _7 => _7.min])));
705
+ const max = _nullishCoalesce(size, () => ( _optionalChain([options, 'optionalAccess', _8 => _8.max])));
706
+ const atLeast = min === max ? "" : "at least ";
707
+ const atMost = min === max ? "" : "at most ";
708
+ const tooShort = min !== void 0 && `Too short, must be ${atLeast}${min} chars`;
709
+ const tooLong = max !== void 0 && `Too long, must be ${atMost}${max} chars`;
710
+ return tooShort && tooLong ? (s) => s.length < min ? tooShort : s.length > max ? tooLong : null : tooShort ? (s) => s.length < min ? tooShort : null : tooLong ? (s) => s.length > max ? tooLong : null : () => null;
711
+ }
712
+
723
713
  // src/strings.ts
724
714
  var url_re = /^([A-Za-z]{3,9}(?:[+][A-Za-z]{3,9})?):\/\/(?:([-;:&=+$,\w]+)@)?(?:([A-Za-z0-9.-]+)(?::([0-9]{2,5}))?)(\/(?:[-+~%/.,\w]*)?(?:\?[-+=&;%@.,/\w]*)?(?:#[.,!/\w]*)?)?$/;
725
715
  var string = define(
@@ -742,6 +732,15 @@ var httpsUrl = url.refine(
742
732
  (value) => value.protocol === "https:",
743
733
  "Must be an HTTPS URL"
744
734
  );
735
+ var identifier = regex(
736
+ /^[a-z_][a-z0-9_]*$/i,
737
+ "Must be valid identifier"
738
+ );
739
+ function nanoid(options) {
740
+ return regex(/^[a-z0-9_-]+$/i, "Must be nano ID").reject(
741
+ bySizeOptions(_nullishCoalesce(options, () => ( { size: 21 })))
742
+ );
743
+ }
745
744
  var uuid = regex(
746
745
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
747
746
  "Must be uuid"
@@ -781,6 +780,30 @@ var iso8601 = (
781
780
  );
782
781
  var datelike = either(date, iso8601).describe("Must be a Date or date string");
783
782
 
783
+ // src/numbers.ts
784
+ var anyNumber = define(
785
+ (blob, ok2, err2) => isNumber(blob) ? ok2(blob) : err2("Must be number")
786
+ );
787
+ var number = anyNumber.refine(
788
+ (n) => Number.isFinite(n),
789
+ "Number must be finite"
790
+ );
791
+ var integer = number.refine(
792
+ (n) => Number.isInteger(n),
793
+ "Number must be an integer"
794
+ );
795
+ var positiveNumber = number.refine(
796
+ (n) => n >= 0 && !Object.is(n, -0),
797
+ "Number must be positive"
798
+ );
799
+ var positiveInteger = integer.refine(
800
+ (n) => n >= 0 && !Object.is(n, -0),
801
+ "Number must be positive"
802
+ );
803
+ var bigint = define(
804
+ (blob, ok2, err2) => isBigInt(blob) ? ok2(blob) : err2("Must be bigint")
805
+ );
806
+
784
807
  // src/json.ts
785
808
  var jsonObject = lazy(() => record(json));
786
809
  var jsonArray = lazy(() => array(json));
@@ -859,4 +882,5 @@ var json = either(
859
882
 
860
883
 
861
884
 
862
- exports.always = always; exports.anyNumber = anyNumber; exports.array = array; exports.bigint = bigint; exports.boolean = boolean; exports.constant = constant; exports.date = date; exports.datelike = datelike; exports.decimal = decimal; exports.define = define; exports.dict = dict; exports.either = either; exports.email = email; exports.enum_ = enum_; exports.err = err; exports.exact = exact; exports.fail = fail; exports.formatInline = formatInline; exports.formatShort = formatShort; exports.hardcoded = hardcoded; exports.hexadecimal = hexadecimal; exports.httpsUrl = httpsUrl; exports.inexact = inexact; exports.instanceOf = instanceOf; exports.integer = integer; exports.iso8601 = iso8601; exports.json = json; exports.jsonArray = jsonArray; exports.jsonObject = jsonObject; exports.lazy = lazy; exports.mapping = mapping; exports.maybe = maybe; exports.mixed = mixed; exports.never = never; exports.nonEmptyArray = nonEmptyArray; exports.nonEmptyString = nonEmptyString; exports.null_ = null_; exports.nullable = nullable; exports.nullish = nullish; exports.number = number; exports.numeric = numeric; exports.numericBoolean = numericBoolean; exports.object = object; exports.ok = ok; exports.oneOf = oneOf; exports.optional = optional; exports.poja = poja; exports.pojo = pojo; exports.positiveInteger = positiveInteger; exports.positiveNumber = positiveNumber; exports.prep = prep; exports.record = record; exports.regex = regex; exports.select = select; exports.set = set; exports.setFromArray = setFromArray; exports.string = string; exports.taggedUnion = taggedUnion; exports.truthy = truthy; exports.tuple = tuple; exports.undefined_ = undefined_; exports.unknown = unknown; exports.url = url; exports.uuid = uuid; exports.uuidv1 = uuidv1; exports.uuidv4 = uuidv4;
885
+
886
+ exports.always = always; exports.anyNumber = anyNumber; exports.array = array; exports.bigint = bigint; exports.boolean = boolean; exports.constant = constant; exports.date = date; exports.datelike = datelike; exports.decimal = decimal; exports.define = define; exports.dict = dict; exports.either = either; exports.email = email; exports.enum_ = enum_; exports.err = err; exports.exact = exact; exports.fail = fail; exports.formatInline = formatInline; exports.formatShort = formatShort; exports.hardcoded = hardcoded; exports.hexadecimal = hexadecimal; exports.httpsUrl = httpsUrl; exports.identifier = identifier; exports.inexact = inexact; exports.instanceOf = instanceOf; exports.integer = integer; exports.iso8601 = iso8601; exports.json = json; exports.jsonArray = jsonArray; exports.jsonObject = jsonObject; exports.lazy = lazy; exports.mapping = mapping; exports.maybe = maybe; exports.mixed = mixed; exports.nanoid = nanoid; exports.never = never; exports.nonEmptyArray = nonEmptyArray; exports.nonEmptyString = nonEmptyString; exports.null_ = null_; exports.nullable = nullable; exports.nullish = nullish; exports.number = number; exports.numeric = numeric; exports.object = object; exports.ok = ok; exports.oneOf = oneOf; exports.optional = optional; exports.poja = poja; exports.pojo = pojo; exports.positiveInteger = positiveInteger; exports.positiveNumber = positiveNumber; exports.prep = prep; exports.record = record; exports.regex = regex; exports.select = select; exports.set = set; exports.setFromArray = setFromArray; exports.string = string; exports.taggedUnion = taggedUnion; exports.truthy = truthy; exports.tuple = tuple; exports.undefined_ = undefined_; exports.unknown = unknown; exports.url = url; exports.uuid = uuid; exports.uuidv1 = uuidv1; exports.uuidv4 = uuidv4;
package/dist/index.d.cts CHANGED
@@ -59,7 +59,8 @@ type DecodeResult<T> = Result<T, Annotation>;
59
59
  * `ok()` and `err()` constructor functions are provided as the 2nd and 3rd
60
60
  * param. One of these should be called and its value returned.
61
61
  */
62
- type AcceptanceFn<T, InputT = unknown> = (blob: InputT, ok: (value: T) => DecodeResult<T>, err: (msg: string | Annotation) => DecodeResult<T>) => DecodeResult<T>;
62
+ type AcceptanceFn<O, I = unknown> = (blob: I, ok: (value: O) => DecodeResult<O>, err: (msg: string | Annotation) => DecodeResult<O>) => DecodeResult<O>;
63
+ type Next<O, I = unknown> = Decoder<O> | ((blob: I, ok: (value: O) => DecodeResult<O>, err: (msg: string | Annotation) => DecodeResult<O>) => DecodeResult<O> | Decoder<O>);
63
64
  interface Decoder<T> {
64
65
  /**
65
66
  * Verifies untrusted input. Either returns a value, or throws a decoding
@@ -96,23 +97,31 @@ interface Decoder<T> {
96
97
  */
97
98
  describe(message: string): Decoder<T>;
98
99
  /**
99
- * Send the output of the current decoder into another acceptance function.
100
- * The given acceptance function will receive the output of the current
101
- * decoder as its input, making it partially trusted.
102
- *
103
- * This works similar to how you would `define()` a new decoder, except
104
- * that the ``blob`` param will now be ``T`` (a known type), rather than
105
- * ``unknown``. This will allow the function to make a stronger assumption
106
- * about its input and avoid re-refining inputs.
100
+ * Send the output of the current decoder into another decoder or acceptance
101
+ * function. The given acceptance function will receive the output of the
102
+ * current decoder as its input.
107
103
  *
108
104
  * > _**NOTE:** This is an advanced, low-level, API. It's not recommended
109
105
  * > to reach for this construct unless there is no other way. Most cases can
110
- * > be covered more elegantly by `.transform()` or `.refine()` instead._
106
+ * > be covered more elegantly by `.transform()`, `.refine()`, or `.pipe()`
107
+ * > instead._
108
+ */
109
+ then<V>(next: Next<V, T>): Decoder<V>;
110
+ /**
111
+ * Send the output of this decoder as input to another decoder.
112
+ *
113
+ * This can be useful to validate the results of a transform, i.e.:
111
114
  *
112
- * If it helps, you can think of `define(...)` as equivalent to
113
- * `unknown.then(...)`.
115
+ * string
116
+ * .transform((s) => s.split(','))
117
+ * .pipe(array(nonEmptyString))
118
+ *
119
+ * You can also conditionally pipe:
120
+ *
121
+ * string.pipe((s) => s.startsWith('@') ? username : email)
114
122
  */
115
- then<V>(next: AcceptanceFn<V, T>): Decoder<V>;
123
+ pipe<V, D extends Decoder<V>>(next: D): Decoder<DecoderType<D>>;
124
+ pipe<V, D extends Decoder<V>>(next: (blob: T) => D): Decoder<DecoderType<D>>;
116
125
  }
117
126
  /**
118
127
  * Helper type to return the output type of a Decoder.
@@ -264,13 +273,6 @@ declare const boolean: Decoder<boolean>;
264
273
  * Accepts anything and will return its "truth" value. Will never reject.
265
274
  */
266
275
  declare const truthy: Decoder<boolean>;
267
- /**
268
- * Accepts numbers, but return their boolean representation.
269
- *
270
- * @deprecated This decoder will be removed in a future version. You can use
271
- * `truthy` to get almost the same effect.
272
- */
273
- declare const numericBoolean: Decoder<boolean>;
274
276
 
275
277
  /**
276
278
  * Accepts objects where all values match the given decoder, and returns the
@@ -358,6 +360,12 @@ declare const jsonArray: Decoder<JSONArray>;
358
360
  */
359
361
  declare const json: Decoder<JSONValue>;
360
362
 
363
+ type SizeOptions = {
364
+ min?: number;
365
+ max?: number;
366
+ size?: number;
367
+ };
368
+
361
369
  interface Klass<T> extends Function {
362
370
  new (...args: readonly any[]): T;
363
371
  }
@@ -490,6 +498,17 @@ declare const url: Decoder<URL>;
490
498
  * as a URL instance.
491
499
  */
492
500
  declare const httpsUrl: Decoder<URL>;
501
+ /**
502
+ * Accepts and returns strings that are valid identifiers in most programming
503
+ * languages.
504
+ */
505
+ declare const identifier: Decoder<string>;
506
+ /**
507
+ * Accepts and returns [nanoid](https://zelark.github.io/nano-id-cc) string
508
+ * values. It assumes the default nanoid alphabet. If you're using a custom
509
+ * alphabet, use `regex()` instead.
510
+ */
511
+ declare function nanoid(options?: SizeOptions): Decoder<string>;
493
512
  /**
494
513
  * Accepts strings that are valid
495
514
  * [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
@@ -581,4 +600,4 @@ declare function taggedUnion<O extends Record<string, Decoder<unknown>>, T = Dec
581
600
  */
582
601
  declare function select<T, D extends Decoder<unknown>>(scout: Decoder<T>, selectFn: (result: T) => D): Decoder<DecoderType<D>>;
583
602
 
584
- export { type DecodeResult, type Decoder, type DecoderType, type Err, type Formatter, type JSONArray, type JSONObject, type JSONValue, type Ok, type Result, type Scalar, always, anyNumber, array, bigint, boolean, constant, date, datelike, decimal, define, dict, either, email, enum_, err, exact, fail, formatInline, formatShort, hardcoded, hexadecimal, httpsUrl, inexact, instanceOf, integer, iso8601, json, jsonArray, jsonObject, lazy, mapping, maybe, mixed, never, nonEmptyArray, nonEmptyString, null_, nullable, nullish, number, numeric, numericBoolean, object, ok, oneOf, optional, poja, pojo, positiveInteger, positiveNumber, prep, record, regex, select, set, setFromArray, string, taggedUnion, truthy, tuple, undefined_, unknown, url, uuid, uuidv1, uuidv4 };
603
+ export { type DecodeResult, type Decoder, type DecoderType, type Err, type Formatter, type JSONArray, type JSONObject, type JSONValue, type Ok, type Result, type Scalar, type SizeOptions, always, anyNumber, array, bigint, boolean, constant, date, datelike, decimal, define, dict, either, email, enum_, err, exact, fail, formatInline, formatShort, hardcoded, hexadecimal, httpsUrl, identifier, inexact, instanceOf, integer, iso8601, json, jsonArray, jsonObject, lazy, mapping, maybe, mixed, nanoid, never, nonEmptyArray, nonEmptyString, null_, nullable, nullish, number, numeric, object, ok, oneOf, optional, poja, pojo, positiveInteger, positiveNumber, prep, record, regex, select, set, setFromArray, string, taggedUnion, truthy, tuple, undefined_, unknown, url, uuid, uuidv1, uuidv4 };
package/dist/index.d.ts CHANGED
@@ -59,7 +59,8 @@ type DecodeResult<T> = Result<T, Annotation>;
59
59
  * `ok()` and `err()` constructor functions are provided as the 2nd and 3rd
60
60
  * param. One of these should be called and its value returned.
61
61
  */
62
- type AcceptanceFn<T, InputT = unknown> = (blob: InputT, ok: (value: T) => DecodeResult<T>, err: (msg: string | Annotation) => DecodeResult<T>) => DecodeResult<T>;
62
+ type AcceptanceFn<O, I = unknown> = (blob: I, ok: (value: O) => DecodeResult<O>, err: (msg: string | Annotation) => DecodeResult<O>) => DecodeResult<O>;
63
+ type Next<O, I = unknown> = Decoder<O> | ((blob: I, ok: (value: O) => DecodeResult<O>, err: (msg: string | Annotation) => DecodeResult<O>) => DecodeResult<O> | Decoder<O>);
63
64
  interface Decoder<T> {
64
65
  /**
65
66
  * Verifies untrusted input. Either returns a value, or throws a decoding
@@ -96,23 +97,31 @@ interface Decoder<T> {
96
97
  */
97
98
  describe(message: string): Decoder<T>;
98
99
  /**
99
- * Send the output of the current decoder into another acceptance function.
100
- * The given acceptance function will receive the output of the current
101
- * decoder as its input, making it partially trusted.
102
- *
103
- * This works similar to how you would `define()` a new decoder, except
104
- * that the ``blob`` param will now be ``T`` (a known type), rather than
105
- * ``unknown``. This will allow the function to make a stronger assumption
106
- * about its input and avoid re-refining inputs.
100
+ * Send the output of the current decoder into another decoder or acceptance
101
+ * function. The given acceptance function will receive the output of the
102
+ * current decoder as its input.
107
103
  *
108
104
  * > _**NOTE:** This is an advanced, low-level, API. It's not recommended
109
105
  * > to reach for this construct unless there is no other way. Most cases can
110
- * > be covered more elegantly by `.transform()` or `.refine()` instead._
106
+ * > be covered more elegantly by `.transform()`, `.refine()`, or `.pipe()`
107
+ * > instead._
108
+ */
109
+ then<V>(next: Next<V, T>): Decoder<V>;
110
+ /**
111
+ * Send the output of this decoder as input to another decoder.
112
+ *
113
+ * This can be useful to validate the results of a transform, i.e.:
111
114
  *
112
- * If it helps, you can think of `define(...)` as equivalent to
113
- * `unknown.then(...)`.
115
+ * string
116
+ * .transform((s) => s.split(','))
117
+ * .pipe(array(nonEmptyString))
118
+ *
119
+ * You can also conditionally pipe:
120
+ *
121
+ * string.pipe((s) => s.startsWith('@') ? username : email)
114
122
  */
115
- then<V>(next: AcceptanceFn<V, T>): Decoder<V>;
123
+ pipe<V, D extends Decoder<V>>(next: D): Decoder<DecoderType<D>>;
124
+ pipe<V, D extends Decoder<V>>(next: (blob: T) => D): Decoder<DecoderType<D>>;
116
125
  }
117
126
  /**
118
127
  * Helper type to return the output type of a Decoder.
@@ -264,13 +273,6 @@ declare const boolean: Decoder<boolean>;
264
273
  * Accepts anything and will return its "truth" value. Will never reject.
265
274
  */
266
275
  declare const truthy: Decoder<boolean>;
267
- /**
268
- * Accepts numbers, but return their boolean representation.
269
- *
270
- * @deprecated This decoder will be removed in a future version. You can use
271
- * `truthy` to get almost the same effect.
272
- */
273
- declare const numericBoolean: Decoder<boolean>;
274
276
 
275
277
  /**
276
278
  * Accepts objects where all values match the given decoder, and returns the
@@ -358,6 +360,12 @@ declare const jsonArray: Decoder<JSONArray>;
358
360
  */
359
361
  declare const json: Decoder<JSONValue>;
360
362
 
363
+ type SizeOptions = {
364
+ min?: number;
365
+ max?: number;
366
+ size?: number;
367
+ };
368
+
361
369
  interface Klass<T> extends Function {
362
370
  new (...args: readonly any[]): T;
363
371
  }
@@ -490,6 +498,17 @@ declare const url: Decoder<URL>;
490
498
  * as a URL instance.
491
499
  */
492
500
  declare const httpsUrl: Decoder<URL>;
501
+ /**
502
+ * Accepts and returns strings that are valid identifiers in most programming
503
+ * languages.
504
+ */
505
+ declare const identifier: Decoder<string>;
506
+ /**
507
+ * Accepts and returns [nanoid](https://zelark.github.io/nano-id-cc) string
508
+ * values. It assumes the default nanoid alphabet. If you're using a custom
509
+ * alphabet, use `regex()` instead.
510
+ */
511
+ declare function nanoid(options?: SizeOptions): Decoder<string>;
493
512
  /**
494
513
  * Accepts strings that are valid
495
514
  * [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
@@ -581,4 +600,4 @@ declare function taggedUnion<O extends Record<string, Decoder<unknown>>, T = Dec
581
600
  */
582
601
  declare function select<T, D extends Decoder<unknown>>(scout: Decoder<T>, selectFn: (result: T) => D): Decoder<DecoderType<D>>;
583
602
 
584
- export { type DecodeResult, type Decoder, type DecoderType, type Err, type Formatter, type JSONArray, type JSONObject, type JSONValue, type Ok, type Result, type Scalar, always, anyNumber, array, bigint, boolean, constant, date, datelike, decimal, define, dict, either, email, enum_, err, exact, fail, formatInline, formatShort, hardcoded, hexadecimal, httpsUrl, inexact, instanceOf, integer, iso8601, json, jsonArray, jsonObject, lazy, mapping, maybe, mixed, never, nonEmptyArray, nonEmptyString, null_, nullable, nullish, number, numeric, numericBoolean, object, ok, oneOf, optional, poja, pojo, positiveInteger, positiveNumber, prep, record, regex, select, set, setFromArray, string, taggedUnion, truthy, tuple, undefined_, unknown, url, uuid, uuidv1, uuidv4 };
603
+ export { type DecodeResult, type Decoder, type DecoderType, type Err, type Formatter, type JSONArray, type JSONObject, type JSONValue, type Ok, type Result, type Scalar, type SizeOptions, always, anyNumber, array, bigint, boolean, constant, date, datelike, decimal, define, dict, either, email, enum_, err, exact, fail, formatInline, formatShort, hardcoded, hexadecimal, httpsUrl, identifier, inexact, instanceOf, integer, iso8601, json, jsonArray, jsonObject, lazy, mapping, maybe, mixed, nanoid, never, nonEmptyArray, nonEmptyString, null_, nullable, nullish, number, numeric, object, ok, oneOf, optional, poja, pojo, positiveInteger, positiveNumber, prep, record, regex, select, set, setFromArray, string, taggedUnion, truthy, tuple, undefined_, unknown, url, uuid, uuidv1, uuidv4 };
package/dist/index.js CHANGED
@@ -66,7 +66,8 @@ function annotateArray(arr, text, seen) {
66
66
  function annotateObject(obj, text, seen) {
67
67
  seen.add(obj);
68
68
  const fields = /* @__PURE__ */ new Map();
69
- for (const [key, value] of Object.entries(obj)) {
69
+ for (const key of Object.keys(obj)) {
70
+ const value = obj[key];
70
71
  fields.set(key, annotate(value, void 0, seen));
71
72
  }
72
73
  return makeObjectAnn(fields, text);
@@ -116,6 +117,10 @@ function indent(s, prefix = INDENT) {
116
117
  return `${prefix}${s}`;
117
118
  }
118
119
  }
120
+ var quotePattern = /'/g;
121
+ function quote(value) {
122
+ return typeof value === "string" ? "'" + value.replace(quotePattern, "\\'") + "'" : JSON.stringify(value);
123
+ }
119
124
 
120
125
  // src/core/format.ts
121
126
  function summarize(ann, keypath = []) {
@@ -144,9 +149,9 @@ function summarize(ann, keypath = []) {
144
149
  if (keypath.length === 0) {
145
150
  prefix = "";
146
151
  } else if (keypath.length === 1) {
147
- prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${JSON.stringify(keypath[0])}: `;
152
+ prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${quote(keypath[0])}: `;
148
153
  } else {
149
- prefix = `Value at keypath ${keypath.map(String).join(".")}: `;
154
+ prefix = `Value at keypath ${quote(keypath.map(String).join("."))}: `;
150
155
  }
151
156
  return [...result, `${prefix}${text}`];
152
157
  }
@@ -202,7 +207,7 @@ function serializeValue(value) {
202
207
  return "undefined";
203
208
  } else {
204
209
  if (isDate(value)) {
205
- return `new Date(${JSON.stringify(value.toISOString())})`;
210
+ return `new Date(${quote(value.toISOString())})`;
206
211
  } else if (value instanceof Date) {
207
212
  return "(Invalid Date)";
208
213
  } else {
@@ -311,10 +316,16 @@ function define(fn) {
311
316
  }
312
317
  function then(next) {
313
318
  return define((blob, ok2, err2) => {
314
- const result = decode(blob);
315
- return result.ok ? next(result.value, ok2, err2) : result;
319
+ const r1 = decode(blob);
320
+ if (!r1.ok)
321
+ return r1;
322
+ const r2 = isDecoder(next) ? next : next(r1.value, ok2, err2);
323
+ return isDecoder(r2) ? r2.decode(r1.value) : r2;
316
324
  });
317
325
  }
326
+ function pipe(next) {
327
+ return then(next);
328
+ }
318
329
  function reject(rejectFn) {
319
330
  return then((blob, ok2, err2) => {
320
331
  const errmsg = rejectFn(blob);
@@ -331,13 +342,7 @@ function define(fn) {
331
342
  }
332
343
  });
333
344
  }
334
- function peek(next) {
335
- return define((blob, ok2, err2) => {
336
- const result = decode(blob);
337
- return result.ok ? next([blob, result.value], ok2, err2) : result;
338
- });
339
- }
340
- return Object.freeze({
345
+ return brand2({
341
346
  verify,
342
347
  value,
343
348
  decode,
@@ -346,9 +351,17 @@ function define(fn) {
346
351
  reject,
347
352
  describe,
348
353
  then,
349
- peek
354
+ pipe
350
355
  });
351
356
  }
357
+ var _register2 = /* @__PURE__ */ new WeakSet();
358
+ function brand2(decoder) {
359
+ _register2.add(decoder);
360
+ return decoder;
361
+ }
362
+ function isDecoder(thing) {
363
+ return _register2.has(thing);
364
+ }
352
365
 
353
366
  // src/arrays.ts
354
367
  var poja = define((blob, ok2, err2) => {
@@ -489,7 +502,7 @@ function object(decoders) {
489
502
  objAnn = merge(objAnn, errors);
490
503
  }
491
504
  if (missingKeys.size > 0) {
492
- const errMsg = Array.from(missingKeys).map((key) => `"${key}"`).join(", ");
505
+ const errMsg = Array.from(missingKeys).map(quote).join(", ");
493
506
  const pluralized = missingKeys.size > 1 ? "keys" : "key";
494
507
  objAnn = updateText(objAnn, `Missing ${pluralized}: ${errMsg}`);
495
508
  }
@@ -503,17 +516,14 @@ function exact(decoders) {
503
516
  const checked = pojo.reject((plainObj) => {
504
517
  const actualKeys = new Set(Object.keys(plainObj));
505
518
  const extraKeys = difference(actualKeys, allowedKeys);
506
- return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).join(", ")}` : (
507
- // Don't reject
508
- null
509
- );
519
+ return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).map(quote).join(", ")}` : null;
510
520
  });
511
- return checked.then(object(decoders).decode);
521
+ return checked.pipe(object(decoders));
512
522
  }
513
523
  function inexact(decoders) {
514
- return pojo.then((plainObj) => {
524
+ return pojo.pipe((plainObj) => {
515
525
  const allkeys = new Set(Object.keys(plainObj));
516
- const decoder = object(decoders).transform((safepart) => {
526
+ return object(decoders).transform((safepart) => {
517
527
  const safekeys = new Set(Object.keys(decoders));
518
528
  for (const k of safekeys)
519
529
  allkeys.add(k);
@@ -530,7 +540,6 @@ function inexact(decoders) {
530
540
  }
531
541
  return rv;
532
542
  });
533
- return decoder.decode(plainObj);
534
543
  });
535
544
  }
536
545
 
@@ -566,9 +575,7 @@ function oneOf(constants) {
566
575
  if (winner !== void 0) {
567
576
  return ok2(winner);
568
577
  }
569
- return err2(
570
- `Must be one of ${constants.map((value) => JSON.stringify(value)).join(", ")}`
571
- );
578
+ return err2(`Must be one of ${constants.map((value) => quote(value)).join(", ")}`);
572
579
  });
573
580
  }
574
581
  function enum_(enumObj) {
@@ -594,9 +601,9 @@ function taggedUnion(field, mapping2) {
594
601
  );
595
602
  }
596
603
  function select(scout, selectFn) {
597
- return scout.peek(([blob, peekResult]) => {
598
- const decoder = selectFn(peekResult);
599
- return decoder.decode(blob);
604
+ return define((blob) => {
605
+ const result = scout.decode(blob);
606
+ return result.ok ? selectFn(result.value).decode(blob) : result;
600
607
  });
601
608
  }
602
609
 
@@ -627,9 +634,7 @@ function nullish(decoder, defaultValue) {
627
634
  }
628
635
  function constant(value) {
629
636
  return define(
630
- (blob, ok2, err2) => blob === value ? ok2(value) : err2(
631
- `Must be ${typeof value === "symbol" ? String(value) : JSON.stringify(value)}`
632
- )
637
+ (blob, ok2, err2) => blob === value ? ok2(value) : err2(`Must be ${typeof value === "symbol" ? String(value) : quote(value)}`)
633
638
  );
634
639
  }
635
640
  function always(value) {
@@ -645,36 +650,11 @@ var hardcoded = always;
645
650
  var unknown = define((blob, ok2, _) => ok2(blob));
646
651
  var mixed = unknown;
647
652
 
648
- // src/numbers.ts
649
- var anyNumber = define(
650
- (blob, ok2, err2) => isNumber(blob) ? ok2(blob) : err2("Must be number")
651
- );
652
- var number = anyNumber.refine(
653
- (n) => Number.isFinite(n),
654
- "Number must be finite"
655
- );
656
- var integer = number.refine(
657
- (n) => Number.isInteger(n),
658
- "Number must be an integer"
659
- );
660
- var positiveNumber = number.refine(
661
- (n) => n >= 0 && !Object.is(n, -0),
662
- "Number must be positive"
663
- );
664
- var positiveInteger = integer.refine(
665
- (n) => n >= 0 && !Object.is(n, -0),
666
- "Number must be positive"
667
- );
668
- var bigint = define(
669
- (blob, ok2, err2) => isBigInt(blob) ? ok2(blob) : err2("Must be bigint")
670
- );
671
-
672
653
  // src/booleans.ts
673
654
  var boolean = define((blob, ok2, err2) => {
674
655
  return typeof blob === "boolean" ? ok2(blob) : err2("Must be boolean");
675
656
  });
676
657
  var truthy = define((blob, ok2, _) => ok2(!!blob));
677
- var numericBoolean = number.transform((n) => !!n);
678
658
 
679
659
  // src/collections.ts
680
660
  function record(fst, snd) {
@@ -683,14 +663,12 @@ function record(fst, snd) {
683
663
  return pojo.then((input, ok2, err2) => {
684
664
  let rv = {};
685
665
  const errors = /* @__PURE__ */ new Map();
686
- for (const [key, value] of Object.entries(input)) {
666
+ for (const key of Object.keys(input)) {
667
+ const value = input[key];
687
668
  const keyResult = keyDecoder?.decode(key);
688
669
  if (keyResult?.ok === false) {
689
670
  return err2(
690
- public_annotate(
691
- input,
692
- `Invalid key ${JSON.stringify(key)}: ${formatShort(keyResult.error)}`
693
- )
671
+ public_annotate(input, `Invalid key ${quote(key)}: ${formatShort(keyResult.error)}`)
694
672
  );
695
673
  }
696
674
  const k = keyResult?.value ?? key;
@@ -720,6 +698,18 @@ function mapping(decoder) {
720
698
  return record(decoder).transform((obj) => new Map(Object.entries(obj)));
721
699
  }
722
700
 
701
+ // src/lib/size-options.ts
702
+ function bySizeOptions(options) {
703
+ const size = options?.size;
704
+ const min = size ?? options?.min;
705
+ const max = size ?? options?.max;
706
+ const atLeast = min === max ? "" : "at least ";
707
+ const atMost = min === max ? "" : "at most ";
708
+ const tooShort = min !== void 0 && `Too short, must be ${atLeast}${min} chars`;
709
+ const tooLong = max !== void 0 && `Too long, must be ${atMost}${max} chars`;
710
+ return tooShort && tooLong ? (s) => s.length < min ? tooShort : s.length > max ? tooLong : null : tooShort ? (s) => s.length < min ? tooShort : null : tooLong ? (s) => s.length > max ? tooLong : null : () => null;
711
+ }
712
+
723
713
  // src/strings.ts
724
714
  var url_re = /^([A-Za-z]{3,9}(?:[+][A-Za-z]{3,9})?):\/\/(?:([-;:&=+$,\w]+)@)?(?:([A-Za-z0-9.-]+)(?::([0-9]{2,5}))?)(\/(?:[-+~%/.,\w]*)?(?:\?[-+=&;%@.,/\w]*)?(?:#[.,!/\w]*)?)?$/;
725
715
  var string = define(
@@ -742,6 +732,15 @@ var httpsUrl = url.refine(
742
732
  (value) => value.protocol === "https:",
743
733
  "Must be an HTTPS URL"
744
734
  );
735
+ var identifier = regex(
736
+ /^[a-z_][a-z0-9_]*$/i,
737
+ "Must be valid identifier"
738
+ );
739
+ function nanoid(options) {
740
+ return regex(/^[a-z0-9_-]+$/i, "Must be nano ID").reject(
741
+ bySizeOptions(options ?? { size: 21 })
742
+ );
743
+ }
745
744
  var uuid = regex(
746
745
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
747
746
  "Must be uuid"
@@ -781,6 +780,30 @@ var iso8601 = (
781
780
  );
782
781
  var datelike = either(date, iso8601).describe("Must be a Date or date string");
783
782
 
783
+ // src/numbers.ts
784
+ var anyNumber = define(
785
+ (blob, ok2, err2) => isNumber(blob) ? ok2(blob) : err2("Must be number")
786
+ );
787
+ var number = anyNumber.refine(
788
+ (n) => Number.isFinite(n),
789
+ "Number must be finite"
790
+ );
791
+ var integer = number.refine(
792
+ (n) => Number.isInteger(n),
793
+ "Number must be an integer"
794
+ );
795
+ var positiveNumber = number.refine(
796
+ (n) => n >= 0 && !Object.is(n, -0),
797
+ "Number must be positive"
798
+ );
799
+ var positiveInteger = integer.refine(
800
+ (n) => n >= 0 && !Object.is(n, -0),
801
+ "Number must be positive"
802
+ );
803
+ var bigint = define(
804
+ (blob, ok2, err2) => isBigInt(blob) ? ok2(blob) : err2("Must be bigint")
805
+ );
806
+
784
807
  // src/json.ts
785
808
  var jsonObject = lazy(() => record(json));
786
809
  var jsonArray = lazy(() => array(json));
@@ -815,6 +838,7 @@ export {
815
838
  hardcoded,
816
839
  hexadecimal,
817
840
  httpsUrl,
841
+ identifier,
818
842
  inexact,
819
843
  instanceOf,
820
844
  integer,
@@ -826,6 +850,7 @@ export {
826
850
  mapping,
827
851
  maybe,
828
852
  mixed,
853
+ nanoid,
829
854
  never,
830
855
  nonEmptyArray,
831
856
  nonEmptyString,
@@ -834,7 +859,6 @@ export {
834
859
  nullish,
835
860
  number,
836
861
  numeric,
837
- numericBoolean,
838
862
  object,
839
863
  ok,
840
864
  oneOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decoders",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Elegant and battle-tested validation library for type-safe input data for TypeScript",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -63,23 +63,23 @@
63
63
  "devDependencies": {
64
64
  "@arethetypeswrong/cli": "^0.13.5",
65
65
  "@release-it/keep-a-changelog": "^5.0.0",
66
- "@typescript-eslint/eslint-plugin": "^6.18.1",
67
- "@typescript-eslint/parser": "^6.18.1",
68
- "@vitest/coverage-istanbul": "^1.1.3",
66
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
67
+ "@typescript-eslint/parser": "^6.19.0",
68
+ "@vitest/coverage-istanbul": "^1.2.1",
69
69
  "eslint": "^8.56.0",
70
70
  "eslint-plugin-import": "^2.29.1",
71
71
  "eslint-plugin-simple-import-sort": "^10.0.0",
72
72
  "fast-check": "^3.15.0",
73
- "itertools": "^2.2.1",
74
- "prettier": "^3.1.1",
73
+ "itertools": "^2.2.3",
74
+ "prettier": "^3.2.4",
75
75
  "publint": "^0.2.7",
76
76
  "release-it": "^17.0.1",
77
77
  "ts-morph": "^21.0.1",
78
- "tsd": "^0.30.3",
78
+ "tsd": "^0.30.4",
79
79
  "tsup": "^8.0.1",
80
80
  "typescript": "^5.3.3",
81
- "vite-tsconfig-paths": "^4.2.3",
82
- "vitest": "^1.1.3"
81
+ "vite-tsconfig-paths": "^4.3.1",
82
+ "vitest": "^1.2.1"
83
83
  },
84
84
  "githubUrl": "https://github.com/nvie/decoders",
85
85
  "sideEffects": false