Skip to content
This repository has been archived by the owner on Sep 29, 2023. It is now read-only.

Commit

Permalink
feat: omit some properties from error message, close #34
Browse files Browse the repository at this point in the history
  • Loading branch information
bahmutov committed May 2, 2018
1 parent 0ff1cfe commit 2ff32b0
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 40 deletions.
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Now every time you use your schemas, pass the formats too so that the validator

```typescript
// example JSON schema using uuid custom format
const person100: ObjectSchema = {
const employee100: ObjectSchema = {
// has semantic version numbers
version: {
major: 1,
Expand All @@ -108,7 +108,7 @@ const person100: ObjectSchema = {
// JSON schema
schema: {
type: 'object',
title: 'Person',
title: 'Employee',
properties: {
id: {
type: 'string',
Expand All @@ -120,8 +120,8 @@ const person100: ObjectSchema = {
id: 'a368dbfd-08e4-4698-b9a3-b2b660a11835',
},
}
// person100 goes into "schemas", then
assertSchema(schemas, formats)('person', '1.0.0')(someObject)
// employee100 goes into "schemas", then
assertSchema(schemas, formats)('Employee', '1.0.0')(someObject)
```

## API
Expand Down Expand Up @@ -207,6 +207,36 @@ try {
}
```

You can substitute some fields from example object to help with dynamic data. For example, to avoid breaking on invalid `id` value, we can tell `assertSchema` to use `id` value from the example object.

```js
const o = {
name: 'Mary',
age: -1,
}
assertSchema(schemas, formats)('Person', '1.0.0', {
substitutions: ['age'],
})(o)
// everything is good, because the actual object asserted was
// {name: 'Mary', age: 10}
```

You can also limit the error message and omit some properties. Typically the error message with include list of errors, current and example objects, which might create a wall of text. To omit `object` and `example` but leave other fields when forming error message use

```js
const o = {
name: 'Mary',
age: -1,
}
assertSchema(schemas, formats)('Person', '1.0.0', {
omit: {
object: true,
example: true,
},
})(o)
// Error message is much much shorter, only "errors" and label will be there
```

### bind

There are multiple methods to validate, assert or sanitize an object against a schema. All take schemas and (optional) formats. But a project using schema tools probably has a single collection of schemas that it wants to use again and again. The `bind` method makes it easy to bind the first argument in each function to a schema collection and just call methods with an object later.
Expand Down
77 changes: 57 additions & 20 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
prop,
map,
filter,
mergeDeepLeft,
} from 'ramda'
import stringify from 'json-stable-stringify'

Expand Down Expand Up @@ -270,43 +271,77 @@ export class SchemaError extends Error {
}
}

type ErrorMessageWhiteList = {
errors: boolean
object: boolean
example: boolean
}

type AssertBySchemaOptions = {
substitutions: string[]
omit: Partial<ErrorMessageWhiteList>
}

const AssertBySchemaDefaults: AssertBySchemaOptions = {
substitutions: [],
omit: {
errors: false,
object: false,
example: false,
},
}

export const assertBySchema = (
schema: JsonSchema,
example: PlainObject = {},
substitutions: string[] = [],
options?: Partial<AssertBySchemaOptions>,
label?: string,
formats?: JsonSchemaFormats,
schemaVersion?: SchemaVersion,
) => (object: PlainObject) => {
const allOptions = mergeDeepLeft(
options || AssertBySchemaDefaults,
AssertBySchemaDefaults,
)

const replace = () => {
const cloned = cloneDeep(object)
substitutions.forEach(property => {
allOptions.substitutions.forEach(property => {
const value = get(example, property)
set(cloned, property, value)
})
return cloned
}

const replaced = substitutions.length ? replace() : object
const replaced = allOptions.substitutions.length ? replace() : object
const result = validateBySchema(schema, formats)(replaced)
if (result === true) {
return object
}

const title = label ? `Schema ${label} violated` : 'Schema violated'
const start = [title, '', 'Errors:']
.concat(result)
.concat(['', 'Current object:'])
const objectString = stringify(replaced, { space: ' ' })
const exampleString = stringify(example, { space: ' ' })

const message =
start.join('\n') +
'\n' +
objectString +
'\n\n' +
'Expected object like this:\n' +
exampleString
const emptyLine = ''
let parts = [title]

if (!allOptions.omit.errors) {
parts = parts.concat([emptyLine, 'Errors:']).concat(result)
}

if (!allOptions.omit.object) {
const objectString = stringify(replaced, { space: ' ' })
parts = parts.concat([emptyLine, 'Current object:', objectString])
}

if (!allOptions.omit.example) {
const exampleString = stringify(example, { space: ' ' })
parts = parts.concat([
emptyLine,
'Expected object like this:',
exampleString,
])
}

const message = parts.join('\n')

throw new SchemaError(
message,
Expand Down Expand Up @@ -338,9 +373,11 @@ export const assertBySchema = (
export const assertSchema = (
schemas: SchemaCollection,
formats?: JsonSchemaFormats,
) => (name: string, version: string, substitutions: string[] = []) => (
object: PlainObject,
) => {
) => (
name: string,
version: string,
options?: Partial<AssertBySchemaOptions>,
) => (object: PlainObject) => {
const example = getExample(schemas)(name)(version)
const schema = getObjectSchema(schemas)(name)(version)
if (!schema) {
Expand All @@ -352,7 +389,7 @@ export const assertSchema = (
return assertBySchema(
schema.schema,
example,
substitutions,
options,
label,
formats,
utils.semverToString(schema.version),
Expand Down
29 changes: 27 additions & 2 deletions test/assert-schema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ test('passing membership invitation 1.0.0 with field substitution', t => {
age: -1,
}
// replace "age" value with value from the example
const assert = assertSchema(schemas)('Person', '1.0.0', ['age'])
const assert = assertSchema(schemas)('Person', '1.0.0', {
substitutions: ['age'],
})
const fn = () => assert(o)
t.notThrows(fn)
})
Expand All @@ -98,7 +100,10 @@ test('error message has object with substitutions', t => {
}
// replace "age" value with value from the example
// but the "name" does not match schema format
const assert = assertSchema(schemas, formats)('Person', '1.0.0', ['age'])
const assert = assertSchema(schemas, formats)('Person', '1.0.0', {
substitutions: ['age'],
})

try {
assert(o)
} catch (e) {
Expand Down Expand Up @@ -130,3 +135,23 @@ test('lists additional properties', t => {
t.deepEqual(e.errors, ['data has additional properties: foo'])
}
})

test('whitelist errors only', t => {
t.plan(1)

const o = {
name: 'test',
age: -2,
}
const assert = assertSchema(schemas)('Person', '1.0.0', {
omit: {
object: true,
example: true,
},
})
try {
assert(o)
} catch (e) {
t.snapshot(e.message)
}
})
37 changes: 23 additions & 14 deletions test/snapshots/assert-schema-test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,45 @@ The actual snapshot is saved in `assert-schema-test.ts.snap`.

Generated by [AVA](https://ava.li).

## missing name membership invitation 1.0.0
## error message has object with substitutions

> Snapshot 1
> error message
`Schema Person@1.0.0 violated␊
Errors:␊
data.name is required
data.name must be name format
Current object:␊
{␊
"age": 10␊
"age": 10,␊
"name": "lowercase"␊
}␊
Expected object like this:␊
{␊
"age": 10,␊
"name": "Joe"␊
}
}`

## error message has object with substitutions
> list of errors
> error message
[
'data.name must be name format',
]

## missing name membership invitation 1.0.0

> Snapshot 1
`Schema Person@1.0.0 violated␊
Errors:␊
data.name must be name format
data.name is required
Current object:␊
{␊
"age": 10,␊
"name": "lowercase"␊
"age": 10␊
}␊
Expected object like this:␊
Expand All @@ -45,8 +51,11 @@ Generated by [AVA](https://ava.li).
"name": "Joe"␊
}`

> list of errors
## whitelist errors only

[
'data.name must be name format',
]
> Snapshot 1
`Schema Person@1.0.0 violated␊
Errors:␊
data.age is less than minimum`
Binary file modified test/snapshots/assert-schema-test.ts.snap
Binary file not shown.

0 comments on commit 2ff32b0

Please sign in to comment.