From 2ff32b05613c6fa494e9cb729684d11510ecdcb5 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 2 May 2018 15:06:12 -0400 Subject: [PATCH] feat: omit some properties from error message, close #34 --- README.md | 38 +++++++++-- src/api.ts | 77 ++++++++++++++++------ test/assert-schema-test.ts | 29 +++++++- test/snapshots/assert-schema-test.ts.md | 37 +++++++---- test/snapshots/assert-schema-test.ts.snap | Bin 338 -> 391 bytes 5 files changed, 141 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 0e9e0c6..43af1e7 100644 --- a/README.md +++ b/README.md @@ -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, @@ -108,7 +108,7 @@ const person100: ObjectSchema = { // JSON schema schema: { type: 'object', - title: 'Person', + title: 'Employee', properties: { id: { type: 'string', @@ -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 @@ -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. diff --git a/src/api.ts b/src/api.ts index a403ab5..b80059a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -31,6 +31,7 @@ import { prop, map, filter, + mergeDeepLeft, } from 'ramda' import stringify from 'json-stable-stringify' @@ -270,43 +271,77 @@ export class SchemaError extends Error { } } +type ErrorMessageWhiteList = { + errors: boolean + object: boolean + example: boolean +} + +type AssertBySchemaOptions = { + substitutions: string[] + omit: Partial +} + +const AssertBySchemaDefaults: AssertBySchemaOptions = { + substitutions: [], + omit: { + errors: false, + object: false, + example: false, + }, +} + export const assertBySchema = ( schema: JsonSchema, example: PlainObject = {}, - substitutions: string[] = [], + options?: Partial, 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, @@ -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, +) => (object: PlainObject) => { const example = getExample(schemas)(name)(version) const schema = getObjectSchema(schemas)(name)(version) if (!schema) { @@ -352,7 +389,7 @@ export const assertSchema = ( return assertBySchema( schema.schema, example, - substitutions, + options, label, formats, utils.semverToString(schema.version), diff --git a/test/assert-schema-test.ts b/test/assert-schema-test.ts index bbb7eb1..dc830ba 100644 --- a/test/assert-schema-test.ts +++ b/test/assert-schema-test.ts @@ -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) }) @@ -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) { @@ -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) + } +}) diff --git a/test/snapshots/assert-schema-test.ts.md b/test/snapshots/assert-schema-test.ts.md index 5615b4d..767877c 100644 --- a/test/snapshots/assert-schema-test.ts.md +++ b/test/snapshots/assert-schema-test.ts.md @@ -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:␊ @@ -45,8 +51,11 @@ Generated by [AVA](https://ava.li). "name": "Joe"␊ }` -> list of errors +## whitelist errors only - [ - 'data.name must be name format', - ] \ No newline at end of file +> Snapshot 1 + + `Schema Person@1.0.0 violated␊ + ␊ + Errors:␊ + data.age is less than minimum` diff --git a/test/snapshots/assert-schema-test.ts.snap b/test/snapshots/assert-schema-test.ts.snap index e4476171db468a761600c9cfa2c5ce7621b39bb3..8216dcc636534a779f7e49497a5f72fe01e3bb02 100644 GIT binary patch delta 354 zcmV-o0iFKR0*3=KK~_N^Q*L2!b7*gLAa*ed0RZ~zj7kAj$8e}1w(_f@&x0R}2mk;8 z0000Ji(z13U}ljrFn=IkUUkvV$c80(%i?#dZpO;iX-Eh^5>b1>91&@)gd%goP7EJ;n_;&Lr2$}cLm;z~&@Nz}_r%uQ9uELJE= zEiBC}0!cfU78RxDl_=yVWu+#USaDTzDJUoVP;P^MO1iul!Ucu38j#?jYHnxuwM=3Q4I7U`|?oQEp-h0r#S~At%2) zwJ15U7~~S_c!QmhNgqg?08|00cvk ADgXcg delta 300 zcmV+{0n`461JVLAK~_N^Q*L2!b7*gLAa*ed0RXhLv#f=X2xrg)#mnZg#M0MivGkrkM=E$r-7+i3$O!MaB7f4u*OLdIkz*nfW=1 zC8;S~T&_h$`9;N6Tq%hqiF$d7xv2`7#R^5Kg{7HAAZh2)qN3Ei5{3MvtkmQZE3RrT z1qG$V^i(A)1w#X_TA==l0-!`{3RHzcPG)whLP%t^~H%1taG;9e9rus~AQoX{7h+^} yEGkN@WMqV~1Ox<_{?{`waxyS7GB9!j6)`c0GBN}GFT^N|*Ixiu{VjnM0ssIf#B|sI