packages/refine/Refine_UtilityCheckers.js (136 lines of code) (raw):
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* refine: type-refinement combinator library for checking mixed values
* see wiki for more info: https://fburl.com/wiki/14q16qqy
*
* @emails oncall+monitoring_interfaces
* @flow strict
* @format
*/
'use strict';
import type {Checker, CheckResult} from './Refine_Checkers';
const {Path, compose, failure, success} = require('./Refine_Checkers');
/**
* Cast the type of a value after passing a given checker
*
* For example:
*
* ```javascript
* import {string, asType} from 'refine';
*
* opaque type ID = string;
*
* const IDChecker: Checker<ID> = asType(string(), s => (s: ID));
* ```
*/
function asType<A, B>(checker: Checker<A>, cast: A => B): Checker<B> {
return compose(checker, ({value, warnings}) =>
success(cast(value), warnings),
);
}
/**
* checker which asserts the value matches
* at least one of the two provided checkers
*/
function or<A, B>(aChecker: Checker<A>, bChecker: Checker<B>): Checker<A | B> {
return (value, path = new Path()) => {
const a = aChecker(value, path);
if (a.type === 'success') {
return success(a.value, a.warnings);
}
const b = bChecker(value, path);
if (b.type === 'success') {
return success(b.value, b.warnings);
}
return failure('value did not match any types in or()', path);
};
}
/**
* checker which asserts the value matches
* at least one of the provided checkers
*
* NOTE: the reason `union` and `or` both exist is that there is a bug
* within flow that prevents extracting the type from `union` without
* annotation -- see https://fburl.com/gz7u6401
*/
function union<V>(...checkers: $ReadOnlyArray<Checker<V>>): Checker<V> {
return (value, path = new Path()) => {
for (const checker of checkers) {
const result = checker(value, path);
if (result.type === 'success') {
return success(result.value, result.warnings);
}
}
return failure('value did not match any types in union', path);
};
}
/**
* Provide a set of checkers to check in sequence to use the first match.
* This is similar to union(), but all checkers must have the same type.
*
* This can be helpful for supporting backward compatibility. For example the
* following loads a string type, but can also convert from a number as the
* previous version or pull from an object as an even older version:
*
* ```jsx
* const backwardCompatibilityChecker: Checker<string> = match(
* string(),
* asType(number(), num => `${num}`),
* asType(object({num: number()}), obj => `${obj.num}`),
* );
* ```
*/
function match<T>(...checkers: $ReadOnlyArray<Checker<T>>): Checker<T> {
return union(...checkers);
}
/**
* wraps a given checker, making the valid value nullable
*
* By default, a value passed to nullable must match the checker spec exactly
* when it is not null, or it will fail.
*
* passing the `nullWithWarningWhenInvalid` enables gracefully handling invalid
* values that are less important -- if the provided checker is invalid,
* the new checker will return null.
*
* For example:
*
* ```javascript
* import {nullable, record, string} from 'refine';
*
* const Options = object({
* // this must be a non-null string,
* // or Options is not valid
* filename: string(),
*
* // if this field is not a string,
* // it will be null and Options will pass the checker
* description: nullable(string(), {
* nullWithWarningWhenInvalid: true,
* })
* })
*
* const result = Options({filename: 'test', description: 1});
*
* invariant(result.type === 'success');
* invariant(result.value.description === null);
* invariant(result.warnings.length === 1); // there will be a warning
* ```
*/
function nullable<T>(
checker: Checker<T>,
options?: $ReadOnly<{
// if this is true, the checker will not fail
// validation if an invalid value is provided, instead
// returning null and including a warning as to the invalid type.
nullWithWarningWhenInvalid?: boolean,
}>,
): Checker<?T> {
const {nullWithWarningWhenInvalid = false} = options ?? {};
return (value, parentPath = new Path()): CheckResult<?T> => {
if (value == null) {
return success(value, []);
}
const result = checker(value, parentPath);
if (result.type === 'success') {
return success(result.value, result.warnings);
}
// if this is enabled, "succeed" the checker with a warning
// if the non-null value does not match expectation
if (nullWithWarningWhenInvalid) {
return success(null, [result]);
}
const {message, path} = result;
return failure(message, path);
};
}
/**
* wraps a given checker, making the valid value voidable
*
* By default, a value passed to voidable must match the checker spec exactly
* when it is not undefined, or it will fail.
*
* passing the `undefinedWithWarningWhenInvalid` enables gracefully handling invalid
* values that are less important -- if the provided checker is invalid,
* the new checker will return undefined.
*
* For example:
*
* ```javascript
* import {voidable, record, string} from 'refine';
*
* const Options = object({
* // this must be a string, or Options is not valid
* filename: string(),
*
* // this must be a string or undefined,
* // or Options is not valid
* displayName: voidable(string()),
*
* // if this field is not a string,
* // it will be undefined and Options will pass the checker
* description: voidable(string(), {
* undefinedWithWarningWhenInvalid: true,
* })
* })
*
* const result = Options({filename: 'test', description: 1});
*
* invariant(result.type === 'success');
* invariant(result.value.description === undefined);
* invariant(result.warnings.length === 1); // there will be a warning
* ```
*/
function voidable<T>(
checker: Checker<T>,
options?: $ReadOnly<{
// if this is true, the checker will not fail
// validation if an invalid value is provided, instead
// returning undefined and including a warning as to the invalid type.
undefinedWithWarningWhenInvalid?: boolean,
}>,
): Checker<T | void> {
const {undefinedWithWarningWhenInvalid = false} = options ?? {};
return (value, parentPath = new Path()): CheckResult<T | void> => {
if (value === undefined) {
return success(undefined, []);
}
const result = checker(value, parentPath);
if (result.type === 'success') {
return success(result.value, result.warnings);
}
// if this is enabled, "succeed" the checker with a warning
// if the non-void value does not match expectation
if (undefinedWithWarningWhenInvalid) {
return success(undefined, [result]);
}
const {message, path} = result;
return failure(message, path);
};
}
/**
* a checker that provides a withDefault value if the provided value is nullable.
*
* For example:
* ```jsx
* const objPropertyWithDefault = object({
* foo: withDefault(number(), 123),
* });
* ```
* Both `{}` and `{num: 123}` will refine to `{num: 123}`
*/
function withDefault<T>(checker: Checker<T>, fallback: T): Checker<T> {
return (value, path = new Path()) => {
if (value == null) {
return success(fallback, []);
}
const result = checker(value, path);
return result.type === 'failure' || result.value != null
? result
: success(fallback, []);
};
}
/**
* wraps a checker with a logical constraint.
*
* Predicate function can return either a boolean result or
* a tuple with a result and message
*
* For example:
*
* ```javascript
* import {number, constraint} from 'refine';
*
* const evenNumber = constraint(
* number(),
* n => n % 2 === 0
* );
*
* const passes = evenNumber(2);
* // passes.type === 'success';
*
* const fails = evenNumber(1);
* // fails.type === 'failure';
* ```
*/
function constraint<T>(
checker: Checker<T>,
predicate: T => boolean | [boolean, string],
): Checker<T> {
return compose(checker, ({value, warnings}, path) => {
const result = predicate(value);
const [passed, message] =
typeof result === 'boolean'
? [result, 'value failed constraint check']
: result;
return passed ? success(value, warnings) : failure(message, path);
});
}
/**
* wrapper to allow for passing a lazy checker value. This enables
* recursive types by allowing for passing in the returned value of
* another checker. For example:
*
* ```javascript
* const user = object({
* id: number(),
* name: string(),
* friends: array(lazy(() => user))
* });
* ```
*
* Example of array with arbitrary nesting depth:
* ```jsx
* const entry = or(number(), array(lazy(() => entry)));
* const nestedArray = array(entry);
* ```
*/
function lazy<T>(getChecker: () => Checker<T>): Checker<T> {
return (value, path = new Path()) => {
const checker = getChecker();
return checker(value, path);
};
}
/**
* helper to create a custom checker from a provided function.
* If the function returns a non-nullable value, the checker succeeds.
*
* ```jsx
* const myClassChecker = custom(x => x instanceof MyClass ? x : null);
* ```
*
* Nullable custom types can be created by composing with `nullable()` or
* `voidable()` checkers:
*
* ```jsx
* const maybeMyClassChecker =
* nullable(custom(x => x instanceof MyClass ? x : null));
* ```
*/
function custom<T>(
checkValue: (value: mixed) => ?T,
failureMessage: string = `failed to return non-null from custom checker.`,
): Checker<T> {
return (value, path = new Path()) => {
try {
const checked = checkValue(value);
return checked != null
? success(checked, [])
: failure(failureMessage, path);
} catch (error) {
return failure(error.message, path);
}
};
}
module.exports = {
or,
union,
match,
nullable,
voidable,
withDefault,
constraint,
asType,
lazy,
custom,
};