-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…94078) Co-authored-by: Patryk Kopyciński <[email protected]>
- Loading branch information
1 parent
198d89d
commit 274a649
Showing
146 changed files
with
7,309 additions
and
795 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
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,177 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import * as t from 'io-ts'; | ||
import { left, right, Either } from 'fp-ts/lib/Either'; | ||
import { pipe } from 'fp-ts/lib/pipeable'; | ||
|
||
import { exactCheck, findDifferencesRecursive } from './exact_check'; | ||
import { foldLeftRight, getPaths } from './test_utils'; | ||
|
||
describe('exact_check', () => { | ||
test('it returns an error if given extra object properties', () => { | ||
const someType = t.exact( | ||
t.type({ | ||
a: t.string, | ||
}) | ||
); | ||
const payload = { a: 'test', b: 'test' }; | ||
const decoded = someType.decode(payload); | ||
const checked = exactCheck(payload, decoded); | ||
const message = pipe(checked, foldLeftRight); | ||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "b"']); | ||
expect(message.schema).toEqual({}); | ||
}); | ||
|
||
test('it returns an error if the data type is not as expected', () => { | ||
type UnsafeCastForTest = Either< | ||
t.Errors, | ||
{ | ||
a: number; | ||
} | ||
>; | ||
|
||
const someType = t.exact( | ||
t.type({ | ||
a: t.string, | ||
}) | ||
); | ||
|
||
const payload = { a: 1 }; | ||
const decoded = someType.decode(payload); | ||
const checked = exactCheck(payload, decoded as UnsafeCastForTest); | ||
const message = pipe(checked, foldLeftRight); | ||
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']); | ||
expect(message.schema).toEqual({}); | ||
}); | ||
|
||
test('it does NOT return an error if given normal object properties', () => { | ||
const someType = t.exact( | ||
t.type({ | ||
a: t.string, | ||
}) | ||
); | ||
const payload = { a: 'test' }; | ||
const decoded = someType.decode(payload); | ||
const checked = exactCheck(payload, decoded); | ||
const message = pipe(checked, foldLeftRight); | ||
expect(getPaths(left(message.errors))).toEqual([]); | ||
expect(message.schema).toEqual(payload); | ||
}); | ||
|
||
test('it will return an existing error and not validate', () => { | ||
const payload = { a: 'test' }; | ||
const validationError: t.ValidationError = { | ||
value: 'Some existing error', | ||
context: [], | ||
message: 'some error', | ||
}; | ||
const error: t.Errors = [validationError]; | ||
const leftValue = left(error); | ||
const checked = exactCheck(payload, leftValue); | ||
const message = pipe(checked, foldLeftRight); | ||
expect(getPaths(left(message.errors))).toEqual(['some error']); | ||
expect(message.schema).toEqual({}); | ||
}); | ||
|
||
test('it will work with a regular "right" payload without any decoding', () => { | ||
const payload = { a: 'test' }; | ||
const rightValue = right(payload); | ||
const checked = exactCheck(payload, rightValue); | ||
const message = pipe(checked, foldLeftRight); | ||
expect(getPaths(left(message.errors))).toEqual([]); | ||
expect(message.schema).toEqual({ a: 'test' }); | ||
}); | ||
|
||
test('it will work with decoding a null payload when the schema expects a null', () => { | ||
const someType = t.union([ | ||
t.exact( | ||
t.type({ | ||
a: t.string, | ||
}) | ||
), | ||
t.null, | ||
]); | ||
const payload = null; | ||
const decoded = someType.decode(payload); | ||
const checked = exactCheck(payload, decoded); | ||
const message = pipe(checked, foldLeftRight); | ||
expect(getPaths(left(message.errors))).toEqual([]); | ||
expect(message.schema).toEqual(null); | ||
}); | ||
|
||
test('it should find no differences recursively with two empty objects', () => { | ||
const difference = findDifferencesRecursive({}, {}); | ||
expect(difference).toEqual([]); | ||
}); | ||
|
||
test('it should find a single difference with two objects with different keys', () => { | ||
const difference = findDifferencesRecursive({ a: 1 }, { b: 1 }); | ||
expect(difference).toEqual(['a']); | ||
}); | ||
|
||
test('it should find a two differences with two objects with multiple different keys', () => { | ||
const difference = findDifferencesRecursive({ a: 1, c: 1 }, { b: 1 }); | ||
expect(difference).toEqual(['a', 'c']); | ||
}); | ||
|
||
test('it should find no differences with two objects with the same keys', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: 1 }, { a: 1, b: 1 }); | ||
expect(difference).toEqual([]); | ||
}); | ||
|
||
test('it should find a difference with two deep objects with different same keys', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1, b: { d: 1 } }); | ||
expect(difference).toEqual(['c']); | ||
}); | ||
|
||
test('it should find a difference within an array', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ a: 1 }] }); | ||
expect(difference).toEqual(['c']); | ||
}); | ||
|
||
test('it should find a no difference when using arrays that are identical', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ c: 1 }] }); | ||
expect(difference).toEqual([]); | ||
}); | ||
|
||
test('it should find differences when one has an array and the other does not', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1 }); | ||
expect(difference).toEqual(['b', '[{"c":1}]']); | ||
}); | ||
|
||
test('it should find differences when one has an deep object and the other does not', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1 }); | ||
expect(difference).toEqual(['b', '{"c":1}']); | ||
}); | ||
|
||
test('it should find differences when one has a deep object with multiple levels and the other does not', () => { | ||
const difference = findDifferencesRecursive({ a: 1, b: { c: { d: 1 } } }, { a: 1 }); | ||
expect(difference).toEqual(['b', '{"c":{"d":1}}']); | ||
}); | ||
|
||
test('it tests two deep objects as the same with no key differences', () => { | ||
const difference = findDifferencesRecursive( | ||
{ a: 1, b: { c: { d: 1 } } }, | ||
{ a: 1, b: { c: { d: 1 } } } | ||
); | ||
expect(difference).toEqual([]); | ||
}); | ||
|
||
test('it tests two deep objects with just one deep key difference', () => { | ||
const difference = findDifferencesRecursive( | ||
{ a: 1, b: { c: { d: 1 } } }, | ||
{ a: 1, b: { c: { e: 1 } } } | ||
); | ||
expect(difference).toEqual(['d']); | ||
}); | ||
|
||
test('it should not find any differences when the original and decoded are both null', () => { | ||
const difference = findDifferencesRecursive(null, null); | ||
expect(difference).toEqual([]); | ||
}); | ||
}); |
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,93 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import * as t from 'io-ts'; | ||
import { left, Either, fold, right } from 'fp-ts/lib/Either'; | ||
import { pipe } from 'fp-ts/lib/pipeable'; | ||
import { isObject, get } from 'lodash/fp'; | ||
|
||
/** | ||
* Given an original object and a decoded object this will return an error | ||
* if and only if the original object has additional keys that the decoded | ||
* object does not have. If the original decoded already has an error, then | ||
* this will return the error as is and not continue. | ||
* | ||
* NOTE: You MUST use t.exact(...) for this to operate correctly as your schema | ||
* needs to remove additional keys before the compare | ||
* | ||
* You might not need this in the future if the below issue is solved: | ||
* https://github.com/gcanti/io-ts/issues/322 | ||
* | ||
* @param original The original to check if it has additional keys | ||
* @param decoded The decoded either which has either an existing error or the | ||
* decoded object which could have additional keys stripped from it. | ||
*/ | ||
export const exactCheck = <T>( | ||
original: unknown, | ||
decoded: Either<t.Errors, T> | ||
): Either<t.Errors, T> => { | ||
const onLeft = (errors: t.Errors): Either<t.Errors, T> => left(errors); | ||
const onRight = (decodedValue: T): Either<t.Errors, T> => { | ||
const differences = findDifferencesRecursive(original, decodedValue); | ||
if (differences.length !== 0) { | ||
const validationError: t.ValidationError = { | ||
value: differences, | ||
context: [], | ||
message: `invalid keys "${differences.join(',')}"`, | ||
}; | ||
const error: t.Errors = [validationError]; | ||
return left(error); | ||
} else { | ||
return right(decodedValue); | ||
} | ||
}; | ||
return pipe(decoded, fold(onLeft, onRight)); | ||
}; | ||
|
||
export const findDifferencesRecursive = <T>(original: unknown, decodedValue: T): string[] => { | ||
if (decodedValue === null && original === null) { | ||
// both the decodedValue and the original are null which indicates that they are equal | ||
// so do not report differences | ||
return []; | ||
} else if (decodedValue == null) { | ||
try { | ||
// It is null and painful when the original contains an object or an array | ||
// the the decoded value does not have. | ||
return [JSON.stringify(original)]; | ||
} catch (err) { | ||
return ['circular reference']; | ||
} | ||
} else if (typeof original !== 'object' || original == null) { | ||
// We are not an object or null so do not report differences | ||
return []; | ||
} else { | ||
const decodedKeys = Object.keys(decodedValue); | ||
const differences = Object.keys(original).flatMap((originalKey) => { | ||
const foundKey = decodedKeys.some((key) => key === originalKey); | ||
const topLevelKey = foundKey ? [] : [originalKey]; | ||
// I use lodash to cheat and get an any (not going to lie ;-)) | ||
const valueObjectOrArrayOriginal = get(originalKey, original); | ||
const valueObjectOrArrayDecoded = get(originalKey, decodedValue); | ||
if (isObject(valueObjectOrArrayOriginal)) { | ||
return [ | ||
...topLevelKey, | ||
...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), | ||
]; | ||
} else if (Array.isArray(valueObjectOrArrayOriginal)) { | ||
return [ | ||
...topLevelKey, | ||
...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => | ||
findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) | ||
), | ||
]; | ||
} else { | ||
return topLevelKey; | ||
} | ||
}); | ||
return differences; | ||
} | ||
}; |
Oops, something went wrong.