UNPKG

object-shape-tester

Version:
171 lines (170 loc) 5.57 kB
import { assert, check } from '@augment-vir/assert'; import { stringify } from '@augment-vir/common'; import { Kind, Type, } from '@sinclair/typebox'; import { TypeCompiler } from '@sinclair/typebox/compiler'; import { setShapeDefinitionErrorMessage } from '../errors/error-message.js'; /** * A special key string which is used to tag {@link Shape} instances so that we know they're shapes * instead of part of the shape itself. * * We don't use `instanceof` with a Class constructor for this because that breaks when you have * multiple versions of object-shape-tester defining and consuming shapes. * * @category Internal */ export const shapeIdentifier = Symbol.for('object-shape-tester.shape-identifier'); /** * Defines a shape from the given init. * * @category Define */ export function defineShape(init) { setShapeDefinitionErrorMessage(); if (isShape(init)) { /** * If `init` is already a {@link Shape}, don't construct a new {@link Shape} instance, just * return the original one. */ return init; } const schema = shapeInitToSchema(init); const schemaNoExtraKeys = forceAdditionalProperties(schema, false); const schemaExtraKeys = forceAdditionalProperties(schema, true); const shape = { $_schema: schema, $_schemaNoExtraKeys: schemaNoExtraKeys, $_schemaExtraKeys: schemaExtraKeys, default: schema.default, $_compiledSchema: TypeCompiler.Compile(schema), $_compiledSchemaNoExtraKeys: TypeCompiler.Compile(schemaNoExtraKeys), $_compiledSchemaExtraKeys: TypeCompiler.Compile(schemaExtraKeys), }; Object.defineProperties(shape, { runtimeType: { configurable: false, enumerable: false, get() { throw new Error(`runtimeType cannot be used as a value, it is only for types.`); }, }, [shapeIdentifier]: { configurable: false, enumerable: false, writable: false, value: true, }, }); return shape; } /** * Checks if `input` is a Shape. * * @category Internal */ export function isShape(input) { return check.hasKey(input, shapeIdentifier) && !!input[shapeIdentifier]; } /** * Checks if `input` is a TSchema. * * @category Internal */ export function isSchema(input) { return check.hasKey(input, Kind); } /** * Creates a copy of the given schema and applies `additionalProperties: false` to all object * schemas contained within. * * @category Internal */ function forceAdditionalProperties(current, forcedValue) { const clone = { ...current }; if (Array.isArray(current.anyOf)) { clone.anyOf = current.anyOf.map((entry) => forceAdditionalProperties(entry, forcedValue)); } if (Array.isArray(current.allOf)) { clone.allOf = current.allOf.map((entry) => forceAdditionalProperties(entry, forcedValue)); } if (isSchema(current.items)) { clone.items = forceAdditionalProperties(current.items, forcedValue); } else if (Array.isArray(current.items)) { clone.items = current.items.map((entry) => forceAdditionalProperties(entry, forcedValue)); } if (check.isObject(current.properties)) { const newProps = {}; Object.entries(current.properties).forEach(([key, value,]) => { newProps[key] = forceAdditionalProperties(value, forcedValue); }); clone.properties = newProps; } clone.additionalProperties = forcedValue; return clone; } /** * Converts the shape init to a TSchema. * * @category Internal */ export function shapeInitToSchema(init) { if (isSchema(init)) { return init; } else if (isShape(init)) { return init.$_schema; } else if (check.isFunction(init)) { return Type.Function([], Type.Any(), { default: init, }); } else if (check.isObject(init)) { const objectDefault = {}; const objectType = {}; Object.entries(init).forEach(([key, value,]) => { const valueSchema = shapeInitToSchema(value); objectType[key] = valueSchema; objectDefault[key] = valueSchema.default; }); return Type.Object(objectType, { default: objectDefault, }); } else if (check.isArray(init)) { return Type.Array(Type.Union(init.map((entry) => shapeInitToSchema(entry))), { default: [], }); } else if (check.isPrimitive(init)) { if (check.isString(init)) { return Type.String({ default: init }); } else if (check.isNumber(init)) { return Type.Number({ default: init }); } else if (check.isBoolean(init)) { return Type.Boolean({ default: init }); } else if (check.isSymbol(init)) { return Type.Symbol({ default: init }); } else if (check.isNull(init)) { return Type.Null({ default: null }); } else if (check.isUndefined(init)) { return Type.Undefined({ default: undefined }); } else if (check.isBigInt(init)) { return Type.BigInt({ default: init }); /* node:coverage ignore next 7 */ } else { assert.tsType(init).equals(); assert.never(`Unexpected primitive shape value type: '${typeof init}'`); } } else { throw new Error(`Invalid shape: ${stringify(init)}`); } }