-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: handle disjoint unions cleanly (#24)
This gives a clear error message for disjoint unions, providing there is a single key that is a string, number of enum and has a unique, distinct value in all items within the union. If this is the case, instead of construcing an `anyOf` we use a series of `if`/`then`/`else` constructs to select the appropriate schema based on the value of that key, if the key doesn't match any schemas, we default to a schema that lists the valid values for that key.
- Loading branch information
Forbes Lindesay
authored
Aug 15, 2019
1 parent
cd3bec5
commit 1432be7
Showing
10 changed files
with
461 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export enum EntityTypes { | ||
TypeOne = 'TypeOne', | ||
TypeTwo = 'TypeTwo', | ||
TypeThree = 'TypeThree', | ||
} | ||
export interface EntityOne { | ||
type: EntityTypes.TypeOne; | ||
foo: string; | ||
} | ||
export interface EntityTwo { | ||
type: EntityTypes.TypeTwo; | ||
bar: string; | ||
} | ||
export type Entity = | ||
| EntityOne | ||
| EntityTwo | ||
| {type: EntityTypes.TypeThree; baz: number}; | ||
|
||
export type Value = | ||
| {number: 0; foo: string} | ||
| {number: 1; bar: string} | ||
| {number: 2; baz: string}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
/* tslint:disable */ | ||
// generated by typescript-json-validator | ||
import Ajv = require('ajv'); | ||
import { | ||
EntityTypes, | ||
EntityOne, | ||
EntityTwo, | ||
Entity, | ||
Value, | ||
} from './DisjointUnionExample'; | ||
export const ajv = new Ajv({ | ||
allErrors: true, | ||
coerceTypes: false, | ||
format: 'fast', | ||
nullable: true, | ||
unicode: true, | ||
uniqueItems: true, | ||
useDefaults: true, | ||
}); | ||
|
||
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); | ||
|
||
export {EntityTypes, EntityOne, EntityTwo, Entity, Value}; | ||
export const Schema = { | ||
$schema: 'http://json-schema.org/draft-07/schema#', | ||
definitions: { | ||
Entity: { | ||
else: { | ||
else: { | ||
else: { | ||
properties: { | ||
type: { | ||
enum: ['TypeOne', 'TypeTwo', 'TypeThree'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['type'], | ||
}, | ||
if: { | ||
properties: { | ||
type: { | ||
enum: ['TypeThree'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['type'], | ||
}, | ||
then: { | ||
defaultProperties: [], | ||
properties: { | ||
baz: { | ||
type: 'number', | ||
}, | ||
type: { | ||
enum: ['TypeThree'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['baz', 'type'], | ||
type: 'object', | ||
}, | ||
}, | ||
if: { | ||
properties: { | ||
type: { | ||
enum: ['TypeTwo'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['type'], | ||
}, | ||
then: { | ||
$ref: '#/definitions/EntityTwo', | ||
}, | ||
}, | ||
if: { | ||
properties: { | ||
type: { | ||
enum: ['TypeOne'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['type'], | ||
}, | ||
then: { | ||
$ref: '#/definitions/EntityOne', | ||
}, | ||
}, | ||
EntityOne: { | ||
defaultProperties: [], | ||
properties: { | ||
foo: { | ||
type: 'string', | ||
}, | ||
type: { | ||
enum: ['TypeOne'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['foo', 'type'], | ||
type: 'object', | ||
}, | ||
EntityTwo: { | ||
defaultProperties: [], | ||
properties: { | ||
bar: { | ||
type: 'string', | ||
}, | ||
type: { | ||
enum: ['TypeTwo'], | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['bar', 'type'], | ||
type: 'object', | ||
}, | ||
EntityTypes: { | ||
enum: ['TypeOne', 'TypeThree', 'TypeTwo'], | ||
type: 'string', | ||
}, | ||
Value: { | ||
else: { | ||
else: { | ||
else: { | ||
properties: { | ||
number: { | ||
enum: [0, 1, 2], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['number'], | ||
}, | ||
if: { | ||
properties: { | ||
number: { | ||
enum: [2], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['number'], | ||
}, | ||
then: { | ||
defaultProperties: [], | ||
properties: { | ||
baz: { | ||
type: 'string', | ||
}, | ||
number: { | ||
enum: [2], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['baz', 'number'], | ||
type: 'object', | ||
}, | ||
}, | ||
if: { | ||
properties: { | ||
number: { | ||
enum: [1], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['number'], | ||
}, | ||
then: { | ||
defaultProperties: [], | ||
properties: { | ||
bar: { | ||
type: 'string', | ||
}, | ||
number: { | ||
enum: [1], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['bar', 'number'], | ||
type: 'object', | ||
}, | ||
}, | ||
if: { | ||
properties: { | ||
number: { | ||
enum: [0], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['number'], | ||
}, | ||
then: { | ||
defaultProperties: [], | ||
properties: { | ||
foo: { | ||
type: 'string', | ||
}, | ||
number: { | ||
enum: [0], | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['foo', 'number'], | ||
type: 'object', | ||
}, | ||
}, | ||
}, | ||
}; | ||
ajv.addSchema(Schema, 'Schema'); | ||
export function validate( | ||
typeName: 'EntityTypes', | ||
): (value: unknown) => EntityTypes; | ||
export function validate(typeName: 'EntityOne'): (value: unknown) => EntityOne; | ||
export function validate(typeName: 'EntityTwo'): (value: unknown) => EntityTwo; | ||
export function validate(typeName: 'Entity'): (value: unknown) => Entity; | ||
export function validate(typeName: 'Value'): (value: unknown) => Value; | ||
export function validate(typeName: string): (value: unknown) => any { | ||
const validator: any = ajv.getSchema(`Schema#/definitions/${typeName}`); | ||
return (value: unknown): any => { | ||
if (!validator) { | ||
throw new Error( | ||
`No validator defined for Schema#/definitions/${typeName}`, | ||
); | ||
} | ||
|
||
const valid = validator(value); | ||
|
||
if (!valid) { | ||
throw new Error( | ||
'Invalid ' + | ||
typeName + | ||
': ' + | ||
ajv.errorsText( | ||
validator.errors!.filter((e: any) => e.keyword !== 'if'), | ||
{dataVar: typeName}, | ||
), | ||
); | ||
} | ||
|
||
return value as any; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import {validate} from '../DisjointUnionExample.validator'; | ||
|
||
// let validate: any; | ||
|
||
test('Enum Keys', () => { | ||
expect(() => | ||
validate('Entity')({type: 'TypeOne'}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Entity: Entity should have required property 'foo'"`, | ||
); | ||
expect(() => | ||
validate('Entity')({type: 'TypeTwo'}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Entity: Entity should have required property 'bar'"`, | ||
); | ||
expect(() => | ||
validate('Entity')({type: 'TypeThree'}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Entity: Entity should have required property 'baz'"`, | ||
); | ||
expect(() => | ||
validate('Entity')({type: 'TypeFour'}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Entity: Entity.type should be equal to one of the allowed values"`, | ||
); | ||
}); | ||
|
||
test('Number Keys', () => { | ||
expect(() => | ||
validate('Value')({number: 0}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Value: Value should have required property 'foo'"`, | ||
); | ||
expect(() => | ||
validate('Value')({number: 1}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Value: Value should have required property 'bar'"`, | ||
); | ||
expect(() => | ||
validate('Value')({number: 2}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Value: Value should have required property 'baz'"`, | ||
); | ||
expect(() => | ||
validate('Value')({type: 'TypeFour'}), | ||
).toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid Value: Value should have required property 'number'"`, | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.