packages/refine/Refine_ContainerCheckers.js (222 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} from './Refine_Checkers';
const {Path, compose, failure, success} = require('./Refine_Checkers');
// Check that the provided value is a plain object and not an instance of some
// other container type, built-in, or user class.
function isPlainObject<T: {...}>(value: T) {
// $FlowIssue[method-unbinding]
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
/**
* checker to assert if a mixed value is an array of
* values determined by a provided checker
*/
function array<V>(valueChecker: Checker<V>): Checker<$ReadOnlyArray<V>> {
return (value, path = new Path()) => {
if (!Array.isArray(value)) {
return failure('value is not an array', path);
}
const len = value.length;
const out = new Array(len);
const warnings = [];
for (let i = 0; i < len; i++) {
const element = value[i];
const result = valueChecker(element, path.extend(`[${i}]`));
if (result.type === 'failure') {
return failure(result.message, result.path);
}
out[i] = result.value;
if (result.warnings.length !== 0) {
warnings.push(...result.warnings);
}
}
return success(out, warnings);
};
}
/**
* checker to assert if a mixed value is a tuple of values
* determined by provided checkers. Extra entries are ignored.
*
* Example:
* ```jsx
* const checker = tuple( number(), string() );
* ```
*
* Example with optional trailing entry:
* ```jsx
* const checker = tuple( number(), voidable(string()));
* ```
*/
function tuple<Checkers: $ReadOnlyArray<Checker<mixed>>>(
...checkers: Checkers
): Checker<$TupleMap<Checkers, <T>(Checker<T>) => T>> {
return (value, path = new Path()) => {
if (!Array.isArray(value)) {
return failure('value is not an array', path);
}
const out = new Array(checkers.length);
const warnings = [];
for (const [i, checker] of checkers.entries()) {
const result = checker(value[i], path.extend(`[${i}]`));
if (result.type === 'failure') {
return failure(result.message, result.path);
}
out[i] = result.value;
if (result.warnings.length !== 0) {
warnings.push(...result.warnings);
}
}
return success(out, warnings);
};
}
/**
* checker to assert if a mixed value is a string-keyed dict of
* values determined by a provided checker
*/
function dict<V>(
valueChecker: Checker<V>,
): Checker<$ReadOnly<{[key: string]: V}>> {
return (value, path = new Path()) => {
if (typeof value !== 'object' || value === null || !isPlainObject(value)) {
return failure('value is not an object', path);
}
const out: {[key: string]: V} = {};
const warnings = [];
for (const [key, element] of Object.entries(value)) {
const result = valueChecker(element, path.extend(`.${key}`));
if (result.type === 'failure') {
return failure(result.message, result.path);
}
out[key] = result.value;
if (result.warnings.length !== 0) {
warnings.push(...result.warnings);
}
}
return success(out, warnings);
};
}
// expose opaque version of optional property as public api,
// forcing consistent usage of built-in `optional` to define optional properties
export opaque type OptionalPropertyChecker<+T> = OptionalProperty<T>;
// not a public api, don't export at root
class OptionalProperty<+T> {
+checker: Checker<T>;
constructor(checker: Checker<T>) {
this.checker = checker;
}
}
/**
* checker which can only be used with `object` or `writablObject`. Marks a
* field as optional, skipping the key in the result if it doesn't
* exist in the input.
*
* @example
* ```jsx
* import {object, string, optional} from 'refine';
*
* const checker = object({a: string(), b: optional(string())});
* assert(checker({a: 1}).type === 'success');
* ```
*/
function optional<+T>(checker: Checker<T>): OptionalPropertyChecker<T | void> {
return new OptionalProperty<T>((value, path = new Path()) => {
const result = checker(value, path);
if (result.type === 'failure') {
return {
...result,
message: '(optional property) ' + result.message,
};
} else {
return result;
}
});
}
/**
* checker to assert if a mixed value is a fixed-property object,
* with key-value pairs determined by a provided object of checkers.
* Any extra properties in the input object values are ignored.
* Class instances are not supported, use the custom() checker for those.
*
* Example:
* ```jsx
* const myObject = object({
* name: string(),
* job: object({
* years: number(),
* title: string(),
* }),
* });
* ```
*
* Properties can be optional using `voidable()` or have default values
* using `withDefault()`:
* ```jsx
* const customer = object({
* name: string(),
* reference: voidable(string()),
* method: withDefault(string(), 'email'),
* });
* ```
*/
function object<
Checkers: $ReadOnly<{
[key: string]: Checker<mixed> | OptionalPropertyChecker<mixed>,
}>,
>(
checkers: Checkers,
): Checker<
$ReadOnly<
$ObjMap<Checkers, <T>(c: Checker<T> | OptionalPropertyChecker<T>) => T>,
>,
> {
const checkerProperties: $ReadOnlyArray<string> = Object.keys(checkers);
return (value, path = new Path()) => {
if (typeof value !== 'object' || value === null || !isPlainObject(value)) {
return failure('value is not an object', path);
}
const out: {[string]: mixed} = {};
const warnings = [];
for (const key of checkerProperties) {
const provided: Checker<mixed> | OptionalProperty<mixed> = checkers[key];
let check: Checker<mixed>;
let element: mixed;
if (provided instanceof OptionalProperty) {
check = provided.checker;
if (!value.hasOwnProperty(key)) {
continue;
}
element = value[key];
} else {
check = provided;
element = value.hasOwnProperty(key) ? value[key] : undefined;
}
const result = check(element, path.extend(`.${key}`));
if (result.type === 'failure') {
return failure(result.message, result.path);
}
out[key] = result.value;
if (result.warnings.length !== 0) {
warnings.push(...result.warnings);
}
}
return success(out, warnings);
};
}
/**
* checker to assert if a mixed value is a Set type
*/
function set<T>(checker: Checker<T>): Checker<$ReadOnlySet<T>> {
return (value, path = new Path()) => {
if (!(value instanceof Set)) {
return failure('value is not a Set', path);
}
const out = new Set();
const warnings = [];
for (const item of value) {
const result = checker(item, path.extend('[]'));
if (result.type === 'failure') {
return failure(result.message, result.path);
}
out.add(result.value);
if (result.warnings.length) {
warnings.push(...result.warnings);
}
}
return success(out, warnings);
};
}
/**
* checker to assert if a mixed value is a Map.
*/
function map<K, V>(
keyChecker: Checker<K>,
valueChecker: Checker<V>,
): Checker<$ReadOnlyMap<K, V>> {
return (value, path = new Path()) => {
if (!(value instanceof Map)) {
return failure('value is not a Map', path);
}
const out = new Map();
const warnings = [];
for (const [k, v] of value.entries()) {
const keyResult = keyChecker(k, path.extend(`[${k}] key`));
if (keyResult.type === 'failure') {
return failure(keyResult.message, keyResult.path);
}
const valueResult = valueChecker(v, path.extend(`[${k}]`));
if (valueResult.type === 'failure') {
return failure(valueResult.message, valueResult.path);
}
out.set(k, v);
warnings.push(...keyResult.warnings, ...valueResult.warnings);
}
return success(out, warnings);
};
}
/**
* identical to `array()` except the resulting value is a writable flow type.
*/
function writableArray<V>(valueChecker: Checker<V>): Checker<Array<V>> {
return compose(array(valueChecker), ({value, warnings}) =>
success([...value], warnings),
);
}
/**
* identical to `dict()` except the resulting value is a writable flow type.
*/
function writableDict<V>(
valueChecker: Checker<V>,
): Checker<{[key: string]: V}> {
return compose(dict(valueChecker), ({value, warnings}) =>
success({...value}, warnings),
);
}
/**
* identical to `object()` except the resulting value is a writable flow type.
*/
function writableObject<
Checkers: $ReadOnly<{
[key: string]: Checker<mixed> | OptionalPropertyChecker<mixed>,
}>,
>(
checkers: Checkers,
): Checker<
$ObjMap<Checkers, <T>(c: Checker<T> | OptionalPropertyChecker<T>) => T>,
> {
return compose(object(checkers), ({value, warnings}) =>
success({...value}, warnings),
);
}
module.exports = {
array,
tuple,
object,
optional,
dict,
set,
map,
writableArray,
writableDict,
writableObject,
};