decoders 2.0.0-beta6 → 2.0.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.
Files changed (162) hide show
  1. package/CHANGELOG.md +55 -9
  2. package/Decoder.d.ts +94 -0
  3. package/Decoder.js +222 -0
  4. package/Decoder.js.flow +286 -0
  5. package/Decoder.mjs +215 -0
  6. package/NotSupportedTSVersion.d.ts +1 -0
  7. package/README.md +123 -961
  8. package/_utils.d.ts +9 -0
  9. package/_utils.js +12 -18
  10. package/_utils.js.flow +15 -19
  11. package/{_esm/_utils.js → _utils.mjs} +11 -15
  12. package/annotate.d.ts +62 -0
  13. package/{_esm/annotate.js → annotate.mjs} +0 -0
  14. package/format.d.ts +6 -0
  15. package/{format/inline.js → format.js} +7 -2
  16. package/{_esm/format/inline.js.flow → format.js.flow} +9 -3
  17. package/{_esm/format/inline.js → format.mjs} +5 -2
  18. package/index.d.ts +40 -0
  19. package/index.js +62 -93
  20. package/index.js.flow +39 -58
  21. package/index.mjs +11 -0
  22. package/lib/_helpers.d.ts +79 -0
  23. package/lib/arrays.d.ts +59 -0
  24. package/lib/arrays.js +139 -0
  25. package/lib/arrays.js.flow +138 -0
  26. package/lib/arrays.mjs +124 -0
  27. package/lib/basics.d.ts +93 -0
  28. package/lib/basics.js +144 -0
  29. package/lib/basics.js.flow +124 -0
  30. package/lib/basics.mjs +120 -0
  31. package/lib/booleans.d.ts +16 -0
  32. package/lib/booleans.js +35 -0
  33. package/lib/booleans.js.flow +22 -0
  34. package/lib/booleans.mjs +25 -0
  35. package/lib/dates.d.ts +15 -0
  36. package/lib/dates.js +44 -0
  37. package/lib/dates.js.flow +40 -0
  38. package/lib/dates.mjs +34 -0
  39. package/lib/json.d.ts +35 -0
  40. package/lib/json.js +55 -0
  41. package/lib/json.js.flow +50 -0
  42. package/lib/json.mjs +40 -0
  43. package/lib/numbers.d.ts +31 -0
  44. package/lib/numbers.js +51 -0
  45. package/lib/numbers.js.flow +48 -0
  46. package/lib/numbers.mjs +41 -0
  47. package/lib/objects.d.ts +75 -0
  48. package/lib/objects.js +240 -0
  49. package/lib/objects.js.flow +246 -0
  50. package/lib/objects.mjs +223 -0
  51. package/lib/strings.d.ts +56 -0
  52. package/lib/strings.js +101 -0
  53. package/lib/strings.js.flow +90 -0
  54. package/lib/strings.mjs +82 -0
  55. package/lib/unions.d.ts +55 -0
  56. package/lib/unions.js +160 -0
  57. package/lib/unions.js.flow +155 -0
  58. package/lib/unions.mjs +146 -0
  59. package/lib/utilities.d.ts +34 -0
  60. package/lib/utilities.js +75 -0
  61. package/lib/utilities.js.flow +65 -0
  62. package/lib/utilities.mjs +60 -0
  63. package/package.json +78 -19
  64. package/result.d.ts +16 -0
  65. package/result.js +2 -132
  66. package/result.js.flow +4 -136
  67. package/result.mjs +27 -0
  68. package/_esm/_guard.js +0 -15
  69. package/_esm/_guard.js.flow +0 -20
  70. package/_esm/_types.js +0 -0
  71. package/_esm/_types.js.flow +0 -20
  72. package/_esm/_utils.js.flow +0 -97
  73. package/_esm/annotate.js.flow +0 -218
  74. package/_esm/core/array.js +0 -91
  75. package/_esm/core/array.js.flow +0 -103
  76. package/_esm/core/boolean.js +0 -28
  77. package/_esm/core/boolean.js.flow +0 -29
  78. package/_esm/core/composition.js +0 -42
  79. package/_esm/core/composition.js.flow +0 -43
  80. package/_esm/core/constants.js +0 -46
  81. package/_esm/core/constants.js.flow +0 -46
  82. package/_esm/core/date.js +0 -28
  83. package/_esm/core/date.js.flow +0 -40
  84. package/_esm/core/describe.js +0 -16
  85. package/_esm/core/describe.js.flow +0 -17
  86. package/_esm/core/dispatch.js +0 -51
  87. package/_esm/core/dispatch.js.flow +0 -58
  88. package/_esm/core/either.js +0 -90
  89. package/_esm/core/either.js.flow +0 -151
  90. package/_esm/core/fail.js +0 -11
  91. package/_esm/core/fail.js.flow +0 -12
  92. package/_esm/core/instanceOf.js +0 -8
  93. package/_esm/core/instanceOf.js.flow +0 -20
  94. package/_esm/core/json.js +0 -15
  95. package/_esm/core/json.js.flow +0 -28
  96. package/_esm/core/lazy.js +0 -11
  97. package/_esm/core/lazy.js.flow +0 -15
  98. package/_esm/core/mapping.js +0 -54
  99. package/_esm/core/mapping.js.flow +0 -54
  100. package/_esm/core/number.js +0 -25
  101. package/_esm/core/number.js.flow +0 -34
  102. package/_esm/core/object.js +0 -175
  103. package/_esm/core/object.js.flow +0 -203
  104. package/_esm/core/optional.js +0 -38
  105. package/_esm/core/optional.js.flow +0 -41
  106. package/_esm/core/string.js +0 -76
  107. package/_esm/core/string.js.flow +0 -82
  108. package/_esm/core/tuple.js +0 -159
  109. package/_esm/core/tuple.js.flow +0 -214
  110. package/_esm/format/index.js +0 -2
  111. package/_esm/format/index.js.flow +0 -4
  112. package/_esm/format/short.js +0 -4
  113. package/_esm/format/short.js.flow +0 -8
  114. package/_esm/index.js +0 -37
  115. package/_esm/index.js.flow +0 -63
  116. package/_esm/result.js +0 -135
  117. package/_esm/result.js.flow +0 -158
  118. package/_guard.js +0 -26
  119. package/_guard.js.flow +0 -20
  120. package/_types.js +0 -1
  121. package/_types.js.flow +0 -20
  122. package/core/array.js +0 -108
  123. package/core/array.js.flow +0 -103
  124. package/core/boolean.js +0 -44
  125. package/core/boolean.js.flow +0 -29
  126. package/core/composition.js +0 -56
  127. package/core/composition.js.flow +0 -43
  128. package/core/constants.js +0 -69
  129. package/core/constants.js.flow +0 -46
  130. package/core/date.js +0 -46
  131. package/core/date.js.flow +0 -40
  132. package/core/describe.js +0 -26
  133. package/core/describe.js.flow +0 -17
  134. package/core/dispatch.js +0 -62
  135. package/core/dispatch.js.flow +0 -58
  136. package/core/either.js +0 -117
  137. package/core/either.js.flow +0 -151
  138. package/core/fail.js +0 -21
  139. package/core/fail.js.flow +0 -12
  140. package/core/instanceOf.js +0 -19
  141. package/core/instanceOf.js.flow +0 -20
  142. package/core/json.js +0 -31
  143. package/core/json.js.flow +0 -28
  144. package/core/lazy.js +0 -16
  145. package/core/lazy.js.flow +0 -15
  146. package/core/mapping.js +0 -67
  147. package/core/mapping.js.flow +0 -54
  148. package/core/number.js +0 -40
  149. package/core/number.js.flow +0 -34
  150. package/core/object.js +0 -194
  151. package/core/object.js.flow +0 -203
  152. package/core/optional.js +0 -54
  153. package/core/optional.js.flow +0 -41
  154. package/core/string.js +0 -98
  155. package/core/string.js.flow +0 -82
  156. package/core/tuple.js +0 -177
  157. package/core/tuple.js.flow +0 -214
  158. package/format/index.js +0 -12
  159. package/format/index.js.flow +0 -4
  160. package/format/inline.js.flow +0 -122
  161. package/format/short.js +0 -10
  162. package/format/short.js.flow +0 -8
