feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,46 @@
import { BaseConstraint } from "../constraint.ts";
import type { RootSchema, nodeOfKind } from "../kinds.ts";
import { type BaseNode, type DeepNodeTransformContext, type DeepNodeTransformation } from "../node.ts";
import type { BaseRoot } from "../roots/root.ts";
import type { BaseNormalizedSchema, declareNode } from "../shared/declare.ts";
import { type RootKind, type nodeImplementationOf } from "../shared/implement.ts";
import { type TraverseAllows, type TraverseApply } from "../shared/traversal.ts";
export declare namespace Index {
type KeyKind = Exclude<RootKind, "unit">;
type KeyNode = nodeOfKind<KeyKind>;
interface Schema extends BaseNormalizedSchema {
readonly signature: RootSchema<KeyKind>;
readonly value: RootSchema;
}
interface Inner {
readonly signature: KeyNode;
readonly value: BaseRoot;
}
interface Declaration extends declareNode<{
kind: "index";
schema: Schema;
normalizedSchema: Schema;
inner: Inner;
prerequisite: object;
intersectionIsOpen: true;
childKind: RootKind;
}> {
}
type Node = IndexNode;
}
export declare class IndexNode extends BaseConstraint<Index.Declaration> {
impliedBasis: BaseRoot;
expression: string;
flatRefs: import("../node.ts").FlatRef<BaseRoot<import("../roots/root.ts").InternalRootDeclaration>>[];
traverseAllows: TraverseAllows<object>;
traverseApply: TraverseApply<object>;
protected _transform(mapper: DeepNodeTransformation, ctx: DeepNodeTransformContext): BaseNode | null;
compile(): void;
}
export declare const Index: {
implementation: nodeImplementationOf<Index.Declaration>;
Node: typeof IndexNode;
};
export declare const writeEnumerableIndexBranches: (keys: string[]) => string;
export declare const writeInvalidPropertyKeyMessage: <indexSchema extends string>(indexSchema: indexSchema) => writeInvalidPropertyKeyMessage<indexSchema>;
export type writeInvalidPropertyKeyMessage<indexSchema extends string> = `Indexed key definition '${indexSchema}' must be a string or symbol`;

View File

@@ -0,0 +1,89 @@
import { append, printable, stringAndSymbolicEntriesOf, throwParseError } from "@ark/util";
import { BaseConstraint } from "../constraint.js";
import { flatRef } from "../node.js";
import { Disjoint } from "../shared/disjoint.js";
import { implementNode } from "../shared/implement.js";
import { intersectOrPipeNodes } from "../shared/intersections.js";
import { $ark } from "../shared/registry.js";
import { traverseKey } from "../shared/traversal.js";
const implementation = implementNode({
kind: "index",
hasAssociatedError: false,
intersectionIsOpen: true,
keys: {
signature: {
child: true,
parse: (schema, ctx) => {
const key = ctx.$.parseSchema(schema);
if (!key.extends($ark.intrinsic.key)) {
return throwParseError(writeInvalidPropertyKeyMessage(key.expression));
}
const enumerableBranches = key.branches.filter(b => b.hasKind("unit"));
if (enumerableBranches.length) {
return throwParseError(writeEnumerableIndexBranches(enumerableBranches.map(b => printable(b.unit))));
}
return key;
}
},
value: {
child: true,
parse: (schema, ctx) => ctx.$.parseSchema(schema)
}
},
normalize: schema => schema,
defaults: {
description: node => `[${node.signature.expression}]: ${node.value.description}`
},
intersections: {
index: (l, r, ctx) => {
if (l.signature.equals(r.signature)) {
const valueIntersection = intersectOrPipeNodes(l.value, r.value, ctx);
const value = valueIntersection instanceof Disjoint ?
$ark.intrinsic.never.internal
: valueIntersection;
return ctx.$.node("index", { signature: l.signature, value });
}
// if r constrains all of l's keys to a subtype of l's value, r is a subtype of l
if (l.signature.extends(r.signature) && l.value.subsumes(r.value))
return r;
// if l constrains all of r's keys to a subtype of r's value, l is a subtype of r
if (r.signature.extends(l.signature) && r.value.subsumes(l.value))
return l;
// other relationships between index signatures can't be generally reduced
return null;
}
}
});
export class IndexNode extends BaseConstraint {
impliedBasis = $ark.intrinsic.object.internal;
expression = `[${this.signature.expression}]: ${this.value.expression}`;
flatRefs = append(this.value.flatRefs.map(ref => flatRef([this.signature, ...ref.path], ref.node)), flatRef([this.signature], this.value));
traverseAllows = (data, ctx) => stringAndSymbolicEntriesOf(data).every(entry => {
if (this.signature.traverseAllows(entry[0], ctx)) {
return traverseKey(entry[0], () => this.value.traverseAllows(entry[1], ctx), ctx);
}
return true;
});
traverseApply = (data, ctx) => {
for (const entry of stringAndSymbolicEntriesOf(data)) {
if (this.signature.traverseAllows(entry[0], ctx)) {
traverseKey(entry[0], () => this.value.traverseApply(entry[1], ctx), ctx);
}
}
};
_transform(mapper, ctx) {
ctx.path.push(this.signature);
const result = super._transform(mapper, ctx);
ctx.path.pop();
return result;
}
compile() {
// this is currently handled by StructureNode
}
}
export const Index = {
implementation,
Node: IndexNode
};
export const writeEnumerableIndexBranches = (keys) => `Index keys ${keys.join(", ")} should be specified as named props.`;
export const writeInvalidPropertyKeyMessage = (indexSchema) => `Indexed key definition '${indexSchema}' must be a string or symbol`;

View File

@@ -0,0 +1,49 @@
import { type requireKeys } from "@ark/util";
import type { Morph } from "../roots/morph.ts";
import type { BaseRoot } from "../roots/root.ts";
import type { declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import { BaseProp, type Prop } from "./prop.ts";
export declare namespace Optional {
interface Schema extends Prop.Schema {
default?: unknown;
}
interface Inner extends Prop.Inner {
default?: unknown;
}
type Declaration = declareNode<Prop.Declaration<"optional"> & {
schema: Schema;
normalizedSchema: Schema;
inner: Inner;
}>;
type Node = OptionalNode;
namespace Node {
type withDefault = requireKeys<Node, "default" | "defaultValueMorph" | "defaultValueMorphRef">;
}
}
export declare class OptionalNode extends BaseProp<"optional"> {
constructor(...args: ConstructorParameters<typeof BaseProp>);
get rawIn(): OptionalNode;
get outProp(): Prop.Node;
expression: string;
defaultValueMorph: Morph | undefined;
defaultValueMorphRef: string | undefined;
}
export declare const Optional: {
implementation: nodeImplementationOf<{
reducibleTo: "optional";
errorContext: null;
kind: "optional";
prerequisite: object;
intersectionIsOpen: true;
childKind: import("../shared/implement.ts").RootKind;
schema: Optional.Schema;
normalizedSchema: Optional.Schema;
inner: Optional.Inner;
}>;
Node: typeof OptionalNode;
};
export declare const computeDefaultValueMorph: (key: PropertyKey, value: BaseRoot, defaultInput: unknown) => Morph<any>;
export declare const assertDefaultValueAssignability: (node: BaseRoot, value: unknown, key: PropertyKey | null) => unknown;
export type writeUnassignableDefaultValueMessage<baseDef extends string, defaultValue extends string> = `Default value ${defaultValue} must be assignable to ${baseDef}`;
export declare const writeNonPrimitiveNonFunctionDefaultValueMessage: (key: PropertyKey | null) => string;

View File

@@ -0,0 +1,127 @@
import { hasDomain, isThunk, omit, printable, throwParseError } from "@ark/util";
import { intrinsic } from "../intrinsic.js";
import { compileSerializedValue } from "../shared/compile.js";
import { ArkErrors } from "../shared/errors.js";
import { defaultValueSerializer, implementNode } from "../shared/implement.js";
import { registeredReference } from "../shared/registry.js";
import { traverseKey } from "../shared/traversal.js";
import { BaseProp, intersectProps } from "./prop.js";
const implementation = implementNode({
kind: "optional",
hasAssociatedError: false,
intersectionIsOpen: true,
keys: {
key: {},
value: {
child: true,
parse: (schema, ctx) => ctx.$.parseSchema(schema)
},
default: {
preserveUndefined: true
}
},
normalize: schema => schema,
reduce: (inner, $) => {
if ($.resolvedConfig.exactOptionalPropertyTypes === false) {
if (!inner.value.allows(undefined)) {
return $.node("optional", { ...inner, value: inner.value.or(intrinsic.undefined) }, { prereduced: true });
}
}
},
defaults: {
description: node => `${node.compiledKey}?: ${node.value.description}`
},
intersections: {
optional: intersectProps
}
});
export class OptionalNode extends BaseProp {
constructor(...args) {
super(...args);
if ("default" in this.inner)
assertDefaultValueAssignability(this.value, this.inner.default, this.key);
}
get rawIn() {
const baseIn = super.rawIn;
if (!this.hasDefault())
return baseIn;
return this.$.node("optional", omit(baseIn.inner, { default: true }), {
prereduced: true
});
}
get outProp() {
if (!this.hasDefault())
return this;
const { default: defaultValue, ...requiredInner } = this.inner;
return this.cacheGetter("outProp", this.$.node("required", requiredInner, { prereduced: true }));
}
expression = this.hasDefault() ?
`${this.compiledKey}: ${this.value.expression} = ${printable(this.inner.default)}`
: `${this.compiledKey}?: ${this.value.expression}`;
defaultValueMorph = getDefaultableMorph(this);
defaultValueMorphRef = this.defaultValueMorph && registeredReference(this.defaultValueMorph);
}
export const Optional = {
implementation,
Node: OptionalNode
};
const defaultableMorphCache = {};
const getDefaultableMorph = (node) => {
if (!node.hasDefault())
return;
const cacheKey = `{${node.compiledKey}: ${node.value.id} = ${defaultValueSerializer(node.default)}}`;
return (defaultableMorphCache[cacheKey] ??= computeDefaultValueMorph(node.key, node.value, node.default));
};
export const computeDefaultValueMorph = (key, value, defaultInput) => {
if (typeof defaultInput === "function") {
// if the value has a morph, pipe context through it
return value.includesTransform ?
(data, ctx) => {
traverseKey(key, () => value((data[key] = defaultInput()), ctx), ctx);
return data;
}
: data => {
data[key] = defaultInput();
return data;
};
}
// non-functional defaults can be safely cached as long as the morph is
// guaranteed to be pure and the output is primitive
const precomputedMorphedDefault = value.includesTransform ? value.assert(defaultInput) : defaultInput;
return hasDomain(precomputedMorphedDefault, "object") ?
// the type signature only allows this if the value was morphed
(data, ctx) => {
traverseKey(key, () => value((data[key] = defaultInput), ctx), ctx);
return data;
}
: data => {
data[key] = precomputedMorphedDefault;
return data;
};
};
export const assertDefaultValueAssignability = (node, value, key) => {
const wrapped = isThunk(value);
if (hasDomain(value, "object") && !wrapped)
throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key));
// if the node has a default value, finalize it and apply JIT optimizations
// if applicable to ensure behavior + error logging is externally consistent
// (using .in here insead of .rawIn triggers finalization)
const out = node.in(wrapped ? value() : value);
if (out instanceof ArkErrors) {
if (key === null) {
// e.g. "Default must be assignable to number (was string)"
throwParseError(`Default ${out.summary}`);
}
const atPath = out.transform(e => e.transform(input => ({ ...input, prefixPath: [key] })));
// e.g. "Default for bar must be assignable to number (was string)"
// e.g. "Default for value at [0] must be assignable to number (was string)"
throwParseError(`Default for ${atPath.summary}`);
}
return value;
};
export const writeNonPrimitiveNonFunctionDefaultValueMessage = (key) => {
const keyDescription = key === null ? ""
: typeof key === "number" ? `for value at [${key}] `
: `for ${compileSerializedValue(key)} `;
return `Non-primitive default ${keyDescription}must be specified as a function like () => ({my: 'object'})`;
};

