decoders 2.0.0-beta12 → 2.0.0-beta13
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/CHANGELOG.md +53 -35
- package/Decoder.d.ts +71 -5
- package/Decoder.js +41 -42
- package/Decoder.js.flow +102 -66
- package/Decoder.mjs +41 -42
- package/README.md +81 -62
- package/format.d.ts +4 -2
- package/format.js.flow +2 -0
- package/index.d.ts +1 -1
- package/index.js +1 -2
- package/index.js.flow +1 -1
- package/index.mjs +1 -1
- package/lib/arrays.d.ts +22 -0
- package/lib/arrays.js +5 -4
- package/lib/arrays.js.flow +6 -6
- package/lib/arrays.mjs +5 -4
- package/lib/basics.d.ts +64 -9
- package/lib/basics.js +22 -12
- package/lib/basics.js.flow +23 -12
- package/lib/basics.mjs +22 -12
- package/lib/booleans.d.ts +11 -0
- package/lib/booleans.js +1 -1
- package/lib/booleans.js.flow +1 -1
- package/lib/booleans.mjs +1 -1
- package/lib/dates.d.ts +11 -0
- package/lib/dates.js +1 -1
- package/lib/dates.js.flow +1 -1
- package/lib/dates.mjs +1 -1
- package/lib/json.d.ts +24 -0
- package/lib/numbers.d.ts +26 -2
- package/lib/numbers.js +4 -5
- package/lib/numbers.js.flow +4 -5
- package/lib/numbers.mjs +4 -5
- package/lib/objects.d.ts +38 -1
- package/lib/strings.d.ts +43 -0
- package/lib/unions.d.ts +40 -63
- package/lib/unions.js +10 -11
- package/lib/unions.js.flow +10 -13
- package/lib/unions.mjs +9 -8
- package/lib/utilities.d.ts +24 -0
- package/lib/utilities.js +0 -19
- package/lib/utilities.js.flow +0 -19
- package/lib/utilities.mjs +0 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,47 +1,65 @@
|
|
|
1
1
|
## v2.0.0-beta
|
|
2
2
|
|
|
3
|
-
This is a breaking change, which brings numerous benefits
|
|
4
|
-
and simplicity. Please see the [migration guide](./MIGRATING-v2.md) for instructions on
|
|
5
|
-
how to adjust your code.
|
|
3
|
+
This is a breaking change, which brings numerous benefits:
|
|
6
4
|
|
|
7
|
-
|
|
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
|
+
Please see the [migration guide](./MIGRATING-v2.md) for precise instructions on how to
|
|
13
|
+
adjust your v1 code.
|
|
14
|
+
|
|
15
|
+
The main change is the brand new `Decoder<T>` API! The **tl;dr** is:
|
|
16
|
+
|
|
17
|
+
| Replace this v1 pattern... | | ...with this v2 API | Notes |
|
|
18
|
+
| :----------------------------------- | --- | :----------------------------------------- | :--------------------------------------------------------------------------- |
|
|
19
|
+
| `mydecoder(input)` | → | `mydecoder.decode(input)` | [migration instructions](./MIGRATING-v2.md#stop-calling-decoders) |
|
|
20
|
+
| `guard(mydecoder)(input)` | → | `mydecoder.verify(input)` | [migration instructions](./MIGRATING-v2.md#guards-are-no-longer-a-thing) |
|
|
21
|
+
| `map(mydecoder, ...)` | → | `mydecoder.transform(...)` | [migration instructions](./MIGRATING-v2.md#map-is-now-transform) |
|
|
22
|
+
| `compose(mydecoder, predicate(...))` | → | `mydecoder.refine(...)` | [migration instructions](./MIGRATING-v2.md#compose--predicate-is-now-refine) |
|
|
23
|
+
| `describe(mydecoder, ...)` | → | `mydecoder.describe(...)` | |
|
|
24
|
+
| `mydecoder(input).value()` | → | `mydecoder.value(input)` | |
|
|
25
|
+
| `either`, `either3`, ..., `either9` | → | `either` | [migration instructions](./MIGRATING-v2.md#eitherN-is-now-simply-either) |
|
|
26
|
+
| `tuple1`, `tuple2`, ... `tuple6` | → | `tuple` | [migration instructions](./MIGRATING-v2.md#tupleN-is-now-simply-tuple) |
|
|
27
|
+
| `dispatch` | → | `taggedUnion` | [migration instructions](./MIGRATING-v2.md#dispatch-is-now-taggedUnion) |
|
|
28
|
+
| `url(...)` | → | `httpsUrl` / `url` (signature has changed) | [migration instructions](./MIGRATING-v2.md#signature-of-url-has-changed) |
|
|
29
|
+
|
|
30
|
+
The full documentation is available on [**decoders.cc**](https://decoders.cc).
|
|
31
|
+
|
|
32
|
+
Other features:
|
|
33
|
+
|
|
34
|
+
- Include ES modules in published NPM builds (yay tree-shaking! 🍃)
|
|
35
|
+
- Much smaller total bundle size (**67% smaller** compared to v1 😱)
|
|
36
|
+
|
|
37
|
+
Other potentially breaking changes:
|
|
8
38
|
|
|
9
39
|
- Drop support for all Node versions below 12.x
|
|
10
|
-
- Drop support for Flow versions below 0.142.0
|
|
11
40
|
- Drop support for TypeScript versions below 4.1.0
|
|
41
|
+
- Drop support for Flow versions below 0.142.0
|
|
12
42
|
- Drop all package dependencies
|
|
13
|
-
-
|
|
14
|
-
- `guard()` no longer exists, see
|
|
15
|
-
[migration instructions](./MIGRATING-v2.md#guards-are-no-longer-a-thing)
|
|
16
|
-
- `eitherN()` (now simply `either()`, see
|
|
17
|
-
[migration instructions](./MIGRATING-v2.md#eitherN-is-now-simply-either))
|
|
18
|
-
- `tupleN()` (now simply `tuple()`, see
|
|
19
|
-
[migration instructions](./MIGRATING-v2.md#tupleN-is-now-simply-tuple))
|
|
20
|
-
- Renamed decoders:
|
|
21
|
-
- `map()` → `transform()` - see
|
|
22
|
-
[migration instructions](./MIGRATING-v2.md#map-is-now-transform)
|
|
23
|
-
- `dispatch()` → `taggedUnion()` - see
|
|
24
|
-
[migration instructions](./MIGRATING-v2.md#dispatch-is-now-taggedUnion)
|
|
25
|
-
- Decoders that have changed:
|
|
26
|
-
- API of `predicate()` has changed - see
|
|
27
|
-
[migration instructions](./MIGRATING-v2.md#predicate-is-now-a-first-class-citizen)
|
|
28
|
-
- API of `url` decoder has changed - see
|
|
29
|
-
[migration instructions](./MIGRATING-v2.md#url-decoder-has-changed)
|
|
30
|
-
|
|
31
|
-
New features:
|
|
43
|
+
- Direct reliance on `lemons` has been removed
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
New decoders:
|
|
46
|
+
|
|
47
|
+
- [`always`](https://decoders.cc/api.html#always)
|
|
48
|
+
- [`anyNumber`](https://decoders.cc/api.html#anyNumber)
|
|
49
|
+
- [`never`](https://decoders.cc/api.html#never)
|
|
50
|
+
- [`prep()`](https://decoders.cc/api.html#prep)
|
|
51
|
+
- [`set()`](https://decoders.cc/api.html#set)
|
|
52
|
+
- [`uuid`](https://decoders.cc/api.html#uuid)
|
|
53
|
+
- [`uuidv1`](https://decoders.cc/api.html#uuidv1)
|
|
54
|
+
- [`uuidv4`](https://decoders.cc/api.html#uuidv4)
|
|
55
|
+
|
|
56
|
+
Other improvements:
|
|
57
|
+
|
|
58
|
+
- [`optional()`](https://decoders.cc/api.html#optional),
|
|
59
|
+
[`nullable()`](https://decoders.cc/api.html#nullable), and
|
|
60
|
+
[`maybe()`](https://decoders.cc/api.html#maybe) now each take an optional 2nd param to
|
|
61
|
+
specify a default value
|
|
43
62
|
- Better error messages for nested `either`s
|
|
44
|
-
- Guard API now has a simpler way to specify formatters
|
|
45
63
|
|
|
46
64
|
Implementation changes:
|
|
47
65
|
|
package/Decoder.d.ts
CHANGED
|
@@ -4,25 +4,91 @@ import { Result } from './result';
|
|
|
4
4
|
export type Scalar = string | number | boolean | symbol | undefined | null;
|
|
5
5
|
|
|
6
6
|
export type DecodeResult<T> = Result<T, Annotation>;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
export type AcceptanceFn<T, InputT = unknown> = (
|
|
9
|
+
blob: InputT,
|
|
9
10
|
ok: (value: T) => DecodeResult<T>,
|
|
10
11
|
err: (msg: string | Annotation) => DecodeResult<T>,
|
|
11
12
|
) => DecodeResult<T>;
|
|
12
13
|
|
|
13
14
|
export interface Decoder<T> {
|
|
15
|
+
/**
|
|
16
|
+
* Verifies untrusted input. Either returns a value, or throws a decoding
|
|
17
|
+
* error.
|
|
18
|
+
*/
|
|
14
19
|
verify(blob: unknown, formatterFn?: (ann: Annotation) => string | Error): T;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Verifies untrusted input. Either returns a value, or returns undefined.
|
|
23
|
+
*/
|
|
15
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
|
+
*/
|
|
16
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
|
+
*/
|
|
17
36
|
refine<N extends T>(predicate: (value: T) => value is N, msg: string): Decoder<N>;
|
|
18
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
|
+
*/
|
|
19
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
|
+
*/
|
|
20
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
|
+
*/
|
|
21
54
|
describe(message: string): Decoder<T>;
|
|
22
|
-
|
|
23
|
-
|
|
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>;
|
|
24
63
|
}
|
|
25
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
|
+
*/
|
|
26
79
|
export type DecoderType<T> = T extends Decoder<infer V> ? V : never;
|
|
27
80
|
|
|
28
|
-
|
|
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
CHANGED
|
@@ -19,38 +19,52 @@ function noThrow(fn) {
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
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
|
+
}
|
|
22
37
|
/**
|
|
23
38
|
* Defines a new `Decoder<T>`, by implementing a custom acceptance function.
|
|
24
39
|
* The function receives three arguments:
|
|
25
40
|
*
|
|
26
41
|
* 1. `blob` - the raw/unknown input (aka your external data)
|
|
27
42
|
* 2. `ok` - Call `ok(value)` to accept the input and return ``value``
|
|
28
|
-
* 3. `err` - Call `err(message)` to reject the input
|
|
29
|
-
* annotation
|
|
43
|
+
* 3. `err` - Call `err(message)` to reject the input with error ``message``
|
|
30
44
|
*
|
|
31
45
|
* The expected return value should be a `DecodeResult<T>`, which can be
|
|
32
|
-
* obtained by returning the result
|
|
33
|
-
* functions.
|
|
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.
|
|
34
49
|
*/
|
|
35
50
|
|
|
36
51
|
|
|
37
|
-
function define(
|
|
52
|
+
function define(fn) {
|
|
38
53
|
/**
|
|
39
|
-
*
|
|
40
|
-
* it.
|
|
54
|
+
* Verifies the untrusted/unknown input and either accepts or rejects it.
|
|
41
55
|
*
|
|
42
56
|
* Contrasted with `.verify()`, calls to `.decode()` will never fail and
|
|
43
57
|
* instead return a result type.
|
|
44
58
|
*/
|
|
45
59
|
function decode(blob) {
|
|
46
|
-
return
|
|
60
|
+
return fn(blob, _result.ok, function (msg) {
|
|
47
61
|
return (0, _result.err)(typeof msg === 'string' ? (0, _annotate.annotate)(blob, msg) : msg);
|
|
48
62
|
});
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
54
68
|
*/
|
|
55
69
|
|
|
56
70
|
|
|
@@ -64,26 +78,13 @@ function define(decodeFn) {
|
|
|
64
78
|
if (result.ok) {
|
|
65
79
|
return result.value;
|
|
66
80
|
} else {
|
|
67
|
-
|
|
68
|
-
// writing them. If it already returns an Error, throw it
|
|
69
|
-
// unmodified. If it returns a string, wrap it in a "Decoding
|
|
70
|
-
// error" instance from it and throw that.
|
|
71
|
-
var strOrErr = formatter(result.error);
|
|
72
|
-
|
|
73
|
-
if (typeof strOrErr === 'string') {
|
|
74
|
-
var _err = new Error('\n' + strOrErr);
|
|
75
|
-
|
|
76
|
-
_err.name = 'Decoding error';
|
|
77
|
-
throw _err;
|
|
78
|
-
} else {
|
|
79
|
-
throw strOrErr;
|
|
80
|
-
}
|
|
81
|
+
throw format(result.error, formatter);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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`.
|
|
87
88
|
*
|
|
88
89
|
* Use this when you're not interested in programmatically handling the
|
|
89
90
|
* error message.
|
|
@@ -121,23 +122,21 @@ function define(decodeFn) {
|
|
|
121
122
|
/**
|
|
122
123
|
* Chain together the current decoder with another.
|
|
123
124
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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._
|
|
127
128
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* a `.then()` call the provided ``next`` function will receive a ``T`` as
|
|
132
|
-
* its input. This will allow the function to make a stronger assumption
|
|
133
|
-
* about its input.
|
|
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.
|
|
134
132
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
137
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* cases can be covered by `.transform()` or `.refine()`.
|
|
138
|
+
* If it helps, you can think of `define(...)` as equivalent to
|
|
139
|
+
* `unknown.then(...)`.
|
|
141
140
|
*/
|
|
142
141
|
|
|
143
142
|
|
package/Decoder.js.flow
CHANGED
|
@@ -4,47 +4,85 @@ import { annotate } from './annotate';
|
|
|
4
4
|
import { formatInline } from './format';
|
|
5
5
|
import { err as makeErr, ok as makeOk } from './result';
|
|
6
6
|
import type { Annotation } from './annotate';
|
|
7
|
+
import type { Formatter } from './format';
|
|
7
8
|
import type { Result } from './result';
|
|
8
9
|
|
|
9
10
|
export type Scalar = string | number | boolean | symbol | void | null;
|
|
10
11
|
|
|
11
12
|
export type DecodeResult<T> = Result<T, Annotation>;
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
export type AcceptanceFn<T, InputT = mixed> = (
|
|
15
|
+
blob: InputT,
|
|
14
16
|
ok: (value: T) => DecodeResult<T>,
|
|
15
17
|
err: (msg: string | Annotation) => DecodeResult<T>,
|
|
16
18
|
) => DecodeResult<T>;
|
|
17
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
|
+
|
|
18
70
|
/**
|
|
19
71
|
* Helper type to return the "type" of a Decoder.
|
|
20
72
|
*
|
|
21
73
|
* You can use it on types:
|
|
22
74
|
*
|
|
23
|
-
* DecoderType<Decoder<string>>
|
|
24
|
-
* DecoderType<Decoder<number[]>>
|
|
75
|
+
* DecoderType<Decoder<string>> // string
|
|
76
|
+
* DecoderType<Decoder<number[]>> // number[]
|
|
25
77
|
*
|
|
26
78
|
* Or on "values", by using the `typeof` keyword:
|
|
27
79
|
*
|
|
28
|
-
* DecoderType<typeof
|
|
29
|
-
* DecoderType<typeof truthy>
|
|
80
|
+
* DecoderType<typeof string> // string
|
|
81
|
+
* DecoderType<typeof truthy> // boolean
|
|
30
82
|
*
|
|
31
83
|
*/
|
|
32
84
|
export type DecoderType<D> = $Call<<T>(Decoder<T>) => T, D>;
|
|
33
85
|
|
|
34
|
-
export type Decoder<T> = {|
|
|
35
|
-
verify(blob: mixed, formatterFn?: (Annotation) => string | Error): T,
|
|
36
|
-
value(blob: mixed): T | void,
|
|
37
|
-
decode(blob: mixed): DecodeResult<T>,
|
|
38
|
-
refine(predicateFn: (value: T) => boolean, errmsg: string): Decoder<T>,
|
|
39
|
-
reject(rejectFn: (value: T) => string | Annotation | null): Decoder<T>,
|
|
40
|
-
transform<V>(transformFn: (value: T) => V): Decoder<V>,
|
|
41
|
-
describe(message: string): Decoder<T>,
|
|
42
|
-
then<V>(next: DecodeFn<V, T>): Decoder<V>,
|
|
43
|
-
|
|
44
|
-
// Experimental APIs (please don't rely on these yet)
|
|
45
|
-
peek_UNSTABLE<V>(next: DecodeFn<V, [mixed, T]>): Decoder<V>,
|
|
46
|
-
|};
|
|
47
|
-
|
|
48
86
|
function noThrow<T, V>(fn: (value: T) => V): (T) => DecodeResult<V> {
|
|
49
87
|
return (t) => {
|
|
50
88
|
try {
|
|
@@ -56,65 +94,65 @@ function noThrow<T, V>(fn: (value: T) => V): (T) => DecodeResult<V> {
|
|
|
56
94
|
};
|
|
57
95
|
}
|
|
58
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
|
+
|
|
59
112
|
/**
|
|
60
113
|
* Defines a new `Decoder<T>`, by implementing a custom acceptance function.
|
|
61
114
|
* The function receives three arguments:
|
|
62
115
|
*
|
|
63
116
|
* 1. `blob` - the raw/unknown input (aka your external data)
|
|
64
117
|
* 2. `ok` - Call `ok(value)` to accept the input and return ``value``
|
|
65
|
-
* 3. `err` - Call `err(message)` to reject the input
|
|
66
|
-
* annotation
|
|
118
|
+
* 3. `err` - Call `err(message)` to reject the input with error ``message``
|
|
67
119
|
*
|
|
68
120
|
* The expected return value should be a `DecodeResult<T>`, which can be
|
|
69
|
-
* obtained by returning the result
|
|
70
|
-
* functions.
|
|
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.
|
|
71
124
|
*/
|
|
72
|
-
export function define<T>(
|
|
125
|
+
export function define<T>(fn: AcceptanceFn<T>): Decoder<T> {
|
|
73
126
|
/**
|
|
74
|
-
*
|
|
75
|
-
* it.
|
|
127
|
+
* Verifies the untrusted/unknown input and either accepts or rejects it.
|
|
76
128
|
*
|
|
77
129
|
* Contrasted with `.verify()`, calls to `.decode()` will never fail and
|
|
78
130
|
* instead return a result type.
|
|
79
131
|
*/
|
|
80
132
|
function decode(blob: mixed): DecodeResult<T> {
|
|
81
|
-
return
|
|
133
|
+
return fn(blob, makeOk, (msg: Annotation | string) =>
|
|
82
134
|
makeErr(typeof msg === 'string' ? annotate(blob, msg) : msg),
|
|
83
135
|
);
|
|
84
136
|
}
|
|
85
137
|
|
|
86
138
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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.
|
|
90
142
|
*/
|
|
91
|
-
function verify(
|
|
92
|
-
blob: mixed,
|
|
93
|
-
formatter: (Annotation) => string | Error = formatInline,
|
|
94
|
-
): T {
|
|
143
|
+
function verify(blob: mixed, formatter: Formatter = formatInline): T {
|
|
95
144
|
const result = decode(blob);
|
|
96
145
|
if (result.ok) {
|
|
97
146
|
return result.value;
|
|
98
147
|
} else {
|
|
99
|
-
|
|
100
|
-
// writing them. If it already returns an Error, throw it
|
|
101
|
-
// unmodified. If it returns a string, wrap it in a "Decoding
|
|
102
|
-
// error" instance from it and throw that.
|
|
103
|
-
const strOrErr = formatter(result.error);
|
|
104
|
-
if (typeof strOrErr === 'string') {
|
|
105
|
-
const err = new Error('\n' + strOrErr);
|
|
106
|
-
err.name = 'Decoding error';
|
|
107
|
-
throw err;
|
|
108
|
-
} else {
|
|
109
|
-
throw strOrErr;
|
|
110
|
-
}
|
|
148
|
+
throw format(result.error, formatter);
|
|
111
149
|
}
|
|
112
150
|
}
|
|
113
151
|
|
|
114
152
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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`.
|
|
118
156
|
*
|
|
119
157
|
* Use this when you're not interested in programmatically handling the
|
|
120
158
|
* error message.
|
|
@@ -151,25 +189,23 @@ export function define<T>(decodeFn: DecodeFn<T>): Decoder<T> {
|
|
|
151
189
|
/**
|
|
152
190
|
* Chain together the current decoder with another.
|
|
153
191
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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._
|
|
157
195
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* a `.then()` call the provided ``next`` function will receive a ``T`` as
|
|
162
|
-
* its input. This will allow the function to make a stronger assumption
|
|
163
|
-
* about its input.
|
|
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.
|
|
164
199
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
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.
|
|
167
204
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* cases can be covered by `.transform()` or `.refine()`.
|
|
205
|
+
* If it helps, you can think of `define(...)` as equivalent to
|
|
206
|
+
* `unknown.then(...)`.
|
|
171
207
|
*/
|
|
172
|
-
function then<V>(next:
|
|
208
|
+
function then<V>(next: AcceptanceFn<V, T>): Decoder<V> {
|
|
173
209
|
return define((blob, ok, err) => {
|
|
174
210
|
const result = decode(blob);
|
|
175
211
|
return result.ok ? next(result.value, ok, err) : result;
|
|
@@ -227,7 +263,7 @@ export function define<T>(decodeFn: DecodeFn<T>): Decoder<T> {
|
|
|
227
263
|
*
|
|
228
264
|
* This is an advanced, low-level, decoder.
|
|
229
265
|
*/
|
|
230
|
-
function peek_UNSTABLE<V>(next:
|
|
266
|
+
function peek_UNSTABLE<V>(next: AcceptanceFn<V, [mixed, T]>): Decoder<V> {
|
|
231
267
|
return define((blob, ok, err) => {
|
|
232
268
|
const result = decode(blob);
|
|
233
269
|
return result.ok ? next([blob, result.value], ok, err) : result;
|