package/CHANGELOG.md CHANGED
@@ -1,21 +1,67 @@
1
- ## v2.0.0-beta
1
+ ## v2.0.0
2
2
 
3
- Upgrading to v2 _can_, but doesn't _have_ to be a breaking change for you. If upgrading
4
- causes errors for you, please see the [migration guide](./MIGRATING-v2.md) for
5
- instructions.
3
+ This is a breaking change, which brings numerous benefits:
6
4
 
7
- Potentially breaking changes:
5
+ - A **simpler API** 😇
6
+ - Smaller **bundle size** (67% reduction 😱)
7
+ - **Tree-shaking** support 🍃
8
+ - Runtime **speed** 🏎️
9
+ - Better documentation 📚
10
+ - Better support for writing your own decoders 🛠️
11
+
12
+ <img alt="Bundle size comparison between v1 and v2" src="./docs/assets/size-comparison@2x.png" style="width: 100%; max-width: 829px" width="829" />
13
+
14
+ Please see the [migration guide](./MIGRATING-v2.md) for precise instructions on how to
15
+ adjust your v1 code.
16
+
17
+ The main change is the brand new `Decoder<T>` API! The **tl;dr** is:
18
+
19
+ | Replace this v1 pattern... | | ...with this v2 API | Notes |
20
+ | :----------------------------------- | --- | :----------------------------------------- | :--------------------------------------------------------------------------- |
21
+ | `mydecoder(input)` | → | `mydecoder.decode(input)` | [migration instructions](./MIGRATING-v2.md#stop-calling-decoders) |
22
+ | `guard(mydecoder)(input)` | → | `mydecoder.verify(input)` | [migration instructions](./MIGRATING-v2.md#guards-are-no-longer-a-thing) |
23
+ | `map(mydecoder, ...)` | → | `mydecoder.transform(...)` | [migration instructions](./MIGRATING-v2.md#map-is-now-transform) |
24
+ | `compose(mydecoder, predicate(...))` | → | `mydecoder.refine(...)` | [migration instructions](./MIGRATING-v2.md#compose--predicate-is-now-refine) |
25
+ | `describe(mydecoder, ...)` | → | `mydecoder.describe(...)` | |
26
+ | `mydecoder(input).value()` | → | `mydecoder.value(input)` | |
27
+ | `either`, `either3`, ..., `either9` | → | `either` | [migration instructions](./MIGRATING-v2.md#eitherN-is-now-simply-either) |
28
+ | `tuple1`, `tuple2`, ... `tuple6` | → | `tuple` | [migration instructions](./MIGRATING-v2.md#tupleN-is-now-simply-tuple) |
29
+ | `dispatch` | → | `taggedUnion` | [migration instructions](./MIGRATING-v2.md#dispatch-is-now-taggedUnion) |
30
+ | `url(...)` | → | `httpsUrl` / `url` (signature has changed) | [migration instructions](./MIGRATING-v2.md#signature-of-url-has-changed) |
31
+
32
+ The full documentation is available on [**decoders.cc**](https://decoders.cc).
33
+
34
+ Other features:
35
+
36
+ - Include ES modules in published NPM builds (yay tree-shaking! 🍃)
37
+ - Much smaller total bundle size (**67% smaller** compared to v1 😱)
38
+
39
+ Other potentially breaking changes:
8
40
 
9
41
  - Drop support for all Node versions below 12.x
42
+ - Drop support for TypeScript versions below 4.1.0
10
43
  - Drop support for Flow versions below 0.142.0
11
44
  - Drop all package dependencies
45
+ - Direct reliance on `lemons` has been removed
12
46
 
13
- New features:
47
+ New decoders:
14
48
 
15
- - Include ES modules in published NPM builds (yay tree-shaking! 🍃)
16
- - Much smaller total bundle size
49
+ - [`always`](https://decoders.cc/api.html#always)
50
+ - [`anyNumber`](https://decoders.cc/api.html#anyNumber)
51
+ - [`never`](https://decoders.cc/api.html#never)
52
+ - [`prep()`](https://decoders.cc/api.html#prep)
53
+ - [`set()`](https://decoders.cc/api.html#set)
54
+ - [`uuid`](https://decoders.cc/api.html#uuid)
55
+ - [`uuidv1`](https://decoders.cc/api.html#uuidv1)
56
+ - [`uuidv4`](https://decoders.cc/api.html#uuidv4)
57
+
58
+ Other improvements:
59
+
60
+ - [`optional()`](https://decoders.cc/api.html#optional),
61
+ [`nullable()`](https://decoders.cc/api.html#nullable), and
62
+ [`maybe()`](https://decoders.cc/api.html#maybe) now each take an optional 2nd param to
63
+ specify a default value
17
64
  - Better error messages for nested `either`s
18
- - Guard API now has a simpler way to specify formatters
19
65
 
20
66
  Implementation changes:
21
67
 
package/Decoder.d.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { Annotation } from './annotate';
2
+ import { Result } from './result';
3
+
4
+ export type Scalar = string | number | boolean | symbol | undefined | null;
5
+
6
+ export type DecodeResult<T> = Result<T, Annotation>;
7
+
8
+ export type AcceptanceFn<T, InputT = unknown> = (
9
+ blob: InputT,
10
+ ok: (value: T) => DecodeResult<T>,
11
+ err: (msg: string | Annotation) => DecodeResult<T>,
12
+ ) => DecodeResult<T>;
13
+
14
+ export interface Decoder<T> {
15
+ /**
16
+ * Verifies untrusted input. Either returns a value, or throws a decoding
17
+ * error.
18
+ */
19
+ verify(blob: unknown, formatterFn?: (ann: Annotation) => string | Error): T;
20
+
21
+ /**
22
+ * Verifies untrusted input. Either returns a value, or returns undefined.
23
+ */
24
+ value(blob: unknown): T | undefined;
25
+
26
+ /**
27
+ * Verifies untrusted input. Always returns a DecodeResult, which is either
28
+ * an "ok" value or an "error" annotation.
29
+ */
30
+ decode(blob: unknown): DecodeResult<T>;
31
+
32
+ /**
33
+ * Build a new decoder from the the current one, with an extra acceptance
34
+ * criterium.
35
+ */
36
+ refine<N extends T>(predicate: (value: T) => value is N, msg: string): Decoder<N>;
37
+ refine(predicate: (value: T) => boolean, msg: string): Decoder<T>;
38
+
39
+ /**
40
+ * Build a new decoder from the current one, with an extra rejection
41
+ * criterium.
42
+ */
43
+ reject(rejectFn: (value: T) => string | Annotation | null): Decoder<T>;
44
+
45
+ /**
46
+ * Build a new decoder from the current one, modifying its outputted value.
47
+ */
48
+ transform<V>(transformFn: (value: T) => V): Decoder<V>;
49
+
50
+ /**
51
+ * Build a new decoder from the current one, with a mutated error message
52
+ * in case of a rejection.
53
+ */
54
+ describe(message: string): Decoder<T>;
55
+
56
+ /**
57
+ * Chain together the current decoder with another acceptance function.
58
+ */
59
+ then<V>(next: AcceptanceFn<V, T>): Decoder<V>;
60
+
61
+ // Experimental APIs (please don't rely on these yet)
62
+ peek_UNSTABLE<V>(next: AcceptanceFn<V, [unknown, T]>): Decoder<V>;
63
+ }
64
+
65
+ /**
66
+ * Helper type to return the "type" of a Decoder.
67
+ *
68
+ * You can use it on types:
69
+ *
70
+ * DecoderType<Decoder<string>> // string
71
+ * DecoderType<Decoder<number[]>> // number[]
72
+ *
73
+ * Or on "values", by using the `typeof` keyword:
74
+ *
75
+ * DecoderType<typeof string> // string
76
+ * DecoderType<typeof truthy> // boolean
77
+ *
78
+ */
79
+ export type DecoderType<T> = T extends Decoder<infer V> ? V : never;
80
+
81
+ /**
82
+ * Defines a new `Decoder<T>`, by implementing a custom acceptance function.
83
+ * The function receives three arguments:
84
+ *
85
+ * 1. `blob` - the raw/unknown input (aka your external data)
86
+ * 2. `ok` - Call `ok(value)` to accept the input and return ``value``
87
+ * 3. `err` - Call `err(message)` to reject the input with error ``message``
88
+ *
89
+ * The expected return value should be a `DecodeResult<T>`, which can be
90
+ * obtained by returning the result of calling the provided `ok` or `err`
91
+ * helper functions. Please note that `ok()` and `err()` don't perform side
92
+ * effects! You'll need to _return_ those values.
93
+ */
94
+ export function define<T>(fn: AcceptanceFn<T>): Decoder<T>;
package/Decoder.js ADDED
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.define = define;
5
+
6
+ var _annotate = require("./annotate");
7
+
8
+ var _format = require("./format");
9
+
10
+ var _result = require("./result");
11
+
12
+ function noThrow(fn) {
13
+ return function (t) {
14
+ try {
15
+ var v = fn(t);
16
+ return (0, _result.ok)(v);
17
+ } catch (e) {
18
+ return (0, _result.err)((0, _annotate.annotate)(t, e instanceof Error ? e.message : String(e)));
19
+ }
20
+ };
21
+ }
22
+
23
+ function format(err, formatter) {
24
+ var formatted = formatter(err); // Formatter functions may return a string or an error for convenience of
25
+ // writing them. If it already returns an Error, return it unmodified. If
26
+ // it returns a string, wrap it in a "Decoding error" instance.
27
+
28
+ if (typeof formatted === 'string') {
29
+ var _err = new Error('\n' + formatted);
30
+
31
+ _err.name = 'Decoding error';
32
+ return _err;
33
+ } else {
34
+ return formatted;
35
+ }
36
+ }
37
+ /**
38
+ * Defines a new `Decoder<T>`, by implementing a custom acceptance function.
39
+ * The function receives three arguments:
40
+ *
41
+ * 1. `blob` - the raw/unknown input (aka your external data)
42
+ * 2. `ok` - Call `ok(value)` to accept the input and return ``value``
43
+ * 3. `err` - Call `err(message)` to reject the input with error ``message``
44
+ *
45
+ * The expected return value should be a `DecodeResult<T>`, which can be
46
+ * obtained by returning the result of calling the provided `ok` or `err`
47
+ * helper functions. Please note that `ok()` and `err()` don't perform side
48
+ * effects! You'll need to _return_ those values.
49
+ */
50
+
51
+
52
+ function define(fn) {
53
+ /**
54
+ * Verifies the untrusted/unknown input and either accepts or rejects it.
55
+ *
56
+ * Contrasted with `.verify()`, calls to `.decode()` will never fail and
57
+ * instead return a result type.
58
+ */
59
+ function decode(blob) {
60
+ return fn(blob, _result.ok, function (msg) {
61
+ return (0, _result.err)(typeof msg === 'string' ? (0, _annotate.annotate)(blob, msg) : msg);
62
+ });
63
+ }
64
+ /**
65
+ * Verifies the untrusted/unknown input and either accepts or rejects it.
66
+ * When accepted, returns a value of type `T`. Otherwise fail with
67
+ * a runtime error.
68
+ */
69
+
70
+
71
+ function verify(blob, formatter) {
72
+ if (formatter === void 0) {
73
+ formatter = _format.formatInline;
74
+ }
75
+
76
+ var result = decode(blob);
77
+
78
+ if (result.ok) {
79
+ return result.value;
80
+ } else {
81
+ throw format(result.error, formatter);
82
+ }
83
+ }
84
+ /**
85
+ * Verifies the untrusted/unknown input and either accepts or rejects it.
86
+ * When accepted, returns the decoded `T` value directly. Otherwise returns
87
+ * `undefined`.
88
+ *
89
+ * Use this when you're not interested in programmatically handling the
90
+ * error message.
91
+ */
92
+
93
+
94
+ function value(blob) {
95
+ return decode(blob).value;
96
+ }
97
+ /**
98
+ * Accepts any value the given decoder accepts, and on success, will call
99
+ * the given function **on the decoded result**. If the transformation
100
+ * function throws an error, the whole decoder will fail using the error
101
+ * message as the failure reason.
102
+ */
103
+
104
+
105
+ function transform(transformFn) {
106
+ return then(noThrow(transformFn));
107
+ }
108
+ /**
109
+ * Adds an extra predicate to a decoder. The new decoder is like the
110
+ * original decoder, but only accepts values that also meet the
111
+ * predicate.
112
+ */
113
+
114
+
115
+ function refine(predicateFn, errmsg) {
116
+ return reject(function (value) {
117
+ return predicateFn(value) ? // Don't reject
118
+ null : // Reject with the given error message
119
+ errmsg;
120
+ });
121
+ }
122
+ /**
123
+ * Chain together the current decoder with another.
124
+ *
125
+ * > _**NOTE:** This is an advanced, low-level, API. It's not recommended
126
+ * > to reach for this construct unless there is no other way. Most cases can
127
+ * > be covered more elegantly by `.transform()` or `.refine()` instead._
128
+ *
129
+ * If the current decoder accepts an input, the resulting ``T`` value will
130
+ * get passed into the given ``next`` acceptance function to further decide
131
+ * whether or not the value should get accepted or rejected.
132
+ *
133
+ * This works similar to how you would `define()` a new decoder, except
134
+ * that the ``blob`` param will now be ``T`` (a known type), rather than
135
+ * ``unknown``. This will allow the function to make a stronger assumption
136
+ * about its input and avoid re-refining inputs.
137
+ *
138
+ * If it helps, you can think of `define(...)` as equivalent to
139
+ * `unknown.then(...)`.
140
+ */
141
+
142
+
143
+ function then(next) {
144
+ return define(function (blob, ok, err) {
145
+ var result = decode(blob);
146
+ return result.ok ? next(result.value, ok, err) : result;
147
+ });
148
+ }
149
+ /**
150
+ * Adds an extra predicate to a decoder. The new decoder is like the
151
+ * original decoder, but only accepts values that aren't rejected by the
152
+ * given function.
153
+ *
154
+ * The given function can return `null` to accept the decoded value, or
155
+ * return a specific error message to reject.
156
+ *
157
+ * Unlike `.refine()`, you can use this function to return a dynamic error
158
+ * message.
159
+ */
160
+
161
+
162
+ function reject(rejectFn) {
163
+ return then(function (value, ok, err) {
164
+ var errmsg = rejectFn(value);
165
+ return errmsg === null ? ok(value) : err(typeof errmsg === 'string' ? (0, _annotate.annotate)(value, errmsg) : errmsg);
166
+ });
167
+ }
168
+ /**
169
+ * Uses the given decoder, but will use an alternative error message in
170
+ * case it rejects. This can be used to simplify or shorten otherwise
171
+ * long or low-level/technical errors.
172
+ */
173
+
174
+
175
+ function describe(message) {
176
+ return define(function (blob, _, err) {
177
+ // Decode using the given decoder...
178
+ var result = decode(blob);
179
+
180
+ if (result.ok) {
181
+ return result;
182
+ } else {
183
+ // ...but in case of error, annotate this with the custom given
184
+ // message instead
185
+ return err((0, _annotate.annotate)(result.error, message));
186
+ }
187
+ });
188
+ }
189
+ /**
190
+ * WARNING: This is an EXPERIMENTAL API that will likely change in the
191
+ * future. Please DO NOT rely on it.
192
+ *
193
+ * Chain together the current decoder with another, but also pass along
194
+ * the original input.
195
+ *
196
+ * This is like `.then()`, but instead of this function receiving just
197
+ * the decoded result ``T``, it also receives the original input.
198
+ *
199
+ * This is an advanced, low-level, decoder.
200
+ */
201
+
202
+
203
+ function peek_UNSTABLE(next) {
204
+ return define(function (blob, ok, err) {
205
+ var result = decode(blob);
206
+ return result.ok ? next([blob, result.value], ok, err) : result;
207
+ });
208
+ }
209
+
210
+ return Object.freeze({
211
+ verify: verify,
212
+ value: value,
213
+ decode: decode,
214
+ transform: transform,
215
+ refine: refine,
216
+ reject: reject,
217
+ describe: describe,
218
+ then: then,
219
+ // EXPERIMENTAL - please DO NOT rely on this method
220
+ peek_UNSTABLE: peek_UNSTABLE
221
+ });
222
+ }
@@ -0,0 +1,286 @@
1
+ // @flow strict
2
+
3
+ import { annotate } from './annotate';
4
+ import { formatInline } from './format';
5
+ import { err as makeErr, ok as makeOk } from './result';
6
+ import type { Annotation } from './annotate';
7
+ import type { Formatter } from './format';
8
+ import type { Result } from './result';
9
+
10
+ export type Scalar = string | number | boolean | symbol | void | null;
11
+
12
+ export type DecodeResult<T> = Result<T, Annotation>;
13
+
14
+ export type AcceptanceFn<T, InputT = mixed> = (
15
+ blob: InputT,
16
+ ok: (value: T) => DecodeResult<T>,
17
+ err: (msg: string | Annotation) => DecodeResult<T>,
18
+ ) => DecodeResult<T>;
19
+
20
+ export type Decoder<T> = {|
21
+ /**
22
+ * Verifies untrusted input. Either returns a value, or throws a decoding
23
+ * error.
24
+ */
25
+ verify(blob: mixed, formatter?: Formatter): T,
26
+
27
+ /**
28
+ * Verifies untrusted input. Either returns a value, or returns undefined.
29
+ */
30
+ value(blob: mixed): T | void,
31
+
32
+ /**
33
+ * Verifies untrusted input. Always returns a DecodeResult, which is either
34
+ * an "ok" value or an "error" annotation.
35
+ */
36
+ decode(blob: mixed): DecodeResult<T>,
37
+
38
+ /**
39
+ * Build a new decoder from the the current one, with an extra acceptance
40
+ * criterium.
41
+ */
42
+ refine(predicateFn: (value: T) => boolean, errmsg: string): Decoder<T>,
43
+
44
+ /**
45
+ * Build a new decoder from the current one, with an extra rejection
46
+ * criterium.
47
+ */
48
+ reject(rejectFn: (value: T) => string | Annotation | null): Decoder<T>,
49
+
50
+ /**
51
+ * Build a new decoder from the current one, modifying its outputted value.
52
+ */
53
+ transform<V>(transformFn: (value: T) => V): Decoder<V>,
54
+
55
+ /**
56
+ * Build a new decoder from the current one, with a mutated error message
57
+ * in case of a rejection.
58
+ */
59
+ describe(message: string): Decoder<T>,
60
+
61
+ /**
62
+ * Chain together the current decoder with another acceptance function.
63
+ */
64
+ then<V>(next: AcceptanceFn<V, T>): Decoder<V>,
65
+
66
+ // Experimental APIs (please don't rely on these yet)
67
+ peek_UNSTABLE<V>(next: AcceptanceFn<V, [mixed, T]>): Decoder<V>,
68
+ |};
69
+
70
+ /**
71
+ * Helper type to return the "type" of a Decoder.
72
+ *
73
+ * You can use it on types:
74
+ *
75
+ * DecoderType<Decoder<string>> // string
76
+ * DecoderType<Decoder<number[]>> // number[]
77
+ *
78
+ * Or on "values", by using the `typeof` keyword:
79
+ *
80
+ * DecoderType<typeof string> // string
81
+ * DecoderType<typeof truthy> // boolean
82
+ *
83
+ */
84
+ export type DecoderType<D> = $Call<<T>(Decoder<T>) => T, D>;
85
+
86
+ function noThrow<T, V>(fn: (value: T) => V): (T) => DecodeResult<V> {
87
+ return (t) => {
88
+ try {
89
+ const v = fn(t);
90
+ return makeOk(v);
91
+ } catch (e) {
92
+ return makeErr(annotate(t, e instanceof Error ? e.message : String(e)));
93
+ }
94
+ };
95
+ }
96
+
97
+ function format(err: Annotation, formatter: Formatter): Error {
98
+ const formatted = formatter(err);
99
+
100
+ // Formatter functions may return a string or an error for convenience of
101
+ // writing them. If it already returns an Error, return it unmodified. If
102
+ // it returns a string, wrap it in a "Decoding error" instance.
103
+ if (typeof formatted === 'string') {
104
+ const err = new Error('\n' + formatted);
105
+ err.name = 'Decoding error';
106
+ return err;
107
+ } else {
108
+ return formatted;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Defines a new `Decoder<T>`, by implementing a custom acceptance function.
114
+ * The function receives three arguments:
115
+ *
116
+ * 1. `blob` - the raw/unknown input (aka your external data)
117
+ * 2. `ok` - Call `ok(value)` to accept the input and return ``value``
118
+ * 3. `err` - Call `err(message)` to reject the input with error ``message``
119
+ *
120
+ * The expected return value should be a `DecodeResult<T>`, which can be
121
+ * obtained by returning the result of calling the provided `ok` or `err`
122
+ * helper functions. Please note that `ok()` and `err()` don't perform side
123
+ * effects! You'll need to _return_ those values.
124
+ */
125
+ export function define<T>(fn: AcceptanceFn<T>): Decoder<T> {
126
+ /**
127
+ * Verifies the untrusted/unknown input and either accepts or rejects it.
128
+ *
129
+ * Contrasted with `.verify()`, calls to `.decode()` will never fail and
130
+ * instead return a result type.
131
+ */
132
+ function decode(blob: mixed): DecodeResult<T> {
133
+ return fn(blob, makeOk, (msg: Annotation | string) =>
134
+ makeErr(typeof msg === 'string' ? annotate(blob, msg) : msg),
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Verifies the untrusted/unknown input and either accepts or rejects it.
140
+ * When accepted, returns a value of type `T`. Otherwise fail with
141
+ * a runtime error.
142
+ */
143
+ function verify(blob: mixed, formatter: Formatter = formatInline): T {
144
+ const result = decode(blob);
145
+ if (result.ok) {
146
+ return result.value;
147
+ } else {
148
+ throw format(result.error, formatter);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Verifies the untrusted/unknown input and either accepts or rejects it.
154
+ * When accepted, returns the decoded `T` value directly. Otherwise returns
155
+ * `undefined`.
156
+ *
157
+ * Use this when you're not interested in programmatically handling the
158
+ * error message.
159
+ */
160
+ function value(blob: mixed): T | void {
161
+ return decode(blob).value;
162
+ }
163
+
164
+ /**
165
+ * Accepts any value the given decoder accepts, and on success, will call
166
+ * the given function **on the decoded result**. If the transformation
167
+ * function throws an error, the whole decoder will fail using the error
168
+ * message as the failure reason.
169
+ */
170
+ function transform<V>(transformFn: (T) => V): Decoder<V> {
171
+ return then(noThrow(transformFn));
172
+ }
173
+
174
+ /**
175
+ * Adds an extra predicate to a decoder. The new decoder is like the
176
+ * original decoder, but only accepts values that also meet the
177
+ * predicate.
178
+ */
179
+ function refine(predicateFn: (value: T) => boolean, errmsg: string): Decoder<T> {
180
+ return reject((value) =>
181
+ predicateFn(value)
182
+ ? // Don't reject
183
+ null
184
+ : // Reject with the given error message
185
+ errmsg,
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Chain together the current decoder with another.
191
+ *
192
+ * > _**NOTE:** This is an advanced, low-level, API. It's not recommended
193
+ * > to reach for this construct unless there is no other way. Most cases can
194
+ * > be covered more elegantly by `.transform()` or `.refine()` instead._
195
+ *
196
+ * If the current decoder accepts an input, the resulting ``T`` value will
197
+ * get passed into the given ``next`` acceptance function to further decide
198
+ * whether or not the value should get accepted or rejected.
199
+ *
200
+ * This works similar to how you would `define()` a new decoder, except
201
+ * that the ``blob`` param will now be ``T`` (a known type), rather than
202
+ * ``unknown``. This will allow the function to make a stronger assumption
203
+ * about its input and avoid re-refining inputs.
204
+ *
205
+ * If it helps, you can think of `define(...)` as equivalent to
206
+ * `unknown.then(...)`.
207
+ */
208
+ function then<V>(next: AcceptanceFn<V, T>): Decoder<V> {
209
+ return define((blob, ok, err) => {
210
+ const result = decode(blob);
211
+ return result.ok ? next(result.value, ok, err) : result;
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Adds an extra predicate to a decoder. The new decoder is like the
217
+ * original decoder, but only accepts values that aren't rejected by the
218
+ * given function.
219
+ *
220
+ * The given function can return `null` to accept the decoded value, or
221
+ * return a specific error message to reject.
222
+ *
223
+ * Unlike `.refine()`, you can use this function to return a dynamic error
224
+ * message.
225
+ */
226
+ function reject(rejectFn: (value: T) => string | Annotation | null): Decoder<T> {
227
+ return then((value, ok, err) => {
228
+ const errmsg = rejectFn(value);
229
+ return errmsg === null
230
+ ? ok(value)
231
+ : err(typeof errmsg === 'string' ? annotate(value, errmsg) : errmsg);
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Uses the given decoder, but will use an alternative error message in
237
+ * case it rejects. This can be used to simplify or shorten otherwise
238
+ * long or low-level/technical errors.
239
+ */
240
+ function describe(message: string): Decoder<T> {
241
+ return define((blob, _, err) => {
242
+ // Decode using the given decoder...
243
+ const result = decode(blob);
244
+ if (result.ok) {
245
+ return result;
246
+ } else {
247
+ // ...but in case of error, annotate this with the custom given
248
+ // message instead
249
+ return err(annotate(result.error, message));
250
+ }
251
+ });
252
+ }
253
+
254
+ /**
255
+ * WARNING: This is an EXPERIMENTAL API that will likely change in the
256
+ * future. Please DO NOT rely on it.
257
+ *
258
+ * Chain together the current decoder with another, but also pass along
259
+ * the original input.
260
+ *
261
+ * This is like `.then()`, but instead of this function receiving just
262
+ * the decoded result ``T``, it also receives the original input.
263
+ *
264
+ * This is an advanced, low-level, decoder.
265
+ */
266
+ function peek_UNSTABLE<V>(next: AcceptanceFn<V, [mixed, T]>): Decoder<V> {
267
+ return define((blob, ok, err) => {
268
+ const result = decode(blob);
269
+ return result.ok ? next([blob, result.value], ok, err) : result;
270
+ });
271
+ }
272
+
273
+ return Object.freeze({
274
+ verify,
275
+ value,
276
+ decode,
277
+ transform,
278
+ refine,
279
+ reject,
280
+ describe,
281
+ then,
282
+
283
+ // EXPERIMENTAL - please DO NOT rely on this method
284
+ peek_UNSTABLE,
285
+ });
286
+ }