View File

@@ -0,0 +1,45 @@
import { type Key } from "@ark/util";
import { BaseConstraint } from "../constraint.ts";
import type { nodeOfKind, RootSchema } from "../kinds.ts";
import { type BaseNode, type DeepNodeTransformation, type DeepNodeTransformContext, type FlatRef } from "../node.ts";
import type { BaseRoot } from "../roots/root.ts";
import { type NodeCompiler } from "../shared/compile.ts";
import type { BaseNormalizedSchema } from "../shared/declare.ts";
import { Disjoint } from "../shared/disjoint.ts";
import type { IntersectionContext, RootKind } from "../shared/implement.ts";
import { type TraverseAllows, type TraverseApply } from "../shared/traversal.ts";
import type { Optional } from "./optional.ts";
import type { Required } from "./required.ts";
export declare namespace Prop {
type Kind = "required" | "optional";
type Node = nodeOfKind<Kind>;
interface Schema extends BaseNormalizedSchema {
readonly key: Key;
readonly value: RootSchema;
}
interface Inner {
readonly key: Key;
readonly value: BaseRoot;
}
interface Declaration<kind extends Kind = Kind> {
kind: kind;
prerequisite: object;
intersectionIsOpen: true;
childKind: RootKind;
}
}
export declare const intersectProps: (l: nodeOfKind<Prop.Kind>, r: nodeOfKind<Prop.Kind>, ctx: IntersectionContext) => nodeOfKind<Prop.Kind> | Disjoint | null;
export declare abstract class BaseProp<kind extends Prop.Kind = Prop.Kind> extends BaseConstraint<kind extends "required" ? Required.Declaration : Optional.Declaration> {
required: boolean;
optional: boolean;
impliedBasis: BaseRoot;
serializedKey: string;
compiledKey: string;
flatRefs: FlatRef[];
protected _transform(mapper: DeepNodeTransformation, ctx: DeepNodeTransformContext): BaseNode | null;
hasDefault(): this is Optional.Node.withDefault;
traverseAllows: TraverseAllows<object>;
traverseApply: TraverseApply<object>;
compile(js: NodeCompiler): void;
}
export declare const writeDefaultIntersectionMessage: (lValue: unknown, rValue: unknown) => string;

View File

@@ -0,0 +1,85 @@
import { append, printable, throwParseError, unset } from "@ark/util";
import { BaseConstraint } from "../constraint.js";
import { flatRef } from "../node.js";
import { compileSerializedValue } from "../shared/compile.js";
import { Disjoint } from "../shared/disjoint.js";
import { intersectOrPipeNodes } from "../shared/intersections.js";
import { $ark } from "../shared/registry.js";
import { traverseKey } from "../shared/traversal.js";
export const intersectProps = (l, r, ctx) => {
if (l.key !== r.key)
return null;
const key = l.key;
let value = intersectOrPipeNodes(l.value, r.value, ctx);
const kind = l.required || r.required ? "required" : "optional";
if (value instanceof Disjoint) {
if (kind === "optional")
value = $ark.intrinsic.never.internal;
else {
// if either operand was optional, the Disjoint has to be treated as optional
return value.withPrefixKey(l.key, l.required && r.required ? "required" : "optional");
}
}
if (kind === "required") {
return ctx.$.node("required", {
key,
value
});
}
const defaultIntersection = l.hasDefault() ?
r.hasDefault() ?
l.default === r.default ?
l.default
: throwParseError(writeDefaultIntersectionMessage(l.default, r.default))
: l.default
: r.hasDefault() ? r.default
: unset;
return ctx.$.node("optional", {
key,
value,
// unset is stripped during parsing
default: defaultIntersection
});
};
export class BaseProp extends BaseConstraint {
required = this.kind === "required";
optional = this.kind === "optional";
impliedBasis = $ark.intrinsic.object.internal;
serializedKey = compileSerializedValue(this.key);
compiledKey = typeof this.key === "string" ? this.key : this.serializedKey;
flatRefs = append(this.value.flatRefs.map(ref => flatRef([this.key, ...ref.path], ref.node)), flatRef([this.key], this.value));
_transform(mapper, ctx) {
ctx.path.push(this.key);
const result = super._transform(mapper, ctx);
ctx.path.pop();
return result;
}
hasDefault() {
return "default" in this.inner;
}
traverseAllows = (data, ctx) => {
if (this.key in data) {
// ctx will be undefined if this node isn't context-dependent
return traverseKey(this.key, () => this.value.traverseAllows(data[this.key], ctx), ctx);
}
return this.optional;
};
traverseApply = (data, ctx) => {
if (this.key in data) {
traverseKey(this.key, () => this.value.traverseApply(data[this.key], ctx), ctx);
}
else if (this.hasKind("required"))
ctx.errorFromNodeContext(this.errorContext);
};
compile(js) {
js.if(`${this.serializedKey} in data`, () => js.traverseKey(this.serializedKey, `data${js.prop(this.key)}`, this.value));
if (this.hasKind("required")) {
js.else(() => js.traversalKind === "Apply" ?
js.line(`ctx.errorFromNodeContext(${this.compiledErrorContext})`)
: js.return(false));
}
if (js.traversalKind === "Allows")
js.return(true);
}
}
export const writeDefaultIntersectionMessage = (lValue, rValue) => `Invalid intersection of default values ${printable(lValue)} & ${printable(rValue)}`;

View File

@@ -0,0 +1,39 @@
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import type { NodeErrorContextInput } from "../shared/errors.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import { BaseProp, type Prop } from "./prop.ts";
export declare namespace Required {
interface ErrorContext extends BaseErrorContext<"required"> {
missingValueDescription: string;
}
interface Schema extends Prop.Schema {
}
interface Inner extends Prop.Inner {
}
type Declaration = declareNode<Prop.Declaration<"required"> & {
schema: Schema;
normalizedSchema: Schema;
inner: Inner;
errorContext: ErrorContext;
}>;
type Node = RequiredNode;
}
export declare class RequiredNode extends BaseProp<"required"> {
expression: string;
errorContext: NodeErrorContextInput<"required">;
compiledErrorContext: string;
}
export declare const Required: {
implementation: nodeImplementationOf<{
reducibleTo: "required";
kind: "required";
prerequisite: object;
intersectionIsOpen: true;
childKind: import("../shared/implement.ts").RootKind;
schema: Required.Schema;
normalizedSchema: Required.Schema;
inner: Required.Inner;
errorContext: Required.ErrorContext;
}>;
Node: typeof RequiredNode;
};

View File

