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.
Files changed (136) hide show
  1. package/CHANGELOG.md +56 -29
  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/README.md +122 -1507
  7. package/_utils.d.ts +0 -1
  8. package/_utils.js +11 -17
  9. package/_utils.js.flow +13 -17
  10. package/_utils.mjs +10 -14
  11. package/format.d.ts +4 -2
  12. package/format.js +1 -1
  13. package/format.js.flow +3 -1
  14. package/format.mjs +1 -1
  15. package/index.d.ts +29 -31
  16. package/index.js +62 -84
  17. package/index.js.flow +30 -48
  18. package/index.mjs +11 -36
  19. package/{core → lib}/_helpers.d.ts +0 -0
  20. package/lib/arrays.d.ts +59 -0
  21. package/lib/arrays.js +139 -0
  22. package/lib/arrays.js.flow +138 -0
  23. package/lib/arrays.mjs +124 -0
  24. package/lib/basics.d.ts +93 -0
  25. package/lib/basics.js +144 -0
  26. package/lib/basics.js.flow +124 -0
  27. package/lib/basics.mjs +120 -0
  28. package/lib/booleans.d.ts +16 -0
  29. package/lib/booleans.js +35 -0
  30. package/lib/booleans.js.flow +22 -0
  31. package/lib/booleans.mjs +25 -0
  32. package/lib/dates.d.ts +15 -0
  33. package/lib/dates.js +44 -0
  34. package/lib/dates.js.flow +40 -0
  35. package/lib/dates.mjs +34 -0
  36. package/lib/json.d.ts +35 -0
  37. package/lib/json.js +55 -0
  38. package/lib/json.js.flow +50 -0
  39. package/lib/json.mjs +40 -0
  40. package/lib/numbers.d.ts +31 -0
  41. package/lib/numbers.js +51 -0
  42. package/lib/numbers.js.flow +48 -0
  43. package/lib/numbers.mjs +41 -0
  44. package/lib/objects.d.ts +75 -0
  45. package/{core/object.js → lib/objects.js} +78 -85
  46. package/{core/object.js.flow → lib/objects.js.flow} +89 -102
  47. package/{core/object.mjs → lib/objects.mjs} +77 -82
  48. package/lib/strings.d.ts +56 -0
  49. package/lib/strings.js +101 -0
  50. package/lib/strings.js.flow +90 -0
  51. package/lib/strings.mjs +82 -0
  52. package/lib/unions.d.ts +55 -0
  53. package/lib/unions.js +160 -0
  54. package/{core/either.js.flow → lib/unions.js.flow} +67 -17
  55. package/lib/unions.mjs +146 -0
  56. package/lib/utilities.d.ts +34 -0
  57. package/lib/utilities.js +75 -0
  58. package/lib/utilities.js.flow +65 -0
  59. package/lib/utilities.mjs +60 -0
  60. package/package.json +64 -17
  61. package/result.d.ts +0 -23
  62. package/result.js +0 -68
  63. package/result.js.flow +0 -72
  64. package/result.mjs +0 -54
  65. package/_guard.d.ts +0 -7
  66. package/_guard.js +0 -22
  67. package/_guard.js.flow +0 -20
  68. package/_guard.mjs +0 -15
  69. package/_types.d.ts +0 -13
  70. package/_types.js +0 -1
  71. package/_types.js.flow +0 -20
  72. package/_types.mjs +0 -0
  73. package/core/array.d.ts +0 -8
  74. package/core/array.js +0 -115
  75. package/core/array.js.flow +0 -107
  76. package/core/array.mjs +0 -100
  77. package/core/boolean.d.ts +0 -5
  78. package/core/boolean.js +0 -40
  79. package/core/boolean.js.flow +0 -27
  80. package/core/boolean.mjs +0 -28
  81. package/core/composition.d.ts +0 -18
  82. package/core/composition.js +0 -82
  83. package/core/composition.js.flow +0 -74
  84. package/core/composition.mjs +0 -70
  85. package/core/constants.d.ts +0 -11
  86. package/core/constants.js +0 -65
  87. package/core/constants.js.flow +0 -44
  88. package/core/constants.mjs +0 -46
  89. package/core/date.d.ts +0 -4
  90. package/core/date.js +0 -42
  91. package/core/date.js.flow +0 -38
  92. package/core/date.mjs +0 -28
  93. package/core/describe.d.ts +0 -3
  94. package/core/describe.js +0 -22
  95. package/core/describe.js.flow +0 -17
  96. package/core/describe.mjs +0 -16
  97. package/core/dispatch.d.ts +0 -8
  98. package/core/dispatch.js +0 -60
  99. package/core/dispatch.js.flow +0 -59
  100. package/core/dispatch.mjs +0 -52
  101. package/core/either.d.ts +0 -66
  102. package/core/either.js +0 -101
  103. package/core/either.mjs +0 -90
  104. package/core/fail.d.ts +0 -3
  105. package/core/fail.js +0 -17
  106. package/core/fail.js.flow +0 -12
  107. package/core/fail.mjs +0 -11
  108. package/core/instanceOf.d.ts +0 -3
  109. package/core/instanceOf.js +0 -15
  110. package/core/instanceOf.js.flow +0 -20
  111. package/core/instanceOf.mjs +0 -8
  112. package/core/json.d.ts +0 -11
  113. package/core/json.js +0 -31
  114. package/core/json.js.flow +0 -28
  115. package/core/json.mjs +0 -15
  116. package/core/lazy.d.ts +0 -3
  117. package/core/lazy.js +0 -16
  118. package/core/lazy.js.flow +0 -15
  119. package/core/lazy.mjs +0 -11
  120. package/core/number.d.ts +0 -6
  121. package/core/number.js +0 -36
  122. package/core/number.js.flow +0 -40
  123. package/core/number.mjs +0 -25
  124. package/core/object.d.ts +0 -38
  125. package/core/optional.d.ts +0 -5
  126. package/core/optional.js +0 -50
  127. package/core/optional.js.flow +0 -41
  128. package/core/optional.mjs +0 -38
  129. package/core/string.d.ts +0 -13
  130. package/core/string.js +0 -80
  131. package/core/string.js.flow +0 -72
  132. package/core/string.mjs +0 -58
  133. package/core/tuple.d.ts +0 -30
  134. package/core/tuple.js +0 -54
  135. package/core/tuple.js.flow +0 -51
  136. package/core/tuple.mjs +0 -45
