import type { Hook } from "@fncts/schema/ASTAnnotation";
import type { Sized } from "@fncts/test/control/Sized";
import { ASTTag } from "@fncts/schema/AST";
import { ASTAnnotation } from "@fncts/schema/ASTAnnotation";
import { InvalidInterpretationError } from "@fncts/schema/InvalidInterpretationError";
import { memoize } from "@fncts/schema/utils";
/**
* @tsplus getter fncts.schema.Schema genFrom
*/
export function genFrom(self: Schema): Gen {
return go(self.ast.getFrom);
}
/**
* @tsplus getter fncts.schema.Schema genTo
*/
export function genTo(self: Schema): Gen {
return go(self.ast);
}
function getHook(ast: AST): Maybe>> {
return ast.annotations.get(ASTAnnotation.GenHook);
}
function record(
key: Gen,
value: Gen,
): Gen {
return Gen.tuple(key, value)
.arrayWith({ maxLength: 10 })
.map((tuples) => {
const out: { [k in K]: V } = {} as any;
for (const [k, v] of tuples) {
out[k] = v;
}
return out;
});
}
const go = memoize(function go(ast: AST): Gen {
AST.concrete(ast);
switch (ast._tag) {
case ASTTag.Declaration:
return getHook(ast).match(
() =>
Gen.fromIO(
IO.haltNow(new InvalidInterpretationError("cannot build a Gen for a Declaration without a Gen hook")),
),
(hook) => hook(...ast.typeParameters.map(go)),
);
case ASTTag.Literal:
return Gen.constant(ast.literal);
case ASTTag.UniqueSymbol:
return Gen.constant(ast.symbol);
case ASTTag.UndefinedKeyword:
return Gen.constant(undefined);
case ASTTag.VoidKeyword:
return Gen.constant(undefined);
case ASTTag.NeverKeyword:
return Gen.fromIO(IO.haltNow(new InvalidInterpretationError("cannot build a Gen for `never`")));
case ASTTag.UnknownKeyword:
return Gen.anything();
case ASTTag.AnyKeyword:
return Gen.anything();
case ASTTag.StringKeyword:
return Gen.fullUnicodeString();
case ASTTag.NumberKeyword:
return Gen.float;
case ASTTag.BooleanKeyword:
return Gen.boolean;
case ASTTag.BigIntKeyword:
return Gen.bigInt;
case ASTTag.SymbolKeyword:
return Gen.fullUnicodeString().map((s) => Symbol.for(s));
case ASTTag.ObjectKeyword:
return Gen.anything();
case ASTTag.TemplateLiteral: {
const components: Array> = [Gen.constant(ast.head)];
for (const span of ast.spans) {
components.push(Gen.fullUnicodeString({ maxLength: 5 }));
components.push(Gen.constant(span.literal));
}
return Gen.tuple(...components).map((spans) => spans.join(""));
}
case ASTTag.Tuple: {
const elements = ast.elements.map((e) => go(e.type));
const rest = ast.rest.map((restElement) => restElement.map(go));
let output = Gen.tuple(...elements);
if (elements.length > 0 && rest.isNothing()) {
const firstOptionalIndex = ast.elements.findIndex((e) => e.isOptional);
if (firstOptionalIndex !== -1) {
output = output.flatMap((as) =>
Gen.intWith({ min: firstOptionalIndex, max: elements.length - 1 }).map((i) => as.slice(0, i)),
);
}
}
if (rest.isJust()) {
const head = rest.value.unsafeHead!;
const tail = rest.value.tail;
output = output.flatMap((as) => head.arrayWith({ maxLength: 5 }).map((rest) => [...as, ...rest]));
for (let j = 0; j < tail.length; j++) {
output = output.flatMap((as) => tail[j]!.map((a) => [...as, a]));
}
}
return output as Gen;
}
case ASTTag.TypeLiteral: {
const propertySignatureTypes = ast.propertySignatures.map((ps) => go(ps.type));
const indexSignatures = ast.indexSignatures.map((is) => [go(is.parameter), go(is.type)] as const);
const requiredGens: Record> = {};
const optionalGens: Record> = {};
for (let i = 0; i < propertySignatureTypes.length; i++) {
const ps = ast.propertySignatures[i]!;
const name = ps.name;
if (!ps.isOptional) {
requiredGens[name] = propertySignatureTypes[i]!;
} else {
optionalGens[name] = propertySignatureTypes[i]!;
}
}
let output = Gen.struct(requiredGens).zipWith(Gen.partial(optionalGens), (a, b) => ({ ...a, ...b }));
for (let i = 0; i < indexSignatures.length; i++) {
const parameter = indexSignatures[i]![0]!;
const type = indexSignatures[i]![1]!;
output = output.flatMap((o) => {
return record(parameter, type).map((d) => ({ ...d, ...o }));
});
}
return output;
}
case ASTTag.Union: {
const types = ast.types.map(go);
return Gen.oneOf(...types) as Gen;
}
case ASTTag.Lazy: {
return getHook(ast).match(
() => {
const f = () => go(ast.getAST());
const get = memoize>(f);
return Gen.defer(() => get(f));
},
(handler) => handler(),
);
}
case ASTTag.Enum: {
if (ast.enums.length === 0) {
return Gen.fromIO(IO.haltNow(new InvalidInterpretationError("cannot build a Gen for an empty enum")));
}
return Gen.oneOf(...ast.enums.map(([_, value]) => Gen.constant(value))) as Gen;
}
case ASTTag.Refinement: {
const from = go(ast.from);
return getHook(ast).match(
() => from.filter((a) => ast.decode(a).isRight()),
(handler) => handler(from),
);
}
case ASTTag.Transform:
return go(ast.to);
case ASTTag.Validation: {
const from = go(ast.from);
return getHook(ast).match(
() => from.filter((a) => ast.validation.every((v) => v.validate(a))),
(handler) => handler(from),
);
}
}
});