UNPKG

6.27 kBJavaScriptView Raw
1import is from '@sindresorhus/is';
2import { hasProperty } from 'dot-prop';
3import { deepEqual } from 'fast-equals';
4import hasItems from '../utils/has-items.js';
5import ofType from '../utils/of-type.js';
6import ofTypeDeep from '../utils/of-type-deep.js';
7import { partial, exact, } from '../utils/match-shape.js';
8import { Predicate } from './predicate.js';
9export class ObjectPredicate extends Predicate {
10 /**
11 @hidden
12 */
13 constructor(options) {
14 super('object', options);
15 }
16 /**
17 Test if an Object is a plain object.
18 */
19 get plain() {
20 return this.addValidator({
21 message: (_, label) => `Expected ${label} to be a plain object`,
22 validator: object => is.plainObject(object),
23 });
24 }
25 /**
26 Test an object to be empty.
27 */
28 get empty() {
29 return this.addValidator({
30 message: (object, label) => `Expected ${label} to be empty, got \`${JSON.stringify(object)}\``,
31 validator: object => Object.keys(object).length === 0,
32 });
33 }
34 /**
35 Test an object to be not empty.
36 */
37 get nonEmpty() {
38 return this.addValidator({
39 message: (_, label) => `Expected ${label} to not be empty`,
40 validator: object => Object.keys(object).length > 0,
41 });
42 }
43 /**
44 Test all the values in the object to match the provided predicate.
45
46 @param predicate - The predicate that should be applied against every value in the object.
47 */
48 valuesOfType(predicate) {
49 return this.addValidator({
50 message: (_, label, error) => `(${label}) ${error}`,
51 validator: object => ofType(Object.values(object), 'values', predicate),
52 });
53 }
54 /**
55 Test all the values in the object deeply to match the provided predicate.
56
57 @param predicate - The predicate that should be applied against every value in the object.
58 */
59 deepValuesOfType(predicate) {
60 return this.addValidator({
61 message: (_, label, error) => `(${label}) ${error}`,
62 validator: object => ofTypeDeep(object, predicate),
63 });
64 }
65 /**
66 Test an object to be deeply equal to the provided object.
67
68 @param expected - Expected object to match.
69 */
70 deepEqual(expected) {
71 return this.addValidator({
72 message: (object, label) => `Expected ${label} to be deeply equal to \`${JSON.stringify(expected)}\`, got \`${JSON.stringify(object)}\``,
73 validator: object => deepEqual(object, expected),
74 });
75 }
76 /**
77 Test an object to be of a specific instance type.
78
79 @param instance - The expected instance type of the object.
80 */
81 instanceOf(instance) {
82 return this.addValidator({
83 message(object, label) {
84 let { name } = object?.constructor ?? {};
85 if (!name || name === 'Object') {
86 name = JSON.stringify(object);
87 }
88 return `Expected ${label} \`${name}\` to be of type \`${instance.name}\``;
89 },
90 validator: object => object instanceof instance,
91 });
92 }
93 /**
94 Test an object to include all the provided keys. You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties.
95
96 @param keys - The keys that should be present in the object.
97 */
98 hasKeys(...keys) {
99 return this.addValidator({
100 message: (_, label, missingKeys) => `Expected ${label} to have keys \`${JSON.stringify(missingKeys)}\``,
101 validator: object => hasItems({
102 has: item => hasProperty(object, item),
103 }, keys),
104 });
105 }
106 /**
107 Test an object to include any of the provided keys. You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties.
108
109 @param keys - The keys that could be a key in the object.
110 */
111 hasAnyKeys(...keys) {
112 return this.addValidator({
113 message: (_, label) => `Expected ${label} to have any key of \`${JSON.stringify(keys)}\``,
114 validator: object => keys.some(key => hasProperty(object, key)),
115 });
116 }
117 /**
118 Test an object to match the `shape` partially. This means that it ignores unexpected properties. The shape comparison is deep.
119
120 The shape is an object which describes how the tested object should look like. The keys are the same as the source object and the values are predicates.
121
122 @param shape - Shape to test the object against.
123
124 @example
125 ```
126 import ow from 'ow';
127
128 const object = {
129 unicorn: '🦄',
130 rainbow: '🌈'
131 };
132
133 ow(object, ow.object.partialShape({
134 unicorn: ow.string
135 }));
136 ```
137 */
138 partialShape(shape) {
139 return this.addValidator({
140 // TODO: Improve this when message handling becomes smarter
141 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
142 message: (_, label, message) => `${message.replace('Expected', 'Expected property')} in ${label}`,
143 validator: object => partial(object, shape),
144 });
145 }
146 /**
147 Test an object to match the `shape` exactly. This means that will fail if it comes across unexpected properties. The shape comparison is deep.
148
149 The shape is an object which describes how the tested object should look like. The keys are the same as the source object and the values are predicates.
150
151 @param shape - Shape to test the object against.
152
153 @example
154 ```
155 import ow from 'ow';
156
157 ow({unicorn: '🦄'}, ow.object.exactShape({
158 unicorn: ow.string
159 }));
160 ```
161 */
162 exactShape(shape) {
163 // TODO [typescript@>=6] If higher-kinded types are supported natively by typescript, refactor `addValidator` to use them to avoid the usage of `any`. Otherwise, bump or remove this TODO.
164 return this.addValidator({
165 // TODO: Improve this when message handling becomes smarter
166 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
167 message: (_, label, message) => `${message.replace('Expected', 'Expected property')} in ${label}`,
168 validator: object => exact(object, shape),
169 });
170 }
171}