@@ -0,0 +1,38 @@
import { compileObjectLiteral, implementNode } from "../shared/implement.js";
import { BaseProp, intersectProps } from "./prop.js";
const implementation = implementNode({
kind: "required",
hasAssociatedError: true,
intersectionIsOpen: true,
keys: {
key: {},
value: {
child: true,
parse: (schema, ctx) => ctx.$.parseSchema(schema)
}
},
normalize: schema => schema,
defaults: {
description: node => `${node.compiledKey}: ${node.value.description}`,
expected: ctx => ctx.missingValueDescription,
actual: () => "missing"
},
intersections: {
required: intersectProps,
optional: intersectProps
}
});
export class RequiredNode extends BaseProp {
expression = `${this.compiledKey}: ${this.value.expression}`;
errorContext = Object.freeze({
code: "required",
missingValueDescription: this.value.defaultShortDescription,
relativePath: [this.key],
meta: this.meta
});
compiledErrorContext = compileObjectLiteral(this.errorContext);
}
export const Required = {
implementation,
Node: RequiredNode
};

View File

@@ -0,0 +1,110 @@
import { type array, type satisfy } from "@ark/util";
import { BaseConstraint } from "../constraint.ts";
import type { RootSchema } from "../kinds.ts";
import { type BaseNode, type DeepNodeTransformContext, type DeepNodeTransformation, type FlatRef } from "../node.ts";
import type { ExactLengthNode } from "../refinements/exactLength.ts";
import type { MaxLengthNode } from "../refinements/maxLength.ts";
import type { MinLengthNode } from "../refinements/minLength.ts";
import type { Morph } from "../roots/morph.ts";
import type { BaseRoot } from "../roots/root.ts";
import type { NodeCompiler } from "../shared/compile.ts";
import type { BaseNormalizedSchema, declareNode } from "../shared/declare.ts";
import { type RootKind, type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { ToJsonSchema } from "../shared/toJsonSchema.ts";
import { type TraverseAllows, type TraverseApply } from "../shared/traversal.ts";
export declare namespace Sequence {
interface NormalizedSchema extends BaseNormalizedSchema {
readonly prefix?: array<RootSchema>;
readonly defaultables?: array<DefaultableSchema>;
readonly optionals?: array<RootSchema>;
readonly variadic?: RootSchema;
readonly minVariadicLength?: number;
readonly postfix?: array<RootSchema>;
}
type Schema = NormalizedSchema | RootSchema;
type DefaultableSchema = [schema: RootSchema, defaultValue: unknown];
type DefaultableElement = [node: BaseRoot, defaultValue: unknown];
interface Inner {
readonly prefix?: array<BaseRoot>;
readonly defaultables?: array<DefaultableElement>;
readonly optionals?: array<BaseRoot>;
readonly variadic?: BaseRoot;
readonly minVariadicLength?: number;
readonly postfix?: array<BaseRoot>;
}
interface Declaration extends declareNode<{
kind: "sequence";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: array;
reducibleTo: "sequence";
childKind: RootKind;
}> {
}
type Node = SequenceNode;
}
export declare class SequenceNode extends BaseConstraint<Sequence.Declaration> {
impliedBasis: BaseRoot;
tuple: SequenceTuple;
prefixLength: number;
defaultablesLength: number;
optionalsLength: number;
postfixLength: number;
defaultablesAndOptionals: BaseRoot[];
prevariadic: array<PrevariadicSequenceElement>;
variadicOrPostfix: array<BaseRoot>;
flatRefs: FlatRef[];
protected addFlatRefs(): FlatRef[];
isVariadicOnly: boolean;
minVariadicLength: number;
minLength: number;
minLengthNode: MinLengthNode | null;
maxLength: number | null;
maxLengthNode: MaxLengthNode | ExactLengthNode | null;
impliedSiblings: array<MaxLengthNode | MinLengthNode | ExactLengthNode>;
defaultValueMorphs: Morph[];
defaultValueMorphsReference: `$ark.${string}` | `$ark0.${string}` | `$ark${`2${string}` & `${bigint}`}.${string}` | `$ark${`1${string}` & `${bigint}`}.${string}` | `$ark${`3${string}` & `${bigint}`}.${string}` | `$ark${`4${string}` & `${bigint}`}.${string}` | `$ark${`5${string}` & `${bigint}`}.${string}` | `$ark${`6${string}` & `${bigint}`}.${string}` | `$ark${`7${string}` & `${bigint}`}.${string}` | `$ark${`8${string}` & `${bigint}`}.${string}` | `$ark${`9${string}` & `${bigint}`}.${string}` | undefined;
protected elementAtIndex(data: array, index: number): SequenceElement;
traverseAllows: TraverseAllows<array>;
traverseApply: TraverseApply<array>;
get element(): BaseRoot;
compile(js: NodeCompiler): void;
protected _transform(mapper: DeepNodeTransformation, ctx: DeepNodeTransformContext): BaseNode | null;
expression: string;
reduceJsonSchema(schema: JsonSchema.Array, ctx: ToJsonSchema.Context): JsonSchema.Array;
}
export declare const Sequence: {
implementation: nodeImplementationOf<Sequence.Declaration>;
Node: typeof SequenceNode;
};
export declare const postfixAfterOptionalOrDefaultableMessage = "A postfix required element cannot follow an optional or defaultable element";
export type postfixAfterOptionalOrDefaultableMessage = typeof postfixAfterOptionalOrDefaultableMessage;
export declare const postfixWithoutVariadicMessage = "A postfix element requires a variadic element";
export type postfixWithoutVariadicMessage = typeof postfixWithoutVariadicMessage;
export type SequenceElement = PrevariadicSequenceElement | VariadicSequenceElement | PostfixSequenceElement;
export type SequenceElementKind = satisfy<keyof Sequence.Inner, SequenceElement["kind"]>;
export type PrevariadicSequenceElement = PrefixSequenceElement | DefaultableSequenceElement | OptionalSequenceElement;
export type PrefixSequenceElement = {
kind: "prefix";
node: BaseRoot;
};
export type OptionalSequenceElement = {
kind: "optionals";
node: BaseRoot;
};
export type PostfixSequenceElement = {
kind: "postfix";
node: BaseRoot;
};
export type VariadicSequenceElement = {
kind: "variadic";
node: BaseRoot;
};
export type DefaultableSequenceElement = {
kind: "defaultables";
node: BaseRoot;
default: unknown;
};
export type SequenceTuple = array<SequenceElement>;

View File

@@ -0,0 +1,521 @@
import { append, conflatenate, printable, throwInternalError, throwParseError } from "@ark/util";
import { BaseConstraint } from "../constraint.js";
import { appendUniqueFlatRefs, flatRef } from "../node.js";
import { Disjoint } from "../shared/disjoint.js";
import { defaultValueSerializer, implementNode } from "../shared/implement.js";
import { intersectOrPipeNodes } from "../shared/intersections.js";
import { $ark, registeredReference } from "../shared/registry.js";
import { traverseKey } from "../shared/traversal.js";
import { assertDefaultValueAssignability, computeDefaultValueMorph } from "./optional.js";
import { writeDefaultIntersectionMessage } from "./prop.js";
const implementation = implementNode({
kind: "sequence",
hasAssociatedError: false,
collapsibleKey: "variadic",
keys: {
prefix: {
child: true,
parse: (schema, ctx) => {
// empty affixes are omitted. an empty array should therefore
// be specified as `{ proto: Array, length: 0 }`
if (schema.length === 0)
return undefined;
return schema.map(element => ctx.$.parseSchema(element));
}
},
optionals: {
child: true,
parse: (schema, ctx) => {
if (schema.length === 0)
return undefined;
return schema.map(element => ctx.$.parseSchema(element));
}
},
defaultables: {
child: defaultables => defaultables.map(element => element[0]),
parse: (defaultables, ctx) => {
if (defaultables.length === 0)
return undefined;
return defaultables.map(element => {
const node = ctx.$.parseSchema(element[0]);
assertDefaultValueAssignability(node, element[1], null);
return [node, element[1]];
});
},
serialize: defaults => defaults.map(element => [
element[0].collapsibleJson,
defaultValueSerializer(element[1])
]),
reduceIo: (ioKind, inner, defaultables) => {
if (ioKind === "in") {
inner.optionals = defaultables.map(d => d[0].rawIn);
return;
}
inner.prefix = defaultables.map(d => d[0].rawOut);
return;
}
},
variadic: {
child: true,
parse: (schema, ctx) => ctx.$.parseSchema(schema, ctx)
},
minVariadicLength: {
// minVariadicLength is reflected in the id of this node,
// but not its IntersectionNode parent since it is superceded by the minLength
// node it implies
parse: min => (min === 0 ? undefined : min)
},
postfix: {
child: true,
parse: (schema, ctx) => {
if (schema.length === 0)
return undefined;
return schema.map(element => ctx.$.parseSchema(element));
}
}
},
normalize: schema => {
if (typeof schema === "string")
return { variadic: schema };
if ("variadic" in schema ||
"prefix" in schema ||
"defaultables" in schema ||
"optionals" in schema ||
"postfix" in schema ||
"minVariadicLength" in schema) {
if (schema.postfix?.length) {
if (!schema.variadic)
return throwParseError(postfixWithoutVariadicMessage);
if (schema.optionals?.length || schema.defaultables?.length)
return throwParseError(postfixAfterOptionalOrDefaultableMessage);
}
if (schema.minVariadicLength && !schema.variadic) {
return throwParseError("minVariadicLength may not be specified without a variadic element");
}
return schema;
}
return { variadic: schema };
},
reduce: (raw, $) => {
let minVariadicLength = raw.minVariadicLength ?? 0;
const prefix = raw.prefix?.slice() ?? [];
const defaultables = raw.defaultables?.slice() ?? [];
const optionals = raw.optionals?.slice() ?? [];
const postfix = raw.postfix?.slice() ?? [];
if (raw.variadic) {
// optional elements equivalent to the variadic parameter are redundant
while (optionals[optionals.length - 1]?.equals(raw.variadic))
optionals.pop();
if (optionals.length === 0 && defaultables.length === 0) {
// If there are no optional, normalize prefix
// elements adjacent and equivalent to variadic:
// { variadic: number, prefix: [string, number] }
// reduces to:
// { variadic: number, prefix: [string], minVariadicLength: 1 }
while (prefix[prefix.length - 1]?.equals(raw.variadic)) {
prefix.pop();
minVariadicLength++;
}
}
// Normalize postfix elements adjacent and equivalent to variadic:
// { variadic: number, postfix: [number, number, 5] }
// reduces to:
// { variadic: number, postfix: [5], minVariadicLength: 2 }
while (postfix[0]?.equals(raw.variadic)) {
postfix.shift();
minVariadicLength++;
}
}
else if (optionals.length === 0 && defaultables.length === 0) {
// if there's no variadic, optional or defaultable elements,
// postfix can just be appended to prefix
prefix.push(...postfix.splice(0));
}
if (
// if any variadic adjacent elements were moved to minVariadicLength
minVariadicLength !== raw.minVariadicLength ||
// or any postfix elements were moved to prefix
(raw.prefix && raw.prefix.length !== prefix.length)) {
// reparse the reduced def
return $.node("sequence", {
...raw,
// empty lists will be omitted during parsing
prefix,
defaultables,
optionals,
postfix,
minVariadicLength
}, { prereduced: true });
}
},
defaults: {
description: node => {
if (node.isVariadicOnly)
return `${node.variadic.nestableExpression}[]`;
const innerDescription = node.tuple
.map(element => element.kind === "defaultables" ?
`${element.node.nestableExpression} = ${printable(element.default)}`
: element.kind === "optionals" ?
`${element.node.nestableExpression}?`
: element.kind === "variadic" ?
`...${element.node.nestableExpression}[]`
: element.node.expression)
.join(", ");
return `[${innerDescription}]`;
}
},
intersections: {
sequence: (l, r, ctx) => {
const rootState = _intersectSequences({
l: l.tuple,
r: r.tuple,
disjoint: new Disjoint(),
result: [],
fixedVariants: [],
ctx
});
const viableBranches = rootState.disjoint.length === 0 ?
[rootState, ...rootState.fixedVariants]
: rootState.fixedVariants;
return (viableBranches.length === 0 ? rootState.disjoint
: viableBranches.length === 1 ?
ctx.$.node("sequence", sequenceTupleToInner(viableBranches[0].result))
: ctx.$.node("union", viableBranches.map(state => ({
proto: Array,
sequence: sequenceTupleToInner(state.result)
}))));
}
// exactLength, minLength, and maxLength don't need to be defined
// here since impliedSiblings guarantees they will be added
// directly to the IntersectionNode parent of the SequenceNode
// they exist on
}
});
export class SequenceNode extends BaseConstraint {
impliedBasis = $ark.intrinsic.Array.internal;
tuple = sequenceInnerToTuple(this.inner);
prefixLength = this.prefix?.length ?? 0;
defaultablesLength = this.defaultables?.length ?? 0;
optionalsLength = this.optionals?.length ?? 0;
postfixLength = this.postfix?.length ?? 0;
defaultablesAndOptionals = [];
prevariadic = this.tuple.filter((el) => {
if (el.kind === "defaultables" || el.kind === "optionals") {
// populate defaultablesAndOptionals while filtering prevariadic
this.defaultablesAndOptionals.push(el.node);
return true;
}
return el.kind === "prefix";
});
variadicOrPostfix = conflatenate(this.variadic && [this.variadic], this.postfix);
// have to wait until prevariadic and variadicOrPostfix are set to calculate
flatRefs = this.addFlatRefs();
addFlatRefs() {
appendUniqueFlatRefs(this.flatRefs, this.prevariadic.flatMap((element, i) => append(element.node.flatRefs.map(ref => flatRef([`${i}`, ...ref.path], ref.node)), flatRef([`${i}`], element.node))));
appendUniqueFlatRefs(this.flatRefs, this.variadicOrPostfix.flatMap(element =>
// a postfix index can't be directly represented as a type
// key, so we just use the same matcher for variadic
append(element.flatRefs.map(ref => flatRef([$ark.intrinsic.nonNegativeIntegerString.internal, ...ref.path], ref.node)), flatRef([$ark.intrinsic.nonNegativeIntegerString.internal], element))));
return this.flatRefs;
}
isVariadicOnly = this.prevariadic.length + this.postfixLength === 0;
minVariadicLength = this.inner.minVariadicLength ?? 0;
minLength = this.prefixLength + this.minVariadicLength + this.postfixLength;
minLengthNode = this.minLength === 0 ?
null
// cast is safe here as the only time this would not be a
// MinLengthNode would be when minLength is 0
: this.$.node("minLength", this.minLength);
maxLength = this.variadic ? null : this.tuple.length;
maxLengthNode = this.maxLength === null ? null : this.$.node("maxLength", this.maxLength);
impliedSiblings = this.minLengthNode ?
this.maxLengthNode ?
[this.minLengthNode, this.maxLengthNode]
: [this.minLengthNode]
: this.maxLengthNode ? [this.maxLengthNode]
: [];
defaultValueMorphs = getDefaultableMorphs(this);
defaultValueMorphsReference = this.defaultValueMorphs.length ?
registeredReference(this.defaultValueMorphs)
: undefined;
elementAtIndex(data, index) {
if (index < this.prevariadic.length)
return this.tuple[index];
const firstPostfixIndex = data.length - this.postfixLength;
if (index >= firstPostfixIndex)
return { kind: "postfix", node: this.postfix[index - firstPostfixIndex] };
return {
kind: "variadic",
node: this.variadic ??
throwInternalError(`Unexpected attempt to access index ${index} on ${this}`)
};
}
// minLength/maxLength should be checked by Intersection before either traversal
traverseAllows = (data, ctx) => {
for (let i = 0; i < data.length; i++) {
if (!this.elementAtIndex(data, i).node.traverseAllows(data[i], ctx))
return false;
}
return true;
};
traverseApply = (data, ctx) => {
let i = 0;
for (; i < data.length; i++) {
traverseKey(i, () => this.elementAtIndex(data, i).node.traverseApply(data[i], ctx), ctx);
}
};
get element() {
return this.cacheGetter("element", this.$.node("union", this.children));
}
// minLength/maxLength compilation should be handled by Intersection
compile(js) {
if (this.prefix) {
for (const [i, node] of this.prefix.entries())
js.traverseKey(`${i}`, `data[${i}]`, node);
}
for (const [i, node] of this.defaultablesAndOptionals.entries()) {
const dataIndex = `${i + this.prefixLength}`;
js.if(`${dataIndex} >= data.length`, () => js.traversalKind === "Allows" ? js.return(true) : js.return());
js.traverseKey(dataIndex, `data[${dataIndex}]`, node);
}
if (this.variadic) {
if (this.postfix) {
js.const("firstPostfixIndex", `data.length${this.postfix ? `- ${this.postfix.length}` : ""}`);
}
js.for(`i < ${this.postfix ? "firstPostfixIndex" : "data.length"}`, () => js.traverseKey("i", "data[i]", this.variadic), this.prevariadic.length);
if (this.postfix) {
for (const [i, node] of this.postfix.entries()) {
const keyExpression = `firstPostfixIndex + ${i}`;
js.traverseKey(keyExpression, `data[${keyExpression}]`, node);
}
}
}
if (js.traversalKind === "Allows")
js.return(true);
}
_transform(mapper, ctx) {
ctx.path.push($ark.intrinsic.nonNegativeIntegerString.internal);
const result = super._transform(mapper, ctx);
ctx.path.pop();
return result;
}
// this depends on tuple so needs to come after it
expression = this.description;
reduceJsonSchema(schema, ctx) {
const isDraft07 = ctx.target === "draft-07";
if (this.prevariadic.length) {
const prefixSchemas = this.prevariadic.map(el => {
const valueSchema = el.node.toJsonSchemaRecurse(ctx);
if (el.kind === "defaultables") {
const value = typeof el.default === "function" ? el.default() : el.default;
valueSchema.default =
$ark.intrinsic.jsonData.allows(value) ?
value
: ctx.fallback.defaultValue({
code: "defaultValue",
base: valueSchema,
value
});
}
return valueSchema;
});
// draft-07 uses items as array, draft-2020-12 uses prefixItems
if (isDraft07)
schema.items = prefixSchemas;
else
schema.prefixItems = prefixSchemas;
}
// by default JSON schema prefixElements are optional
// add minLength here if there are any required prefix elements
if (this.minLength)
schema.minItems = this.minLength;
if (this.variadic) {
const variadicItemSchema = this.variadic.toJsonSchemaRecurse(ctx);
// draft-07 uses additionalItems when items is an array (tuple),
// draft-2020-12 uses items
if (isDraft07 && this.prevariadic.length)
schema.additionalItems = variadicItemSchema;
else
schema.items = variadicItemSchema;
// maxLength constraint will be enforced by items: false
// for non-variadic arrays
if (this.maxLength)
schema.maxItems = this.maxLength;
// postfix can only be present if variadic is present so nesting this is fine
if (this.postfix) {
const elements = this.postfix.map(el => el.toJsonSchemaRecurse(ctx));
schema = ctx.fallback.arrayPostfix({
code: "arrayPostfix",
base: schema,
elements
});
}
}
else {
// For fixed-length tuples without variadic elements
// draft-07 uses additionalItems: false, draft-2020-12 uses items: false
if (isDraft07)
schema.additionalItems = false;
else
schema.items = false;
// delete maxItems constraint that will have been added by the
// base intersection node to enforce fixed length
delete schema.maxItems;
}
return schema;
}
}
const defaultableMorphsCache = {};
const getDefaultableMorphs = (node) => {
if (!node.defaultables)
return [];
const morphs = [];
let cacheKey = "[";
const lastDefaultableIndex = node.prefixLength + node.defaultablesLength - 1;
for (let i = node.prefixLength; i <= lastDefaultableIndex; i++) {
const [elementNode, defaultValue] = node.defaultables[i - node.prefixLength];
morphs.push(computeDefaultValueMorph(i, elementNode, defaultValue));
cacheKey += `${i}: ${elementNode.id} = ${defaultValueSerializer(defaultValue)}, `;
}
cacheKey += "]";
return (defaultableMorphsCache[cacheKey] ??= morphs);
};
export const Sequence = {
implementation,
Node: SequenceNode
};
const sequenceInnerToTuple = (inner) => {
const tuple = [];
if (inner.prefix)
for (const node of inner.prefix)
tuple.push({ kind: "prefix", node });
if (inner.defaultables) {
for (const [node, defaultValue] of inner.defaultables)
tuple.push({ kind: "defaultables", node, default: defaultValue });
}
if (inner.optionals)
for (const node of inner.optionals)
tuple.push({ kind: "optionals", node });
if (inner.variadic)
tuple.push({ kind: "variadic", node: inner.variadic });
if (inner.postfix)
for (const node of inner.postfix)
tuple.push({ kind: "postfix", node });
return tuple;
};
const sequenceTupleToInner = (tuple) => tuple.reduce((result, element) => {
if (element.kind === "variadic")
result.variadic = element.node;
else if (element.kind === "defaultables") {
result.defaultables = append(result.defaultables, [
[element.node, element.default]
]);
}
else
result[element.kind] = append(result[element.kind], element.node);
return result;
}, {});
export const postfixAfterOptionalOrDefaultableMessage = "A postfix required element cannot follow an optional or defaultable element";
export const postfixWithoutVariadicMessage = "A postfix element requires a variadic element";
const _intersectSequences = (s) => {
const [lHead, ...lTail] = s.l;
const [rHead, ...rTail] = s.r;
if (!lHead || !rHead)
return s;
const lHasPostfix = lTail[lTail.length - 1]?.kind === "postfix";
const rHasPostfix = rTail[rTail.length - 1]?.kind === "postfix";
const kind = lHead.kind === "prefix" || rHead.kind === "prefix" ? "prefix"
: lHead.kind === "postfix" || rHead.kind === "postfix" ? "postfix"
: lHead.kind === "variadic" && rHead.kind === "variadic" ? "variadic"
// if either operand has postfix elements, the full-length
// intersection can't include optional elements (though they may
// exist in some of the fixed length variants)
: lHasPostfix || rHasPostfix ? "prefix"
: lHead.kind === "defaultables" || rHead.kind === "defaultables" ?
"defaultables"
: "optionals";
if (lHead.kind === "prefix" && rHead.kind === "variadic" && rHasPostfix) {
const postfixBranchResult = _intersectSequences({
...s,
fixedVariants: [],
r: rTail.map(element => ({ ...element, kind: "prefix" }))
});
if (postfixBranchResult.disjoint.length === 0)
s.fixedVariants.push(postfixBranchResult);
}
else if (rHead.kind === "prefix" &&
lHead.kind === "variadic" &&
lHasPostfix) {
const postfixBranchResult = _intersectSequences({
...s,
fixedVariants: [],
l: lTail.map(element => ({ ...element, kind: "prefix" }))
});
if (postfixBranchResult.disjoint.length === 0)
s.fixedVariants.push(postfixBranchResult);
}
const result = intersectOrPipeNodes(lHead.node, rHead.node, s.ctx);
if (result instanceof Disjoint) {
if (kind === "prefix" || kind === "postfix") {
s.disjoint.push(...result.withPrefixKey(
// ideally we could handle disjoint paths more precisely here,
// but not trivial to serialize postfix elements as keys
kind === "prefix" ? s.result.length : `-${lTail.length + 1}`,
// both operands must be required for the disjoint to be considered required
elementIsRequired(lHead) && elementIsRequired(rHead) ?
"required"
: "optional"));
s.result = [...s.result, { kind, node: $ark.intrinsic.never.internal }];
}
else if (kind === "optionals" || kind === "defaultables") {
// if the element result is optional and unsatisfiable, the
// intersection can still be satisfied as long as the tuple
// ends before the disjoint element would occur
return s;
}
else {
// if the element is variadic and unsatisfiable, the intersection
// can be satisfied with a fixed length variant including zero
// variadic elements
return _intersectSequences({
...s,
fixedVariants: [],
// if there were any optional elements, there will be no postfix elements
// so this mapping will never occur (which would be illegal otherwise)
l: lTail.map(element => ({ ...element, kind: "prefix" })),
r: lTail.map(element => ({ ...element, kind: "prefix" }))
});
}
}
else if (kind === "defaultables") {
if (lHead.kind === "defaultables" &&
rHead.kind === "defaultables" &&
lHead.default !== rHead.default) {
throwParseError(writeDefaultIntersectionMessage(lHead.default, rHead.default));
}
s.result = [
...s.result,
{
kind,
node: result,
default: lHead.kind === "defaultables" ? lHead.default
: rHead.kind === "defaultables" ? rHead.default
: throwInternalError(`Unexpected defaultable intersection from ${lHead.kind} and ${rHead.kind} elements.`)
}
];
}
else
s.result = [...s.result, { kind, node: result }];
const lRemaining = s.l.length;
const rRemaining = s.r.length;
if (lHead.kind !== "variadic" ||
(lRemaining >= rRemaining &&
(rHead.kind === "variadic" || rRemaining === 1)))
s.l = lTail;
if (rHead.kind !== "variadic" ||
(rRemaining >= lRemaining &&
(lHead.kind === "variadic" || lRemaining === 1)))
s.r = rTail;
return _intersectSequences(s);
};
const elementIsRequired = (el) => el.kind === "prefix" || el.kind === "postfix";

View File

@@ -0,0 +1,4 @@
import { type RegisteredReference } from "../shared/registry.ts";
export declare const arrayIndexSource = "^(?:0|[1-9]\\d*)$";
export declare const arrayIndexMatcher: RegExp;
export declare const arrayIndexMatcherReference: RegisteredReference;

View File

@@ -0,0 +1,4 @@
import { registeredReference } from "../shared/registry.js";
export const arrayIndexSource = `^(?:0|[1-9]\\d*)$`;
export const arrayIndexMatcher = new RegExp(arrayIndexSource);
export const arrayIndexMatcherReference = registeredReference(arrayIndexMatcher);

View File

@@ -0,0 +1,113 @@
import { type array, type describe, type Key, type listable } from "@ark/util";
import { BaseConstraint } from "../constraint.ts";
import type { GettableKeyOrNode, KeyOrKeyNode } from "../node.ts";
import type { Morph } from "../roots/morph.ts";
import { type BaseRoot } from "../roots/root.ts";
import type { BaseScope } from "../scope.ts";
import { type NodeCompiler } from "../shared/compile.ts";
import type { BaseNormalizedSchema, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf, type StructuralKind } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import { type RegisteredReference } from "../shared/registry.ts";
import { ToJsonSchema } from "../shared/toJsonSchema.ts";
import { type InternalTraversal, type TraversalKind, type TraverseAllows, type TraverseApply } from "../shared/traversal.ts";
import { makeRootAndArrayPropertiesMutable } from "../shared/utils.ts";
import type { Index } from "./index.ts";
import { Optional } from "./optional.ts";
import type { Prop } from "./prop.ts";
import type { Required } from "./required.ts";
import type { Sequence } from "./sequence.ts";
/**
* - `"ignore"` (default) - allow and preserve extra properties
* - `"reject"` - disallow extra properties
* - `"delete"` - clone and remove extra properties from output
*/
export type UndeclaredKeyBehavior = "ignore" | UndeclaredKeyHandling;
export type UndeclaredKeyHandling = "reject" | "delete";
export declare namespace Structure {
interface Schema extends BaseNormalizedSchema {
readonly optional?: readonly Optional.Schema[];
readonly required?: readonly Required.Schema[];
readonly index?: readonly Index.Schema[];
readonly sequence?: Sequence.Schema;
readonly undeclared?: UndeclaredKeyBehavior;
}
interface Inner {
readonly optional?: readonly Optional.Node[];
readonly required?: readonly Required.Node[];
readonly index?: readonly Index.Node[];
readonly sequence?: Sequence.Node;
readonly undeclared?: UndeclaredKeyHandling;
}
namespace Inner {
type mutable = makeRootAndArrayPropertiesMutable<Inner>;
}
interface Declaration extends declareNode<{
kind: "structure";
schema: Schema;
normalizedSchema: Schema;
inner: Inner;
prerequisite: object;
childKind: StructuralKind;
}> {
}
type Node = StructureNode;
}
export declare class StructureNode extends BaseConstraint<Structure.Declaration> {
impliedBasis: BaseRoot;
impliedSiblings: BaseConstraint<import("../constraint.ts").Constraint.Declaration>[];
props: array<Prop.Node>;
propsByKey: Record<Key, Prop.Node | undefined>;
propsByKeyReference: RegisteredReference;
expression: string;
requiredKeys: Key[];
optionalKeys: Key[];
literalKeys: Key[];
_keyof: BaseRoot | undefined;
keyof(): BaseRoot;
map(flatMapProp: PropFlatMapper): StructureNode;
assertHasKeys(keys: array<KeyOrKeyNode>): void;
get(indexer: GettableKeyOrNode, ...path: array<GettableKeyOrNode>): BaseRoot;
pick(...keys: KeyOrKeyNode[]): StructureNode;
omit(...keys: KeyOrKeyNode[]): StructureNode;
optionalize(): StructureNode;
require(): StructureNode;
merge(r: StructureNode): StructureNode;
private filterKeys;
traverseAllows: TraverseAllows<object>;
traverseApply: TraverseApply<object>;
protected _traverse: (traversalKind: TraversalKind, data: object, ctx: InternalTraversal) => boolean;
get defaultable(): Optional.Node.withDefault[];
declaresKey: (k: Key) => boolean;
_compileDeclaresKey(js: NodeCompiler): string;
get structuralMorph(): Morph | undefined;
structuralMorphRef: RegisteredReference | undefined;
compile(js: NodeCompiler): unknown;
protected compileExhaustiveEntry(js: NodeCompiler): NodeCompiler;
reduceJsonSchema(schema: JsonSchema.Structure, ctx: ToJsonSchema.Context): JsonSchema.Structure;
reduceObjectJsonSchema(schema: JsonSchema.Object, ctx: ToJsonSchema.Context): JsonSchema.Object;
}
export type PropFlatMapper = (entry: Prop.Node) => listable<MappedPropInner>;
export type MappedPropInner = BaseMappedPropInner | OptionalMappedPropInner;
export interface BaseMappedPropInner extends Required.Schema {
kind?: "required" | "optional";
}
export interface OptionalMappedPropInner extends Optional.Schema {
kind: "optional";
}
export declare const Structure: {
implementation: nodeImplementationOf<Structure.Declaration>;
Node: typeof StructureNode;
};
export declare const writeNumberIndexMessage: (indexExpression: string, sequenceExpression: string) => string;
export type NormalizedIndex = {
index?: Index.Node;
required?: Required.Node[];
optional?: Optional.Node[];
};
/** extract enumerable named props from an index signature */
export declare const normalizeIndex: (signature: BaseRoot, value: BaseRoot, $: BaseScope) => NormalizedIndex;
export declare const typeKeyToString: (k: KeyOrKeyNode) => string;
export declare const writeInvalidKeysMessage: <o extends string, keys extends array<KeyOrKeyNode>>(o: o, keys: keys) => string;
export declare const writeDuplicateKeyMessage: <key extends Key>(key: key) => writeDuplicateKeyMessage<key>;
export type writeDuplicateKeyMessage<key extends Key> = `Duplicate key '${describe<key>}'`;

View File

@@ -0,0 +1,747 @@
import { append, conflatenate, flatMorph, printable, spliterate, throwInternalError, throwParseError } from "@ark/util";
import { BaseConstraint, constraintKeyParser, flattenConstraints, intersectConstraints } from "../constraint.js";
import { intrinsic } from "../intrinsic.js";
import { typeOrTermExtends } from "../roots/root.js";
import { compileSerializedValue } from "../shared/compile.js";
import { Disjoint } from "../shared/disjoint.js";
import { implementNode } from "../shared/implement.js";
import { intersectNodesRoot } from "../shared/intersections.js";
import { $ark, registeredReference } from "../shared/registry.js";
import { ToJsonSchema } from "../shared/toJsonSchema.js";
import { traverseKey } from "../shared/traversal.js";
import { hasArkKind, isNode, makeRootAndArrayPropertiesMutable } from "../shared/utils.js";
import { Optional } from "./optional.js";
const createStructuralWriter = (childStringProp) => (node) => {
if (node.props.length || node.index) {
const parts = node.index?.map(index => index[childStringProp]) ?? [];
for (const prop of node.props)
parts.push(prop[childStringProp]);
if (node.undeclared)
parts.push(`+ (undeclared): ${node.undeclared}`);
const objectLiteralDescription = `{ ${parts.join(", ")} }`;
return node.sequence ?
`${objectLiteralDescription} & ${node.sequence.description}`
: objectLiteralDescription;
}
return node.sequence?.description ?? "{}";
};
const structuralDescription = createStructuralWriter("description");
const structuralExpression = createStructuralWriter("expression");
const intersectPropsAndIndex = (l, r, $) => {
const kind = l.required ? "required" : "optional";
if (!r.signature.allows(l.key))
return null;
const value = intersectNodesRoot(l.value, r.value, $);
if (value instanceof Disjoint) {
return kind === "optional" ?
$.node("optional", {
key: l.key,
value: $ark.intrinsic.never.internal
})
: value.withPrefixKey(l.key, l.kind);
}
return null;
};
const implementation = implementNode({
kind: "structure",
hasAssociatedError: false,
normalize: schema => schema,
applyConfig: (schema, config) => {
if (!schema.undeclared && config.onUndeclaredKey !== "ignore") {
return {
...schema,
undeclared: config.onUndeclaredKey
};
}
return schema;
},
keys: {
required: {
child: true,
parse: constraintKeyParser("required"),
reduceIo: (ioKind, inner, nodes) => {
// ensure we don't overwrite nodes added by optional
inner.required = append(inner.required, nodes.map(node => (ioKind === "in" ? node.rawIn : node.rawOut)));
return;
}
},
optional: {
child: true,
parse: constraintKeyParser("optional"),
reduceIo: (ioKind, inner, nodes) => {
if (ioKind === "in") {
inner.optional = nodes.map(node => node.rawIn);
return;
}
for (const node of nodes) {
inner[node.outProp.kind] = append(inner[node.outProp.kind], node.outProp.rawOut);
}
}
},
index: {
child: true,
parse: constraintKeyParser("index")
},
sequence: {
child: true,
parse: constraintKeyParser("sequence")
},
undeclared: {
parse: behavior => (behavior === "ignore" ? undefined : behavior),
reduceIo: (ioKind, inner, value) => {
if (value === "reject") {
inner.undeclared = "reject";
return;
}
// if base is "delete", undeclared keys are "ignore" (i.e. unconstrained)
// on input and "reject" on output
if (ioKind === "in")
delete inner.undeclared;
else
inner.undeclared = "reject";
}
}
},
defaults: {
description: structuralDescription
},
intersections: {
structure: (l, r, ctx) => {
const lInner = { ...l.inner };
const rInner = { ...r.inner };
const disjointResult = new Disjoint();
if (l.undeclared) {
const lKey = l.keyof();
for (const k of r.requiredKeys) {
if (!lKey.allows(k)) {
disjointResult.add("presence", $ark.intrinsic.never.internal, r.propsByKey[k].value, {
path: [k]
});
}
}
if (rInner.optional)
rInner.optional = rInner.optional.filter(n => lKey.allows(n.key));
if (rInner.index) {
rInner.index = rInner.index.flatMap(n => {
if (n.signature.extends(lKey))
return n;
const indexOverlap = intersectNodesRoot(lKey, n.signature, ctx.$);
if (indexOverlap instanceof Disjoint)
return [];
const normalized = normalizeIndex(indexOverlap, n.value, ctx.$);
if (normalized.required) {
rInner.required = conflatenate(rInner.required, normalized.required);
}
if (normalized.optional) {
rInner.optional = conflatenate(rInner.optional, normalized.optional);
}
return normalized.index ?? [];
});
}
}
if (r.undeclared) {
const rKey = r.keyof();
for (const k of l.requiredKeys) {
if (!rKey.allows(k)) {
disjointResult.add("presence", l.propsByKey[k].value, $ark.intrinsic.never.internal, {
path: [k]
});
}
}
if (lInner.optional)
lInner.optional = lInner.optional.filter(n => rKey.allows(n.key));
if (lInner.index) {
lInner.index = lInner.index.flatMap(n => {
if (n.signature.extends(rKey))
return n;
const indexOverlap = intersectNodesRoot(rKey, n.signature, ctx.$);
if (indexOverlap instanceof Disjoint)
return [];
const normalized = normalizeIndex(indexOverlap, n.value, ctx.$);
if (normalized.required) {
lInner.required = conflatenate(lInner.required, normalized.required);
}
if (normalized.optional) {
lInner.optional = conflatenate(lInner.optional, normalized.optional);
}
return normalized.index ?? [];
});
}
}
const baseInner = {};
if (l.undeclared || r.undeclared) {
baseInner.undeclared =
l.undeclared === "reject" || r.undeclared === "reject" ?
"reject"
: "delete";
}
const childIntersectionResult = intersectConstraints({
kind: "structure",
baseInner,
l: flattenConstraints(lInner),
r: flattenConstraints(rInner),
roots: [],
ctx
});
if (childIntersectionResult instanceof Disjoint)
disjointResult.push(...childIntersectionResult);
if (disjointResult.length)
return disjointResult;
return childIntersectionResult;
}
},
reduce: (inner, $) => {
if (!inner.required && !inner.optional)
return;
const seen = {};
let updated = false;
const newOptionalProps = inner.optional ? [...inner.optional] : [];
// check required keys for duplicates and handle index intersections
if (inner.required) {
for (let i = 0; i < inner.required.length; i++) {
const requiredProp = inner.required[i];
if (requiredProp.key in seen)
throwParseError(writeDuplicateKeyMessage(requiredProp.key));
seen[requiredProp.key] = true;
if (inner.index) {
for (const index of inner.index) {
const intersection = intersectPropsAndIndex(requiredProp, index, $);
if (intersection instanceof Disjoint)
return intersection;
}
}
}
}
// check optional keys for duplicates and handle index intersections
if (inner.optional) {
for (let i = 0; i < inner.optional.length; i++) {
const optionalProp = inner.optional[i];
if (optionalProp.key in seen)
throwParseError(writeDuplicateKeyMessage(optionalProp.key));
seen[optionalProp.key] = true;
if (inner.index) {
for (const index of inner.index) {
const intersection = intersectPropsAndIndex(optionalProp, index, $);
if (intersection instanceof Disjoint)
return intersection;
if (intersection !== null) {
newOptionalProps[i] = intersection;
updated = true;
}
}
}
}
}
if (updated) {
return $.node("structure", { ...inner, optional: newOptionalProps }, { prereduced: true });
}
}
});
export class StructureNode extends BaseConstraint {
impliedBasis = $ark.intrinsic.object.internal;
impliedSiblings = this.children.flatMap(n => n.impliedSiblings ?? []);
props = conflatenate(this.required, this.optional);
propsByKey = flatMorph(this.props, (i, node) => [node.key, node]);
propsByKeyReference = registeredReference(this.propsByKey);
expression = structuralExpression(this);
requiredKeys = this.required?.map(node => node.key) ?? [];
optionalKeys = this.optional?.map(node => node.key) ?? [];
literalKeys = [...this.requiredKeys, ...this.optionalKeys];
_keyof;
keyof() {
if (this._keyof)
return this._keyof;
let branches = this.$.units(this.literalKeys).branches;
if (this.index) {
for (const { signature } of this.index)
branches = branches.concat(signature.branches);
}
return (this._keyof = this.$.node("union", branches));
}
map(flatMapProp) {
return this.$.node("structure", this.props
.flatMap(flatMapProp)
.reduce((structureInner, mapped) => {
const originalProp = this.propsByKey[mapped.key];
if (isNode(mapped)) {
if (mapped.kind !== "required" && mapped.kind !== "optional") {
return throwParseError(`Map result must have kind "required" or "optional" (was ${mapped.kind})`);
}
structureInner[mapped.kind] = append(structureInner[mapped.kind], mapped);
return structureInner;
}
const mappedKind = mapped.kind ?? originalProp?.kind ?? "required";
// extract the inner keys from the map result in case a node was spread,
// which would otherwise lead to invalid keys
const mappedPropInner = flatMorph(mapped, (k, v) => (k in Optional.implementation.keys ? [k, v] : []));
structureInner[mappedKind] = append(structureInner[mappedKind], this.$.node(mappedKind, mappedPropInner));
return structureInner;
}, {}));
}
assertHasKeys(keys) {
const invalidKeys = keys.filter(k => !typeOrTermExtends(k, this.keyof()));
if (invalidKeys.length) {
return throwParseError(writeInvalidKeysMessage(this.expression, invalidKeys));
}
}
get(indexer, ...path) {
let value;
let required = false;
const key = indexerToKey(indexer);
if ((typeof key === "string" || typeof key === "symbol") &&
this.propsByKey[key]) {
value = this.propsByKey[key].value;
required = this.propsByKey[key].required;
}
if (this.index) {
for (const n of this.index) {
if (typeOrTermExtends(key, n.signature))
value = value?.and(n.value) ?? n.value;
}
}
if (this.sequence &&
typeOrTermExtends(key, $ark.intrinsic.nonNegativeIntegerString)) {
if (hasArkKind(key, "root")) {
if (this.sequence.variadic)
// if there is a variadic element and we're accessing an index, return a union
// of all possible elements. If there is no variadic expression, we're in a tuple
// so this access wouldn't be safe based on the array indices
value = value?.and(this.sequence.element) ?? this.sequence.element;
}
else {
const index = Number.parseInt(key);
if (index < this.sequence.prevariadic.length) {
const fixedElement = this.sequence.prevariadic[index].node;
value = value?.and(fixedElement) ?? fixedElement;
required ||= index < this.sequence.prefixLength;
}
else if (this.sequence.variadic) {
// ideally we could return something more specific for postfix
// but there is no way to represent it using an index alone
const nonFixedElement = this.$.node("union", this.sequence.variadicOrPostfix);
value = value?.and(nonFixedElement) ?? nonFixedElement;
}
}
}
if (!value) {
if (this.sequence?.variadic &&
hasArkKind(key, "root") &&
key.extends($ark.intrinsic.number)) {
return throwParseError(writeNumberIndexMessage(key.expression, this.sequence.expression));
}
return throwParseError(writeInvalidKeysMessage(this.expression, [key]));
}
const result = value.get(...path);
return required ? result : result.or($ark.intrinsic.undefined);
}
pick(...keys) {
this.assertHasKeys(keys);
return this.$.node("structure", this.filterKeys("pick", keys));
}
omit(...keys) {
this.assertHasKeys(keys);
return this.$.node("structure", this.filterKeys("omit", keys));
}
optionalize() {
const { required, ...inner } = this.inner;
return this.$.node("structure", {
...inner,
optional: this.props.map(prop => prop.hasKind("required") ? this.$.node("optional", prop.inner) : prop)
});
}
require() {
const { optional, ...inner } = this.inner;
return this.$.node("structure", {
...inner,
required: this.props.map(prop => prop.hasKind("optional") ?
{
key: prop.key,
value: prop.value
}
: prop)
});
}
merge(r) {
const inner = this.filterKeys("omit", [r.keyof()]);
if (r.required)
inner.required = append(inner.required, r.required);
if (r.optional)
inner.optional = append(inner.optional, r.optional);
if (r.index)
inner.index = append(inner.index, r.index);
if (r.sequence)
inner.sequence = r.sequence;
if (r.undeclared)
inner.undeclared = r.undeclared;
else
delete inner.undeclared;
return this.$.node("structure", inner);
}
filterKeys(operation, keys) {
const result = makeRootAndArrayPropertiesMutable(this.inner);
const shouldKeep = (key) => {
const matchesKey = keys.some(k => typeOrTermExtends(key, k));
return operation === "pick" ? matchesKey : !matchesKey;
};
if (result.required)
result.required = result.required.filter(prop => shouldKeep(prop.key));
if (result.optional)
result.optional = result.optional.filter(prop => shouldKeep(prop.key));
if (result.index)
result.index = result.index.filter(index => shouldKeep(index.signature));
return result;
}
traverseAllows = (data, ctx) => this._traverse("Allows", data, ctx);
traverseApply = (data, ctx) => this._traverse("Apply", data, ctx);
_traverse = (traversalKind, data, ctx) => {
const errorCount = ctx?.currentErrorCount ?? 0;
for (let i = 0; i < this.props.length; i++) {
if (traversalKind === "Allows") {
if (!this.props[i].traverseAllows(data, ctx))
return false;
}
else {
this.props[i].traverseApply(data, ctx);
if (ctx.failFast && ctx.currentErrorCount > errorCount)
return false;
}
}
if (this.sequence) {
if (traversalKind === "Allows") {
if (!this.sequence.traverseAllows(data, ctx))
return false;
}
else {
this.sequence.traverseApply(data, ctx);
if (ctx.failFast && ctx.currentErrorCount > errorCount)
return false;
}
}
if (this.index || this.undeclared === "reject") {
const keys = Object.keys(data);
keys.push(...Object.getOwnPropertySymbols(data));
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (this.index) {
for (const node of this.index) {
if (node.signature.traverseAllows(k, ctx)) {
if (traversalKind === "Allows") {
const result = traverseKey(k, () => node.value.traverseAllows(data[k], ctx), ctx);
if (!result)
return false;
}
else {
traverseKey(k, () => node.value.traverseApply(data[k], ctx), ctx);
if (ctx.failFast && ctx.currentErrorCount > errorCount)
return false;
}
}
}
}
if (this.undeclared === "reject" && !this.declaresKey(k)) {
if (traversalKind === "Allows")
return false;
// this should have its own error code:
// https://github.com/arktypeio/arktype/issues/1403
ctx.errorFromNodeContext({
code: "predicate",
expected: "removed",
actual: "",
relativePath: [k],
meta: this.meta
});
if (ctx.failFast)
return false;
}
}
}
// added additional ctx check here to address
// https://github.com/arktypeio/arktype/issues/1346
if (this.structuralMorph && ctx && !ctx.hasError())
ctx.queueMorphs([this.structuralMorph]);
return true;
};
get defaultable() {
return this.cacheGetter("defaultable", this.optional?.filter(o => o.hasDefault()) ?? []);
}
declaresKey = (k) => k in this.propsByKey ||
this.index?.some(n => n.signature.allows(k)) ||
(this.sequence !== undefined &&
$ark.intrinsic.nonNegativeIntegerString.allows(k));
_compileDeclaresKey(js) {
const parts = [];
if (this.props.length)
parts.push(`k in ${this.propsByKeyReference}`);
if (this.index) {
for (const index of this.index)
parts.push(js.invoke(index.signature, { kind: "Allows", arg: "k" }));
}
if (this.sequence)
parts.push("$ark.intrinsic.nonNegativeIntegerString.allows(k)");
// if parts is empty, this is a structure like { "+": "reject" }
// that declares no keys, so return false
return parts.join(" || ") || "false";
}
get structuralMorph() {
return this.cacheGetter("structuralMorph", getPossibleMorph(this));
}
structuralMorphRef = this.structuralMorph && registeredReference(this.structuralMorph);
compile(js) {
if (js.traversalKind === "Apply")
js.initializeErrorCount();
for (const prop of this.props) {
js.check(prop);
if (js.traversalKind === "Apply")
js.returnIfFailFast();
}
if (this.sequence) {
js.check(this.sequence);
if (js.traversalKind === "Apply")
js.returnIfFailFast();
}
if (this.index || this.undeclared === "reject") {
js.const("keys", "Object.keys(data)");
js.line("keys.push(...Object.getOwnPropertySymbols(data))");
js.for("i < keys.length", () => this.compileExhaustiveEntry(js));
}
if (js.traversalKind === "Allows")
return js.return(true);
// always queue deleteUndeclared on valid traversal for "delete"
if (this.structuralMorphRef) {
// added additional ctx check here to address
// https://github.com/arktypeio/arktype/issues/1346
js.if("ctx && !ctx.hasError()", () => {
js.line(`ctx.queueMorphs([`);
precompileMorphs(js, this);
return js.line("])");
});
}
}
compileExhaustiveEntry(js) {
js.const("k", "keys[i]");
if (this.index) {
for (const node of this.index) {
js.if(`${js.invoke(node.signature, { arg: "k", kind: "Allows" })}`, () => js.traverseKey("k", "data[k]", node.value));
}
}
if (this.undeclared === "reject") {
js.if(`!(${this._compileDeclaresKey(js)})`, () => {
if (js.traversalKind === "Allows")
return js.return(false);
return js
.line(`ctx.errorFromNodeContext({ code: "predicate", expected: "removed", actual: "", relativePath: [k], meta: ${this.compiledMeta} })`)
.if("ctx.failFast", () => js.return());
});
}
return js;
}
reduceJsonSchema(schema, ctx) {
switch (schema.type) {
case "object":
return this.reduceObjectJsonSchema(schema, ctx);
case "array":
const arraySchema = this.sequence?.reduceJsonSchema(schema, ctx) ?? schema;
if (this.props.length || this.index) {
return ctx.fallback.arrayObject({
code: "arrayObject",
base: arraySchema,
object: this.reduceObjectJsonSchema({ type: "object" }, ctx)
});
}
return arraySchema;
default:
return ToJsonSchema.throwInternalOperandError("structure", schema);
}
}
reduceObjectJsonSchema(schema, ctx) {
if (this.props.length) {
schema.properties = {};
for (const prop of this.props) {
const valueSchema = prop.value.toJsonSchemaRecurse(ctx);
if (typeof prop.key === "symbol") {
ctx.fallback.symbolKey({
code: "symbolKey",
base: schema,
key: prop.key,
value: valueSchema,
optional: prop.optional
});
continue;
}
if (prop.hasDefault()) {
const value = typeof prop.default === "function" ? prop.default() : prop.default;
valueSchema.default =
$ark.intrinsic.jsonData.allows(value) ?
value
: ctx.fallback.defaultValue({
code: "defaultValue",
base: valueSchema,
value
});
}
schema.properties[prop.key] = valueSchema;
}
if (this.requiredKeys.length && schema.properties) {
schema.required = this.requiredKeys.filter((k) => typeof k === "string" && k in schema.properties);
}
}
if (this.index) {
for (const index of this.index) {
const valueJsonSchema = index.value.toJsonSchemaRecurse(ctx);
if (index.signature.equals($ark.intrinsic.string)) {
schema.additionalProperties = valueJsonSchema;
continue;
}
for (const keyBranch of index.signature.branches) {
if (!keyBranch.extends($ark.intrinsic.string)) {
schema = ctx.fallback.symbolKey({
code: "symbolKey",
base: schema,
key: null,
value: valueJsonSchema,
optional: false
});
continue;
}
let keySchema = { type: "string" };
if (keyBranch.hasKind("morph")) {
keySchema = ctx.fallback.morph({
code: "morph",
base: keyBranch.rawIn.toJsonSchemaRecurse(ctx),
out: keyBranch.rawOut.toJsonSchemaRecurse(ctx)
});
}
if (!keyBranch.hasKind("intersection")) {
return throwInternalError(`Unexpected index branch kind ${keyBranch.kind}.`);
}
const { pattern } = keyBranch.inner;
if (pattern) {
const keySchemaWithPattern = Object.assign(keySchema, {
pattern: pattern[0].rule
});
for (let i = 1; i < pattern.length; i++) {
keySchema = ctx.fallback.patternIntersection({
code: "patternIntersection",
base: keySchemaWithPattern,
pattern: pattern[i].rule
});
}
schema.patternProperties ??= {};
schema.patternProperties[keySchemaWithPattern.pattern] =
valueJsonSchema;
}
}
}
}
if (this.undeclared && !schema.additionalProperties)
schema.additionalProperties = false;
return schema;
}
}
const defaultableMorphsCache = {};
const constructStructuralMorphCacheKey = (node) => {
let cacheKey = "";
for (let i = 0; i < node.defaultable.length; i++)
cacheKey += node.defaultable[i].defaultValueMorphRef;
if (node.sequence?.defaultValueMorphsReference)
cacheKey += node.sequence?.defaultValueMorphsReference;
if (node.undeclared === "delete") {
cacheKey += "delete !(";
if (node.required)
for (const n of node.required)
cacheKey += n.compiledKey + " | ";
if (node.optional)
for (const n of node.optional)
cacheKey += n.compiledKey + " | ";
if (node.index)
for (const index of node.index)
cacheKey += index.signature.id + " | ";
if (node.sequence) {
if (node.sequence.maxLength === null)
cacheKey += intrinsic.nonNegativeIntegerString.id;
else {
for (let i = 0; i < node.sequence.tuple.length; i++)
cacheKey += i + " | ";
}
}
cacheKey += ")";
}
return cacheKey;
};
const getPossibleMorph = (node) => {
const cacheKey = constructStructuralMorphCacheKey(node);
if (!cacheKey)
return undefined;
if (defaultableMorphsCache[cacheKey])
return defaultableMorphsCache[cacheKey];
const $arkStructuralMorph = (data, ctx) => {
for (let i = 0; i < node.defaultable.length; i++) {
if (!(node.defaultable[i].key in data))
node.defaultable[i].defaultValueMorph(data, ctx);
}
if (node.sequence?.defaultables) {
for (let i = data.length - node.sequence.prefixLength; i < node.sequence.defaultables.length; i++)
node.sequence.defaultValueMorphs[i](data, ctx);
}
if (node.undeclared === "delete")
for (const k in data)
if (!node.declaresKey(k))
delete data[k];
return data;
};
return (defaultableMorphsCache[cacheKey] = $arkStructuralMorph);
};
const precompileMorphs = (js, node) => {
const requiresContext = node.defaultable.some(node => node.defaultValueMorph.length === 2) ||
node.sequence?.defaultValueMorphs.some(morph => morph.length === 2);
const args = `(data${requiresContext ? ", ctx" : ""})`;
return js.block(`${args} => `, js => {
for (let i = 0; i < node.defaultable.length; i++) {
const { serializedKey, defaultValueMorphRef } = node.defaultable[i];
js.if(`!(${serializedKey} in data)`, js => js.line(`${defaultValueMorphRef}${args}`));
}
if (node.sequence?.defaultables) {
js.for(`i < ${node.sequence.defaultables.length}`, js => js.set(`data[i]`, 5), `data.length - ${node.sequence.prefixLength}`);
}
if (node.undeclared === "delete") {
js.forIn("data", js => js.if(`!(${node._compileDeclaresKey(js)})`, js => js.line(`delete data[k]`)));
}
return js.return("data");
});
};
export const Structure = {
implementation,
Node: StructureNode
};
const indexerToKey = (indexable) => {
if (hasArkKind(indexable, "root") && indexable.hasKind("unit"))
indexable = indexable.unit;
if (typeof indexable === "number")
indexable = `${indexable}`;
return indexable;
};
export const writeNumberIndexMessage = (indexExpression, sequenceExpression) => `${indexExpression} is not allowed as an array index on ${sequenceExpression}. Use the 'nonNegativeIntegerString' keyword instead.`;
/** extract enumerable named props from an index signature */
export const normalizeIndex = (signature, value, $) => {
const [enumerableBranches, nonEnumerableBranches] = spliterate(signature.branches, k => k.hasKind("unit"));
if (!enumerableBranches.length)
return { index: $.node("index", { signature, value }) };
const normalized = {};
for (const n of enumerableBranches) {
// since required can be reduced to optional if it has a default or
// optional meta on its value, we have to assign it depending on the
// compiled kind
const prop = $.node("required", { key: n.unit, value });
normalized[prop.kind] = append(normalized[prop.kind], prop);
}
if (nonEnumerableBranches.length) {
normalized.index = $.node("index", {
signature: nonEnumerableBranches,
value
});
}
return normalized;
};
export const typeKeyToString = (k) => hasArkKind(k, "root") ? k.expression : printable(k);
export const writeInvalidKeysMessage = (o, keys) => `Key${keys.length === 1 ? "" : "s"} ${keys.map(typeKeyToString).join(", ")} ${keys.length === 1 ? "does" : "do"} not exist on ${o}`;
export const writeDuplicateKeyMessage = (key) => `Duplicate key ${compileSerializedValue(key)}`;