- 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
748 lines
32 KiB
JavaScript
748 lines
32 KiB
JavaScript
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)}`;
|