1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184 |
x1
x1
x320
x132
x132
x320
x2
x2
x186
x186
x320
x8
x2
x2
x6
x320
x320
x320
x320
x320
x320
x320
x320
x320
x133
x320
x45
x320
x320
x48
x48
x48
x1
x67
x67
x138
x102
x102
x102
x138
x65
x67
x1
x71
x36
x36
x35
x71
x69
x48
x48
x48
x48
x69
x35
x71
x1
x1
x1
x1
x145
x145
x16
x14
x14
x14
x16
x2
x2
x16
x145
x145
x145
x6
x6
x123
x145
x1
x1
x1
x1
x127
x127
x127
x72
x127
x32
x32
x55
x55
x55
x55
x55
x10
x23
x6
x6
x6
x6
x6
x13
x7
x7
x7
x7
x1
x1
x7
x127
x127
x39
x39
x39
x7
x7
x7
x7
x7
x39
x32
x32
x39
x1 |
|
const hasOwn = Object.prototype.hasOwnProperty
export function objectType(obj: unknown): string {
if (typeof obj === 'undefined') {
return 'undefined';
}
// Consider: typeof null === object
if (obj === null) {
return 'null';
}
// slice(8, -1) extracts the type name from "[object Foo]" without a regex
const type = Object.prototype.toString.call(obj).slice(8, -1);
switch (type) {
case 'Number':
if (isNaN(obj as number)) {
return 'nan';
}
return 'number';
case 'String':
case 'Boolean':
case 'Array':
case 'Set':
case 'Map':
case 'Date':
case 'RegExp':
case 'Function':
case 'Symbol':
return type.toLowerCase();
default:
return typeof obj;
}
}
function is(type: string, obj: unknown): boolean {
return objectType(obj) === type;
}
export function objectValues(obj: unknown, allowArray = true): unknown {
const vals: Record<string, unknown> | unknown[] = allowArray && is('array', obj) ? [] : {};
for (const key in obj as object) {
if (hasOwn.call(obj, key)) {
const val = (obj as Record<string, unknown>)[key];
(vals as Record<string, unknown>)[key] = val === Object(val) ? objectValues(val, allowArray) : val;
}
}
return vals;
}
/**
*
* Recursively clone an object into a plain object, taking only the
* subset of own enumerable properties that exist a given model.
*
* @param {any} obj
* @param {any} model
* @return {Object}
*/
export function objectValuesSubset(obj: unknown, model: unknown): unknown {
// Return primitive values unchanged to avoid false positives or confusing
// results from assert.propContains().
// E.g. an actual null or false wrongly equaling an empty object,
// or an actual string being reported as object not matching a partial object.
if (obj !== Object(obj)) {
return obj;
}
// Unlike objectValues(), subset arrays to a plain objects as well.
// This enables subsetting [20, 30] with {1: 30}.
const subset: Record<string, unknown> = {};
for (const key in model as object) {
if (hasOwn.call(model, key) && hasOwn.call(obj, key)) {
subset[key] = objectValuesSubset(
(obj as Record<string, unknown>)[key],
(model as Record<string, unknown>)[key],
);
}
}
return subset;
}
export function validateExpectedExceptionArgs(
expected: unknown,
message: string | undefined,
assertionMethod: string,
): [unknown, string | undefined] {
const expectedType = objectType(expected);
// 'expected' is optional unless doing string comparison
if (expectedType === 'string') {
if (message === undefined) {
message = expected as string;
expected = undefined;
return [expected, message];
} else {
throw new Error('assert.' + assertionMethod + ' does not accept a string value for the expected argument.\n' + 'Use a non-string object value (e.g. RegExp or validator function) ' + 'instead if necessary.');
}
}
const valid = !expected ||
// TODO: be more explicit here
expectedType === 'regexp' || expectedType === 'function' || expectedType === 'object';
if (!valid) {
throw new Error('Invalid expected value type (' + expectedType + ') ' + 'provided to assert.' + assertionMethod + '.');
}
return [expected, message];
}
export function validateException(
actual: unknown,
expected: unknown,
message: string | undefined,
): [boolean, unknown, string | undefined] {
let result = false;
const expectedType = objectType(expected);
// These branches should be exhaustive, based on validation done in validateExpectedException
// We don't want to validate
if (!expected) {
result = true;
// Expected is a regexp
} else if (expectedType === 'regexp') {
result = (expected as RegExp).test(errorString(actual));
// Log the string form of the regexp
expected = String(expected);
// Expected is a constructor, maybe an Error constructor.
// Note the extra check on its prototype - this is an implicit
// requirement of "instanceof", else it will throw a TypeError.
} else if (
expectedType === 'function' &&
(expected as { prototype: unknown }).prototype !== undefined &&
actual instanceof (expected as new (...args: unknown[]) => unknown)
) {
result = true;
// Expected is an Error object
} else if (expectedType === 'object') {
result =
actual instanceof (expected as { constructor: new (...args: unknown[]) => unknown }).constructor &&
(actual as Error).name === (expected as Error).name &&
(actual as Error).message === (expected as Error).message;
// Log the string form of the Error object
expected = errorString(expected);
// Expected is a validation function which returns true if validation passed
} else if (expectedType === 'function') {
// protect against accidental semantics which could hard error in the test
try {
result = (expected as (e: unknown) => boolean).call({}, actual) === true;
expected = null;
} catch (e) {
// assign the "expected" to a nice error string to communicate the local failure to the user
expected = errorString(e);
}
}
return [result, expected, message];
}
function errorString(error: unknown): string {
// Use String() instead of toString() to handle non-object values like undefined or null.
const resultErrorString = String(error);
// If the error wasn't a subclass of Error but something like
// an object literal with name and message properties...
if (resultErrorString.slice(0, 7) === '[object') {
// Based on https://es5.github.io/#x15.11.4.4
return (
((error as { name?: string }).name || 'Error') +
((error as { message?: string }).message
? ': '.concat((error as { message: string }).message)
: '')
);
} else {
return resultErrorString;
}
}
export default { objectValues, objectValuesSubset, validateExpectedExceptionArgs, validateException };
|