Skip to content

Commit 3624477

Browse files
committed
Extending cases and thinking aloud
1 parent 9ae436a commit 3624477

File tree

3 files changed

+143
-125
lines changed

3 files changed

+143
-125
lines changed

.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
printWidth: 80
2+
semi: false
3+
singleQuote: true
4+
tabWidth: 2

src/index.test.ts

+103-86
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,135 @@
1-
import { isNumber, isString } from "lodash";
1+
import { isNumber, isString } from 'lodash'
22

3-
import { be, beArray, beTrue, decode } from "./index";
3+
import {be, decodeArray, decode, makeDecoder} from './index'
44

5-
test("be", () => {
6-
const numError = "Not a number!";
5+
test('be', () => {
6+
const numError = 'Not a number!'
77

8-
expect(be(20, isNumber, numError)).toBe(20);
9-
expect(() => be("20", isNumber, numError)).toThrow(numError);
10-
});
8+
expect(be(20, isNumber, numError)).toBe(20)
9+
expect(() => be('20', isNumber, numError)).toThrow(numError)
10+
})
1111

12-
test("beTrue", () => {
13-
const fiverError = "Not equals to 5.5!";
14-
const eq = (m: number) => (n: number) => n === m;
12+
test('beArray: happy path', () => {
13+
expect(decodeArray([1, false, 'Bob'], el => el)).toStrictEqual([1, false, 'Bob'])
14+
})
1515

16-
expect(beTrue(5.5, eq(5.5), fiverError)).toBe(5.5);
17-
expect(() => beTrue(5.7, eq(5.5), fiverError)).toThrow(fiverError);
18-
});
19-
20-
test("decode: validation ok", () => {
21-
expect(
22-
decode(
23-
["foo", "bar"],
24-
([foo, bar]) => ({
25-
foo: be(foo, isString, "Failed"),
26-
bar: beTrue(bar, s => s === "bar", "Failed")
27-
}),
28-
null
29-
)
30-
).toStrictEqual({
31-
foo: "foo",
32-
bar: "bar"
33-
});
34-
});
35-
36-
test("decode: falling back", () => {
37-
const logger = jest.fn();
38-
39-
expect(
40-
decode(
41-
void 0,
42-
() => ({
43-
foo: be("foo", isString, "Failed"),
44-
bar: beTrue("bar", s => s === "foo", "Failed")
45-
}),
46-
null,
47-
{ logger }
48-
)
49-
).toBe(null);
50-
51-
expect(logger).toBeCalledWith(expect.objectContaining({ message: "Failed" }));
52-
});
53-
54-
test("beArray: happy path", () => {
55-
expect(beArray([1, false, "Bob"], el => el)).toStrictEqual([1, false, "Bob"]);
56-
});
57-
58-
test("beArray: filtering", () => {
16+
test('beArray: filtering', () => {
5917
expect(
60-
beArray(["a", 0, "b", 1, "c"], s => {
61-
if (typeof s !== "string") throw Error("!");
62-
return { s };
18+
decodeArray(['a', 0, 'b', 1, 'c'], s => {
19+
if (typeof s !== 'string') throw Error('!')
20+
return { s }
6321
})
64-
).toStrictEqual([{ s: "a" }, { s: "b" }, { s: "c" }]);
65-
});
22+
).toStrictEqual([{ s: 'a' }, { s: 'b' }, { s: 'c' }])
23+
})
6624

67-
test("beArray: invalidate all", () => {
25+
test('beArray: invalidate all', () => {
6826
expect(() =>
69-
beArray(
70-
["a", 0, "b", 1, "c"],
27+
decodeArray(
28+
['a', 0, 'b', 1, 'c'],
7129
x => {
72-
if (typeof x !== "string") throw Error("!!!");
73-
return x;
30+
if (typeof x !== 'string') throw Error('!!!')
31+
return x
7432
},
7533
{ invalidateAll: true }
7634
)
77-
).toThrow("!!!");
78-
});
35+
).toThrow('!!!')
36+
})
7937

80-
test("beArray: not an array", () => {
38+
test('beArray: not an array', () => {
8139
expect(() => {
82-
beArray("[]", x => x);
83-
}).toThrow("“[]” (string) is not an array");
40+
decodeArray('[]', x => x)
41+
}).toThrow('“[]” (string) is not an array')
8442

8543
expect(() => {
86-
beArray(3.14, x => x);
87-
}).toThrow("3.14 (number) is not an array");
44+
decodeArray(3.14, x => x)
45+
}).toThrow('3.14 (number) is not an array')
8846

8947
expect(() => {
90-
beArray("[]", x => x, {
91-
notAnArrayError: "Not quite an array one would expect"
92-
});
93-
}).toThrow("Not quite an array one would expect");
94-
});
48+
decodeArray('[]', x => x, {
49+
notAnArrayError: 'Not quite an array one would expect'
50+
})
51+
}).toThrow('Not quite an array one would expect')
52+
})
9553

