decoders 2.0.0-beta1 → 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 -5
- package/Decoder.d.ts +94 -0
- package/Decoder.js +222 -0
- package/Decoder.js.flow +286 -0
- package/Decoder.mjs +215 -0
- package/NotSupportedTSVersion.d.ts +1 -0
- package/README.md +124 -961
- package/_utils.d.ts +9 -0
- package/{cjs/_utils.js → _utils.js} +12 -18
- package/{cjs/_utils.js.flow → _utils.js.flow} +15 -19
- package/{es/_utils.js → _utils.mjs} +11 -15
- package/{ts/annotate.d.ts → annotate.d.ts} +25 -21
- package/{cjs/annotate.js → annotate.js} +0 -0
- package/{cjs/annotate.js.flow → annotate.js.flow} +0 -0
- package/{es/annotate.js → annotate.mjs} +0 -0
- package/format.d.ts +6 -0
- package/{cjs/format/inline.js → format.js} +7 -2
- package/{cjs/format/inline.js.flow → format.js.flow} +9 -3
- package/{es/format/inline.js → format.mjs} +5 -2
- package/index.d.ts +40 -0
- package/index.js +89 -0
- package/index.js.flow +44 -0
- package/index.mjs +11 -0
- package/{ts → lib}/_helpers.d.ts +0 -0
- package/lib/arrays.d.ts +59 -0
- package/lib/arrays.js +139 -0
- package/lib/arrays.js.flow +138 -0
- package/lib/arrays.mjs +124 -0
- package/lib/basics.d.ts +93 -0
- package/lib/basics.js +144 -0
- package/lib/basics.js.flow +124 -0
- package/lib/basics.mjs +120 -0
- package/lib/booleans.d.ts +16 -0
- package/lib/booleans.js +35 -0
- package/lib/booleans.js.flow +22 -0
- package/lib/booleans.mjs +25 -0
- package/lib/dates.d.ts +15 -0
- package/lib/dates.js +44 -0
- package/lib/dates.js.flow +40 -0
- package/lib/dates.mjs +34 -0
- package/lib/json.d.ts +35 -0
- package/lib/json.js +55 -0
- package/lib/json.js.flow +50 -0
- package/lib/json.mjs +40 -0
- package/lib/numbers.d.ts +31 -0
- package/lib/numbers.js +51 -0
- package/lib/numbers.js.flow +48 -0
- package/lib/numbers.mjs +41 -0
- package/lib/objects.d.ts +75 -0
- package/lib/objects.js +240 -0
- package/lib/objects.js.flow +246 -0
- package/lib/objects.mjs +223 -0
- package/lib/strings.d.ts +56 -0
- package/lib/strings.js +101 -0
- package/lib/strings.js.flow +90 -0
- package/lib/strings.mjs +82 -0
- package/lib/unions.d.ts +55 -0
- package/lib/unions.js +160 -0
- package/lib/unions.js.flow +155 -0
- package/lib/unions.mjs +146 -0
- package/lib/utilities.d.ts +34 -0
- package/lib/utilities.js +75 -0
- package/lib/utilities.js.flow +65 -0
- package/lib/utilities.mjs +60 -0
- package/package.json +79 -29
- package/result.d.ts +16 -0
- package/result.js +34 -0
- package/result.js.flow +26 -0
- package/result.mjs +27 -0
- package/cjs/_guard.js +0 -26
- package/cjs/_guard.js.flow +0 -20
- package/cjs/_types.js +0 -1
- package/cjs/_types.js.flow +0 -20
- package/cjs/format/index.js +0 -12
- package/cjs/format/index.js.flow +0 -4
- package/cjs/format/short.js +0 -10
- package/cjs/format/short.js.flow +0 -8
- package/cjs/index.js +0 -120
- package/cjs/index.js.flow +0 -63
- package/cjs/result.js +0 -172
- package/cjs/result.js.flow +0 -166
- package/cjs/stdlib/array.js +0 -108
- package/cjs/stdlib/array.js.flow +0 -103
- package/cjs/stdlib/boolean.js +0 -44
- package/cjs/stdlib/boolean.js.flow +0 -29
- package/cjs/stdlib/composition.js +0 -56
- package/cjs/stdlib/composition.js.flow +0 -43
- package/cjs/stdlib/constants.js +0 -69
- package/cjs/stdlib/constants.js.flow +0 -46
- package/cjs/stdlib/date.js +0 -46
- package/cjs/stdlib/date.js.flow +0 -40
- package/cjs/stdlib/describe.js +0 -26
- package/cjs/stdlib/describe.js.flow +0 -17
- package/cjs/stdlib/dispatch.js +0 -62
- package/cjs/stdlib/dispatch.js.flow +0 -58
- package/cjs/stdlib/either.js +0 -117
- package/cjs/stdlib/either.js.flow +0 -151
- package/cjs/stdlib/fail.js +0 -21
- package/cjs/stdlib/fail.js.flow +0 -12
- package/cjs/stdlib/instanceOf.js +0 -19
- package/cjs/stdlib/instanceOf.js.flow +0 -20
- package/cjs/stdlib/json.js +0 -31
- package/cjs/stdlib/json.js.flow +0 -28
- package/cjs/stdlib/lazy.js +0 -16
- package/cjs/stdlib/lazy.js.flow +0 -15
- package/cjs/stdlib/mapping.js +0 -67
- package/cjs/stdlib/mapping.js.flow +0 -54
- package/cjs/stdlib/number.js +0 -40
- package/cjs/stdlib/number.js.flow +0 -34
- package/cjs/stdlib/object.js +0 -194
- package/cjs/stdlib/object.js.flow +0 -203
- package/cjs/stdlib/optional.js +0 -54
- package/cjs/stdlib/optional.js.flow +0 -41
- package/cjs/stdlib/string.js +0 -98
- package/cjs/stdlib/string.js.flow +0 -82
- package/cjs/stdlib/tuple.js +0 -173
- package/cjs/stdlib/tuple.js.flow +0 -220
- package/es/_guard.js +0 -15
- package/es/_types.js +0 -0
- package/es/format/index.js +0 -2
- package/es/format/short.js +0 -4
- package/es/index.js +0 -37
- package/es/result.js +0 -139
- package/es/stdlib/array.js +0 -91
- package/es/stdlib/boolean.js +0 -28
- package/es/stdlib/composition.js +0 -42
- package/es/stdlib/constants.js +0 -46
- package/es/stdlib/date.js +0 -28
- package/es/stdlib/describe.js +0 -16
- package/es/stdlib/dispatch.js +0 -51
- package/es/stdlib/either.js +0 -90
- package/es/stdlib/fail.js +0 -11
- package/es/stdlib/instanceOf.js +0 -8
- package/es/stdlib/json.js +0 -15
- package/es/stdlib/lazy.js +0 -11
- package/es/stdlib/mapping.js +0 -54
- package/es/stdlib/number.js +0 -25
- package/es/stdlib/object.js +0 -175
- package/es/stdlib/optional.js +0 -38
- package/es/stdlib/string.js +0 -76
- package/es/stdlib/tuple.js +0 -155
- package/ts/_guard.d.ts +0 -7
- package/ts/_types.d.ts +0 -16
- package/ts/_utils.d.ts +0 -13
- package/ts/array.d.ts +0 -5
- package/ts/boolean.d.ts +0 -5
- package/ts/constants.d.ts +0 -11
- package/ts/date.d.ts +0 -4
- package/ts/describe.d.ts +0 -3
- package/ts/dispatch.d.ts +0 -8
- package/ts/either.d.ts +0 -61
- package/ts/fail.d.ts +0 -3
- package/ts/index.d.ts +0 -42
- package/ts/inline.d.ts +0 -3
- package/ts/instanceOf.d.ts +0 -3
- package/ts/json.d.ts +0 -11
- package/ts/lazy.d.ts +0 -3
- package/ts/mapping.d.ts +0 -4
- package/ts/number.d.ts +0 -6
- package/ts/object.d.ts +0 -33
- package/ts/optional.d.ts +0 -5
- package/ts/result.d.ts +0 -39
- package/ts/short.d.ts +0 -3
- package/ts/string.d.ts +0 -7
- package/ts/tuple.d.ts +0 -30
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @flow strict
|
|
2
|
+
|
|
3
|
+
import { define } from '../Decoder';
|
|
4
|
+
import { either } from './unions';
|
|
5
|
+
import { instanceOf } from './utilities';
|
|
6
|
+
import type { Decoder } from '../Decoder';
|
|
7
|
+
|
|
8
|
+
/** Match groups in this regex:
|
|
9
|
+
* \1 - the scheme
|
|
10
|
+
* \2 - the username/password (optional)
|
|
11
|
+
* \3 - the host
|
|
12
|
+
* \4 - the port (optional)
|
|
13
|
+
* \5 - the path (optional)
|
|
14
|
+
*/
|
|
15
|
+
const url_re =
|
|
16
|
+
/^([A-Za-z]{3,9}(?:[+][A-Za-z]{3,9})?):\/\/(?:([-;:&=+$,\w]+)@)?(?:([A-Za-z0-9.-]+)(?::([0-9]{2,5}))?)(\/(?:[-+~%/.,\w]*)?(?:\?[-+=&;%@.,\w]*)?(?:#[.,!/\w]*)?)?$/;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Accepts and returns strings.
|
|
20
|
+
*/
|
|
21
|
+
export const string: Decoder<string> = define((blob, ok, err) =>
|
|
22
|
+
typeof blob === 'string' ? ok(blob) : err('Must be string'),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Like `string`, but will reject the empty string or strings containing only whitespace.
|
|
27
|
+
*/
|
|
28
|
+
export const nonEmptyString: Decoder<string> = regex(/\S/, 'Must be non-empty string');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Accepts and returns strings that match the given regular expression.
|
|
32
|
+
*/
|
|
33
|
+
export function regex(regex: RegExp, msg: string): Decoder<string> {
|
|
34
|
+
return string.refine((s) => regex.test(s), msg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Accepts and returns strings that are syntactically valid email addresses.
|
|
39
|
+
* (This will not mean that the email address actually exist.)
|
|
40
|
+
*/
|
|
41
|
+
export const email: Decoder<string> = regex(
|
|
42
|
+
// The almost perfect email regex, taken from https://emailregex.com/
|
|
43
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
|
44
|
+
'Must be email',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Accepts strings that are valid URLs, returns the value as a URL instance.
|
|
49
|
+
*/
|
|
50
|
+
export const url: Decoder<URL> = either(
|
|
51
|
+
regex(url_re, 'Must be URL').transform((value) => new URL(value)),
|
|
52
|
+
instanceOf(URL),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Accepts strings that are valid URLs, but only HTTPS ones. Returns the value
|
|
57
|
+
* as a URL instance.
|
|
58
|
+
*/
|
|
59
|
+
export const httpsUrl: Decoder<URL> = url.refine(
|
|
60
|
+
(value) => value.protocol === 'https:',
|
|
61
|
+
'Must be an HTTPS URL',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Accepts strings that are valid
|
|
66
|
+
* [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
|
|
67
|
+
* (universally unique identifier).
|
|
68
|
+
*/
|
|
69
|
+
export const uuid: Decoder<string> = regex(
|
|
70
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
71
|
+
'Must be uuid',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Like `uuid`, but only accepts
|
|
76
|
+
* [UUIDv1](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_%28date-time_and_MAC_address%29)
|
|
77
|
+
* strings.
|
|
78
|
+
*/
|
|
79
|
+
export const uuidv1: Decoder<string> =
|
|
80
|
+
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address)
|
|
81
|
+
uuid.refine((value) => value[14] === '1', 'Must be uuidv1');
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Like `uuid`, but only accepts
|
|
85
|
+
* [UUIDv4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_%28random%29)
|
|
86
|
+
* strings.
|
|
87
|
+
*/
|
|
88
|
+
export const uuidv4: Decoder<string> =
|
|
89
|
+
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)
|
|
90
|
+
uuid.refine((value) => value[14] === '4', 'Must be uuidv4');
|
package/lib/strings.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { define } from '../Decoder.mjs';
|
|
2
|
+
import { either } from './unions.mjs';
|
|
3
|
+
import { instanceOf } from './utilities.mjs';
|
|
4
|
+
|
|
5
|
+
/** Match groups in this regex:
|
|
6
|
+
* \1 - the scheme
|
|
7
|
+
* \2 - the username/password (optional)
|
|
8
|
+
* \3 - the host
|
|
9
|
+
* \4 - the port (optional)
|
|
10
|
+
* \5 - the path (optional)
|
|
11
|
+
*/
|
|
12
|
+
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]*)?)?$/;
|
|
13
|
+
/**
|
|
14
|
+
* Accepts and returns strings.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export var string = define(function (blob, ok, err) {
|
|
18
|
+
return typeof blob === 'string' ? ok(blob) : err('Must be string');
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Like `string`, but will reject the empty string or strings containing only whitespace.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export var nonEmptyString = regex(/\S/, 'Must be non-empty string');
|
|
25
|
+
/**
|
|
26
|
+
* Accepts and returns strings that match the given regular expression.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export function regex(regex, msg) {
|
|
30
|
+
return string.refine(function (s) {
|
|
31
|
+
return regex.test(s);
|
|
32
|
+
}, msg);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Accepts and returns strings that are syntactically valid email addresses.
|
|
36
|
+
* (This will not mean that the email address actually exist.)
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export var email = regex( // The almost perfect email regex, taken from https://emailregex.com/
|
|
40
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 'Must be email');
|
|
41
|
+
/**
|
|
42
|
+
* Accepts strings that are valid URLs, returns the value as a URL instance.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export var url = either(regex(url_re, 'Must be URL').transform(function (value) {
|
|
46
|
+
return new URL(value);
|
|
47
|
+
}), instanceOf(URL));
|
|
48
|
+
/**
|
|
49
|
+
* Accepts strings that are valid URLs, but only HTTPS ones. Returns the value
|
|
50
|
+
* as a URL instance.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
export var httpsUrl = url.refine(function (value) {
|
|
54
|
+
return value.protocol === 'https:';
|
|
55
|
+
}, 'Must be an HTTPS URL');
|
|
56
|
+
/**
|
|
57
|
+
* Accepts strings that are valid
|
|
58
|
+
* [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
|
|
59
|
+
* (universally unique identifier).
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
export var uuid = regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 'Must be uuid');
|
|
63
|
+
/**
|
|
64
|
+
* Like `uuid`, but only accepts
|
|
65
|
+
* [UUIDv1](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_%28date-time_and_MAC_address%29)
|
|
66
|
+
* strings.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
export var uuidv1 = // https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address)
|
|
70
|
+
uuid.refine(function (value) {
|
|
71
|
+
return value[14] === '1';
|
|
72
|
+
}, 'Must be uuidv1');
|
|
73
|
+
/**
|
|
74
|
+
* Like `uuid`, but only accepts
|
|
75
|
+
* [UUIDv4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_%28random%29)
|
|
76
|
+
* strings.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
export var uuidv4 = // https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)
|
|
80
|
+
uuid.refine(function (value) {
|
|
81
|
+
return value[14] === '4';
|
|
82
|
+
}, 'Must be uuidv4');
|
package/lib/unions.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Decoder, DecoderType, Scalar } from '../Decoder';
|
|
2
|
+
|
|
3
|
+
export type Values<T extends object> = T[keyof T];
|
|
4
|
+
|
|
5
|
+
export type DecoderTypes<T> = T extends ReadonlyArray<Decoder<infer U>> ? U : never;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Accepts values accepted by any of the given decoders.
|
|
9
|
+
*
|
|
10
|
+
* The decoders are tried on the input one by one, in the given order. The
|
|
11
|
+
* first one that accepts the input "wins". If all decoders reject the input,
|
|
12
|
+
* the input gets rejected.
|
|
13
|
+
*/
|
|
14
|
+
export function either<T extends ReadonlyArray<Decoder<any>>>(
|
|
15
|
+
...args: T
|
|
16
|
+
): Decoder<DecoderTypes<T>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Accepts any value that is strictly-equal (using `===`) to one of the
|
|
20
|
+
* specified values.
|
|
21
|
+
*/
|
|
22
|
+
export function oneOf<T extends Scalar>(constants: readonly T[]): Decoder<T>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* If you are decoding tagged unions you may want to use the `taggedUnion()`
|
|
26
|
+
* decoder instead of the general purpose `either()` decoder to get better
|
|
27
|
+
* error messages and better performance.
|
|
28
|
+
*
|
|
29
|
+
* This decoder is optimized for [tagged
|
|
30
|
+
* unions](https://en.wikipedia.org/wiki/Tagged_union), i.e. a union of
|
|
31
|
+
* objects where one field is used as the discriminator.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const A = object({ tag: constant('A'), foo: string });
|
|
35
|
+
* const B = object({ tag: constant('B'), bar: number });
|
|
36
|
+
*
|
|
37
|
+
* const AorB = taggedUnion('tag', { A, B });
|
|
38
|
+
* // ^^^
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Decoding now works in two steps:
|
|
42
|
+
*
|
|
43
|
+
* 1. Look at the `'tag'` field in the incoming object (this is the field
|
|
44
|
+
* that decides which decoder will be used)
|
|
45
|
+
* 2. If the value is `'A'`, then decoder `A` will be used. If it's `'B'`, then
|
|
46
|
+
* decoder `B` will be used. Otherwise, this will fail.
|
|
47
|
+
*
|
|
48
|
+
* This is effectively equivalent to `either(A, B)`, but will provide better
|
|
49
|
+
* error messages and is more performant at runtime because it doesn't have to
|
|
50
|
+
* try all decoders one by one.
|
|
51
|
+
*/
|
|
52
|
+
export function taggedUnion<O extends { [key: string]: Decoder<any> }>(
|
|
53
|
+
field: string,
|
|
54
|
+
mapping: O,
|
|
55
|
+
): Decoder<Values<{ [key in keyof O]: DecoderType<O[key]> }>>;
|
package/lib/unions.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.either = void 0;
|
|
5
|
+
exports.oneOf = oneOf;
|
|
6
|
+
exports.taggedUnion = taggedUnion;
|
|
7
|
+
|
|
8
|
+
var _Decoder = require("../Decoder");
|
|
9
|
+
|
|
10
|
+
var _utils = require("../_utils");
|
|
11
|
+
|
|
12
|
+
var _objects = require("./objects");
|
|
13
|
+
|
|
14
|
+
var _utilities = require("./utilities");
|
|
15
|
+
|
|
16
|
+
var EITHER_PREFIX = 'Either:\n';
|
|
17
|
+
/**
|
|
18
|
+
* Indents and adds a dash in front of this (potentially multiline) string.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function itemize(s) {
|
|
22
|
+
return '-' + (0, _utils.indent)(s).substring(1);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Nests another error as an item under a new-to-be-created "Either error". If
|
|
26
|
+
* the given subitem already is an "Either error" of itself, don't indent, but
|
|
27
|
+
* just "inject" its items at the same error level, for nicely flattened either
|
|
28
|
+
* expressions.
|
|
29
|
+
*
|
|
30
|
+
* Avoids:
|
|
31
|
+
*
|
|
32
|
+
* Either:
|
|
33
|
+
* - Either:
|
|
34
|
+
* - Must be P
|
|
35
|
+
* - Either:
|
|
36
|
+
* - Must be Q
|
|
37
|
+
* - Must be R
|
|
38
|
+
* - Must be S
|
|
39
|
+
*
|
|
40
|
+
* And "flattens" these to:
|
|
41
|
+
*
|
|
42
|
+
* Either:
|
|
43
|
+
* - Must be P
|
|
44
|
+
* - Must be Q
|
|
45
|
+
* - Must be R
|
|
46
|
+
* - Must be S
|
|
47
|
+
*
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
function nest(errText) {
|
|
52
|
+
return errText.startsWith(EITHER_PREFIX) ? errText.substr(EITHER_PREFIX.length) : itemize(errText);
|
|
53
|
+
} // prettier-ignore
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
function _either() {
|
|
57
|
+
for (var _len = arguments.length, decoders = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
58
|
+
decoders[_key] = arguments[_key];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (decoders.length === 0) {
|
|
62
|
+
throw new Error('Pass at least one decoder to either()');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (0, _Decoder.define)(function (blob, _, err) {
|
|
66
|
+
// Collect errors here along the way
|
|
67
|
+
var errors = [];
|
|
68
|
+
|
|
69
|
+
for (var _i = 0; _i < decoders.length; _i++) {
|
|
70
|
+
var result = decoders[_i].decode(blob);
|
|
71
|
+
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
return result;
|
|
74
|
+
} else {
|
|
75
|
+
errors.push(result.error);
|
|
76
|
+
}
|
|
77
|
+
} // Decoding all alternatives failed, return the combined error message
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
var text = EITHER_PREFIX + errors.map(function (err) {
|
|
81
|
+
return nest((0, _utils.summarize)(err).join('\n'));
|
|
82
|
+
}).join('\n');
|
|
83
|
+
return err(text);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Accepts values accepted by any of the given decoders.
|
|
88
|
+
*
|
|
89
|
+
* The decoders are tried on the input one by one, in the given order. The
|
|
90
|
+
* first one that accepts the input "wins". If all decoders reject the input,
|
|
91
|
+
* the input gets rejected.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
var either = _either;
|
|
96
|
+
/**
|
|
97
|
+
* Accepts any value that is strictly-equal (using `===`) to one of the
|
|
98
|
+
* specified values.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
exports.either = either;
|
|
102
|
+
|
|
103
|
+
function oneOf(constants) {
|
|
104
|
+
return (0, _Decoder.define)(function (blob, ok, err) {
|
|
105
|
+
var winner = constants.find(function (c) {
|
|
106
|
+
return c === blob;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (winner !== undefined) {
|
|
110
|
+
return ok(winner);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return err("Must be one of " + constants.map(function (value) {
|
|
114
|
+
return JSON.stringify(value);
|
|
115
|
+
}).join(', '));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* If you are decoding tagged unions you may want to use the `taggedUnion()`
|
|
120
|
+
* decoder instead of the general purpose `either()` decoder to get better
|
|
121
|
+
* error messages and better performance.
|
|
122
|
+
*
|
|
123
|
+
* This decoder is optimized for [tagged
|
|
124
|
+
* unions](https://en.wikipedia.org/wiki/Tagged_union), i.e. a union of
|
|
125
|
+
* objects where one field is used as the discriminator.
|
|
126
|
+
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* const A = object({ tag: constant('A'), foo: string });
|
|
129
|
+
* const B = object({ tag: constant('B'), bar: number });
|
|
130
|
+
*
|
|
131
|
+
* const AorB = taggedUnion('tag', { A, B });
|
|
132
|
+
* // ^^^
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* Decoding now works in two steps:
|
|
136
|
+
*
|
|
137
|
+
* 1. Look at the `'tag'` field in the incoming object (this is the field
|
|
138
|
+
* that decides which decoder will be used)
|
|
139
|
+
* 2. If the value is `'A'`, then decoder `A` will be used. If it's `'B'`, then
|
|
140
|
+
* decoder `B` will be used. Otherwise, this will fail.
|
|
141
|
+
*
|
|
142
|
+
* This is effectively equivalent to `either(A, B)`, but will provide better
|
|
143
|
+
* error messages and is more performant at runtime because it doesn't have to
|
|
144
|
+
* try all decoders one by one.
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
function taggedUnion(field, mapping) {
|
|
149
|
+
var _object;
|
|
150
|
+
|
|
151
|
+
var base = (0, _objects.object)((_object = {}, _object[field] = (0, _utilities.prep)(String, oneOf(Object.keys(mapping))), _object)).transform(function (o) {
|
|
152
|
+
return o[field];
|
|
153
|
+
});
|
|
154
|
+
return base.peek_UNSTABLE(function (_ref) {
|
|
155
|
+
var blob = _ref[0],
|
|
156
|
+
key = _ref[1];
|
|
157
|
+
var decoder = mapping[key];
|
|
158
|
+
return decoder.decode(blob);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @flow strict
|
|
2
|
+
|
|
3
|
+
import { define } from '../Decoder';
|
|
4
|
+
import { indent, summarize } from '../_utils';
|
|
5
|
+
import { object } from './objects';
|
|
6
|
+
import { prep } from './utilities';
|
|
7
|
+
import type { _Any } from '../_utils';
|
|
8
|
+
import type { Decoder, DecodeResult, Scalar } from '../Decoder';
|
|
9
|
+
|
|
10
|
+
const EITHER_PREFIX = 'Either:\n';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Indents and adds a dash in front of this (potentially multiline) string.
|
|
14
|
+
*/
|
|
15
|
+
function itemize(s: string): string {
|
|
16
|
+
return '-' + indent(s).substring(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Nests another error as an item under a new-to-be-created "Either error". If
|
|
21
|
+
* the given subitem already is an "Either error" of itself, don't indent, but
|
|
22
|
+
* just "inject" its items at the same error level, for nicely flattened either
|
|
23
|
+
* expressions.
|
|
24
|
+
*
|
|
25
|
+
* Avoids:
|
|
26
|
+
*
|
|
27
|
+
* Either:
|
|
28
|
+
* - Either:
|
|
29
|
+
* - Must be P
|
|
30
|
+
* - Either:
|
|
31
|
+
* - Must be Q
|
|
32
|
+
* - Must be R
|
|
33
|
+
* - Must be S
|
|
34
|
+
*
|
|
35
|
+
* And "flattens" these to:
|
|
36
|
+
*
|
|
37
|
+
* Either:
|
|
38
|
+
* - Must be P
|
|
39
|
+
* - Must be Q
|
|
40
|
+
* - Must be R
|
|
41
|
+
* - Must be S
|
|
42
|
+
*
|
|
43
|
+
*/
|
|
44
|
+
function nest(errText: string): string {
|
|
45
|
+
return errText.startsWith(EITHER_PREFIX)
|
|
46
|
+
? errText.substr(EITHER_PREFIX.length)
|
|
47
|
+
: itemize(errText);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// prettier-ignore
|
|
51
|
+
interface EitherT {
|
|
52
|
+
<A>(a: Decoder<A>): Decoder<A>;
|
|
53
|
+
<A, B>(a: Decoder<A>, b: Decoder<B>): Decoder<A | B>;
|
|
54
|
+
<A, B, C>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>): Decoder<A | B | C>;
|
|
55
|
+
<A, B, C, D>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>): Decoder<A | B | C | D>;
|
|
56
|
+
<A, B, C, D, E>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>): Decoder<A | B | C | D | E>;
|
|
57
|
+
<A, B, C, D, E, F>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>): Decoder<A | B | C | D | E | F>;
|
|
58
|
+
<A, B, C, D, E, F, G>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>): Decoder<A | B | C | D | E | F | G>;
|
|
59
|
+
<A, B, C, D, E, F, G, H>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>, h: Decoder<H>): Decoder<A | B | C | D | E | F | G | H>;
|
|
60
|
+
<A, B, C, D, E, F, G, H, I>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>, h: Decoder<H>, i: Decoder<I>): Decoder<A | B | C | D | E | F | G | H | I>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _either(...decoders: $ReadOnlyArray<Decoder<mixed>>): Decoder<mixed> {
|
|
64
|
+
if (decoders.length === 0) {
|
|
65
|
+
throw new Error('Pass at least one decoder to either()');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return define((blob, _, err) => {
|
|
69
|
+
// Collect errors here along the way
|
|
70
|
+
const errors = [];
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < decoders.length; i++) {
|
|
73
|
+
const result: DecodeResult<mixed> = decoders[i].decode(blob);
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
return result;
|
|
76
|
+
} else {
|
|
77
|
+
errors.push(result.error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Decoding all alternatives failed, return the combined error message
|
|
82
|
+
const text =
|
|
83
|
+
EITHER_PREFIX +
|
|
84
|
+
errors.map((err) => nest(summarize(err).join('\n'))).join('\n');
|
|
85
|
+
return err(text);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Accepts values accepted by any of the given decoders.
|
|
91
|
+
*
|
|
92
|
+
* The decoders are tried on the input one by one, in the given order. The
|
|
93
|
+
* first one that accepts the input "wins". If all decoders reject the input,
|
|
94
|
+
* the input gets rejected.
|
|
95
|
+
*/
|
|
96
|
+
export const either: EitherT = (_either: _Any);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Accepts any value that is strictly-equal (using `===`) to one of the
|
|
100
|
+
* specified values.
|
|
101
|
+
*/
|
|
102
|
+
export function oneOf<T: Scalar>(constants: $ReadOnlyArray<T>): Decoder<T> {
|
|
103
|
+
return define((blob, ok, err) => {
|
|
104
|
+
const winner = constants.find((c) => c === blob);
|
|
105
|
+
if (winner !== undefined) {
|
|
106
|
+
return ok(winner);
|
|
107
|
+
}
|
|
108
|
+
return err(
|
|
109
|
+
`Must be one of ${constants
|
|
110
|
+
.map((value) => JSON.stringify(value))
|
|
111
|
+
.join(', ')}`,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* If you are decoding tagged unions you may want to use the `taggedUnion()`
|
|
118
|
+
* decoder instead of the general purpose `either()` decoder to get better
|
|
119
|
+
* error messages and better performance.
|
|
120
|
+
*
|
|
121
|
+
* This decoder is optimized for [tagged
|
|
122
|
+
* unions](https://en.wikipedia.org/wiki/Tagged_union), i.e. a union of
|
|
123
|
+
* objects where one field is used as the discriminator.
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* const A = object({ tag: constant('A'), foo: string });
|
|
127
|
+
* const B = object({ tag: constant('B'), bar: number });
|
|
128
|
+
*
|
|
129
|
+
* const AorB = taggedUnion('tag', { A, B });
|
|
130
|
+
* // ^^^
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* Decoding now works in two steps:
|
|
134
|
+
*
|
|
135
|
+
* 1. Look at the `'tag'` field in the incoming object (this is the field
|
|
136
|
+
* that decides which decoder will be used)
|
|
137
|
+
* 2. If the value is `'A'`, then decoder `A` will be used. If it's `'B'`, then
|
|
138
|
+
* decoder `B` will be used. Otherwise, this will fail.
|
|
139
|
+
*
|
|
140
|
+
* This is effectively equivalent to `either(A, B)`, but will provide better
|
|
141
|
+
* error messages and is more performant at runtime because it doesn't have to
|
|
142
|
+
* try all decoders one by one.
|
|
143
|
+
*/
|
|
144
|
+
export function taggedUnion<O: { +[field: string]: Decoder<_Any>, ... }>(
|
|
145
|
+
field: string,
|
|
146
|
+
mapping: O,
|
|
147
|
+
): Decoder<$Values<$ObjMap<O, <T>(Decoder<T>) => T>>> {
|
|
148
|
+
const base: Decoder<string> = object({
|
|
149
|
+
[field]: prep(String, oneOf(Object.keys(mapping))),
|
|
150
|
+
}).transform((o) => o[field]);
|
|
151
|
+
return base.peek_UNSTABLE(([blob, key]) => {
|
|
152
|
+
const decoder = mapping[key];
|
|
153
|
+
return decoder.decode(blob);
|
|
154
|
+
});
|
|
155
|
+
}
|
package/lib/unions.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { define } from '../Decoder.mjs';
|
|
2
|
+
import { indent, summarize } from '../_utils.mjs';
|
|
3
|
+
import { object } from './objects.mjs';
|
|
4
|
+
import { prep } from './utilities.mjs';
|
|
5
|
+
var EITHER_PREFIX = 'Either:\n';
|
|
6
|
+
/**
|
|
7
|
+
* Indents and adds a dash in front of this (potentially multiline) string.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function itemize(s) {
|
|
11
|
+
return '-' + indent(s).substring(1);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Nests another error as an item under a new-to-be-created "Either error". If
|
|
15
|
+
* the given subitem already is an "Either error" of itself, don't indent, but
|
|
16
|
+
* just "inject" its items at the same error level, for nicely flattened either
|
|
17
|
+
* expressions.
|
|
18
|
+
*
|
|
19
|
+
* Avoids:
|
|
20
|
+
*
|
|
21
|
+
* Either:
|
|
22
|
+
* - Either:
|
|
23
|
+
* - Must be P
|
|
24
|
+
* - Either:
|
|
25
|
+
* - Must be Q
|
|
26
|
+
* - Must be R
|
|
27
|
+
* - Must be S
|
|
28
|
+
*
|
|
29
|
+
* And "flattens" these to:
|
|
30
|
+
*
|
|
31
|
+
* Either:
|
|
32
|
+
* - Must be P
|
|
33
|
+
* - Must be Q
|
|
34
|
+
* - Must be R
|
|
35
|
+
* - Must be S
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
function nest(errText) {
|
|
41
|
+
return errText.startsWith(EITHER_PREFIX) ? errText.substr(EITHER_PREFIX.length) : itemize(errText);
|
|
42
|
+
} // prettier-ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
function _either() {
|
|
46
|
+
for (var _len = arguments.length, decoders = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
47
|
+
decoders[_key] = arguments[_key];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (decoders.length === 0) {
|
|
51
|
+
throw new Error('Pass at least one decoder to either()');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return define(function (blob, _, err) {
|
|
55
|
+
// Collect errors here along the way
|
|
56
|
+
var errors = [];
|
|
57
|
+
|
|
58
|
+
for (var _i = 0; _i < decoders.length; _i++) {
|
|
59
|
+
var result = decoders[_i].decode(blob);
|
|
60
|
+
|
|
61
|
+
if (result.ok) {
|
|
62
|
+
return result;
|
|
63
|
+
} else {
|
|
64
|
+
errors.push(result.error);
|
|
65
|
+
}
|
|
66
|
+
} // Decoding all alternatives failed, return the combined error message
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
var text = EITHER_PREFIX + errors.map(function (err) {
|
|
70
|
+
return nest(summarize(err).join('\n'));
|
|
71
|
+
}).join('\n');
|
|
72
|
+
return err(text);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Accepts values accepted by any of the given decoders.
|
|
77
|
+
*
|
|
78
|
+
* The decoders are tried on the input one by one, in the given order. The
|
|
79
|
+
* first one that accepts the input "wins". If all decoders reject the input,
|
|
80
|
+
* the input gets rejected.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
export var either = _either;
|
|
85
|
+
/**
|
|
86
|
+
* Accepts any value that is strictly-equal (using `===`) to one of the
|
|
87
|
+
* specified values.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
export function oneOf(constants) {
|
|
91
|
+
return define(function (blob, ok, err) {
|
|
92
|
+
var winner = constants.find(function (c) {
|
|
93
|
+
return c === blob;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (winner !== undefined) {
|
|
97
|
+
return ok(winner);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return err("Must be one of " + constants.map(function (value) {
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
}).join(', '));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* If you are decoding tagged unions you may want to use the `taggedUnion()`
|
|
107
|
+
* decoder instead of the general purpose `either()` decoder to get better
|
|
108
|
+
* error messages and better performance.
|
|
109
|
+
*
|
|
110
|
+
* This decoder is optimized for [tagged
|
|
111
|
+
* unions](https://en.wikipedia.org/wiki/Tagged_union), i.e. a union of
|
|
112
|
+
* objects where one field is used as the discriminator.
|
|
113
|
+
*
|
|
114
|
+
* ```ts
|
|
115
|
+
* const A = object({ tag: constant('A'), foo: string });
|
|
116
|
+
* const B = object({ tag: constant('B'), bar: number });
|
|
117
|
+
*
|
|
118
|
+
* const AorB = taggedUnion('tag', { A, B });
|
|
119
|
+
* // ^^^
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* Decoding now works in two steps:
|
|
123
|
+
*
|
|
124
|
+
* 1. Look at the `'tag'` field in the incoming object (this is the field
|
|
125
|
+
* that decides which decoder will be used)
|
|
126
|
+
* 2. If the value is `'A'`, then decoder `A` will be used. If it's `'B'`, then
|
|
127
|
+
* decoder `B` will be used. Otherwise, this will fail.
|
|
128
|
+
*
|
|
129
|
+
* This is effectively equivalent to `either(A, B)`, but will provide better
|
|
130
|
+
* error messages and is more performant at runtime because it doesn't have to
|
|
131
|
+
* try all decoders one by one.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
export function taggedUnion(field, mapping) {
|
|
135
|
+
var _object;
|
|
136
|
+
|
|
137
|
+
var base = object((_object = {}, _object[field] = prep(String, oneOf(Object.keys(mapping))), _object)).transform(function (o) {
|
|
138
|
+
return o[field];
|
|
139
|
+
});
|
|
140
|
+
return base.peek_UNSTABLE(function (_ref) {
|
|
141
|
+
var blob = _ref[0],
|
|
142
|
+
key = _ref[1];
|
|
143
|
+
var decoder = mapping[key];
|
|
144
|
+
return decoder.decode(blob);
|
|
145
|
+
});
|
|
146
|
+
}
|