import { Callable, appendUnique, flatMorph, includes, isArray, isEmptyObject, isKeyOf, liftArray, printable, stringifyPath, throwError, throwInternalError } from "@ark/util"; import { basisKinds, constraintKinds, precedenceOfKind, refinementKinds, rootKinds, structuralKinds } from "./shared/implement.js"; import { $ark } from "./shared/registry.js"; import { Traversal } from "./shared/traversal.js"; import { isNode } from "./shared/utils.js"; export class BaseNode extends Callable { attachments; $; onFail; includesTransform; includesContextualPredicate; isCyclic; allowsRequiresContext; rootApplyStrategy; contextFreeMorph; rootApply; referencesById; shallowReferences; flatRefs; flatMorphs; allows; get shallowMorphs() { return []; } constructor(attachments, $) { super((data, pipedFromCtx, onFail = this.onFail) => { if (pipedFromCtx) { this.traverseApply(data, pipedFromCtx); return pipedFromCtx.hasError() ? pipedFromCtx.errors : pipedFromCtx.data; } return this.rootApply(data, onFail); }, { attach: attachments }); this.attachments = attachments; this.$ = $; this.onFail = this.meta.onFail ?? this.$.resolvedConfig.onFail; this.includesTransform = this.hasKind("morph") || (this.hasKind("structure") && this.structuralMorph !== undefined) || (this.hasKind("sequence") && this.inner.defaultables !== undefined); // if a predicate accepts exactly one arg, we can safely skip passing context // technically, a predicate could be written like `(data, ...[ctx]) => ctx.mustBe("malicious")` // that would break here, but it feels like a pathological case and is better to let people optimize this.includesContextualPredicate = this.hasKind("predicate") && this.inner.predicate.length !== 1; this.isCyclic = this.kind === "alias"; this.referencesById = { [this.id]: this }; this.shallowReferences = this.hasKind("structure") ? [this, ...this.children] : this.children.reduce((acc, child) => appendUniqueNodes(acc, child.shallowReferences), [this]); const isStructural = this.isStructural(); this.flatRefs = []; this.flatMorphs = []; for (let i = 0; i < this.children.length; i++) { this.includesTransform ||= this.children[i].includesTransform; this.includesContextualPredicate ||= this.children[i].includesContextualPredicate; this.isCyclic ||= this.children[i].isCyclic; if (!isStructural) { const childFlatRefs = this.children[i].flatRefs; for (let j = 0; j < childFlatRefs.length; j++) { const childRef = childFlatRefs[j]; if (!this.flatRefs.some(existing => flatRefsAreEqual(existing, childRef))) { this.flatRefs.push(childRef); for (const branch of childRef.node.branches) { if (branch.hasKind("morph") || (branch.hasKind("intersection") && branch.structure?.structuralMorph !== undefined)) { this.flatMorphs.push({ path: childRef.path, propString: childRef.propString, node: branch }); } } } } } Object.assign(this.referencesById, this.children[i].referencesById); } this.flatRefs.sort((l, r) => l.path.length > r.path.length ? 1 : l.path.length < r.path.length ? -1 : l.propString > r.propString ? 1 : l.propString < r.propString ? -1 : l.node.expression < r.node.expression ? -1 : 1); this.allowsRequiresContext = this.includesContextualPredicate || this.isCyclic; this.rootApplyStrategy = !this.allowsRequiresContext && this.flatMorphs.length === 0 ? this.shallowMorphs.length === 0 ? "allows" : (this.shallowMorphs.every(morph => morph.length === 1 || morph.name === "$arkStructuralMorph")) ? this.hasKind("union") ? // multiple morphs not yet supported for optimistic compilation this.branches.some(branch => branch.shallowMorphs.length > 1) ? "contextual" : "branchedOptimistic" : this.shallowMorphs.length > 1 ? "contextual" : "optimistic" : "contextual" : "contextual"; this.rootApply = this.createRootApply(); this.allows = this.allowsRequiresContext ? data => this.traverseAllows(data, new Traversal(data, this.$.resolvedConfig)) : data => this.traverseAllows(data); } createRootApply() { switch (this.rootApplyStrategy) { case "allows": return (data, onFail) => { if (this.allows(data)) return data; const ctx = new Traversal(data, this.$.resolvedConfig); this.traverseApply(data, ctx); return ctx.finalize(onFail); }; case "contextual": return (data, onFail) => { const ctx = new Traversal(data, this.$.resolvedConfig); this.traverseApply(data, ctx); return ctx.finalize(onFail); }; case "optimistic": this.contextFreeMorph = this.shallowMorphs[0]; const clone = this.$.resolvedConfig.clone; return (data, onFail) => { if (this.allows(data)) { return this.contextFreeMorph((clone && ((typeof data === "object" && data !== null) || typeof data === "function")) ? clone(data) : data); } const ctx = new Traversal(data, this.$.resolvedConfig); this.traverseApply(data, ctx); return ctx.finalize(onFail); }; case "branchedOptimistic": return this.createBranchedOptimisticRootApply(); default: this.rootApplyStrategy; return throwInternalError(`Unexpected rootApplyStrategy ${this.rootApplyStrategy}`); } } compiledMeta = compileMeta(this.metaJson); cacheGetter(name, value) { Object.defineProperty(this, name, { value }); return value; } get description() { return this.cacheGetter("description", this.meta?.description ?? this.$.resolvedConfig[this.kind].description(this)); } // we don't cache this currently since it can be updated once a scope finishes // resolving cyclic references, although it may be possible to ensure it is cached safely get references() { return Object.values(this.referencesById); } precedence = precedenceOfKind(this.kind); precompilation; // defined as an arrow function since it is often detached, e.g. when passing to tRPC // otherwise, would run into issues with this binding assert = (data, pipedFromCtx) => this(data, pipedFromCtx, errors => errors.throw()); traverse(data, pipedFromCtx) { return this(data, pipedFromCtx, null); } /** rawIn should be used internally instead */ get in() { // ensure the node has been finalized if in is being used externally return this.cacheGetter("in", this.rawIn.isRoot() ? this.$.finalize(this.rawIn) : this.rawIn); } get rawIn() { return this.cacheGetter("rawIn", this.getIo("in")); } /** rawOut should be used internally instead */ get out() { // ensure the node has been finalized if out is being used externally return this.cacheGetter("out", this.rawOut.isRoot() ? this.$.finalize(this.rawOut) : this.rawOut); } get rawOut() { return this.cacheGetter("rawOut", this.getIo("out")); } // Should be refactored to use transform // https://github.com/arktypeio/arktype/issues/1020 getIo(ioKind) { if (!this.includesTransform) return this; const ioInner = {}; for (const [k, v] of this.innerEntries) { const keySchemaImplementation = this.impl.keys[k]; if (keySchemaImplementation.reduceIo) keySchemaImplementation.reduceIo(ioKind, ioInner, v); else if (keySchemaImplementation.child) { const childValue = v; ioInner[k] = isArray(childValue) ? childValue.map(child => ioKind === "in" ? child.rawIn : child.rawOut) : ioKind === "in" ? childValue.rawIn : childValue.rawOut; } else ioInner[k] = v; } return this.$.node(this.kind, ioInner); } toJSON() { return this.json; } toString() { return `Type<${this.expression}>`; } equals(r) { const rNode = isNode(r) ? r : this.$.parseDefinition(r); return this.innerHash === rNode.innerHash; } ifEquals(r) { return this.equals(r) ? this : undefined; } hasKind(kind) { return this.kind === kind; } assertHasKind(kind) { if (this.kind !== kind) throwError(`${this.kind} node was not of asserted kind ${kind}`); return this; } hasKindIn(...kinds) { return kinds.includes(this.kind); } assertHasKindIn(...kinds) { if (!includes(kinds, this.kind)) throwError(`${this.kind} node was not one of asserted kinds ${kinds}`); return this; } isBasis() { return includes(basisKinds, this.kind); } isConstraint() { return includes(constraintKinds, this.kind); } isStructural() { return includes(structuralKinds, this.kind); } isRefinement() { return includes(refinementKinds, this.kind); } isRoot() { return includes(rootKinds, this.kind); } isUnknown() { return this.hasKind("intersection") && this.children.length === 0; } isNever() { return this.hasKind("union") && this.children.length === 0; } hasUnit(value) { return this.hasKind("unit") && this.allows(value); } hasOpenIntersection() { return this.impl.intersectionIsOpen; } get nestableExpression() { return this.expression; } select(selector) { const normalized = NodeSelector.normalize(selector); return this._select(normalized); } _select(selector) { let nodes = NodeSelector.applyBoundary[selector.boundary ?? "references"](this); if (selector.kind) nodes = nodes.filter(n => n.kind === selector.kind); if (selector.where) nodes = nodes.filter(selector.where); return NodeSelector.applyMethod[selector.method ?? "filter"](nodes, this, selector); } transform(mapper, opts) { return this._transform(mapper, this._createTransformContext(opts)); } _createTransformContext(opts) { return { root: this, selected: undefined, seen: {}, path: [], parseOptions: { prereduced: opts?.prereduced ?? false }, undeclaredKeyHandling: undefined, ...opts }; } _transform(mapper, ctx) { const $ = ctx.bindScope ?? this.$; if (ctx.seen[this.id]) // Cyclic handling needs to be made more robust // https://github.com/arktypeio/arktype/issues/944 return this.$.lazilyResolve(ctx.seen[this.id]); if (ctx.shouldTransform?.(this, ctx) === false) return this; let transformedNode; ctx.seen[this.id] = () => transformedNode; if (this.hasKind("structure") && this.undeclared !== ctx.undeclaredKeyHandling) { ctx = { ...ctx, undeclaredKeyHandling: this.undeclared }; } const innerWithTransformedChildren = flatMorph(this.inner, (k, v) => { if (!this.impl.keys[k].child) return [k, v]; const children = v; if (!isArray(children)) { const transformed = children._transform(mapper, ctx); return transformed ? [k, transformed] : []; } // if the value was previously explicitly set to an empty list, // (e.g. branches for `never`), ensure it is not pruned if (children.length === 0) return [k, v]; const transformed = children.flatMap(n => { const transformedChild = n._transform(mapper, ctx); return transformedChild ?? []; }); return transformed.length ? [k, transformed] : []; }); delete ctx.seen[this.id]; const innerWithMeta = Object.assign(innerWithTransformedChildren, { meta: this.meta }); const transformedInner = ctx.selected && !ctx.selected.includes(this) ? innerWithMeta : mapper(this.kind, innerWithMeta, ctx); if (transformedInner === null) return null; if (isNode(transformedInner)) return (transformedNode = transformedInner); const transformedKeys = Object.keys(transformedInner); const hasNoTypedKeys = transformedKeys.length === 0 || (transformedKeys.length === 1 && transformedKeys[0] === "meta"); if (hasNoTypedKeys && // if inner was previously an empty object (e.g. unknown) ensure it is not pruned !isEmptyObject(this.inner)) return null; if ((this.kind === "required" || this.kind === "optional" || this.kind === "index") && !("value" in transformedInner)) { return ctx.undeclaredKeyHandling ? { ...transformedInner, value: $ark.intrinsic.unknown } : null; } if (this.kind === "morph") { ; transformedInner.in ??= $ark.intrinsic .unknown; } return (transformedNode = $.node(this.kind, transformedInner, ctx.parseOptions)); } configureReferences(meta, selector = "references") { const normalized = NodeSelector.normalize(selector); const mapper = (typeof meta === "string" ? (kind, inner) => ({ ...inner, meta: { ...inner.meta, description: meta } }) : typeof meta === "function" ? (kind, inner) => ({ ...inner, meta: meta(inner.meta) }) : (kind, inner) => ({ ...inner, meta: { ...inner.meta, ...meta } })); if (normalized.boundary === "self") { return this.$.node(this.kind, mapper(this.kind, { ...this.inner, meta: this.meta })); } const rawSelected = this._select(normalized); const selected = rawSelected && liftArray(rawSelected); const shouldTransform = normalized.boundary === "child" ? (node, ctx) => ctx.root.children.includes(node) : normalized.boundary === "shallow" ? node => node.kind !== "structure" : () => true; return this.$.finalize(this.transform(mapper, { shouldTransform, selected })); } } const NodeSelector = { applyBoundary: { self: node => [node], child: node => [...node.children], shallow: node => [...node.shallowReferences], references: node => [...node.references] }, applyMethod: { filter: nodes => nodes, assertFilter: (nodes, from, selector) => { if (nodes.length === 0) throwError(writeSelectAssertionMessage(from, selector)); return nodes; }, find: nodes => nodes[0], assertFind: (nodes, from, selector) => { if (nodes.length === 0) throwError(writeSelectAssertionMessage(from, selector)); return nodes[0]; } }, normalize: (selector) => typeof selector === "function" ? { boundary: "references", method: "filter", where: selector } : typeof selector === "string" ? isKeyOf(selector, NodeSelector.applyBoundary) ? { method: "filter", boundary: selector } : { boundary: "references", method: "filter", kind: selector } : { boundary: "references", method: "filter", ...selector } }; const writeSelectAssertionMessage = (from, selector) => `${from} had no references matching ${printable(selector)}.`; export const typePathToPropString = (path) => stringifyPath(path, { stringifyNonKey: node => node.expression }); const referenceMatcher = /"(\$ark\.[^"]+)"/g; const compileMeta = (metaJson) => JSON.stringify(metaJson).replace(referenceMatcher, "$1"); export const flatRef = (path, node) => ({ path, node, propString: typePathToPropString(path) }); export const flatRefsAreEqual = (l, r) => l.propString === r.propString && l.node.equals(r.node); export const appendUniqueFlatRefs = (existing, refs) => appendUnique(existing, refs, { isEqual: flatRefsAreEqual }); export const appendUniqueNodes = (existing, refs) => appendUnique(existing, refs, { isEqual: (l, r) => l.equals(r) });