96-
test("beArray: checkLength", () => {
54+
test('beArray: checkLength', () => {
9755
expect(() =>
98-
beArray(
56+
decodeArray(
9957
[false, false, false, false, true],
10058
x => {
101-
if (!x) throw Error("!!!");
102-
return x;
59+
if (!x) throw Error('!!!')
60+
return x
10361
},
104-
{ minLength: 2, minLengthError: "Way too short!" }
62+
{ minLength: 2, minLengthError: 'Way too short!' }
10563
)
106-
).toThrow("Way too short!");
64+
).toThrow('Way too short!')
10765

10866
expect(() =>
109-
beArray(
67+
decodeArray(
11068
[false, false, false, false, true],
11169
x => {
112-
if (!x) throw Error("!!!");
113-
return x;
70+
if (!x) throw Error('!!!')
71+
return x
11472
},
11573
{ minLength: 2 }
11674
)
117-
).toThrow("Array length (1) less than specified (2)");
118-
});
75+
).toThrow('Array length (1) less than specified (2)')
76+
})
77+
78+
test('decode: validation ok', () => {
79+
expect(
80+
decode(
81+
['foo', 'bar'],
82+
([foo, bar]) => ({
83+
foo: be(foo, isString, 'Failed'),
84+
bar: be(bar, isString, 'Failed')
85+
}),
86+
null
87+
)
88+
).toStrictEqual({
89+
foo: 'foo',
90+
bar: 'bar'
91+
})
92+
})
93+
94+
test('decode: falling back', () => {
95+
const logger = jest.fn()
96+
97+
expect(
98+
decode(
99+
void 0,
100+
() => ({
101+
foo: be('foo', isString, 'Failed'),
102+
bar: be('bar', isNumber, 'Failed')
103+
}),
104+
null,
105+
{ logger }
106+
)
107+
).toBe(null)
108+
109+
expect(logger).toBeCalledWith(expect.objectContaining({ message: 'Failed' }))
110+
})
111+
112+
test('decode: successful nested array', () => {
113+
expect(
114+
decode(
115+
{ id: 3, animals: ['Cat', 'Dog', 'Siren'] },
116+
input => ({
117+
id: be(input.id, isNumber, 'Id should be a number'),
118+
animalNames: decodeArray(
119+
input.animals,
120+
makeDecoder(
121+
isString,
122+
'animal should have strings of characters as their names'
123+
)
124+
// fixme: maybe we should add a fallback parameter after all
125+
// I mean, the only way to catch array validation errors here
126+
// is `decode`, and wrapping `decodeArray` (a catching point in its
127+
// own right) in `decode` eels rather weird
128+
//
129+
// maybe we should have `invalidate: 'element' | 'array' | 'parent'
130+
)
131+
}),
132+
null
133+
)
134+
).toStrictEqual({ id: 3, animalNames: ['Cat', 'Dog', 'Siren'] })
135+
})

src/index.ts

+36-39
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import isArray from "lodash/isArray";
1+
import isArray from 'lodash/isArray'
22

