import { flatMorph, hasDomain, includes, isEmptyObject, isKeyOf, throwParseError } from "@ark/util"; import { constraintKeyParser, flattenConstraints, intersectConstraints } from "../constraint.js"; import { Disjoint } from "../shared/disjoint.js"; import { implementNode, prestructuralKinds, structureKeys } from "../shared/implement.js"; import { intersectOrPipeNodes } from "../shared/intersections.js"; import { hasArkKind, isNode } from "../shared/utils.js"; import { BaseRoot } from "./root.js"; import { defineRightwardIntersections } from "./utils.js"; const implementation = implementNode({ kind: "intersection", hasAssociatedError: true, normalize: rawSchema => { if (isNode(rawSchema)) return rawSchema; const { structure, ...schema } = rawSchema; const hasRootStructureKey = !!structure; const normalizedStructure = structure ?? {}; const normalized = flatMorph(schema, (k, v) => { if (isKeyOf(k, structureKeys)) { if (hasRootStructureKey) { throwParseError(`Flattened structure key ${k} cannot be specified alongside a root 'structure' key.`); } normalizedStructure[k] = v; return []; } return [k, v]; }); if (hasArkKind(normalizedStructure, "constraint") || !isEmptyObject(normalizedStructure)) normalized.structure = normalizedStructure; return normalized; }, finalizeInnerJson: ({ structure, ...rest }) => hasDomain(structure, "object") ? { ...structure, ...rest } : rest, keys: { domain: { child: true, parse: (schema, ctx) => ctx.$.node("domain", schema) }, proto: { child: true, parse: (schema, ctx) => ctx.$.node("proto", schema) }, structure: { child: true, parse: (schema, ctx) => ctx.$.node("structure", schema), serialize: node => { if (!node.sequence?.minLength) return node.collapsibleJson; const { sequence, ...structureJson } = node.collapsibleJson; const { minVariadicLength, ...sequenceJson } = sequence; const collapsibleSequenceJson = sequenceJson.variadic && Object.keys(sequenceJson).length === 1 ? sequenceJson.variadic : sequenceJson; return { ...structureJson, sequence: collapsibleSequenceJson }; } }, divisor: { child: true, parse: constraintKeyParser("divisor") }, max: { child: true, parse: constraintKeyParser("max") }, min: { child: true, parse: constraintKeyParser("min") }, maxLength: { child: true, parse: constraintKeyParser("maxLength") }, minLength: { child: true, parse: constraintKeyParser("minLength") }, exactLength: { child: true, parse: constraintKeyParser("exactLength") }, before: { child: true, parse: constraintKeyParser("before") }, after: { child: true, parse: constraintKeyParser("after") }, pattern: { child: true, parse: constraintKeyParser("pattern") }, predicate: { child: true, parse: constraintKeyParser("predicate") } }, // leverage reduction logic from intersection and identity to ensure initial // parse result is reduced reduce: (inner, $) => // we cast union out of the result here since that only occurs when intersecting two sequences // that cannot occur when reducing a single intersection schema using unknown intersectIntersections({}, inner, { $, invert: false, pipe: false }), defaults: { description: node => { if (node.children.length === 0) return "unknown"; if (node.structure) return node.structure.description; const childDescriptions = []; if (node.basis && !node.prestructurals.some(r => r.impl.obviatesBasisDescription)) childDescriptions.push(node.basis.description); if (node.prestructurals.length) { const sortedRefinementDescriptions = node.prestructurals .slice() // override alphabetization to describe min before max .sort((l, r) => (l.kind === "min" && r.kind === "max" ? -1 : 0)) .map(r => r.description); childDescriptions.push(...sortedRefinementDescriptions); } if (node.inner.predicate) { childDescriptions.push(...node.inner.predicate.map(p => p.description)); } return childDescriptions.join(" and "); }, expected: source => ` ◦ ${source.errors.map(e => e.expected).join("\n ◦ ")}`, problem: ctx => `(${ctx.actual}) must be...\n${ctx.expected}` }, intersections: { intersection: (l, r, ctx) => intersectIntersections(l.inner, r.inner, ctx), ...defineRightwardIntersections("intersection", (l, r, ctx) => { // if l is unknown, return r if (l.children.length === 0) return r; const { domain, proto, ...lInnerConstraints } = l.inner; const lBasis = proto ?? domain; const basis = lBasis ? intersectOrPipeNodes(lBasis, r, ctx) : r; return (basis instanceof Disjoint ? basis : l?.basis?.equals(basis) ? // if the basis doesn't change, return the original intesection l // given we've already precluded l being unknown, the result must // be an intersection with the new basis result integrated : l.$.node("intersection", { ...lInnerConstraints, [basis.kind]: basis }, { prereduced: true })); }) } }); export class IntersectionNode extends BaseRoot { basis = this.inner.domain ?? this.inner.proto ?? null; prestructurals = []; refinements = this.children.filter((node) => { if (!node.isRefinement()) return false; if (includes(prestructuralKinds, node.kind)) // mutation is fine during initialization this.prestructurals.push(node); return true; }); structure = this.inner.structure; expression = writeIntersectionExpression(this); get shallowMorphs() { return this.inner.structure?.structuralMorph ? [this.inner.structure.structuralMorph] : []; } get defaultShortDescription() { return this.basis?.defaultShortDescription ?? "present"; } innerToJsonSchema(ctx) { return this.children.reduce( // cast is required since TS doesn't know children have compatible schema prerequisites (schema, child) => child.isBasis() ? child.toJsonSchemaRecurse(ctx) : child.reduceJsonSchema(schema, ctx), {}); } traverseAllows = (data, ctx) => this.children.every(child => child.traverseAllows(data, ctx)); traverseApply = (data, ctx) => { const errorCount = ctx.currentErrorCount; if (this.basis) { this.basis.traverseApply(data, ctx); if (ctx.currentErrorCount > errorCount) return; } if (this.prestructurals.length) { for (let i = 0; i < this.prestructurals.length - 1; i++) { this.prestructurals[i].traverseApply(data, ctx); if (ctx.failFast && ctx.currentErrorCount > errorCount) return; } this.prestructurals[this.prestructurals.length - 1].traverseApply(data, ctx); if (ctx.currentErrorCount > errorCount) return; } if (this.structure) { this.structure.traverseApply(data, ctx); if (ctx.currentErrorCount > errorCount) return; } if (this.inner.predicate) { for (let i = 0; i < this.inner.predicate.length - 1; i++) { this.inner.predicate[i].traverseApply(data, ctx); if (ctx.failFast && ctx.currentErrorCount > errorCount) return; } this.inner.predicate[this.inner.predicate.length - 1].traverseApply(data, ctx); } }; compile(js) { if (js.traversalKind === "Allows") { for (const child of this.children) js.check(child); js.return(true); return; } js.initializeErrorCount(); if (this.basis) { js.check(this.basis); // we only have to return conditionally if this is not the last check if (this.children.length > 1) js.returnIfFail(); } if (this.prestructurals.length) { for (let i = 0; i < this.prestructurals.length - 1; i++) { js.check(this.prestructurals[i]); js.returnIfFailFast(); } js.check(this.prestructurals[this.prestructurals.length - 1]); if (this.structure || this.inner.predicate) js.returnIfFail(); } if (this.structure) { js.check(this.structure); if (this.inner.predicate) js.returnIfFail(); } if (this.inner.predicate) { for (let i = 0; i < this.inner.predicate.length - 1; i++) { js.check(this.inner.predicate[i]); // since predicates can be chained, we have to fail immediately // if one fails js.returnIfFail(); } js.check(this.inner.predicate[this.inner.predicate.length - 1]); } } } export const Intersection = { implementation, Node: IntersectionNode }; const writeIntersectionExpression = (node) => { if (node.structure?.expression) return node.structure.expression; const basisExpression = (node.basis && !node.prestructurals.some(n => n.impl.obviatesBasisExpression)) ? node.basis.nestableExpression : ""; const refinementsExpression = node.prestructurals .map(n => n.expression) .join(" & "); const fullExpression = `${basisExpression}${basisExpression ? " " : ""}${refinementsExpression}`; if (fullExpression === "Array == 0") return "[]"; return fullExpression || "unknown"; }; const intersectIntersections = (l, r, ctx) => { const baseInner = {}; const lBasis = l.proto ?? l.domain; const rBasis = r.proto ?? r.domain; const basisResult = lBasis ? rBasis ? intersectOrPipeNodes(lBasis, rBasis, ctx) : lBasis : rBasis; if (basisResult instanceof Disjoint) return basisResult; if (basisResult) baseInner[basisResult.kind] = basisResult; return intersectConstraints({ kind: "intersection", baseInner, l: flattenConstraints(l), r: flattenConstraints(r), roots: [], ctx }); };