object-shape-tester
Version:
Test object properties and value types.
371 lines (370 loc) • 9.69 kB
JavaScript
import { check } from '@augment-vir/assert';
import { isShapeDefinitionKey, isShapeSpecifierKey } from './shape-keys.js';
/**
* Checks if the given input is a {@link isShapeDefinition}.
*
* @category Util
*/
export function isShapeDefinition(input) {
return check.hasKey(input, isShapeDefinitionKey);
}
/**
* ========================================
*
* Specifier Symbols
*
* ========================================
*/
/**
* Values used to mark the outputs of each sub-shape function (like {@link or}).
*
* @category Internal
*/
export var ShapeSpecifierType;
(function (ShapeSpecifierType) {
ShapeSpecifierType["And"] = "and";
ShapeSpecifierType["Class"] = "class";
ShapeSpecifierType["Enum"] = "enum";
ShapeSpecifierType["Exact"] = "exact";
ShapeSpecifierType["IndexedKeys"] = "indexed-keys";
ShapeSpecifierType["Or"] = "or";
ShapeSpecifierType["Unknown"] = "unknown";
ShapeSpecifierType["NumericRange"] = "numeric-range";
ShapeSpecifierType["Optional"] = "optional";
ShapeSpecifierType["Tuple"] = "tuple";
})(ShapeSpecifierType || (ShapeSpecifierType = {}));
/**
* ========================================
*
* Shape Functions
*
* ========================================
*/
/**
* Create a shape part that combines all of its inputs together with an intersection or "and".
*
* @category Shape Part
* @example
*
* ```ts
* import {and, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: and({q: ''}, {r: -1}, {s: true}),
* });
*
* // `myShape.runtimeType` is `{a: {q: string, r: number, s: boolean}}`
* ```
*/
export function and(...parts) {
return specifier(parts, ShapeSpecifierType.And);
}
/**
* Define a shape part that requires an instance of the given constructor.
*
* @category Shape Part
* @example
*
* ```ts
* import {classShape, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: classShape(RegExp),
* });
*
* // `myShape.runtimeType` is `{a: RegExp}`
* ```
*/
export function classShape(...parts) {
return specifier(parts, ShapeSpecifierType.Class);
}
/**
* Define a shape part that requires an enum value.
*
* @category Shape Part
* @example
*
* ```ts
* import {enumShape, defineShape} from 'object-shape-tester';
*
* enum MyEnum {
* A = 'a',
* B = 'b',
* }
*
* const myShape = defineShape({
* a: enumShape(MyEnum),
* });
*
* // `myShape.runtimeType` is `{a: MyEnum}`
* ```
*/
export function enumShape(...parts) {
return specifier(parts, ShapeSpecifierType.Enum);
}
/**
* Define a shape part that requires _exactly_ the value given.
*
* @category Shape Part
* @example
*
* ```ts
* import {exact, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: or(exact('hi'), exact('bye')),
* });
*
* // `myShape.runtimeType` is `{a: 'hi' | 'bye'}`
* ```
*/
export function exact(...parts) {
return specifier(parts, ShapeSpecifierType.Exact);
}
/**
* Define a shape part that's an object with a specific set of keys and values.
*
* @category Shape Part
* @example
*
* ```ts
* import {exact, defineShape, indexedKeys} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: indexedKeys({
* keys: or(exact('hi'), exact('bye')),
* values: {
* helloThere: 0,
* },
* required: false,
* }),
* });
*
* // `myShape.runtimeType` is `{a: Partial<Record<'hi' | 'bye', {helloThere: number}>>}`
* ```
*/
export function indexedKeys(...parts) {
return specifier(parts, ShapeSpecifierType.IndexedKeys);
}
/**
* Define a shape part requires a tuple.
*
* @category Shape Part
* @example
*
* ```ts
* import {exact, defineShape, tupleShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: tupleShape('a', -1, exact('hi')),
* });
*
* // `myShape.runtimeType` is `[string, number, 'hi']`
* ```
*/
export function tupleShape(...parts) {
return specifier(parts, ShapeSpecifierType.Tuple);
}
/**
* Define a shape part that's a union of all its inputs.
*
* @category Shape Part
* @example
*
* ```ts
* import {or, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: or('', -1),
* });
*
* // `myShape.runtimeType` is `{a: string | number}`
* ```
*/
export function or(...parts) {
return specifier(parts, ShapeSpecifierType.Or);
}
/**
* Define a shape part that resolves simply to `unknown`.
*
* @category Shape Part
* @example
*
* ```ts
* import {unknownShape, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: unknownShape,
* });
*
* // `myShape.runtimeType` is `{a: unknown}`
* ```
*/
export function unknownShape(defaultValue) {
return specifier([defaultValue], ShapeSpecifierType.Unknown);
}
/**
* Define a shape part that requires numbers to be within a specific range, inclusive.
*
* @category Shape Part
* @example
*
* ```ts
* import {numericRange, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* // This will simply produce a type of `number` but will validate runtime values against the range.
* a: numericRange(1, 10),
* });
* // `myShape.runtimeType` is just `{a: number}`
*
* const myShape2 = defineShape({
* // If you want type safety, you must specify the allowed numbers manually
* a: numericRange<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10>(1, 10),
* });
* // `myShape2.runtimeType` is `{a: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}`
* ```
*/
export function numericRange(min, max) {
return specifier([
min,
max,
], ShapeSpecifierType.NumericRange);
}
/**
* Define a shape part that is optional. This only makes sense as a property in an object.
*
* @category Shape Part
* @example
*
* ```ts
* import {optional, defineShape} from 'object-shape-tester';
*
* const myShape = defineShape({
* a: optional(-1),
* });
*
* // `myShape.runtimeType` is `{a?: number}`
* ```
*/
export function optional(part) {
return specifier([part], ShapeSpecifierType.Optional);
}
/**
* ========================================
*
* Shape Specifier Type Guards
*
* ========================================
*/
/**
* Checks if the input is an {@link and} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isAndShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.And);
}
/**
* Checks if the input is a {@link classShape} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isClassShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Class);
}
/**
* Checks if the input is an {@link enumShape} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isEnumShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Enum);
}
/**
* Checks if the input is an {@link exact} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isExactShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Exact);
}
/**
* Checks if the input is an {@link indexedKeys} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isIndexedKeysSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.IndexedKeys);
}
/**
* Checks if the input is an {@link tupleShape} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isTupleShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Tuple);
}
/**
* Checks if the input is an {@link or} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isOrShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Or);
}
/**
* Checks if the input is an {@link unknownShape} shape specifier for internal type guarding
* purposes.
*
* @category Internal
*/
export function isUnknownShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Unknown);
}
/**
* Checks if the input is a {@link numericRange} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isNumericRangeShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.NumericRange);
}
/**
* Checks if the input is a {@link optional} shape specifier for internal type guarding purposes.
*
* @category Internal
*/
export function isOptionalShapeSpecifier(maybeSpecifier) {
return specifierHasSymbol(maybeSpecifier, ShapeSpecifierType.Optional);
}
/**
* ========================================
*
* Specifier Utilities
*
* ========================================
*/
function specifierHasSymbol(maybeSpecifier, symbol) {
const specifier = getShapeSpecifier(maybeSpecifier);
return !!specifier && specifier.specifierType === symbol;
}
function specifier(parts, specifierType) {
return {
[isShapeSpecifierKey]: true,
specifierType,
parts,
};
}
/**
* If the input is a shape specifier, return it type guarded as such.
*
* @category Internal
* @returns `undefined` if the input is not
*/
export function getShapeSpecifier(input) {
if (!check.isObject(input) || !check.hasKey(input, isShapeSpecifierKey)) {
return undefined;
}
return input;
}