663 lines
31 KiB
Plaintext
663 lines
31 KiB
Plaintext
import { TransformEncode, TransformDecode, HasTransform, TransformDecodeCheckError, TransformEncodeCheckError } from '../value/transform/index.mjs';
|
|
import { Errors } from '../errors/index.mjs';
|
|
import { TypeSystemPolicy } from '../system/index.mjs';
|
|
import { TypeBoxError } from '../type/error/index.mjs';
|
|
import { Deref } from '../value/deref/index.mjs';
|
|
import { Hash } from '../value/hash/index.mjs';
|
|
import { Kind } from '../type/symbols/index.mjs';
|
|
import { TypeRegistry, FormatRegistry } from '../type/registry/index.mjs';
|
|
import { KeyOfPattern } from '../type/keyof/index.mjs';
|
|
import { ExtendsUndefinedCheck } from '../type/extends/extends-undefined.mjs';
|
|
import { Never } from '../type/never/index.mjs';
|
|
import { Ref } from '../type/ref/index.mjs';
|
|
// ------------------------------------------------------------------
|
|
// ValueGuard
|
|
// ------------------------------------------------------------------
|
|
import { IsArray, IsString, IsNumber, IsBigInt } from '../value/guard/index.mjs';
|
|
// ------------------------------------------------------------------
|
|
// TypeGuard
|
|
// ------------------------------------------------------------------
|
|
import { IsSchema } from '../type/guard/type.mjs';
|
|
// ------------------------------------------------------------------
|
|
// TypeCheck
|
|
// ------------------------------------------------------------------
|
|
export class TypeCheck {
|
|
constructor(schema, references, checkFunc, code) {
|
|
this.schema = schema;
|
|
this.references = references;
|
|
this.checkFunc = checkFunc;
|
|
this.code = code;
|
|
this.hasTransform = HasTransform(schema, references);
|
|
}
|
|
/** Returns the generated assertion code used to validate this type. */
|
|
Code() {
|
|
return this.code;
|
|
}
|
|
/** Returns the schema type used to validate */
|
|
Schema() {
|
|
return this.schema;
|
|
}
|
|
/** Returns reference types used to validate */
|
|
References() {
|
|
return this.references;
|
|
}
|
|
/** Returns an iterator for each error in this value. */
|
|
Errors(value) {
|
|
return Errors(this.schema, this.references, value);
|
|
}
|
|
/** Returns true if the value matches the compiled type. */
|
|
Check(value) {
|
|
return this.checkFunc(value);
|
|
}
|
|
/** Decodes a value or throws if error */
|
|
Decode(value) {
|
|
if (!this.checkFunc(value))
|
|
throw new TransformDecodeCheckError(this.schema, value, this.Errors(value).First());
|
|
return (this.hasTransform ? TransformDecode(this.schema, this.references, value) : value);
|
|
}
|
|
/** Encodes a value or throws if error */
|
|
Encode(value) {
|
|
const encoded = this.hasTransform ? TransformEncode(this.schema, this.references, value) : value;
|
|
if (!this.checkFunc(encoded))
|
|
throw new TransformEncodeCheckError(this.schema, value, this.Errors(value).First());
|
|
return encoded;
|
|
}
|
|
}
|
|
// ------------------------------------------------------------------
|
|
// Character
|
|
// ------------------------------------------------------------------
|
|
var Character;
|
|
(function (Character) {
|
|
function DollarSign(code) {
|
|
return code === 36;
|
|
}
|
|
Character.DollarSign = DollarSign;
|
|
function IsUnderscore(code) {
|
|
return code === 95;
|
|
}
|
|
Character.IsUnderscore = IsUnderscore;
|
|
function IsAlpha(code) {
|
|
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
}
|
|
Character.IsAlpha = IsAlpha;
|
|
function IsNumeric(code) {
|
|
return code >= 48 && code <= 57;
|
|
}
|
|
Character.IsNumeric = IsNumeric;
|
|
})(Character || (Character = {}));
|
|
// ------------------------------------------------------------------
|
|
// MemberExpression
|
|
// ------------------------------------------------------------------
|
|
var MemberExpression;
|
|
(function (MemberExpression) {
|
|
function IsFirstCharacterNumeric(value) {
|
|
if (value.length === 0)
|
|
return false;
|
|
return Character.IsNumeric(value.charCodeAt(0));
|
|
}
|
|
function IsAccessor(value) {
|
|
if (IsFirstCharacterNumeric(value))
|
|
return false;
|
|
for (let i = 0; i < value.length; i++) {
|
|
const code = value.charCodeAt(i);
|
|
const check = Character.IsAlpha(code) || Character.IsNumeric(code) || Character.DollarSign(code) || Character.IsUnderscore(code);
|
|
if (!check)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function EscapeHyphen(key) {
|
|
return key.replace(/'/g, "\\'");
|
|
}
|
|
function Encode(object, key) {
|
|
return IsAccessor(key) ? `${object}.${key}` : `${object}['${EscapeHyphen(key)}']`;
|
|
}
|
|
MemberExpression.Encode = Encode;
|
|
})(MemberExpression || (MemberExpression = {}));
|
|
// ------------------------------------------------------------------
|
|
// Identifier
|
|
// ------------------------------------------------------------------
|
|
var Identifier;
|
|
(function (Identifier) {
|
|
function Encode($id) {
|
|
const buffer = [];
|
|
for (let i = 0; i < $id.length; i++) {
|
|
const code = $id.charCodeAt(i);
|
|
if (Character.IsNumeric(code) || Character.IsAlpha(code)) {
|
|
buffer.push($id.charAt(i));
|
|
}
|
|
else {
|
|
buffer.push(`_${code}_`);
|
|
}
|
|
}
|
|
return buffer.join('').replace(/__/g, '_');
|
|
}
|
|
Identifier.Encode = Encode;
|
|
})(Identifier || (Identifier = {}));
|
|
// ------------------------------------------------------------------
|
|
// LiteralString
|
|
// ------------------------------------------------------------------
|
|
var LiteralString;
|
|
(function (LiteralString) {
|
|
function Escape(content) {
|
|
return content.replace(/'/g, "\\'");
|
|
}
|
|
LiteralString.Escape = Escape;
|
|
})(LiteralString || (LiteralString = {}));
|
|
// ------------------------------------------------------------------
|
|
// Errors
|
|
// ------------------------------------------------------------------
|
|
export class TypeCompilerUnknownTypeError extends TypeBoxError {
|
|
constructor(schema) {
|
|
super('Unknown type');
|
|
this.schema = schema;
|
|
}
|
|
}
|
|
export class TypeCompilerTypeGuardError extends TypeBoxError {
|
|
constructor(schema) {
|
|
super('Preflight validation check failed to guard for the given schema');
|
|
this.schema = schema;
|
|
}
|
|
}
|
|
// ------------------------------------------------------------------
|
|
// Policy
|
|
// ------------------------------------------------------------------
|
|
export var Policy;
|
|
(function (Policy) {
|
|
function IsExactOptionalProperty(value, key, expression) {
|
|
return TypeSystemPolicy.ExactOptionalPropertyTypes ? `('${key}' in ${value} ? ${expression} : true)` : `(${MemberExpression.Encode(value, key)} !== undefined ? ${expression} : true)`;
|
|
}
|
|
Policy.IsExactOptionalProperty = IsExactOptionalProperty;
|
|
function IsObjectLike(value) {
|
|
return !TypeSystemPolicy.AllowArrayObject ? `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}))` : `(typeof ${value} === 'object' && ${value} !== null)`;
|
|
}
|
|
Policy.IsObjectLike = IsObjectLike;
|
|
function IsRecordLike(value) {
|
|
return !TypeSystemPolicy.AllowArrayObject
|
|
? `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}) && !(${value} instanceof Date) && !(${value} instanceof Uint8Array))`
|
|
: `(typeof ${value} === 'object' && ${value} !== null && !(${value} instanceof Date) && !(${value} instanceof Uint8Array))`;
|
|
}
|
|
Policy.IsRecordLike = IsRecordLike;
|
|
function IsNumberLike(value) {
|
|
return TypeSystemPolicy.AllowNaN ? `typeof ${value} === 'number'` : `Number.isFinite(${value})`;
|
|
}
|
|
Policy.IsNumberLike = IsNumberLike;
|
|
function IsVoidLike(value) {
|
|
return TypeSystemPolicy.AllowNullVoid ? `(${value} === undefined || ${value} === null)` : `${value} === undefined`;
|
|
}
|
|
Policy.IsVoidLike = IsVoidLike;
|
|
})(Policy || (Policy = {}));
|
|
/** Compiles Types for Runtime Type Checking */
|
|
export var TypeCompiler;
|
|
(function (TypeCompiler) {
|
|
// ----------------------------------------------------------------
|
|
// Guards
|
|
// ----------------------------------------------------------------
|
|
function IsAnyOrUnknown(schema) {
|
|
return schema[Kind] === 'Any' || schema[Kind] === 'Unknown';
|
|
}
|
|
// ----------------------------------------------------------------
|
|
// Types
|
|
// ----------------------------------------------------------------
|
|
function* FromAny(schema, references, value) {
|
|
yield 'true';
|
|
}
|
|
function* FromArgument(schema, references, value) {
|
|
yield 'true';
|
|
}
|
|
function* FromArray(schema, references, value) {
|
|
yield `Array.isArray(${value})`;
|
|
const [parameter, accumulator] = [CreateParameter('value', 'any'), CreateParameter('acc', 'number')];
|
|
if (IsNumber(schema.maxItems))
|
|
yield `${value}.length <= ${schema.maxItems}`;
|
|
if (IsNumber(schema.minItems))
|
|
yield `${value}.length >= ${schema.minItems}`;
|
|
const elementExpression = CreateExpression(schema.items, references, 'value');
|
|
yield `${value}.every((${parameter}) => ${elementExpression})`;
|
|
if (IsSchema(schema.contains) || IsNumber(schema.minContains) || IsNumber(schema.maxContains)) {
|
|
const containsSchema = IsSchema(schema.contains) ? schema.contains : Never();
|
|
const checkExpression = CreateExpression(containsSchema, references, 'value');
|
|
const checkMinContains = IsNumber(schema.minContains) ? [`(count >= ${schema.minContains})`] : [];
|
|
const checkMaxContains = IsNumber(schema.maxContains) ? [`(count <= ${schema.maxContains})`] : [];
|
|
const checkCount = `const count = value.reduce((${accumulator}, ${parameter}) => ${checkExpression} ? acc + 1 : acc, 0)`;
|
|
const check = [`(count > 0)`, ...checkMinContains, ...checkMaxContains].join(' && ');
|
|
yield `((${parameter}) => { ${checkCount}; return ${check}})(${value})`;
|
|
}
|
|
if (schema.uniqueItems === true) {
|
|
const check = `const hashed = hash(element); if(set.has(hashed)) { return false } else { set.add(hashed) } } return true`;
|
|
const block = `const set = new Set(); for(const element of value) { ${check} }`;
|
|
yield `((${parameter}) => { ${block} )(${value})`;
|
|
}
|
|
}
|
|
function* FromAsyncIterator(schema, references, value) {
|
|
yield `(typeof value === 'object' && Symbol.asyncIterator in ${value})`;
|
|
}
|
|
function* FromBigInt(schema, references, value) {
|
|
yield `(typeof ${value} === 'bigint')`;
|
|
if (IsBigInt(schema.exclusiveMaximum))
|
|
yield `${value} < BigInt(${schema.exclusiveMaximum})`;
|
|
if (IsBigInt(schema.exclusiveMinimum))
|
|
yield `${value} > BigInt(${schema.exclusiveMinimum})`;
|
|
if (IsBigInt(schema.maximum))
|
|
yield `${value} <= BigInt(${schema.maximum})`;
|
|
if (IsBigInt(schema.minimum))
|
|
yield `${value} >= BigInt(${schema.minimum})`;
|
|
if (IsBigInt(schema.multipleOf))
|
|
yield `(${value} % BigInt(${schema.multipleOf})) === 0`;
|
|
}
|
|
function* FromBoolean(schema, references, value) {
|
|
yield `(typeof ${value} === 'boolean')`;
|
|
}
|
|
function* FromConstructor(schema, references, value) {
|
|
yield* Visit(schema.returns, references, `${value}.prototype`);
|
|
}
|
|
function* FromDate(schema, references, value) {
|
|
yield `(${value} instanceof Date) && Number.isFinite(${value}.getTime())`;
|
|
if (IsNumber(schema.exclusiveMaximumTimestamp))
|
|
yield `${value}.getTime() < ${schema.exclusiveMaximumTimestamp}`;
|
|
if (IsNumber(schema.exclusiveMinimumTimestamp))
|
|
yield `${value}.getTime() > ${schema.exclusiveMinimumTimestamp}`;
|
|
if (IsNumber(schema.maximumTimestamp))
|
|
yield `${value}.getTime() <= ${schema.maximumTimestamp}`;
|
|
if (IsNumber(schema.minimumTimestamp))
|
|
yield `${value}.getTime() >= ${schema.minimumTimestamp}`;
|
|
if (IsNumber(schema.multipleOfTimestamp))
|
|
yield `(${value}.getTime() % ${schema.multipleOfTimestamp}) === 0`;
|
|
}
|
|
function* FromFunction(schema, references, value) {
|
|
yield `(typeof ${value} === 'function')`;
|
|
}
|
|
function* FromImport(schema, references, value) {
|
|
const members = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => {
|
|
return [...result, schema.$defs[key]];
|
|
}, []);
|
|
yield* Visit(Ref(schema.$ref), [...references, ...members], value);
|
|
}
|
|
function* FromInteger(schema, references, value) {
|
|
yield `Number.isInteger(${value})`;
|
|
if (IsNumber(schema.exclusiveMaximum))
|
|
yield `${value} < ${schema.exclusiveMaximum}`;
|
|
if (IsNumber(schema.exclusiveMinimum))
|
|
yield `${value} > ${schema.exclusiveMinimum}`;
|
|
if (IsNumber(schema.maximum))
|
|
yield `${value} <= ${schema.maximum}`;
|
|
if (IsNumber(schema.minimum))
|
|
yield `${value} >= ${schema.minimum}`;
|
|
if (IsNumber(schema.multipleOf))
|
|
yield `(${value} % ${schema.multipleOf}) === 0`;
|
|
}
|
|
function* FromIntersect(schema, references, value) {
|
|
const check1 = schema.allOf.map((schema) => CreateExpression(schema, references, value)).join(' && ');
|
|
if (schema.unevaluatedProperties === false) {
|
|
const keyCheck = CreateVariable(`${new RegExp(KeyOfPattern(schema))};`);
|
|
const check2 = `Object.getOwnPropertyNames(${value}).every(key => ${keyCheck}.test(key))`;
|
|
yield `(${check1} && ${check2})`;
|
|
}
|
|
else if (IsSchema(schema.unevaluatedProperties)) {
|
|
const keyCheck = CreateVariable(`${new RegExp(KeyOfPattern(schema))};`);
|
|
const check2 = `Object.getOwnPropertyNames(${value}).every(key => ${keyCheck}.test(key) || ${CreateExpression(schema.unevaluatedProperties, references, `${value}[key]`)})`;
|
|
yield `(${check1} && ${check2})`;
|
|
}
|
|
else {
|
|
yield `(${check1})`;
|
|
}
|
|
}
|
|
function* FromIterator(schema, references, value) {
|
|
yield `(typeof value === 'object' && Symbol.iterator in ${value})`;
|
|
}
|
|
function* FromLiteral(schema, references, value) {
|
|
if (typeof schema.const === 'number' || typeof schema.const === 'boolean') {
|
|
yield `(${value} === ${schema.const})`;
|
|
}
|
|
else {
|
|
yield `(${value} === '${LiteralString.Escape(schema.const)}')`;
|
|
}
|
|
}
|
|
function* FromNever(schema, references, value) {
|
|
yield `false`;
|
|
}
|
|
function* FromNot(schema, references, value) {
|
|
const expression = CreateExpression(schema.not, references, value);
|
|
yield `(!${expression})`;
|
|
}
|
|
function* FromNull(schema, references, value) {
|
|
yield `(${value} === null)`;
|
|
}
|
|
function* FromNumber(schema, references, value) {
|
|
yield Policy.IsNumberLike(value);
|
|
if (IsNumber(schema.exclusiveMaximum))
|
|
yield `${value} < ${schema.exclusiveMaximum}`;
|
|
if (IsNumber(schema.exclusiveMinimum))
|
|
yield `${value} > ${schema.exclusiveMinimum}`;
|
|
if (IsNumber(schema.maximum))
|
|
yield `${value} <= ${schema.maximum}`;
|
|
if (IsNumber(schema.minimum))
|
|
yield `${value} >= ${schema.minimum}`;
|
|
if (IsNumber(schema.multipleOf))
|
|
yield `(${value} % ${schema.multipleOf}) === 0`;
|
|
}
|
|
function* FromObject(schema, references, value) {
|
|
yield Policy.IsObjectLike(value);
|
|
if (IsNumber(schema.minProperties))
|
|
yield `Object.getOwnPropertyNames(${value}).length >= ${schema.minProperties}`;
|
|
if (IsNumber(schema.maxProperties))
|
|
yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}`;
|
|
const knownKeys = Object.getOwnPropertyNames(schema.properties);
|
|
for (const knownKey of knownKeys) {
|
|
const memberExpression = MemberExpression.Encode(value, knownKey);
|
|
const property = schema.properties[knownKey];
|
|
if (schema.required && schema.required.includes(knownKey)) {
|
|
yield* Visit(property, references, memberExpression);
|
|
if (ExtendsUndefinedCheck(property) || IsAnyOrUnknown(property))
|
|
yield `('${knownKey}' in ${value})`;
|
|
}
|
|
else {
|
|
const expression = CreateExpression(property, references, memberExpression);
|
|
yield Policy.IsExactOptionalProperty(value, knownKey, expression);
|
|
}
|
|
}
|
|
if (schema.additionalProperties === false) {
|
|
if (schema.required && schema.required.length === knownKeys.length) {
|
|
yield `Object.getOwnPropertyNames(${value}).length === ${knownKeys.length}`;
|
|
}
|
|
else {
|
|
const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]`;
|
|
yield `Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key))`;
|
|
}
|
|
}
|
|
if (typeof schema.additionalProperties === 'object') {
|
|
const expression = CreateExpression(schema.additionalProperties, references, `${value}[key]`);
|
|
const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]`;
|
|
yield `(Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key) || ${expression}))`;
|
|
}
|
|
}
|
|
function* FromPromise(schema, references, value) {
|
|
yield `${value} instanceof Promise`;
|
|
}
|
|
function* FromRecord(schema, references, value) {
|
|
yield Policy.IsRecordLike(value);
|
|
if (IsNumber(schema.minProperties))
|
|
yield `Object.getOwnPropertyNames(${value}).length >= ${schema.minProperties}`;
|
|
if (IsNumber(schema.maxProperties))
|
|
yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}`;
|
|
const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0];
|
|
const variable = CreateVariable(`${new RegExp(patternKey)}`);
|
|
const check1 = CreateExpression(patternSchema, references, 'value');
|
|
const check2 = IsSchema(schema.additionalProperties) ? CreateExpression(schema.additionalProperties, references, value) : schema.additionalProperties === false ? 'false' : 'true';
|
|
const expression = `(${variable}.test(key) ? ${check1} : ${check2})`;
|
|
yield `(Object.entries(${value}).every(([key, value]) => ${expression}))`;
|
|
}
|
|
function* FromRef(schema, references, value) {
|
|
const target = Deref(schema, references);
|
|
// Reference: If we have seen this reference before we can just yield and return the function call.
|
|
// If this isn't the case we defer to visit to generate and set the function for subsequent passes.
|
|
if (state.functions.has(schema.$ref))
|
|
return yield `${CreateFunctionName(schema.$ref)}(${value})`;
|
|
yield* Visit(target, references, value);
|
|
}
|
|
function* FromRegExp(schema, references, value) {
|
|
const variable = CreateVariable(`${new RegExp(schema.source, schema.flags)};`);
|
|
yield `(typeof ${value} === 'string')`;
|
|
if (IsNumber(schema.maxLength))
|
|
yield `${value}.length <= ${schema.maxLength}`;
|
|
if (IsNumber(schema.minLength))
|
|
yield `${value}.length >= ${schema.minLength}`;
|
|
yield `${variable}.test(${value})`;
|
|
}
|
|
function* FromString(schema, references, value) {
|
|
yield `(typeof ${value} === 'string')`;
|
|
if (IsNumber(schema.maxLength))
|
|
yield `${value}.length <= ${schema.maxLength}`;
|
|
if (IsNumber(schema.minLength))
|
|
yield `${value}.length >= ${schema.minLength}`;
|
|
if (schema.pattern !== undefined) {
|
|
const variable = CreateVariable(`${new RegExp(schema.pattern)};`);
|
|
yield `${variable}.test(${value})`;
|
|
}
|
|
if (schema.format !== undefined) {
|
|
yield `format('${schema.format}', ${value})`;
|
|
}
|
|
}
|
|
function* FromSymbol(schema, references, value) {
|
|
yield `(typeof ${value} === 'symbol')`;
|
|
}
|
|
function* FromTemplateLiteral(schema, references, value) {
|
|
yield `(typeof ${value} === 'string')`;
|
|
const variable = CreateVariable(`${new RegExp(schema.pattern)};`);
|
|
yield `${variable}.test(${value})`;
|
|
}
|
|
function* FromThis(schema, references, value) {
|
|
// Note: This types are assured to be hoisted prior to this call. Just yield the function.
|
|
yield `${CreateFunctionName(schema.$ref)}(${value})`;
|
|
}
|
|
function* FromTuple(schema, references, value) {
|
|
yield `Array.isArray(${value})`;
|
|
if (schema.items === undefined)
|
|
return yield `${value}.length === 0`;
|
|
yield `(${value}.length === ${schema.maxItems})`;
|
|
for (let i = 0; i < schema.items.length; i++) {
|
|
const expression = CreateExpression(schema.items[i], references, `${value}[${i}]`);
|
|
yield `${expression}`;
|
|
}
|
|
}
|
|
function* FromUndefined(schema, references, value) {
|
|
yield `${value} === undefined`;
|
|
}
|
|
function* FromUnion(schema, references, value) {
|
|
const expressions = schema.anyOf.map((schema) => CreateExpression(schema, references, value));
|
|
yield `(${expressions.join(' || ')})`;
|
|
}
|
|
function* FromUint8Array(schema, references, value) {
|
|
yield `${value} instanceof Uint8Array`;
|
|
if (IsNumber(schema.maxByteLength))
|
|
yield `(${value}.length <= ${schema.maxByteLength})`;
|
|
if (IsNumber(schema.minByteLength))
|
|
yield `(${value}.length >= ${schema.minByteLength})`;
|
|
}
|
|
function* FromUnknown(schema, references, value) {
|
|
yield 'true';
|
|
}
|
|
function* FromVoid(schema, references, value) {
|
|
yield Policy.IsVoidLike(value);
|
|
}
|
|
function* FromKind(schema, references, value) {
|
|
const instance = state.instances.size;
|
|
state.instances.set(instance, schema);
|
|
yield `kind('${schema[Kind]}', ${instance}, ${value})`;
|
|
}
|
|
function* Visit(schema, references, value, useHoisting = true) {
|
|
const references_ = IsString(schema.$id) ? [...references, schema] : references;
|
|
const schema_ = schema;
|
|
// --------------------------------------------------------------
|
|
// Hoisting
|
|
// --------------------------------------------------------------
|
|
if (useHoisting && IsString(schema.$id)) {
|
|
const functionName = CreateFunctionName(schema.$id);
|
|
if (state.functions.has(functionName)) {
|
|
return yield `${functionName}(${value})`;
|
|
}
|
|
else {
|
|
// Note: In the case of cyclic types, we need to create a 'functions' record
|
|
// to prevent infinitely re-visiting the CreateFunction. Subsequent attempts
|
|
// to visit will be caught by the above condition.
|
|
state.functions.set(functionName, '<deferred>');
|
|
const functionCode = CreateFunction(functionName, schema, references, 'value', false);
|
|
state.functions.set(functionName, functionCode);
|
|
return yield `${functionName}(${value})`;
|
|
}
|
|
}
|
|
switch (schema_[Kind]) {
|
|
case 'Any':
|
|
return yield* FromAny(schema_, references_, value);
|
|
case 'Argument':
|
|
return yield* FromArgument(schema_, references_, value);
|
|
case 'Array':
|
|
return yield* FromArray(schema_, references_, value);
|
|
case 'AsyncIterator':
|
|
return yield* FromAsyncIterator(schema_, references_, value);
|
|
case 'BigInt':
|
|
return yield* FromBigInt(schema_, references_, value);
|
|
case 'Boolean':
|
|
return yield* FromBoolean(schema_, references_, value);
|
|
case 'Constructor':
|
|
return yield* FromConstructor(schema_, references_, value);
|
|
case 'Date':
|
|
return yield* FromDate(schema_, references_, value);
|
|
case 'Function':
|
|
return yield* FromFunction(schema_, references_, value);
|
|
case 'Import':
|
|
return yield* FromImport(schema_, references_, value);
|
|
case 'Integer':
|
|
return yield* FromInteger(schema_, references_, value);
|
|
case 'Intersect':
|
|
return yield* FromIntersect(schema_, references_, value);
|
|
case 'Iterator':
|
|
return yield* FromIterator(schema_, references_, value);
|
|
case 'Literal':
|
|
return yield* FromLiteral(schema_, references_, value);
|
|
case 'Never':
|
|
return yield* FromNever(schema_, references_, value);
|
|
case 'Not':
|
|
return yield* FromNot(schema_, references_, value);
|
|
case 'Null':
|
|
return yield* FromNull(schema_, references_, value);
|
|
case 'Number':
|
|
return yield* FromNumber(schema_, references_, value);
|
|
case 'Object':
|
|
return yield* FromObject(schema_, references_, value);
|
|
case 'Promise':
|
|
return yield* FromPromise(schema_, references_, value);
|
|
case 'Record':
|
|
return yield* FromRecord(schema_, references_, value);
|
|
case 'Ref':
|
|
return yield* FromRef(schema_, references_, value);
|
|
case 'RegExp':
|
|
return yield* FromRegExp(schema_, references_, value);
|
|
case 'String':
|
|
return yield* FromString(schema_, references_, value);
|
|
case 'Symbol':
|
|
return yield* FromSymbol(schema_, references_, value);
|
|
case 'TemplateLiteral':
|
|
return yield* FromTemplateLiteral(schema_, references_, value);
|
|
case 'This':
|
|
return yield* FromThis(schema_, references_, value);
|
|
case 'Tuple':
|
|
return yield* FromTuple(schema_, references_, value);
|
|
case 'Undefined':
|
|
return yield* FromUndefined(schema_, references_, value);
|
|
case 'Union':
|
|
return yield* FromUnion(schema_, references_, value);
|
|
case 'Uint8Array':
|
|
return yield* FromUint8Array(schema_, references_, value);
|
|
case 'Unknown':
|
|
return yield* FromUnknown(schema_, references_, value);
|
|
case 'Void':
|
|
return yield* FromVoid(schema_, references_, value);
|
|
default:
|
|
if (!TypeRegistry.Has(schema_[Kind]))
|
|
throw new TypeCompilerUnknownTypeError(schema);
|
|
return yield* FromKind(schema_, references_, value);
|
|
}
|
|
}
|
|
// ----------------------------------------------------------------
|
|
// Compiler State
|
|
// ----------------------------------------------------------------
|
|
// prettier-ignore
|
|
const state = {
|
|
language: 'javascript', // target language
|
|
functions: new Map(), // local functions
|
|
variables: new Map(), // local variables
|
|
instances: new Map() // exterior kind instances
|
|
};
|
|
// ----------------------------------------------------------------
|
|
// Compiler Factory
|
|
// ----------------------------------------------------------------
|
|
function CreateExpression(schema, references, value, useHoisting = true) {
|
|
return `(${[...Visit(schema, references, value, useHoisting)].join(' && ')})`;
|
|
}
|
|
function CreateFunctionName($id) {
|
|
return `check_${Identifier.Encode($id)}`;
|
|
}
|
|
function CreateVariable(expression) {
|
|
const variableName = `local_${state.variables.size}`;
|
|
state.variables.set(variableName, `const ${variableName} = ${expression}`);
|
|
return variableName;
|
|
}
|
|
function CreateFunction(name, schema, references, value, useHoisting = true) {
|
|
const [newline, pad] = ['\n', (length) => ''.padStart(length, ' ')];
|
|
const parameter = CreateParameter('value', 'any');
|
|
const returns = CreateReturns('boolean');
|
|
const expression = [...Visit(schema, references, value, useHoisting)].map((expression) => `${pad(4)}${expression}`).join(` &&${newline}`);
|
|
return `function ${name}(${parameter})${returns} {${newline}${pad(2)}return (${newline}${expression}${newline}${pad(2)})\n}`;
|
|
}
|
|
function CreateParameter(name, type) {
|
|
const annotation = state.language === 'typescript' ? `: ${type}` : '';
|
|
return `${name}${annotation}`;
|
|
}
|
|
function CreateReturns(type) {
|
|
return state.language === 'typescript' ? `: ${type}` : '';
|
|
}
|
|
// ----------------------------------------------------------------
|
|
// Compile
|
|
// ----------------------------------------------------------------
|
|
function Build(schema, references, options) {
|
|
const functionCode = CreateFunction('check', schema, references, 'value'); // will populate functions and variables
|
|
const parameter = CreateParameter('value', 'any');
|
|
const returns = CreateReturns('boolean');
|
|
const functions = [...state.functions.values()];
|
|
const variables = [...state.variables.values()];
|
|
// prettier-ignore
|
|
const checkFunction = IsString(schema.$id) // ensure top level schemas with $id's are hoisted
|
|
? `return function check(${parameter})${returns} {\n return ${CreateFunctionName(schema.$id)}(value)\n}`
|
|
: `return ${functionCode}`;
|
|
return [...variables, ...functions, checkFunction].join('\n');
|
|
}
|
|
/** Generates the code used to assert this type and returns it as a string */
|
|
function Code(...args) {
|
|
const defaults = { language: 'javascript' };
|
|
// prettier-ignore
|
|
const [schema, references, options] = (args.length === 2 && IsArray(args[1]) ? [args[0], args[1], defaults] :
|
|
args.length === 2 && !IsArray(args[1]) ? [args[0], [], args[1]] :
|
|
args.length === 3 ? [args[0], args[1], args[2]] :
|
|
args.length === 1 ? [args[0], [], defaults] :
|
|
[null, [], defaults]);
|
|
// compiler-reset
|
|
state.language = options.language;
|
|
state.variables.clear();
|
|
state.functions.clear();
|
|
state.instances.clear();
|
|
if (!IsSchema(schema))
|
|
throw new TypeCompilerTypeGuardError(schema);
|
|
for (const schema of references)
|
|
if (!IsSchema(schema))
|
|
throw new TypeCompilerTypeGuardError(schema);
|
|
return Build(schema, references, options);
|
|
}
|
|
TypeCompiler.Code = Code;
|
|
/** Compiles a TypeBox type for optimal runtime type checking. Types must be valid TypeBox types of TSchema */
|
|
function Compile(schema, references = []) {
|
|
const generatedCode = Code(schema, references, { language: 'javascript' });
|
|
const compiledFunction = globalThis.Function('kind', 'format', 'hash', generatedCode);
|
|
const instances = new Map(state.instances);
|
|
function typeRegistryFunction(kind, instance, value) {
|
|
if (!TypeRegistry.Has(kind) || !instances.has(instance))
|
|
return false;
|
|
const checkFunc = TypeRegistry.Get(kind);
|
|
const schema = instances.get(instance);
|
|
return checkFunc(schema, value);
|
|
}
|
|
function formatRegistryFunction(format, value) {
|
|
if (!FormatRegistry.Has(format))
|
|
return false;
|
|
const checkFunc = FormatRegistry.Get(format);
|
|
return checkFunc(value);
|
|
}
|
|
function hashFunction(value) {
|
|
return Hash(value);
|
|
}
|
|
const checkFunction = compiledFunction(typeRegistryFunction, formatRegistryFunction, hashFunction);
|
|
return new TypeCheck(schema, references, checkFunction, generatedCode);
|
|
}
|
|
TypeCompiler.Compile = Compile;
|
|
})(TypeCompiler || (TypeCompiler = {}));
|