Skip to content

Commit 26fc46c

Browse files
committed
fix: object property validation and more tests
1 parent be40ce2 commit 26fc46c

9 files changed

+137
-9
lines changed

src/errorMessages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ const errorMessages = {
2626
ERR_EXPECT_THIS: 'must be exactly this',
2727
ERR_EXPECT_VOID: 'must be undefined',
2828
ERR_INVALID_DATE: 'must be a valid date',
29-
ERR_MISSING_PROPERTY: 'does not exist on object',
29+
ERR_MISSING_PROPERTY: 'must have property: $0',
3030
ERR_NO_INDEXER: 'is not one of the permitted indexer types',
3131
ERR_NO_UNION: 'must be one of: $0',
32-
ERR_UNKNOWN_KEY: 'should not contain the key: "$0"',
32+
ERR_UNKNOWN_KEY: 'should not contain the key: $0',
3333
}
3434

3535
export type ErrorKey = keyof typeof errorMessages

src/errorReporting/makeTypeError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function makeTypeError<T>(
1616
const { prefix, input, errors } = validation
1717
const collected = []
1818
for (const [path, message, expectedType] of errors) {
19-
const expected = expectedType ? expectedType.toString() : '*'
19+
const expected = expectedType ? expectedType.toString() : 'any'
2020
const actual = resolvePath(input, path)
2121
const actualType = typeOf(actual)
2222

src/errorReporting/typeOf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function keyToString(key: string | number | symbol): string {
1+
export function keyToString(key: string | number | symbol): string {
22
switch (typeof key) {
33
case 'symbol':
44
return `[${String(key)}]`

src/null.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as t from './'
2+
import { expect } from 'chai'
3+
import dedent from 'dedent-js'
4+
import typeOf from './errorReporting/typeOf'
5+
6+
describe(`t.null`, function() {
7+
it(`accepts null`, function() {
8+
t.null().assert(null)
9+
expect(t.null().accepts(null)).to.be.true
10+
})
11+
it(`rejects everything else`, function() {
12+
for (const value of [true, 'foo', undefined, 2, [], {}]) {
13+
expect(t.null().accepts(value)).to.be.false
14+
expect(() => t.null().assert(value)).to.throw(
15+
t.RuntimeTypeError,
16+
dedent`
17+
Value must be null
18+
19+
Expected: null
20+
21+
Actual Value: ${JSON.stringify(value)}
22+
23+
Actual Type: ${typeOf(value)}`
24+
)
25+
}
26+
})
27+
})

src/object.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as t from './'
2+
import { expect } from 'chai'
3+
import dedent from 'dedent-js'
4+
import typeOf from './errorReporting/typeOf'
5+
6+
describe(`t.object`, function() {
7+
const Person = t.object<{ name: any; age?: any }>()({
8+
name: t.string(),
9+
age: t.optionalNullOr(t.number()),
10+
})
11+
it(`accepts matching object`, function() {
12+
for (const value of [
13+
{ name: 'Jimbo' },
14+
{ name: 'Jimbo', age: null },
15+
{ name: 'Jimbo', age: 20 },
16+
]) {
17+
expect(Person.accepts(value)).to.be.true
18+
Person.assert(value)
19+
}
20+
})
21+
it(`rejects missing properties`, function() {
22+
expect(Person.accepts({ age: 20 })).to.be.false
23+
expect(() => Person.assert({ age: 20 })).to.throw(
24+
t.RuntimeTypeError,
25+
dedent`
26+
Value must have property: name
27+
28+
Expected: ${Person.toString()}
29+
30+
Actual Value: ${JSON.stringify({ age: 20 }, null, 2)}
31+
32+
Actual Type: ${typeOf({ age: 20 })}`
33+
)
34+
})
35+
it(`rejects extraneous properties`, function() {
36+
const value = { name: 'Jimbo', powerLevel: 9001 }
37+
expect(Person.accepts(value)).to.be.false
38+
expect(() => Person.assert(value)).to.throw(
39+
t.RuntimeTypeError,
40+
dedent`
41+
Value should not contain the key: powerLevel
42+
43+
Expected: ${Person.toString()}
44+
45+
Actual Value: ${JSON.stringify(value, null, 2)}
46+
47+
Actual Type: ${typeOf(value)}`
48+
)
49+
})
50+
it(`rejects everything else`, function() {
51+
for (const value of [true, 'foo', undefined, 2, []]) {
52+
expect(Person.accepts(value)).to.be.false
53+
expect(() => Person.assert(value)).to.throw(
54+
t.RuntimeTypeError,
55+
dedent`
56+
Value must be an object
57+
58+
Expected: ${Person.toString()}
59+
60+
Actual Value: ${JSON.stringify(value)}
61+
62+
Actual Type: ${typeOf(value)}`
63+
)
64+
}
65+
})
66+
})

src/ref.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe(`t.ref`, function() {
5858
).to.throw(
5959
t.RuntimeTypeError,
6060
dedent`
61-
node.left.right should not contain the key: "bar"
61+
node.left.right should not contain the key: bar
6262
6363
Expected: {
6464
value: any;

src/types/ObjectType.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
startToStringCycle,
1919
endToStringCycle,
2020
} from '../cyclic'
21+
import { keyToString } from '../errorReporting/typeOf'
2122

2223
export default class ObjectType<T extends {}> extends Type<T> {
2324
typeName = 'ObjectType'
@@ -34,6 +35,7 @@ export default class ObjectType<T extends {}> extends Type<T> {
3435
this.properties = properties
3536
this.indexers = indexers
3637
this.exact = exact
38+
properties.forEach(prop => (prop.__objectType = this))
3739
}
3840

3941
*errors(
@@ -46,7 +48,7 @@ export default class ObjectType<T extends {}> extends Type<T> {
4648
return
4749
}
4850

49-
if (typeof input !== 'object') {
51+
if (typeof input !== 'object' || Array.isArray(input)) {
5052
yield [path, getErrorMessage('ERR_EXPECT_OBJECT'), this]
5153
return
5254
}
@@ -75,7 +77,7 @@ export default class ObjectType<T extends {}> extends Type<T> {
7577
if (input === null) {
7678
return false
7779
}
78-
if (typeof input !== 'object') {
80+
if (typeof input !== 'object' || Array.isArray(input)) {
7981
return false
8082
}
8183
if (inValidationCycle(this, input)) {
@@ -226,7 +228,7 @@ function* collectErrorsExact(
226228
for (const key in input) {
227229
// eslint-disable-line guard-for-in
228230
if (!properties.some(property => property.key === key)) {
229-
yield [path, getErrorMessage('ERR_UNKNOWN_KEY', key), type]
231+
yield [path, getErrorMessage('ERR_UNKNOWN_KEY', keyToString(key)), type]
230232
}
231233
}
232234
}

src/types/ObjectTypeProperty.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010

1111
import Validation, { ErrorTuple, IdentifierPath } from '../Validation'
1212
import getErrorMessage from '../getErrorMessage'
13+
import { keyToString } from '../errorReporting/typeOf'
1314

1415
export default class ObjectTypeProperty<
1516
K extends string | number | symbol,
@@ -20,6 +21,7 @@ export default class ObjectTypeProperty<
2021
readonly value: Type<V>
2122
readonly optional: boolean
2223
readonly constraints: TypeConstraint<V>[] = []
24+
__objectType: Type<any> = null as any
2325

2426
constructor(key: K, value: Type<V>, optional: boolean) {
2527
super()
@@ -50,7 +52,11 @@ export default class ObjectTypeProperty<
5052
// @flowIgnore
5153
const { optional, key, value } = this
5254
if (!optional && !this.existsOn(input)) {
53-
yield [path, getErrorMessage('ERR_MISSING_PROPERTY'), input]
55+
yield [
56+
path,
57+
getErrorMessage('ERR_MISSING_PROPERTY', keyToString(key)),
58+
this.__objectType,
59+
]
5460
return
5561
}
5662
const target = input[key]

src/undefined.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as t from './'
2+
import { expect } from 'chai'
3+
import dedent from 'dedent-js'
4+
import typeOf from './errorReporting/typeOf'
5+
6+
describe(`t.undefined`, function() {
7+
it(`accepts undefined`, function() {
8+
t.undefined().assert(undefined)
9+
expect(t.undefined().accepts(undefined)).to.be.true
10+
})
11+
it(`rejects everything else`, function() {
12+
for (const value of [true, 'foo', null, 2, [], {}]) {
13+
expect(t.undefined().accepts(value)).to.be.false
14+
expect(() => t.undefined().assert(value)).to.throw(
15+
t.RuntimeTypeError,
16+
dedent`
17+
Value must be undefined
18+
19+
Expected: undefined
20+
21+
Actual Value: ${JSON.stringify(value)}
22+
23+
Actual Type: ${typeOf(value)}`
24+
)
25+
}
26+
})
27+
})

0 commit comments

Comments
 (0)