decoders 2.0.0-beta9 → 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.
- package/CHANGELOG.md +56 -29
- package/Decoder.d.ts +94 -0
- package/Decoder.js +222 -0
- package/Decoder.js.flow +286 -0
- package/Decoder.mjs +215 -0
- package/README.md +122 -1507
- package/_utils.d.ts +0 -1
- package/_utils.js +11 -17
- package/_utils.js.flow +13 -17
- package/_utils.mjs +10 -14
- package/format.d.ts +4 -2
- package/format.js +1 -1
- package/format.js.flow +3 -1
- package/format.mjs +1 -1
- package/index.d.ts +29 -31
- package/index.js +62 -84
- package/index.js.flow +30 -48
- package/index.mjs +11 -36
- package/{core → 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/{core/object.js → lib/objects.js} +78 -85
- package/{core/object.js.flow → lib/objects.js.flow} +89 -102
- package/{core/object.mjs → lib/objects.mjs} +77 -82
- 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/{core/either.js.flow → lib/unions.js.flow} +67 -17
- 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 +64 -17
- package/result.d.ts +0 -23
- package/result.js +0 -68
- package/result.js.flow +0 -72
- package/result.mjs +0 -54
- package/_guard.d.ts +0 -7
- package/_guard.js +0 -22
- package/_guard.js.flow +0 -20
- package/_guard.mjs +0 -15
- package/_types.d.ts +0 -13
- package/_types.js +0 -1
- package/_types.js.flow +0 -20
- package/_types.mjs +0 -0
- package/core/array.d.ts +0 -8
- package/core/array.js +0 -115
- package/core/array.js.flow +0 -107
- package/core/array.mjs +0 -100
- package/core/boolean.d.ts +0 -5
- package/core/boolean.js +0 -40
- package/core/boolean.js.flow +0 -27
- package/core/boolean.mjs +0 -28
- package/core/composition.d.ts +0 -18
- package/core/composition.js +0 -82
- package/core/composition.js.flow +0 -74
- package/core/composition.mjs +0 -70
- package/core/constants.d.ts +0 -11
- package/core/constants.js +0 -65
- package/core/constants.js.flow +0 -44
- package/core/constants.mjs +0 -46
- package/core/date.d.ts +0 -4
- package/core/date.js +0 -42
- package/core/date.js.flow +0 -38
- package/core/date.mjs +0 -28
- package/core/describe.d.ts +0 -3
- package/core/describe.js +0 -22
- package/core/describe.js.flow +0 -17
- package/core/describe.mjs +0 -16
- package/core/dispatch.d.ts +0 -8
- package/core/dispatch.js +0 -60
- package/core/dispatch.js.flow +0 -59
- package/core/dispatch.mjs +0 -52
- package/core/either.d.ts +0 -66
- package/core/either.js +0 -101
- package/core/either.mjs +0 -90
- package/core/fail.d.ts +0 -3
- package/core/fail.js +0 -17
- package/core/fail.js.flow +0 -12
- package/core/fail.mjs +0 -11
- package/core/instanceOf.d.ts +0 -3
- package/core/instanceOf.js +0 -15
- package/core/instanceOf.js.flow +0 -20
- package/core/instanceOf.mjs +0 -8
- package/core/json.d.ts +0 -11
- package/core/json.js +0 -31
- package/core/json.js.flow +0 -28
- package/core/json.mjs +0 -15
- package/core/lazy.d.ts +0 -3
- package/core/lazy.js +0 -16
- package/core/lazy.js.flow +0 -15
- package/core/lazy.mjs +0 -11
- package/core/number.d.ts +0 -6
- package/core/number.js +0 -36
- package/core/number.js.flow +0 -40
- package/core/number.mjs +0 -25
- package/core/object.d.ts +0 -38
- package/core/optional.d.ts +0 -5
- package/core/optional.js +0 -50
- package/core/optional.js.flow +0 -41
- package/core/optional.mjs +0 -38
- package/core/string.d.ts +0 -13
- package/core/string.js +0 -80
- package/core/string.js.flow +0 -72
- package/core/string.mjs +0 -58
- package/core/tuple.d.ts +0 -30
- package/core/tuple.js +0 -54
- package/core/tuple.js.flow +0 -51
- package/core/tuple.mjs +0 -45
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
// @flow strict
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { err, ok } from '../result';
|
|
3
|
+
import { annotateObject, merge, updateText } from '../annotate';
|
|
4
|
+
import { define } from '../Decoder';
|
|
6
5
|
import type { Annotation } from '../annotate';
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
// $FlowFixMe[unclear-type] (not really an issue) - deliberate use of `any` - not sure how we should get rid of this
|
|
10
|
-
type AnyDecoder = any;
|
|
11
|
-
|
|
12
|
-
// $FlowFixMe[unclear-type] (not really an issue) - deliberately casting
|
|
13
|
-
type cast = any;
|
|
14
|
-
|
|
15
|
-
function isPojo(o: mixed): boolean %checks {
|
|
16
|
-
return (
|
|
17
|
-
o !== null &&
|
|
18
|
-
o !== undefined &&
|
|
19
|
-
typeof o === 'object' &&
|
|
20
|
-
// This still seems to be the only reliable way to determine whether
|
|
21
|
-
// something is a pojo... ¯\_(ツ)_/¯
|
|
22
|
-
// $FlowFixMe[method-unbinding]
|
|
23
|
-
Object.prototype.toString.call(o) === '[object Object]'
|
|
24
|
-
);
|
|
25
|
-
}
|
|
6
|
+
import type { _Any as AnyDecoder } from '../_utils';
|
|
7
|
+
import type { Decoder, DecodeResult } from '../Decoder';
|
|
26
8
|
|
|
27
9
|
function subtract(xs: Set<string>, ys: Set<string>): Set<string> {
|
|
28
10
|
const result = new Set();
|
|
@@ -34,8 +16,18 @@ function subtract(xs: Set<string>, ys: Set<string>): Set<string> {
|
|
|
34
16
|
return result;
|
|
35
17
|
}
|
|
36
18
|
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Accepts any "plain old JavaScript object", but doesn't validate its keys or
|
|
21
|
+
* values further.
|
|
22
|
+
*/
|
|
23
|
+
export const pojo: Decoder<{| [string]: mixed |}> = define((blob, ok, err) =>
|
|
24
|
+
blob !== null &&
|
|
25
|
+
blob !== undefined &&
|
|
26
|
+
typeof blob === 'object' &&
|
|
27
|
+
// This still seems to be the only reliable way to determine whether
|
|
28
|
+
// something is a pojo... ¯\_(ツ)_/¯
|
|
29
|
+
// $FlowFixMe[method-unbinding]
|
|
30
|
+
Object.prototype.toString.call(blob) === '[object Object]'
|
|
39
31
|
? ok(
|
|
40
32
|
// NOTE:
|
|
41
33
|
// Since Flow 0.98, typeof o === 'object' refines to
|
|
@@ -53,48 +45,35 @@ export const pojo: Decoder<{| [string]: mixed |}> = (blob: mixed) => {
|
|
|
53
45
|
// https://thecodebarbarian.com/object-assign-vs-object-spread.html)
|
|
54
46
|
{ ...blob },
|
|
55
47
|
)
|
|
56
|
-
: err(
|
|
57
|
-
|
|
48
|
+
: err('Must be an object'),
|
|
49
|
+
);
|
|
58
50
|
|
|
59
51
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* For example, given decoders for a number and a string, we can construct an
|
|
63
|
-
* "object description" like so:
|
|
64
|
-
*
|
|
65
|
-
* { id: number, name: string }
|
|
66
|
-
*
|
|
67
|
-
* Which is of type:
|
|
68
|
-
*
|
|
69
|
-
* { id: Decoder<number>, name: Decoder<string> }
|
|
70
|
-
*
|
|
71
|
-
* Passing this to object() will produce the following return type:
|
|
72
|
-
*
|
|
73
|
-
* Decoder<{ id: number, name: string }>
|
|
74
|
-
*
|
|
75
|
-
* Put simply: it'll "peel off" all of the nested Decoders, puts them together
|
|
76
|
-
* in an object, and wraps it in a Decoder<...>.
|
|
52
|
+
* Accepts objects with fields matching the given decoders. Extra fields that
|
|
53
|
+
* exist on the input object are ignored and will not be returned.
|
|
77
54
|
*/
|
|
78
55
|
export function object<O: { +[field: string]: AnyDecoder, ... }>(
|
|
79
|
-
|
|
80
|
-
): Decoder<$ObjMap<O,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
decodersByKey: O,
|
|
57
|
+
): Decoder<$ObjMap<O, <T>(Decoder<T>) => T>> {
|
|
58
|
+
// Compute this set at decoder definition time
|
|
59
|
+
const knownKeys = new Set(Object.keys(decodersByKey));
|
|
60
|
+
|
|
61
|
+
return pojo.then((plainObj, ok, err) => {
|
|
62
|
+
const actualKeys = new Set(Object.keys(plainObj));
|
|
84
63
|
|
|
85
|
-
// At this point, "
|
|
86
|
-
// validly be optional.
|
|
64
|
+
// At this point, "missingKeys" will also include all fields that may
|
|
65
|
+
// validly be optional. We'll let the underlying decoder decide and
|
|
87
66
|
// remove the key from this missing set if the decoder accepts the
|
|
88
67
|
// value.
|
|
89
|
-
const
|
|
68
|
+
const missingKeys = subtract(knownKeys, actualKeys);
|
|
90
69
|
|
|
91
70
|
let record = {};
|
|
92
71
|
let errors: { [key: string]: Annotation } | null = null;
|
|
93
72
|
|
|
94
|
-
Object.keys(
|
|
95
|
-
const decoder =
|
|
96
|
-
const rawValue =
|
|
97
|
-
const result: DecodeResult<mixed> = decoder(rawValue);
|
|
73
|
+
Object.keys(decodersByKey).forEach((key) => {
|
|
74
|
+
const decoder = decodersByKey[key];
|
|
75
|
+
const rawValue = plainObj[key];
|
|
76
|
+
const result: DecodeResult<mixed> = decoder.decode(rawValue);
|
|
98
77
|
|
|
99
78
|
if (result.ok) {
|
|
100
79
|
const value = result.value;
|
|
@@ -104,7 +83,7 @@ export function object<O: { +[field: string]: AnyDecoder, ... }>(
|
|
|
104
83
|
|
|
105
84
|
// If this succeeded, remove the key from the missing keys
|
|
106
85
|
// tracker
|
|
107
|
-
|
|
86
|
+
missingKeys.delete(key);
|
|
108
87
|
} else {
|
|
109
88
|
const ann = result.error;
|
|
110
89
|
|
|
@@ -115,7 +94,7 @@ export function object<O: { +[field: string]: AnyDecoder, ... }>(
|
|
|
115
94
|
// undefined. This covers explicit undefineds to be
|
|
116
95
|
// treated the same as implicit undefineds (aka missing
|
|
117
96
|
// keys).
|
|
118
|
-
|
|
97
|
+
missingKeys.add(key);
|
|
119
98
|
} else {
|
|
120
99
|
if (errors === null) {
|
|
121
100
|
errors = {};
|
|
@@ -129,18 +108,18 @@ export function object<O: { +[field: string]: AnyDecoder, ... }>(
|
|
|
129
108
|
// report. First of all, we want to report any inline errors in this
|
|
130
109
|
// object. Lastly, any fields that are missing should be annotated on
|
|
131
110
|
// the outer object itself.
|
|
132
|
-
if (errors ||
|
|
133
|
-
let objAnn = annotateObject(
|
|
111
|
+
if (errors || missingKeys.size > 0) {
|
|
112
|
+
let objAnn = annotateObject(plainObj);
|
|
134
113
|
|
|
135
114
|
if (errors) {
|
|
136
115
|
objAnn = merge(objAnn, errors);
|
|
137
116
|
}
|
|
138
117
|
|
|
139
|
-
if (
|
|
140
|
-
const errMsg = Array.from(
|
|
118
|
+
if (missingKeys.size > 0) {
|
|
119
|
+
const errMsg = Array.from(missingKeys)
|
|
141
120
|
.map((key) => `"${key}"`)
|
|
142
121
|
.join(', ');
|
|
143
|
-
const pluralized =
|
|
122
|
+
const pluralized = missingKeys.size > 1 ? 'keys' : 'key';
|
|
144
123
|
objAnn = updateText(objAnn, `Missing ${pluralized}: ${errMsg}`);
|
|
145
124
|
}
|
|
146
125
|
|
|
@@ -151,38 +130,44 @@ export function object<O: { +[field: string]: AnyDecoder, ... }>(
|
|
|
151
130
|
});
|
|
152
131
|
}
|
|
153
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Like `object()`, but will reject inputs that contain extra fields that are
|
|
135
|
+
* not specified explicitly.
|
|
136
|
+
*/
|
|
154
137
|
export function exact<O: { +[field: string]: AnyDecoder, ... }>(
|
|
155
|
-
|
|
156
|
-
): Decoder<$ObjMap<$Exact<O>,
|
|
157
|
-
//
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
138
|
+
decodersByKey: O,
|
|
139
|
+
): Decoder<$ObjMap<$Exact<O>, <T>(Decoder<T>) => T>> {
|
|
140
|
+
// Compute this set at decoder definition time
|
|
141
|
+
const allowedKeys = new Set(Object.keys(decodersByKey));
|
|
142
|
+
|
|
143
|
+
// Check the inputted object for any unexpected extra keys
|
|
144
|
+
const checked = pojo.reject((plainObj) => {
|
|
145
|
+
const actualKeys = new Set(Object.keys(plainObj));
|
|
146
|
+
const extraKeys = subtract(actualKeys, allowedKeys);
|
|
147
|
+
return extraKeys.size > 0
|
|
148
|
+
? `Unexpected extra keys: ${Array.from(extraKeys).join(', ')}`
|
|
149
|
+
: // Don't reject
|
|
150
|
+
null;
|
|
168
151
|
});
|
|
169
152
|
|
|
170
153
|
// Defer to the "object" decoder for doing the real decoding work. Since
|
|
171
154
|
// we made sure there are no superfluous keys in this structure, it's now
|
|
172
155
|
// safe to force-cast it to an $Exact<> type.
|
|
173
|
-
|
|
174
|
-
return compose(checked, decoder);
|
|
156
|
+
return checked.then(object(decodersByKey).decode);
|
|
175
157
|
}
|
|
176
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Like `object()`, but will pass through any extra fields on the input object
|
|
161
|
+
* unvalidated that will thus be of `unknown` type statically.
|
|
162
|
+
*/
|
|
177
163
|
export function inexact<O: { +[field: string]: AnyDecoder }>(
|
|
178
|
-
|
|
179
|
-
): Decoder<$ObjMap<O,
|
|
180
|
-
return
|
|
181
|
-
const allkeys = new Set(Object.keys(
|
|
182
|
-
const decoder = transform(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const safekeys = new Set(Object.keys(mapping));
|
|
164
|
+
decodersByKey: O,
|
|
165
|
+
): Decoder<$ObjMap<O, <T>(Decoder<T>) => T> & { +[string]: mixed }> {
|
|
166
|
+
return pojo.then((plainObj) => {
|
|
167
|
+
const allkeys = new Set(Object.keys(plainObj));
|
|
168
|
+
const decoder = object(decodersByKey).transform(
|
|
169
|
+
(safepart: $ObjMap<O, <T>(Decoder<T>) => T>) => {
|
|
170
|
+
const safekeys = new Set(Object.keys(decodersByKey));
|
|
186
171
|
|
|
187
172
|
// To account for hard-coded keys that aren't part of the input
|
|
188
173
|
safekeys.forEach((k) => allkeys.add(k));
|
|
@@ -195,27 +180,34 @@ export function inexact<O: { +[field: string]: AnyDecoder }>(
|
|
|
195
180
|
rv[k] = value;
|
|
196
181
|
}
|
|
197
182
|
} else {
|
|
198
|
-
rv[k] =
|
|
183
|
+
rv[k] = plainObj[k];
|
|
199
184
|
}
|
|
200
185
|
});
|
|
201
186
|
return rv;
|
|
202
187
|
},
|
|
203
188
|
);
|
|
204
|
-
return decoder(
|
|
189
|
+
return decoder.decode(plainObj);
|
|
205
190
|
});
|
|
206
191
|
}
|
|
207
192
|
|
|
208
193
|
/**
|
|
209
|
-
*
|
|
194
|
+
* Accepts objects where all values match the given decoder, and returns the
|
|
195
|
+
* result as a `{ [string]: T }`.
|
|
196
|
+
*
|
|
197
|
+
* The main difference between `object()` and `dict()` is that you'd typically
|
|
198
|
+
* use `object()` if this is a record-like object, where all field names are
|
|
199
|
+
* known and the values are heterogeneous. Whereas with `dict()` the keys are
|
|
200
|
+
* typically dynamic and the values homogeneous, like in a dictionary,
|
|
201
|
+
* a lookup table, or a cache.
|
|
210
202
|
*/
|
|
211
203
|
export function dict<T>(decoder: Decoder<T>): Decoder<{ [string]: T }> {
|
|
212
|
-
return
|
|
204
|
+
return pojo.then((plainObj, ok, err) => {
|
|
213
205
|
let rv: { [key: string]: T } = {};
|
|
214
206
|
let errors: { [key: string]: Annotation } | null = null;
|
|
215
207
|
|
|
216
|
-
Object.keys(
|
|
217
|
-
const value =
|
|
218
|
-
const result = decoder(value);
|
|
208
|
+
Object.keys(plainObj).forEach((key: string) => {
|
|
209
|
+
const value = plainObj[key];
|
|
210
|
+
const result = decoder.decode(value);
|
|
219
211
|
if (result.ok) {
|
|
220
212
|
if (errors === null) {
|
|
221
213
|
rv[key] = result.value;
|
|
@@ -230,7 +222,7 @@ export function dict<T>(decoder: Decoder<T>): Decoder<{ [string]: T }> {
|
|
|
230
222
|
});
|
|
231
223
|
|
|
232
224
|
if (errors !== null) {
|
|
233
|
-
return err(merge(annotateObject(
|
|
225
|
+
return err(merge(annotateObject(plainObj), errors));
|
|
234
226
|
} else {
|
|
235
227
|
return ok(rv);
|
|
236
228
|
}
|
|
@@ -238,17 +230,12 @@ export function dict<T>(decoder: Decoder<T>): Decoder<{ [string]: T }> {
|
|
|
238
230
|
}
|
|
239
231
|
|
|
240
232
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* table structure (of type Map<string, Person>) like so:
|
|
245
|
-
*
|
|
246
|
-
* mapping(person)
|
|
247
|
-
*
|
|
233
|
+
* Similar to `dict()`, but returns the result as a `Map<string, T>` (an [ES6
|
|
234
|
+
* Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map))
|
|
235
|
+
* instead.
|
|
248
236
|
*/
|
|
249
237
|
export function mapping<T>(decoder: Decoder<T>): Decoder<Map<string, T>> {
|
|
250
|
-
return transform(
|
|
251
|
-
dict(decoder),
|
|
238
|
+
return dict(decoder).transform(
|
|
252
239
|
(obj) =>
|
|
253
240
|
new Map(
|
|
254
241
|
// This is effectively Object.entries(obj), but in a way that Flow
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { err, ok } from '../result.mjs';
|
|
6
|
-
|
|
7
|
-
function isPojo(o) {
|
|
8
|
-
return o !== null && o !== undefined && typeof o === 'object' && // This still seems to be the only reliable way to determine whether
|
|
9
|
-
// something is a pojo... ¯\_(ツ)_/¯
|
|
10
|
-
// $FlowFixMe[method-unbinding]
|
|
11
|
-
Object.prototype.toString.call(o) === '[object Object]';
|
|
12
|
-
}
|
|
3
|
+
import { annotateObject, merge, updateText } from '../annotate.mjs';
|
|
4
|
+
import { define } from '../Decoder.mjs';
|
|
13
5
|
|
|
14
6
|
function subtract(xs, ys) {
|
|
15
7
|
var result = new Set();
|
|
@@ -20,9 +12,17 @@ function subtract(xs, ys) {
|
|
|
20
12
|
});
|
|
21
13
|
return result;
|
|
22
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Accepts any "plain old JavaScript object", but doesn't validate its keys or
|
|
17
|
+
* values further.
|
|
18
|
+
*/
|
|
19
|
+
|
|
23
20
|
|
|
24
|
-
export var pojo = function
|
|
25
|
-
return
|
|
21
|
+
export var pojo = define(function (blob, ok, err) {
|
|
22
|
+
return blob !== null && blob !== undefined && typeof blob === 'object' && // This still seems to be the only reliable way to determine whether
|
|
23
|
+
// something is a pojo... ¯\_(ツ)_/¯
|
|
24
|
+
// $FlowFixMe[method-unbinding]
|
|
25
|
+
Object.prototype.toString.call(blob) === '[object Object]' ? ok( // NOTE:
|
|
26
26
|
// Since Flow 0.98, typeof o === 'object' refines to
|
|
27
27
|
// {| +[string]: mixed |}
|
|
28
28
|
// instead of
|
|
@@ -36,43 +36,29 @@ export var pojo = function pojo(blob) {
|
|
|
36
36
|
// way to turn a read-only Object to a writeable one in ES6 seems
|
|
37
37
|
// to be to use object-spread. (Going off this benchmark:
|
|
38
38
|
// https://thecodebarbarian.com/object-assign-vs-object-spread.html)
|
|
39
|
-
_extends({}, blob)) : err(
|
|
40
|
-
};
|
|
39
|
+
_extends({}, blob)) : err('Must be an object');
|
|
40
|
+
});
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* For example, given decoders for a number and a string, we can construct an
|
|
45
|
-
* "object description" like so:
|
|
46
|
-
*
|
|
47
|
-
* { id: number, name: string }
|
|
48
|
-
*
|
|
49
|
-
* Which is of type:
|
|
50
|
-
*
|
|
51
|
-
* { id: Decoder<number>, name: Decoder<string> }
|
|
52
|
-
*
|
|
53
|
-
* Passing this to object() will produce the following return type:
|
|
54
|
-
*
|
|
55
|
-
* Decoder<{ id: number, name: string }>
|
|
56
|
-
*
|
|
57
|
-
* Put simply: it'll "peel off" all of the nested Decoders, puts them together
|
|
58
|
-
* in an object, and wraps it in a Decoder<...>.
|
|
42
|
+
* Accepts objects with fields matching the given decoders. Extra fields that
|
|
43
|
+
* exist on the input object are ignored and will not be returned.
|
|
59
44
|
*/
|
|
60
45
|
|
|
61
|
-
export function object(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
export function object(decodersByKey) {
|
|
47
|
+
// Compute this set at decoder definition time
|
|
48
|
+
var knownKeys = new Set(Object.keys(decodersByKey));
|
|
49
|
+
return pojo.then(function (plainObj, ok, err) {
|
|
50
|
+
var actualKeys = new Set(Object.keys(plainObj)); // At this point, "missingKeys" will also include all fields that may
|
|
51
|
+
// validly be optional. We'll let the underlying decoder decide and
|
|
66
52
|
// remove the key from this missing set if the decoder accepts the
|
|
67
53
|
// value.
|
|
68
54
|
|
|
69
|
-
var
|
|
55
|
+
var missingKeys = subtract(knownKeys, actualKeys);
|
|
70
56
|
var record = {};
|
|
71
57
|
var errors = null;
|
|
72
|
-
Object.keys(
|
|
73
|
-
var decoder =
|
|
74
|
-
var rawValue =
|
|
75
|
-
var result = decoder(rawValue);
|
|
58
|
+
Object.keys(decodersByKey).forEach(function (key) {
|
|
59
|
+
var decoder = decodersByKey[key];
|
|
60
|
+
var rawValue = plainObj[key];
|
|
61
|
+
var result = decoder.decode(rawValue);
|
|
76
62
|
|
|
77
63
|
if (result.ok) {
|
|
78
64
|
var value = result.value;
|
|
@@ -83,7 +69,7 @@ export function object(mapping) {
|
|
|
83
69
|
// tracker
|
|
84
70
|
|
|
85
71
|
|
|
86
|
-
|
|
72
|
+
missingKeys["delete"](key);
|
|
87
73
|
} else {
|
|
88
74
|
var ann = result.error; // Keep track of the annotation, but don't return just yet. We
|
|
89
75
|
// want to collect more error information.
|
|
@@ -93,7 +79,7 @@ export function object(mapping) {
|
|
|
93
79
|
// undefined. This covers explicit undefineds to be
|
|
94
80
|
// treated the same as implicit undefineds (aka missing
|
|
95
81
|
// keys).
|
|
96
|
-
|
|
82
|
+
missingKeys.add(key);
|
|
97
83
|
} else {
|
|
98
84
|
if (errors === null) {
|
|
99
85
|
errors = {};
|
|
@@ -107,18 +93,18 @@ export function object(mapping) {
|
|
|
107
93
|
// object. Lastly, any fields that are missing should be annotated on
|
|
108
94
|
// the outer object itself.
|
|
109
95
|
|
|
110
|
-
if (errors ||
|
|
111
|
-
var objAnn = annotateObject(
|
|
96
|
+
if (errors || missingKeys.size > 0) {
|
|
97
|
+
var objAnn = annotateObject(plainObj);
|
|
112
98
|
|
|
113
99
|
if (errors) {
|
|
114
100
|
objAnn = merge(objAnn, errors);
|
|
115
101
|
}
|
|
116
102
|
|
|
117
|
-
if (
|
|
118
|
-
var errMsg = Array.from(
|
|
103
|
+
if (missingKeys.size > 0) {
|
|
104
|
+
var errMsg = Array.from(missingKeys).map(function (key) {
|
|
119
105
|
return "\"" + key + "\"";
|
|
120
106
|
}).join(', ');
|
|
121
|
-
var pluralized =
|
|
107
|
+
var pluralized = missingKeys.size > 1 ? 'keys' : 'key';
|
|
122
108
|
objAnn = updateText(objAnn, "Missing " + pluralized + ": " + errMsg);
|
|
123
109
|
}
|
|
124
110
|
|
|
@@ -128,30 +114,36 @@ export function object(mapping) {
|
|
|
128
114
|
return ok(record);
|
|
129
115
|
});
|
|
130
116
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return err(annotate(blob, "Superfluous keys: " + Array.from(superfluous).join(', ')));
|
|
140
|
-
}
|
|
117
|
+
/**
|
|
118
|
+
* Like `object()`, but will reject inputs that contain extra fields that are
|
|
119
|
+
* not specified explicitly.
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
export function exact(decodersByKey) {
|
|
123
|
+
// Compute this set at decoder definition time
|
|
124
|
+
var allowedKeys = new Set(Object.keys(decodersByKey)); // Check the inputted object for any unexpected extra keys
|
|
141
125
|
|
|
142
|
-
|
|
126
|
+
var checked = pojo.reject(function (plainObj) {
|
|
127
|
+
var actualKeys = new Set(Object.keys(plainObj));
|
|
128
|
+
var extraKeys = subtract(actualKeys, allowedKeys);
|
|
129
|
+
return extraKeys.size > 0 ? "Unexpected extra keys: " + Array.from(extraKeys).join(', ') : // Don't reject
|
|
130
|
+
null;
|
|
143
131
|
}); // Defer to the "object" decoder for doing the real decoding work. Since
|
|
144
132
|
// we made sure there are no superfluous keys in this structure, it's now
|
|
145
133
|
// safe to force-cast it to an $Exact<> type.
|
|
146
134
|
|
|
147
|
-
|
|
148
|
-
return compose(checked, decoder);
|
|
135
|
+
return checked.then(object(decodersByKey).decode);
|
|
149
136
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Like `object()`, but will pass through any extra fields on the input object
|
|
139
|
+
* unvalidated that will thus be of `unknown` type statically.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
export function inexact(decodersByKey) {
|
|
143
|
+
return pojo.then(function (plainObj) {
|
|
144
|
+
var allkeys = new Set(Object.keys(plainObj));
|
|
145
|
+
var decoder = object(decodersByKey).transform(function (safepart) {
|
|
146
|
+
var safekeys = new Set(Object.keys(decodersByKey)); // To account for hard-coded keys that aren't part of the input
|
|
155
147
|
|
|
156
148
|
safekeys.forEach(function (k) {
|
|
157
149
|
return allkeys.add(k);
|
|
@@ -165,25 +157,32 @@ export function inexact(mapping) {
|
|
|
165
157
|
rv[k] = value;
|
|
166
158
|
}
|
|
167
159
|
} else {
|
|
168
|
-
rv[k] =
|
|
160
|
+
rv[k] = plainObj[k];
|
|
169
161
|
}
|
|
170
162
|
});
|
|
171
163
|
return rv;
|
|
172
164
|
});
|
|
173
|
-
return decoder(
|
|
165
|
+
return decoder.decode(plainObj);
|
|
174
166
|
});
|
|
175
167
|
}
|
|
176
168
|
/**
|
|
177
|
-
*
|
|
169
|
+
* Accepts objects where all values match the given decoder, and returns the
|
|
170
|
+
* result as a `{ [string]: T }`.
|
|
171
|
+
*
|
|
172
|
+
* The main difference between `object()` and `dict()` is that you'd typically
|
|
173
|
+
* use `object()` if this is a record-like object, where all field names are
|
|
174
|
+
* known and the values are heterogeneous. Whereas with `dict()` the keys are
|
|
175
|
+
* typically dynamic and the values homogeneous, like in a dictionary,
|
|
176
|
+
* a lookup table, or a cache.
|
|
178
177
|
*/
|
|
179
178
|
|
|
180
179
|
export function dict(decoder) {
|
|
181
|
-
return
|
|
180
|
+
return pojo.then(function (plainObj, ok, err) {
|
|
182
181
|
var rv = {};
|
|
183
182
|
var errors = null;
|
|
184
|
-
Object.keys(
|
|
185
|
-
var value =
|
|
186
|
-
var result = decoder(value);
|
|
183
|
+
Object.keys(plainObj).forEach(function (key) {
|
|
184
|
+
var value = plainObj[key];
|
|
185
|
+
var result = decoder.decode(value);
|
|
187
186
|
|
|
188
187
|
if (result.ok) {
|
|
189
188
|
if (errors === null) {
|
|
@@ -201,24 +200,20 @@ export function dict(decoder) {
|
|
|
201
200
|
});
|
|
202
201
|
|
|
203
202
|
if (errors !== null) {
|
|
204
|
-
return err(merge(annotateObject(
|
|
203
|
+
return err(merge(annotateObject(plainObj), errors));
|
|
205
204
|
} else {
|
|
206
205
|
return ok(rv);
|
|
207
206
|
}
|
|
208
207
|
});
|
|
209
208
|
}
|
|
210
209
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* table structure (of type Map<string, Person>) like so:
|
|
215
|
-
*
|
|
216
|
-
* mapping(person)
|
|
217
|
-
*
|
|
210
|
+
* Similar to `dict()`, but returns the result as a `Map<string, T>` (an [ES6
|
|
211
|
+
* Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map))
|
|
212
|
+
* instead.
|
|
218
213
|
*/
|
|
219
214
|
|
|
220
215
|
export function mapping(decoder) {
|
|
221
|
-
return
|
|
216
|
+
return dict(decoder).transform(function (obj) {
|
|
222
217
|
return new Map( // This is effectively Object.entries(obj), but in a way that Flow
|
|
223
218
|
// will know the types are okay
|
|
224
219
|
Object.keys(obj).map(function (key) {
|
package/lib/strings.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import { Decoder } from '../Decoder';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Accepts and returns strings.
|
|
7
|
+
*/
|
|
8
|
+
export const string: Decoder<string>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Like `string`, but will reject the empty string or strings containing only whitespace.
|
|
12
|
+
*/
|
|
13
|
+
export const nonEmptyString: Decoder<string>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Accepts and returns strings that match the given regular expression.
|
|
17
|
+
*/
|
|
18
|
+
export function regex(regex: RegExp, msg: string): Decoder<string>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Accepts and returns strings that are syntactically valid email addresses.
|
|
22
|
+
* (This will not mean that the email address actually exist.)
|
|
23
|
+
*/
|
|
24
|
+
export const email: Decoder<string>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Accepts strings that are valid URLs, returns the value as a URL instance.
|
|
28
|
+
*/
|
|
29
|
+
export const url: Decoder<URL>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Accepts strings that are valid URLs, but only HTTPS ones. Returns the value
|
|
33
|
+
* as a URL instance.
|
|
34
|
+
*/
|
|
35
|
+
export const httpsUrl: Decoder<URL>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Accepts strings that are valid
|
|
39
|
+
* [UUIDs](https://en.wikipedia.org/wiki/universally_unique_identifier)
|
|
40
|
+
* (universally unique identifier).
|
|
41
|
+
*/
|
|
42
|
+
export const uuid: Decoder<string>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Like `uuid`, but only accepts
|
|
46
|
+
* [UUIDv1](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_%28date-time_and_MAC_address%29)
|
|
47
|
+
* strings.
|
|
48
|
+
*/
|
|
49
|
+
export const uuidv1: Decoder<string>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Like `uuid`, but only accepts
|
|
53
|
+
* [UUIDv4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_%28random%29)
|
|
54
|
+
* strings.
|
|
55
|
+
*/
|
|
56
|
+
export const uuidv4: Decoder<string>;
|