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";