@@ -1,28 +1,10 @@
1
1
  // @flow strict
2
2
 
3
- import { annotate, annotateObject, merge, updateText } from '../annotate';
4
- import { compose, transform } from './composition';
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 { Decoder, DecodeResult, DecoderType } from '../_types';
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
- export const pojo: Decoder<{| [string]: mixed |}> = (blob: mixed) => {
38
- return isPojo(blob)
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(annotate(blob, 'Must be an object'));
57
- };
48
+ : err('Must be an object'),
49
+ );
58
50
 
59
51
  /**
60
- * Given a mapping of fields-to-decoders, builds a decoder for an object type.
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
- mapping: O,
80
- ): Decoder<$ObjMap<O, DecoderType>> {
81
- const known = new Set(Object.keys(mapping));
82
- return compose(pojo, (blob: { +[string]: mixed }) => {
83
- const actual = new Set(Object.keys(blob));
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, "missing" will also include all fields that may
86
- // validly be optional. We'll let the underlying decoder decide and
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 missing = subtract(known, actual);
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(mapping).forEach((key) => {
95
- const decoder = mapping[key];
96
- const rawValue = blob[key];
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
- missing.delete(key);
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
- missing.add(key);
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 || missing.size > 0) {
133
- let objAnn = annotateObject(blob);
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 (missing.size > 0) {
140
- const errMsg = Array.from(missing)
118
+ if (missingKeys.size > 0) {
119
+ const errMsg = Array.from(missingKeys)
141
120
  .map((key) => `"${key}"`)
142
121
  .join(', ');
143
- const pluralized = missing.size > 1 ? 'keys' : 'key';
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
- mapping: O,
156
- ): Decoder<$ObjMap<$Exact<O>, DecoderType>> {
157
- // Check the inputted object for any superfluous keys
158
- const allowed = new Set(Object.keys(mapping));
159
- const checked = compose(pojo, (blob: {| [string]: mixed |}) => {
160
- const actual = new Set(Object.keys(blob));
161
- const superfluous = subtract(actual, allowed);
162
- if (superfluous.size > 0) {
163
- return err(
164
- annotate(blob, `Superfluous keys: ${Array.from(superfluous).join(', ')}`),
165
- );
166
- }
167
- return ok(blob);
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
- const decoder = ((object(mapping): cast): Decoder<$ObjMap<$Exact<O>, DecoderType>>);
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
- mapping: O,
179
- ): Decoder<$ObjMap<O, DecoderType> & { +[string]: mixed }> {
180
- return compose(pojo, (blob: {| [string]: mixed |}) => {
181
- const allkeys = new Set(Object.keys(blob));
182
- const decoder = transform(
183
- object(mapping),
184
- (safepart: $ObjMap<O, DecoderType>) => {
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] = blob[k];
183
+ rv[k] = plainObj[k];
199
184
  }
200
185
  });
201
186
  return rv;
202
187
  },
203
188
  );
204
- return decoder(blob);
189
+ return decoder.decode(plainObj);
205
190
  });
206
191
  }
207
192
 
208
193
  /**
209
- * Like mapping(), but returns an object rather than a Map instance.
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 compose(pojo, (blob: { +[key: string]: mixed }) => {
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(blob).forEach((key: string) => {
217
- const value = blob[key];
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(blob), errors));
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
- * Given an object, will decode a Map of string keys to whatever values.
242
- *
243
- * For example, given a decoder for a Person, we can verify a Person lookup
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 { annotate, annotateObject, merge, updateText } from '../annotate.mjs';
4
- import { compose, transform } from './composition.mjs';
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 pojo(blob) {
25
- return isPojo(blob) ? ok( // NOTE:
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(annotate(blob, 'Must be an object'));
40
- };
39
+ _extends({}, blob)) : err('Must be an object');
40
+ });
41
41
  /**
42
- * Given a mapping of fields-to-decoders, builds a decoder for an object type.
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(mapping) {
62
- var known = new Set(Object.keys(mapping));
63
- return compose(pojo, function (blob) {
64
- var actual = new Set(Object.keys(blob)); // At this point, "missing" will also include all fields that may
65
- // validly be optional. We'll let the underlying decoder decide and
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 missing = subtract(known, actual);
55
+ var missingKeys = subtract(knownKeys, actualKeys);
70
56
  var record = {};
71
57
  var errors = null;
72
- Object.keys(mapping).forEach(function (key) {
73
- var decoder = mapping[key];
74
- var rawValue = blob[key];
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
- missing["delete"](key);
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
- missing.add(key);
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 || missing.size > 0) {
111
- var objAnn = annotateObject(blob);
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 (missing.size > 0) {
118
- var errMsg = Array.from(missing).map(function (key) {
103
+ if (missingKeys.size > 0) {
104
+ var errMsg = Array.from(missingKeys).map(function (key) {
119
105
  return "\"" + key + "\"";
120
106
  }).join(', ');
121
- var pluralized = missing.size > 1 ? 'keys' : 'key';
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
- export function exact(mapping) {
132
- // Check the inputted object for any superfluous keys
133
- var allowed = new Set(Object.keys(mapping));
134
- var checked = compose(pojo, function (blob) {
135
- var actual = new Set(Object.keys(blob));
136
- var superfluous = subtract(actual, allowed);
137
-
138
- if (superfluous.size > 0) {
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
- return ok(blob);
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
- var decoder = object(mapping);
148
- return compose(checked, decoder);
135
+ return checked.then(object(decodersByKey).decode);
149
136
  }
150
- export function inexact(mapping) {
151
- return compose(pojo, function (blob) {
152
- var allkeys = new Set(Object.keys(blob));
153
- var decoder = transform(object(mapping), function (safepart) {
154
- var safekeys = new Set(Object.keys(mapping)); // To account for hard-coded keys that aren't part of the input
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] = blob[k];
160
+ rv[k] = plainObj[k];
169
161
  }
170
162
  });
171
163
  return rv;
172
164
  });
173
- return decoder(blob);
165
+ return decoder.decode(plainObj);
174
166
  });
175
167
  }
176
168
  /**
177
- * Like mapping(), but returns an object rather than a Map instance.
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 compose(pojo, function (blob) {
180
+ return pojo.then(function (plainObj, ok, err) {
182
181
  var rv = {};
183
182
  var errors = null;
184
- Object.keys(blob).forEach(function (key) {
185
- var value = blob[key];
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(blob), errors));
203
+ return err(merge(annotateObject(plainObj), errors));
205
204
  } else {
206
205
  return ok(rv);
207
206
  }
208
207
  });
209
208
  }
210
209
  /**
211
- * Given an object, will decode a Map of string keys to whatever values.
212
- *
213
- * For example, given a decoder for a Person, we can verify a Person lookup
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 transform(dict(decoder), function (obj) {
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) {
@@ -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>;