33
/**
44
* The base for the whole library. Run predicate on the value.
@@ -15,34 +15,31 @@ export function be<T>(
1515
predicate: (a: any) => a is T,
1616
errorMessage: string
1717
): T {
18-
if (predicate(val)) return val;
19-
20-
throw new TypeError(errorMessage);
18+
/**
19+
* @param val {any} The value itself.
20+
*/
21+
return makeDecoder(predicate, errorMessage)(val);
2122
}
2223

23-
/**
24-
* Checks for a condition that is not a type assertion. Ergo cannot
25-
* narrow done the value type and returns the same type it gets.
26-
* If you want to assert and narrow down types, use `be`.
27-
* @param val {T} The value itself.
28-
* @param predicate: {(a: T) => boolean}.
29-
* @param errorMessage {string}
30-
*/
31-
export function beTrue<T>(
32-
val: T,
33-
predicate: (a: T) => boolean,
24+
export function makeDecoder<T>(
25+
predicate: (a: any) => a is T,
3426
errorMessage: string
35-
): T {
36-
if (predicate(val)) return val;
27+
): (val: any) => T {
28+
/**
29+
* @param val {any} The value itself.
30+
*/
31+
return function decoderFunctionExn(val: any): T {
32+
if (predicate(val)) return val
3733

38-
throw new Error(errorMessage);
34+
throw new TypeError(errorMessage)
35+
}
3936
}
4037

4138
type CatchOptions = {
42-
logger?: (e: Error) => void;
43-
};
39+
logger?: (e: Error) => void
40+
}
4441

45-
const noop = () => {};
42+
const noop = () => {}
4643

4744
/**
4845
* Runs a decoding function, and if all the validations succeed, returns
@@ -61,26 +58,26 @@ export function decode<In, Out, Fb>(
6158
{ logger = noop }: CatchOptions = {}
6259
): Out | Fb {
6360
try {
64-
return decoder(input);
61+
return decoder(input)
6562
} catch (e) {
6663
/* If we ever add a custom error, we should replace all the
6764
`new TypeError` invocations. But frankly, why not catch all
6865
exceptions indiscriminately, even those not foreseen
6966
by a programmer.
7067
*/
71-
logger(e);
72-
return fallback;
68+
logger(e)
69+
return fallback
7370
}
7471
}
7572

7673
type ArrayOptions = {
77-
invalidateAll?: boolean;
78-
minLength?: number;
79-
minLengthError?: string;
80-
notAnArrayError?: string;
81-
};
74+
invalidateAll?: boolean
75+
minLength?: number
76+
minLengthError?: string
77+
notAnArrayError?: string
78+
}
8279

83-
export function beArray<Out>(
80+
export function decodeArray<Out>(
8481
input: unknown,
8582
elementDecoder: (el: unknown) => Out,
8683
{
@@ -93,14 +90,14 @@ export function beArray<Out>(
9390
if (!isArray(input))
9491
throw TypeError(
9592
notAnArrayError || `${printValueInfo(input)} is not an array`
96-
);
93+
)
9794

98-
const result = [];
95+
const result = []
9996
for (const el of input) {
10097
try {
101-
result.push(elementDecoder(el));
98+
result.push(elementDecoder(el))
10299
} catch (e) {
103-
if (invalidateAll) throw e;
100+
if (invalidateAll) throw e
104101
// otherwise, don’t push the result, but swallow the exception,
105102
// effectively invalidating a single element, but not the whole array
106103
}
@@ -110,14 +107,14 @@ export function beArray<Out>(
110107
throw Error(
111108
minLengthError ||
112109
`Array length (${result.length}) less than specified (${minLength})`
113-
);
110+
)
114111
}
115-
return result;
112+
return result
116113
}
117114

118115
function printValueInfo(value: any) {
119-
const valType = typeof value;
120-
const lq = valType === 'string' ? '“' : '';
121-
const rq = valType === 'string' ? '”' : '';
116+
const valType = typeof value
117+
const lq = valType === 'string' ? '“' : ''
118+
const rq = valType === 'string' ? '”' : ''
122119
return `${lq + value + rq} (${typeof value})`
123120
}

0 commit comments

Comments
 (0)