import { globalValue } from "@fncts/base/data/Global";
import { isRecord } from "@fncts/base/util/predicates";
import { getKeysForIndexSignature, memoize } from "@fncts/schema/utils";
import { ASTTag, getSearchTree } from "./AST.js";
import { parserFor } from "./Parser.js";
/**
* @tsplus getter fncts.schema.Schema is
*/
export function is(schema: Schema) {
return (input: unknown): input is A => {
return guardFor(schema).is(input);
};
}
export function guardFor(schema: Schema): Guard {
return goMemo(schema.ast);
}
const guardStrict = (value: unknown) => Guard((inp): inp is any => inp === value);
const guardMemoMap = globalValue(Symbol.for("fncts.schema.Guard.guardMemoMap"), () => new WeakMap>());
function goMemo(ast: AST): Guard {
const memo = guardMemoMap.get(ast);
if (memo) {
return memo;
}
const guard = go(ast);
guardMemoMap.set(ast, guard);
return guard;
}
function go(ast: AST): Guard {
AST.concrete(ast);
switch (ast._tag) {
case ASTTag.Declaration: {
const parser = parserFor(ast, true);
return Guard((inp): inp is any =>
parser(inp).match(
() => false,
() => true,
),
);
}
case ASTTag.Literal: {
return Guard((inp): inp is any => inp === ast.literal);
}
case ASTTag.UniqueSymbol: {
return guardStrict(ast.symbol);
}
case ASTTag.VoidKeyword:
case ASTTag.UndefinedKeyword: {
return guardStrict(undefined);
}
case ASTTag.NeverKeyword: {
return Guard((inp): inp is never => false);
}
case ASTTag.UnknownKeyword:
case ASTTag.AnyKeyword: {
return Guard((inp): inp is any => true);
}
case ASTTag.NumberKeyword: {
return Guard.number;
}
case ASTTag.BooleanKeyword: {
return Guard.boolean;
}
case ASTTag.StringKeyword: {
return Guard.string;
}
case ASTTag.BigIntKeyword: {
return Guard.bigint;
}
case ASTTag.SymbolKeyword: {
return Guard((inp): inp is symbol => typeof inp === "symbol");
}
case ASTTag.ObjectKeyword: {
return Guard(isObject);
}
case ASTTag.TemplateLiteral: {
const parser = parserFor(ast, true);
return Guard((inp): inp is any =>
parser(inp).match(
() => false,
() => true,
),
);
}
case ASTTag.Tuple: {
const elements = ast.elements.map((element) => goMemo(element.type));
const restElements = ast.rest.match(
() => Vector.empty>(),
(rest) => rest.map(goMemo),
);
return Guard((input): input is any => {
if (!Array.isArray(input)) {
return false;
}
let i = 0;
for (; i < elements.length; i++) {
if (input.length < i + 1) {
if (!ast.elements[i]!.isOptional) {
return false;
}
} else {
const guard = elements[i]!;
if (!guard.is(input[i])) {
return false;
}
}
}
if (restElements.length > 0) {
const head = restElements.unsafeHead!;
const tail = restElements.tail;
for (; i < input.length - tail.length; i++) {
if (!head.is(input[i])) {
return false;
}
}
for (let j = 0; j < tail.length; j++) {
i += j;
if (input.length < i + 1) {
return false;
}
const guard = tail[j]!;
if (!guard.is(input[i])) {
return false;
}
}
}
return true;
});
}
case ASTTag.TypeLiteral: {
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
return Guard((input): input is Exclude => input !== null);
}
const propertySignatureTypes = ast.propertySignatures.map((ps) => goMemo(ps.type));
const indexSignatures = ast.indexSignatures.map((is) => [goMemo(is.parameter), goMemo(is.type)] as const);
return Guard((input): input is any => {
if (!isRecord(input)) {
return false;
}
const expectedKeys: any = {};
console.log(ast.propertySignatures);
for (let i = 0; i < propertySignatureTypes.length; i++) {
const ps = ast.propertySignatures[i]!;
const guard = propertySignatureTypes[i]!;
const name = ps.name;
expectedKeys[name] = null;
if (!Object.prototype.hasOwnProperty.call(input, name)) {
if (!ps.isOptional) {
return false;
}
} else {
if (!guard(input[name])) {
return false;
}
}
}
if (indexSignatures.length > 0) {
for (let i = 0; i < indexSignatures.length; i++) {
const [parameter, type] = indexSignatures[i]!;
const keys = getKeysForIndexSignature(input, ast.indexSignatures[i]!.parameter);
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(expectedKeys, key)) {
continue;
}
if (!parameter(key)) {
return false;
}
if (!type(input[key])) {
return false;
}
}
}
}
return true;
});
}
case ASTTag.Union: {
const searchTree = getSearchTree(ast.types, true);
const ownKeys = Reflect.ownKeys(searchTree.keys);
const len = ownKeys.length;
const otherwise = searchTree.otherwise;
const map = new Map>();
ast.types.forEach((ast) => {
map.set(ast, goMemo(ast));
});
return Guard((input): input is any => {
if (len > 0) {
if (isRecord(input)) {
for (let i = 0; i < len; i++) {
const name = ownKeys[i]!;
const buckets = searchTree.keys[name]!.buckets;
if (Object.prototype.hasOwnProperty.call(input, name)) {
const literal = String(input[name]);
if (Object.prototype.hasOwnProperty.call(buckets, literal)) {
const bucket: ReadonlyArray = buckets[literal]!;
for (let i = 0; i < bucket.length; i++) {
if (map.get(bucket[i])!(input)) {
return true;
}
}
}
}
}
}
}
for (let i = 0; i < otherwise.length; i++) {
if (map.get(otherwise[i])!(input)) {
return true;
}
}
return false;
});
}
case ASTTag.Lazy: {
const f = () => goMemo(ast.getAST());
const get = memoize>(f);
return Guard((input): input is any => get()(input));
}
case ASTTag.Enum: {
return Guard((input): input is any => ast.enums.some(([_, value]) => value === input));
}
case ASTTag.Refinement: {
const from = goMemo(ast.from);
return Guard((input): input is any => {
if (!from(input)) {
return false;
}
if (!ast.predicate(input)) {
return false;
}
return true;
});
}
case ASTTag.Transform: {
return goMemo(ast.to);
}
case ASTTag.Validation: {
const from = goMemo(ast.from);
return Guard((input): input is any => {
if (!from(input)) {
return false;
}
for (const validation of ast.validation) {
if (!validation.validate(input)) {
return false;
}
}
return true;
});
}
}
}