decoders 2.3.0 → 2.4.1

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
@@ -29,18 +29,12 @@ function makeObjectAnn(fields, text) {
29
29
  function makeArrayAnn(items, text) {
30
30
  return brand({ type: "array", items, text });
31
31
  }
32
- function makeFunctionAnn(text) {
33
- return brand({ type: "function", text });
34
- }
35
- function makeUnknownAnn(value, text) {
36
- return brand({ type: "unknown", value, text });
32
+ function makeOpaqueAnn(value, text) {
33
+ return brand({ type: "opaque", value, text });
37
34
  }
38
35
  function makeScalarAnn(value, text) {
39
36
  return brand({ type: "scalar", value, text });
40
37
  }
41
- function makeCircularRefAnn(text) {
42
- return brand({ type: "circular-ref", text });
43
- }
44
38
  function updateText(annotation, text) {
45
39
  if (text !== void 0) {
46
40
  return brand({ ...annotation, text });
@@ -66,7 +60,8 @@ function annotateArray(arr, text, seen) {
66
60
  function annotateObject(obj, text, seen) {
67
61
  seen.add(obj);
68
62
  const fields = /* @__PURE__ */ new Map();
69
- for (const [key, value] of Object.entries(obj)) {
63
+ for (const key of Object.keys(obj)) {
64
+ const value = obj[key];
70
65
  fields.set(key, annotate(value, void 0, seen));
71
66
  }
72
67
  return makeObjectAnn(fields, text);
@@ -80,22 +75,22 @@ function annotate(value, text, seen) {
80
75
  }
81
76
  if (Array.isArray(value)) {
82
77
  if (seen.has(value)) {
83
- return makeCircularRefAnn(text);
78
+ return makeOpaqueAnn("<circular ref>", text);
84
79
  } else {
85
80
  return annotateArray(value, text, seen);
86
81
  }
87
82
  }
88
83
  if (isPojo(value)) {
89
84
  if (seen.has(value)) {
90
- return makeCircularRefAnn(text);
85
+ return makeOpaqueAnn("<circular ref>", text);
91
86
  } else {
92
87
  return annotateObject(value, text, seen);
93
88
  }
94
89
  }
95
90
  if (typeof value === "function") {
96
- return makeFunctionAnn(text);
91
+ return makeOpaqueAnn("<function>", text);
97
92
  }
98
- return makeUnknownAnn(value, text);
93
+ return makeOpaqueAnn("???", text);
99
94
  }
100
95
  function public_annotate(value, text) {
101
96
  return annotate(value, text, /* @__PURE__ */ new WeakSet());
@@ -116,6 +111,10 @@ function indent(s, prefix = INDENT) {
116
111
  return `${prefix}${s}`;
117
112
  }
118
113
  }
114
+ var quotePattern = /'/g;
115
+ function quote(value) {
116
+ return typeof value === "string" ? "'" + value.replace(quotePattern, "\\'") + "'" : JSON.stringify(value);
117
+ }
119
118
 
120
119
  // src/core/format.ts
121
120
  function summarize(ann, keypath = []) {
@@ -144,9 +143,9 @@ function summarize(ann, keypath = []) {
144
143
  if (keypath.length === 0) {
145
144
  prefix = "";
146
145
  } else if (keypath.length === 1) {
147
- prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${JSON.stringify(keypath[0])}: `;
146
+ prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${quote(keypath[0])}: `;
148
147
  } else {
149
- prefix = `Value at keypath ${keypath.map(String).join(".")}: `;
148
+ prefix = `Value at keypath ${quote(keypath.map(String).join("."))}: `;
150
149
  }
151
150
  return [...result, `${prefix}${text}`];
152
151
  }
@@ -202,7 +201,7 @@ function serializeValue(value) {
202
201
  return "undefined";
203
202
  } else {
204
203
  if (isDate(value)) {
205
- return `new Date(${JSON.stringify(value.toISOString())})`;
204
+ return `new Date(${quote(value.toISOString())})`;
206
205
  } else if (value instanceof Date) {
207
206
  return "(Invalid Date)";
208
207
  } else {
@@ -216,14 +215,12 @@ function serializeAnnotation(ann, prefix = "") {
216
215
  serialized = serializeArray(ann, prefix);
217
216
  } else if (ann.type === "object") {
218
217
  serialized = serializeObject(ann, prefix);
219
- } else if (ann.type === "function") {
220
- serialized = "<function>";
221
- } else if (ann.type === "circular-ref") {
222
- serialized = "<circular ref>";
223
- } else if (ann.type === "unknown") {
224
- serialized = "???";
225
- } else {
218
+ } else if (ann.type === "scalar") {
226
219
  serialized = serializeValue(ann.value);
220
+ } else {
221
+ /* @__PURE__ */ ((_) => {
222
+ })(ann);
223
+ serialized = ann.value;
227
224
  }
228
225
  const text = ann.text;
229
226
  if (text !== void 0) {
@@ -311,10 +308,15 @@ function define(fn) {
311
308
  }
312
309
  function then(next) {
313
310
  return define((blob, ok2, err2) => {
314
- const result = decode(blob);
315
- return result.ok ? next(result.value, ok2, err2) : result;
311
+ const r1 = decode(blob);
312
+ if (!r1.ok) return r1;
313
+ const r2 = isDecoder(next) ? next : next(r1.value, ok2, err2);
314
+ return isDecoder(r2) ? r2.decode(r1.value) : r2;
316
315
  });
317
316
  }
317
+ function pipe(next) {
318
+ return then(next);
319
+ }
318
320
  function reject(rejectFn) {
319
321
  return then((blob, ok2, err2) => {
320
322
  const errmsg = rejectFn(blob);
@@ -331,13 +333,7 @@ function define(fn) {
331
333
  }
332
334
  });
333
335
  }
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({
336
+ return brand2({
341
337
  verify,
342
338
  value,
343
339
  decode,
@@ -346,9 +342,17 @@ function define(fn) {
346
342
  reject,
347
343
  describe,
348
344
  then,
349
- peek
345
+ pipe
350
346
  });
351
347
  }
348
+ var _register2 = /* @__PURE__ */ new WeakSet();
349
+ function brand2(decoder) {
350
+ _register2.add(decoder);
351
+ return decoder;
352
+ }
353
+ function isDecoder(thing) {
354
+ return _register2.has(thing);
355
+ }
352
356
 
353
357
  // src/arrays.ts
354
358
  var poja = define((blob, ok2, err2) => {
@@ -489,7 +493,7 @@ function object(decoders) {
489
493
  objAnn = merge(objAnn, errors);
490
494
  }
491
495
  if (missingKeys.size > 0) {
492
- const errMsg = Array.from(missingKeys).map((key) => `"${key}"`).join(", ");
496
+ const errMsg = Array.from(missingKeys).map(quote).join(", ");
493
497
  const pluralized = missingKeys.size > 1 ? "keys" : "key";
494
498
  objAnn = updateText(objAnn, `Missing ${pluralized}: ${errMsg}`);
495
499
  }
@@ -503,20 +507,16 @@ function exact(decoders) {
503
507
  const checked = pojo.reject((plainObj) => {
504
508
  const actualKeys = new Set(Object.keys(plainObj));
505
509
  const extraKeys = difference(actualKeys, allowedKeys);
506
- return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).join(", ")}` : (
507
- // Don't reject
508
- null
509
- );
510
+ return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).map(quote).join(", ")}` : null;
510
511
  });
511
- return checked.then(object(decoders).decode);
512
+ return checked.pipe(object(decoders));
512
513
  }
513
514
  function inexact(decoders) {
514
- return pojo.then((plainObj) => {
515
+ return pojo.pipe((plainObj) => {
515
516
  const allkeys = new Set(Object.keys(plainObj));
516
- const decoder = object(decoders).transform((safepart) => {
517
+ return object(decoders).transform((safepart) => {
517
518
  const safekeys = new Set(Object.keys(decoders));
518
- for (const k of safekeys)
519
- allkeys.add(k);
519
+ for (const k of safekeys) allkeys.add(k);
520
520
  const rv = {};
521
521
  for (const k of allkeys) {
522
522
  if (safekeys.has(k)) {
@@ -530,7 +530,6 @@ function inexact(decoders) {
530
530
  }
531
531
  return rv;
532
532
  });
533
- return decoder.decode(plainObj);
534
533
  });
535
534
  }
536
535
 
@@ -566,9 +565,7 @@ function oneOf(constants) {
566
565
  if (winner !== void 0) {
567
566
  return ok2(winner);
568
567
  }
569
- return err2(
570
- `Must be one of ${constants.map((value) => JSON.stringify(value)).join(", ")}`
571
- );
568
+ return err2(`Must be one of ${constants.map((value) => quote(value)).join(", ")}`);
572
569
  });
573
570
  }
574
571
  function enum_(enumObj) {
@@ -594,9 +591,9 @@ function taggedUnion(field, mapping2) {
594
591
  );
595
592
  }
596
593
  function select(scout, selectFn) {
597
- return scout.peek(([blob, peekResult]) => {
598
- const decoder = selectFn(peekResult);
599
- return decoder.decode(blob);
594
+ return define((blob) => {
595
+ const result = scout.decode(blob);
596
+ return result.ok ? selectFn(result.value).decode(blob) : result;
600
597
  });
601
598
  }
602
599
 
@@ -627,9 +624,7 @@ function nullish(decoder, defaultValue) {
627
624
  }
628
625
  function constant(value) {
629
626
  return define(
630
- (blob, ok2, err2) => blob === value ? ok2(value) : err2(
631
- `Must be ${typeof value === "symbol" ? String(value) : JSON.stringify(value)}`
632
- )
627
+ (blob, ok2, err2) => blob === value ? ok2(value) : err2(`Must be ${typeof value === "symbol" ? String(value) : quote(value)}`)
633
628
  );
634
629
  }
635
630
  function always(value) {
@@ -645,36 +640,11 @@ var hardcoded = always;
645
640
  var unknown = define((blob, ok2, _) => ok2(blob));
646
641
  var mixed = unknown;
647
642
 
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
643
  // src/booleans.ts
673
644
  var boolean = define((blob, ok2, err2) => {
674
645
  return typeof blob === "boolean" ? ok2(blob) : err2("Must be boolean");
675
646
  });
676
647
  var truthy = define((blob, ok2, _) => ok2(!!blob));
677
- var numericBoolean = number.transform((n) => !!n);
678
648
 
679
649
  // src/collections.ts
680
650
  function record(fst, snd) {
@@ -683,14 +653,12 @@ function record(fst, snd) {
683
653
  return pojo.then((input, ok2, err2) => {
684
654
  let rv = {};
685
655
  const errors = /* @__PURE__ */ new Map();
686
- for (const [key, value] of Object.entries(input)) {
656
+ for (const key of Object.keys(input)) {
657
+ const value = input[key];
687
658
  const keyResult = _optionalChain([keyDecoder, 'optionalAccess', _2 => _2.decode, 'call', _3 => _3(key)]);
688
659
  if (_optionalChain([keyResult, 'optionalAccess', _4 => _4.ok]) === false) {
689
660
  return err2(
690
- public_annotate(
691
- input,
692
- `Invalid key ${JSON.stringify(key)}: ${formatShort(keyResult.error)}`
693
- )
661
+ public_annotate(input, `Invalid key ${quote(key)}: ${formatShort(keyResult.error)}`)
694
662
  );
695
663
  }
696
664
  const k = _nullishCoalesce(_optionalChain([keyResult, 'optionalAccess', _5 => _5.value]), () => ( key));
@@ -720,6 +688,18 @@ function mapping(decoder) {
720
688
  return record(decoder).transform((obj) => new Map(Object.entries(obj)));
721
689
  }
722
690
 
691
+ // src/lib/size-options.ts
692
+ function bySizeOptions(options) {
693
+ const size = _optionalChain([options, 'optionalAccess', _6 => _6.size]);
694
+ const min = _nullishCoalesce(size, () => ( _optionalChain([options, 'optionalAccess', _7 => _7.min])));
695
+ const max = _nullishCoalesce(size, () => ( _optionalChain([options, 'optionalAccess', _8 => _8.max])));
696
+ const atLeast = min === max ? "" : "at least ";
697
+ const atMost = min === max ? "" : "at most ";
698
+ const tooShort = min !== void 0 && `Too short, must be ${atLeast}${min} chars`;
699
+ const tooLong = max !== void 0 && `Too long, must be ${atMost}${max} chars`;
700
+ 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;
701
+ }
702
+
723
703
  // src/strings.ts
724
704
  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
705
  var string = define(
@@ -742,6 +722,15 @@ var httpsUrl = url.refine(
742
722
  (value) => value.protocol === "https:",
743
723
  "Must be an HTTPS URL"
744
724
  );
725
+ var identifier = regex(
726
+ /^[a-z_][a-z0-9_]*$/i,
727
+ "Must be valid identifier"
728
+ );
729
+ function nanoid(options) {
730
+ return regex(/^[a-z0-9_-]+$/i, "Must be nano ID").reject(
731
+ bySizeOptions(_nullishCoalesce(options, () => ( { size: 21 })))
732
+ );
733
+ }
745
734
  var uuid = regex(
746
735
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
747
736
  "Must be uuid"
@@ -781,6 +770,30 @@ var iso8601 = (
781
770
  );
782
771
  var datelike = either(date, iso8601).describe("Must be a Date or date string");
783
772
 
773
+ // src/numbers.ts
774
+ var anyNumber = define(
775
+ (blob, ok2, err2) => isNumber(blob) ? ok2(blob) : err2("Must be number")
776
+ );
777
+ var number = anyNumber.refine(
778
+ (n) => Number.isFinite(n),
779
+ "Number must be finite"
780
+ );
781
+ var integer = number.refine(
782
+ (n) => Number.isInteger(n),
783
+ "Number must be an integer"
784
+ );
785
+ var positiveNumber = number.refine(
786
+ (n) => n >= 0 && !Object.is(n, -0),
787
+ "Number must be positive"
788
+ );
789
+ var positiveInteger = integer.refine(
790
+ (n) => n >= 0 && !Object.is(n, -0),
791
+ "Number must be positive"
792
+ );
793
+ var bigint = define(
794
+ (blob, ok2, err2) => isBigInt(blob) ? ok2(blob) : err2("Must be bigint")
795
+ );
796
+
784
797
  // src/json.ts
785
798
  var jsonObject = lazy(() => record(json));
786
799
  var jsonArray = lazy(() => array(json));
@@ -859,4 +872,5 @@ var json = either(
859
872
 
860
873
 
861
874
 
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;
875
+
876
+ 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
@@ -13,20 +13,12 @@ interface ScalarAnnotation {
13
13
  readonly value: unknown;
14
14
  readonly text?: string;
15
15
  }
16
- interface FunctionAnnotation {
17
- readonly type: 'function';
16
+ interface OpaqueAnnotation {
17
+ readonly type: 'opaque';
18
+ readonly value: string;
18
19
  readonly text?: string;
19
20
  }
20
- interface CircularRefAnnotation {
21
- readonly type: 'circular-ref';
22
- readonly text?: string;
23
- }
24
- interface UnknownAnnotation {
25
- readonly type: 'unknown';
26
- readonly value: unknown;
27
- readonly text?: string;
28
- }
29
- type Annotation = ObjectAnnotation | ArrayAnnotation | ScalarAnnotation | FunctionAnnotation | CircularRefAnnotation | UnknownAnnotation;
21
+ type Annotation = ObjectAnnotation | ArrayAnnotation | ScalarAnnotation | OpaqueAnnotation;
30
22
 
31
23
  /**
32
24
  * Result <value> <error>
@@ -59,7 +51,8 @@ type DecodeResult<T> = Result<T, Annotation>;
59
51
  * `ok()` and `err()` constructor functions are provided as the 2nd and 3rd
60
52
  * param. One of these should be called and its value returned.
61
53
  */
62
- type AcceptanceFn<T, InputT = unknown> = (blob: InputT, ok: (value: T) => DecodeResult<T>, err: (msg: string | Annotation) => DecodeResult<T>) => DecodeResult<T>;
54
+ type AcceptanceFn<O, I = unknown> = (blob: I, ok: (value: O) => DecodeResult<O>, err: (msg: string | Annotation) => DecodeResult<O>) => DecodeResult<O>;
55
+ 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
56
  interface Decoder<T> {
64
57
  /**
65
58
  * Verifies untrusted input. Either returns a value, or throws a decoding
@@ -96,23 +89,31 @@ interface Decoder<T> {
96
89
  */
97
90
  describe(message: string): Decoder<T>;
98
91
  /**
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.
92
+ * Send the output of the current decoder into another decoder or acceptance
93
+ * function. The given acceptance function will receive the output of the
94
+ * current decoder as its input.
107
95
  *
108
96
  * > _**NOTE:** This is an advanced, low-level, API. It's not recommended
109
97
  * > to reach for this construct unless there is no other way. Most cases can
110
- * > be covered more elegantly by `.transform()` or `.refine()` instead._
98
+ * > be covered more elegantly by `.transform()`, `.refine()`, or `.pipe()`
99
+ * > instead._
100
+ */
101
+ then<V>(next: Next<V, T>): Decoder<V>;
102
+ /**
103
+ * Send the output of this decoder as input to another decoder.
104
+ *
105
+ * This can be useful to validate the results of a transform, i.e.:
106
+ *
107
+ * string
108
+ * .transform((s) => s.split(','))
109
+ * .pipe(array(nonEmptyString))
111
110
  *
112
- * If it helps, you can think of `define(...)` as equivalent to
113
- * `unknown.then(...)`.
111
+ * You can also conditionally pipe:
112
+ *
113
+ * string.pipe((s) => s.startsWith('@') ? username : email)
114
114
  */
115
- then<V>(next: AcceptanceFn<V, T>): Decoder<V>;
115
+ pipe<V, D extends Decoder<V>>(next: D): Decoder<DecoderType<D>>;
116
+ pipe<V, D extends Decoder<V>>(next: (blob: T) => D): Decoder<DecoderType<D>>;
116
117
  }
117
118
  /**
118
119
  * Helper type to return the output type of a Decoder.
@@ -264,13 +265,6 @@ declare const boolean: Decoder<boolean>;
264
265
  * Accepts anything and will return its "truth" value. Will never reject.
265
266
  */
266
267
  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
268
 
275
269
  /**
276
270
  * Accepts objects where all values match the given decoder, and returns the
@@ -358,6 +352,12 @@ declare const jsonArray: Decoder<JSONArray>;
358
352
  */
359
353
  declare const json: Decoder<JSONValue>;
360
354
 
355
+ type SizeOptions = {
356
+ min?: number;
357
+ max?: number;
358
+ size?: number;
359
+ };
360
+
361
361
  interface Klass<T> extends Function {
362
362
  new (...args: readonly any[]): T;
363
363
  }
@@ -490,6 +490,17 @@ declare const url: Decoder<URL>;
490
490
  * as a URL instance.
491
491
  */
492
492
  declare const httpsUrl: Decoder<URL>;
493
+ /**
494
+ * Accepts and returns strings that are valid identifiers in most programming
495
+ * languages.
496
+ */
497
+ declare const identifier: Decoder<string>;
498
+ /**
499
+ * Accepts and returns [nanoid](https://zelark.github.io/nano-id-cc) string
500
+ * values. It assumes the default nanoid alphabet. If you're using a custom
501
+ * alphabet, use `regex()` instead.
502
+ */
503
+ declare function nanoid(options?: SizeOptions): Decoder<string>;
493
504
  /**
494
505
  * Accepts strings that are valid
495
506
  * [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
@@ -581,4 +592,4 @@ declare function taggedUnion<O extends Record<string, Decoder<unknown>>, T = Dec
581
592
  */
582
593
  declare function select<T, D extends Decoder<unknown>>(scout: Decoder<T>, selectFn: (result: T) => D): Decoder<DecoderType<D>>;
583
594
 
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 };
595
+ 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
@@ -13,20 +13,12 @@ interface ScalarAnnotation {
13
13
  readonly value: unknown;
14
14
  readonly text?: string;
15
15
  }
16
- interface FunctionAnnotation {
17
- readonly type: 'function';
16
+ interface OpaqueAnnotation {
17
+ readonly type: 'opaque';
18
+ readonly value: string;
18
19
  readonly text?: string;
19
20
  }
20
- interface CircularRefAnnotation {
21
- readonly type: 'circular-ref';
22
- readonly text?: string;
23
- }
24
- interface UnknownAnnotation {
25
- readonly type: 'unknown';
26
- readonly value: unknown;
27
- readonly text?: string;
28
- }
29
- type Annotation = ObjectAnnotation | ArrayAnnotation | ScalarAnnotation | FunctionAnnotation | CircularRefAnnotation | UnknownAnnotation;
21
+ type Annotation = ObjectAnnotation | ArrayAnnotation | ScalarAnnotation | OpaqueAnnotation;
30
22
 
31
23
  /**
32
24
  * Result <value> <error>
@@ -59,7 +51,8 @@ type DecodeResult<T> = Result<T, Annotation>;
59
51
  * `ok()` and `err()` constructor functions are provided as the 2nd and 3rd
60
52
  * param. One of these should be called and its value returned.
61
53
  */
62
- type AcceptanceFn<T, InputT = unknown> = (blob: InputT, ok: (value: T) => DecodeResult<T>, err: (msg: string | Annotation) => DecodeResult<T>) => DecodeResult<T>;
54
+ type AcceptanceFn<O, I = unknown> = (blob: I, ok: (value: O) => DecodeResult<O>, err: (msg: string | Annotation) => DecodeResult<O>) => DecodeResult<O>;
55
+ 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
56
  interface Decoder<T> {
64
57
  /**
65
58
  * Verifies untrusted input. Either returns a value, or throws a decoding
@@ -96,23 +89,31 @@ interface Decoder<T> {
96
89
  */
97
90
  describe(message: string): Decoder<T>;
98
91
  /**
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.
92
+ * Send the output of the current decoder into another decoder or acceptance
93
+ * function. The given acceptance function will receive the output of the
94
+ * current decoder as its input.
107
95
  *
108
96
  * > _**NOTE:** This is an advanced, low-level, API. It's not recommended
109
97
  * > to reach for this construct unless there is no other way. Most cases can
110
- * > be covered more elegantly by `.transform()` or `.refine()` instead._
98
+ * > be covered more elegantly by `.transform()`, `.refine()`, or `.pipe()`
99
+ * > instead._
100
+ */
101
+ then<V>(next: Next<V, T>): Decoder<V>;
102
+ /**
103
+ * Send the output of this decoder as input to another decoder.
104
+ *
105
+ * This can be useful to validate the results of a transform, i.e.:
106
+ *
107
+ * string
108
+ * .transform((s) => s.split(','))
109
+ * .pipe(array(nonEmptyString))
111
110
  *
112
- * If it helps, you can think of `define(...)` as equivalent to
113
- * `unknown.then(...)`.
111
+ * You can also conditionally pipe:
112
+ *
113
+ * string.pipe((s) => s.startsWith('@') ? username : email)
114
114
  */
115
- then<V>(next: AcceptanceFn<V, T>): Decoder<V>;
115
+ pipe<V, D extends Decoder<V>>(next: D): Decoder<DecoderType<D>>;
116
+ pipe<V, D extends Decoder<V>>(next: (blob: T) => D): Decoder<DecoderType<D>>;
116
117
  }
117
118
  /**
118
119
  * Helper type to return the output type of a Decoder.
@@ -264,13 +265,6 @@ declare const boolean: Decoder<boolean>;
264
265
  * Accepts anything and will return its "truth" value. Will never reject.
265
266
  */
266
267
  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
268
 
275
269
  /**
276
270
  * Accepts objects where all values match the given decoder, and returns the
@@ -358,6 +352,12 @@ declare const jsonArray: Decoder<JSONArray>;
358
352
  */
359
353
  declare const json: Decoder<JSONValue>;
360
354
 
355
+ type SizeOptions = {
356
+ min?: number;
357
+ max?: number;
358
+ size?: number;
359
+ };
360
+
361
361
  interface Klass<T> extends Function {
362
362
  new (...args: readonly any[]): T;
363
363
  }
@@ -490,6 +490,17 @@ declare const url: Decoder<URL>;
490
490
  * as a URL instance.
491
491
  */
492
492
  declare const httpsUrl: Decoder<URL>;
493
+ /**
494
+ * Accepts and returns strings that are valid identifiers in most programming
495
+ * languages.
496
+ */
497
+ declare const identifier: Decoder<string>;
498
+ /**
499
+ * Accepts and returns [nanoid](https://zelark.github.io/nano-id-cc) string
500
+ * values. It assumes the default nanoid alphabet. If you're using a custom
501
+ * alphabet, use `regex()` instead.
502
+ */
503
+ declare function nanoid(options?: SizeOptions): Decoder<string>;
493
504
  /**
494
505
  * Accepts strings that are valid
495
506
  * [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
@@ -581,4 +592,4 @@ declare function taggedUnion<O extends Record<string, Decoder<unknown>>, T = Dec
581
592
  */
582
593
  declare function select<T, D extends Decoder<unknown>>(scout: Decoder<T>, selectFn: (result: T) => D): Decoder<DecoderType<D>>;
583
594
 
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 };
595
+ 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
@@ -29,18 +29,12 @@ function makeObjectAnn(fields, text) {
29
29
  function makeArrayAnn(items, text) {
30
30
  return brand({ type: "array", items, text });
31
31
  }
32
- function makeFunctionAnn(text) {
33
- return brand({ type: "function", text });
34
- }
35
- function makeUnknownAnn(value, text) {
36
- return brand({ type: "unknown", value, text });
32
+ function makeOpaqueAnn(value, text) {
33
+ return brand({ type: "opaque", value, text });
37
34
  }
38
35
  function makeScalarAnn(value, text) {
39
36
  return brand({ type: "scalar", value, text });
40
37
  }
41
- function makeCircularRefAnn(text) {
42
- return brand({ type: "circular-ref", text });
43
- }
44
38
  function updateText(annotation, text) {
45
39
  if (text !== void 0) {
46
40
  return brand({ ...annotation, text });
@@ -66,7 +60,8 @@ function annotateArray(arr, text, seen) {
66
60
  function annotateObject(obj, text, seen) {
67
61
  seen.add(obj);
68
62
  const fields = /* @__PURE__ */ new Map();
69
- for (const [key, value] of Object.entries(obj)) {
63
+ for (const key of Object.keys(obj)) {
64
+ const value = obj[key];
70
65
  fields.set(key, annotate(value, void 0, seen));
71
66
  }
72
67
  return makeObjectAnn(fields, text);
@@ -80,22 +75,22 @@ function annotate(value, text, seen) {
80
75
  }
81
76
  if (Array.isArray(value)) {
82
77
  if (seen.has(value)) {
83
- return makeCircularRefAnn(text);
78
+ return makeOpaqueAnn("<circular ref>", text);
84
79
  } else {
85
80
  return annotateArray(value, text, seen);
86
81
  }
87
82
  }
88
83
  if (isPojo(value)) {
89
84
  if (seen.has(value)) {
90
- return makeCircularRefAnn(text);
85
+ return makeOpaqueAnn("<circular ref>", text);
91
86
  } else {
92
87
  return annotateObject(value, text, seen);
93
88
  }
94
89
  }
95
90
  if (typeof value === "function") {
96
- return makeFunctionAnn(text);
91
+ return makeOpaqueAnn("<function>", text);
97
92
  }
98
- return makeUnknownAnn(value, text);
93
+ return makeOpaqueAnn("???", text);
99
94
  }
100
95
  function public_annotate(value, text) {
101
96
  return annotate(value, text, /* @__PURE__ */ new WeakSet());
@@ -116,6 +111,10 @@ function indent(s, prefix = INDENT) {
116
111
  return `${prefix}${s}`;
117
112
  }
118
113
  }
114
+ var quotePattern = /'/g;
115
+ function quote(value) {
116
+ return typeof value === "string" ? "'" + value.replace(quotePattern, "\\'") + "'" : JSON.stringify(value);
117
+ }
119
118
 
120
119
  // src/core/format.ts
121
120
  function summarize(ann, keypath = []) {
@@ -144,9 +143,9 @@ function summarize(ann, keypath = []) {
144
143
  if (keypath.length === 0) {
145
144
  prefix = "";
146
145
  } else if (keypath.length === 1) {
147
- prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${JSON.stringify(keypath[0])}: `;
146
+ prefix = typeof keypath[0] === "number" ? `Value at index ${keypath[0]}: ` : `Value at key ${quote(keypath[0])}: `;
148
147
  } else {
149
- prefix = `Value at keypath ${keypath.map(String).join(".")}: `;
148
+ prefix = `Value at keypath ${quote(keypath.map(String).join("."))}: `;
150
149
  }
151
150
  return [...result, `${prefix}${text}`];
152
151
  }
@@ -202,7 +201,7 @@ function serializeValue(value) {
202
201
  return "undefined";
203
202
  } else {
204
203
  if (isDate(value)) {
205
- return `new Date(${JSON.stringify(value.toISOString())})`;
204
+ return `new Date(${quote(value.toISOString())})`;
206
205
  } else if (value instanceof Date) {
207
206
  return "(Invalid Date)";
208
207
  } else {
@@ -216,14 +215,12 @@ function serializeAnnotation(ann, prefix = "") {
216
215
  serialized = serializeArray(ann, prefix);
217
216
  } else if (ann.type === "object") {
218
217
  serialized = serializeObject(ann, prefix);
219
- } else if (ann.type === "function") {
220
- serialized = "<function>";
221
- } else if (ann.type === "circular-ref") {
222
- serialized = "<circular ref>";
223
- } else if (ann.type === "unknown") {
224
- serialized = "???";
225
- } else {
218
+ } else if (ann.type === "scalar") {
226
219
  serialized = serializeValue(ann.value);
220
+ } else {
221
+ /* @__PURE__ */ ((_) => {
222
+ })(ann);
223
+ serialized = ann.value;
227
224
  }
228
225
  const text = ann.text;
229
226
  if (text !== void 0) {
@@ -311,10 +308,15 @@ function define(fn) {
311
308
  }
312
309
  function then(next) {
313
310
  return define((blob, ok2, err2) => {
314
- const result = decode(blob);
315
- return result.ok ? next(result.value, ok2, err2) : result;
311
+ const r1 = decode(blob);
312
+ if (!r1.ok) return r1;
313
+ const r2 = isDecoder(next) ? next : next(r1.value, ok2, err2);
314
+ return isDecoder(r2) ? r2.decode(r1.value) : r2;
316
315
  });
317
316
  }
317
+ function pipe(next) {
318
+ return then(next);
319
+ }
318
320
  function reject(rejectFn) {
319
321
  return then((blob, ok2, err2) => {
320
322
  const errmsg = rejectFn(blob);
@@ -331,13 +333,7 @@ function define(fn) {
331
333
  }
332
334
  });
333
335
  }
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({
336
+ return brand2({
341
337
  verify,
342
338
  value,
343
339
  decode,
@@ -346,9 +342,17 @@ function define(fn) {
346
342
  reject,
347
343
  describe,
348
344
  then,
349
- peek
345
+ pipe
350
346
  });
351
347
  }
348
+ var _register2 = /* @__PURE__ */ new WeakSet();
349
+ function brand2(decoder) {
350
+ _register2.add(decoder);
351
+ return decoder;
352
+ }
353
+ function isDecoder(thing) {
354
+ return _register2.has(thing);
355
+ }
352
356
 
353
357
  // src/arrays.ts
354
358
  var poja = define((blob, ok2, err2) => {
@@ -489,7 +493,7 @@ function object(decoders) {
489
493
  objAnn = merge(objAnn, errors);
490
494
  }
491
495
  if (missingKeys.size > 0) {
492
- const errMsg = Array.from(missingKeys).map((key) => `"${key}"`).join(", ");
496
+ const errMsg = Array.from(missingKeys).map(quote).join(", ");
493
497
  const pluralized = missingKeys.size > 1 ? "keys" : "key";
494
498
  objAnn = updateText(objAnn, `Missing ${pluralized}: ${errMsg}`);
495
499
  }
@@ -503,20 +507,16 @@ function exact(decoders) {
503
507
  const checked = pojo.reject((plainObj) => {
504
508
  const actualKeys = new Set(Object.keys(plainObj));
505
509
  const extraKeys = difference(actualKeys, allowedKeys);
506
- return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).join(", ")}` : (
507
- // Don't reject
508
- null
509
- );
510
+ return extraKeys.size > 0 ? `Unexpected extra keys: ${Array.from(extraKeys).map(quote).join(", ")}` : null;
510
511
  });
511
- return checked.then(object(decoders).decode);
512
+ return checked.pipe(object(decoders));
512
513
  }
513
514
  function inexact(decoders) {
514
- return pojo.then((plainObj) => {
515
+ return pojo.pipe((plainObj) => {
515
516
  const allkeys = new Set(Object.keys(plainObj));
516
- const decoder = object(decoders).transform((safepart) => {
517
+ return object(decoders).transform((safepart) => {
517
518
  const safekeys = new Set(Object.keys(decoders));
518
- for (const k of safekeys)
519
- allkeys.add(k);
519
+ for (const k of safekeys) allkeys.add(k);
520
520
  const rv = {};
521
521
  for (const k of allkeys) {
522
522
  if (safekeys.has(k)) {
@@ -530,7 +530,6 @@ function inexact(decoders) {
530
530
  }
531
531
  return rv;
532
532
  });
533
- return decoder.decode(plainObj);
534
533
  });
535
534
  }
536
535
 
@@ -566,9 +565,7 @@ function oneOf(constants) {
566
565
  if (winner !== void 0) {
567
566
  return ok2(winner);
568
567
  }
569
- return err2(
570
- `Must be one of ${constants.map((value) => JSON.stringify(value)).join(", ")}`
571
- );
568
+ return err2(`Must be one of ${constants.map((value) => quote(value)).join(", ")}`);
572
569
  });
573
570
  }
574
571
  function enum_(enumObj) {
@@ -594,9 +591,9 @@ function taggedUnion(field, mapping2) {
594
591
  );
595
592
  }
596
593
  function select(scout, selectFn) {
597
- return scout.peek(([blob, peekResult]) => {
598
- const decoder = selectFn(peekResult);
599
- return decoder.decode(blob);
594
+ return define((blob) => {
595
+ const result = scout.decode(blob);
596
+ return result.ok ? selectFn(result.value).decode(blob) : result;
600
597
  });
601
598
  }
602
599
 
@@ -627,9 +624,7 @@ function nullish(decoder, defaultValue) {
627
624
  }
628
625
  function constant(value) {
629
626
  return define(
630
- (blob, ok2, err2) => blob === value ? ok2(value) : err2(
631
- `Must be ${typeof value === "symbol" ? String(value) : JSON.stringify(value)}`
632
- )
627
+ (blob, ok2, err2) => blob === value ? ok2(value) : err2(`Must be ${typeof value === "symbol" ? String(value) : quote(value)}`)
633
628
  );
634
629
  }
635
630
  function always(value) {
@@ -645,36 +640,11 @@ var hardcoded = always;
645
640
  var unknown = define((blob, ok2, _) => ok2(blob));
646
641
  var mixed = unknown;
647
642
 
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
643
  // src/booleans.ts
673
644
  var boolean = define((blob, ok2, err2) => {
674
645
  return typeof blob === "boolean" ? ok2(blob) : err2("Must be boolean");
675
646
  });
676
647
  var truthy = define((blob, ok2, _) => ok2(!!blob));
677
- var numericBoolean = number.transform((n) => !!n);
678
648
 
679
649
  // src/collections.ts
680
650
  function record(fst, snd) {
@@ -683,14 +653,12 @@ function record(fst, snd) {
683
653
  return pojo.then((input, ok2, err2) => {
684
654
  let rv = {};
685
655
  const errors = /* @__PURE__ */ new Map();
686
- for (const [key, value] of Object.entries(input)) {
656
+ for (const key of Object.keys(input)) {
657
+ const value = input[key];
687
658
  const keyResult = keyDecoder?.decode(key);
688
659
  if (keyResult?.ok === false) {
689
660
  return err2(
690
- public_annotate(
691
- input,
692
- `Invalid key ${JSON.stringify(key)}: ${formatShort(keyResult.error)}`
693
- )
661
+ public_annotate(input, `Invalid key ${quote(key)}: ${formatShort(keyResult.error)}`)
694
662
  );
695
663
  }
696
664
  const k = keyResult?.value ?? key;
@@ -720,6 +688,18 @@ function mapping(decoder) {
720
688
  return record(decoder).transform((obj) => new Map(Object.entries(obj)));
721
689
  }
722
690
 
691
+ // src/lib/size-options.ts
692
+ function bySizeOptions(options) {
693
+ const size = options?.size;
694
+ const min = size ?? options?.min;
695
+ const max = size ?? options?.max;
696
+ const atLeast = min === max ? "" : "at least ";
697
+ const atMost = min === max ? "" : "at most ";
698
+ const tooShort = min !== void 0 && `Too short, must be ${atLeast}${min} chars`;
699
+ const tooLong = max !== void 0 && `Too long, must be ${atMost}${max} chars`;
700
+ 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;
701
+ }
702
+
723
703
  // src/strings.ts
724
704
  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
705
  var string = define(
@@ -742,6 +722,15 @@ var httpsUrl = url.refine(
742
722
  (value) => value.protocol === "https:",
743
723
  "Must be an HTTPS URL"
744
724
  );
725
+ var identifier = regex(
726
+ /^[a-z_][a-z0-9_]*$/i,
727
+ "Must be valid identifier"
728
+ );
729
+ function nanoid(options) {
730
+ return regex(/^[a-z0-9_-]+$/i, "Must be nano ID").reject(
731
+ bySizeOptions(options ?? { size: 21 })
732
+ );
733
+ }
745
734
  var uuid = regex(
746
735
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
747
736
  "Must be uuid"
@@ -781,6 +770,30 @@ var iso8601 = (
781
770
  );
782
771
  var datelike = either(date, iso8601).describe("Must be a Date or date string");
783
772
 
773
+ // src/numbers.ts
774
+ var anyNumber = define(
775
+ (blob, ok2, err2) => isNumber(blob) ? ok2(blob) : err2("Must be number")
776
+ );
777
+ var number = anyNumber.refine(
778
+ (n) => Number.isFinite(n),
779
+ "Number must be finite"
780
+ );
781
+ var integer = number.refine(
782
+ (n) => Number.isInteger(n),
783
+ "Number must be an integer"
784
+ );
785
+ var positiveNumber = number.refine(
786
+ (n) => n >= 0 && !Object.is(n, -0),
787
+ "Number must be positive"
788
+ );
789
+ var positiveInteger = integer.refine(
790
+ (n) => n >= 0 && !Object.is(n, -0),
791
+ "Number must be positive"
792
+ );
793
+ var bigint = define(
794
+ (blob, ok2, err2) => isBigInt(blob) ? ok2(blob) : err2("Must be bigint")
795
+ );
796
+
784
797
  // src/json.ts
785
798
  var jsonObject = lazy(() => record(json));
786
799
  var jsonArray = lazy(() => array(json));
@@ -815,6 +828,7 @@ export {
815
828
  hardcoded,
816
829
  hexadecimal,
817
830
  httpsUrl,
831
+ identifier,
818
832
  inexact,
819
833
  instanceOf,
820
834
  integer,
@@ -826,6 +840,7 @@ export {
826
840
  mapping,
827
841
  maybe,
828
842
  mixed,
843
+ nanoid,
829
844
  never,
830
845
  nonEmptyArray,
831
846
  nonEmptyString,
@@ -834,7 +849,6 @@ export {
834
849
  nullish,
835
850
  number,
836
851
  numeric,
837
- numericBoolean,
838
852
  object,
839
853
  ok,
840
854
  oneOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decoders",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "Elegant and battle-tested validation library for type-safe input data for TypeScript",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "lint": "eslint --color --report-unused-disable-directives src/ test/ && prettier --list-different src/ test/ test-d/",
42
42
  "lint:docs": "cog -c --check docs/*.md || (npm run docs; git diff; echo 'Error: docs not up-to-date, please re-run \"npm docs\" to update them.' && exit 1)",
43
43
  "lint:package": "publint --strict && attw --pack",
44
- "format": "eslint --color --report-unused-disable-directives --fix src/ test/ && prettier --write src/ test/ test-d/",
44
+ "format": "eslint --color --report-unused-disable-directives --fix src/ test/ ; prettier --write src/ test/ test-d/",
45
45
  "test": "vitest run --coverage",
46
46
  "test:completeness": "./bin/check.sh",
47
47
  "test:typescript": "tsc --noEmit",
@@ -61,25 +61,27 @@
61
61
  "verify"
62
62
  ],
63
63
  "devDependencies": {
64
- "@arethetypeswrong/cli": "^0.13.5",
64
+ "@arethetypeswrong/cli": "^0.15.3",
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",
69
- "eslint": "^8.56.0",
66
+ "@types/eslint": "^8.56.10",
67
+ "@typescript-eslint/eslint-plugin": "^7.14.1",
68
+ "@typescript-eslint/parser": "^7.14.1",
69
+ "@vitest/coverage-istanbul": "^1.6.0",
70
+ "eslint": "^8.57.0",
70
71
  "eslint-plugin-import": "^2.29.1",
71
- "eslint-plugin-simple-import-sort": "^10.0.0",
72
- "fast-check": "^3.15.0",
73
- "itertools": "^2.2.1",
74
- "prettier": "^3.1.1",
75
- "publint": "^0.2.7",
76
- "release-it": "^17.0.1",
77
- "ts-morph": "^21.0.1",
78
- "tsd": "^0.30.3",
79
- "tsup": "^8.0.1",
80
- "typescript": "^5.3.3",
81
- "vite-tsconfig-paths": "^4.2.3",
82
- "vitest": "^1.1.3"
72
+ "eslint-plugin-simple-import-sort": "^12.1.0",
73
+ "fast-check": "^3.19.0",
74
+ "itertools": "^2.3.2",
75
+ "pkg-pr-new": "^0.0.9",
76
+ "prettier": "^3.3.2",
77
+ "publint": "^0.2.8",
78
+ "release-it": "^17.4.0",
79
+ "ts-morph": "^23.0.0",
80
+ "tsd": "^0.31.1",
81
+ "tsup": "^8.1.0",
82
+ "typescript": "^5.5.2",
83
+ "vite-tsconfig-paths": "^4.3.2",
84
+ "vitest": "^1.6.0"
83
85
  },
84
86
  "githubUrl": "https://github.com/nvie/decoders",
85
87
  "sideEffects": false