import { arrayEquals, flatMorph, includes, inferred, omit, throwInternalError, throwParseError } from "@ark/util"; import { mergeToJsonSchemaConfigs } from "../config.js"; import { throwInvalidOperandError } from "../constraint.js"; import { BaseNode } from "../node.js"; import { Disjoint, writeUnsatisfiableExpressionError } from "../shared/disjoint.js"; import { ArkErrors } from "../shared/errors.js"; import { structuralKinds } from "../shared/implement.js"; import { intersectNodesRoot, pipeNodesRoot } from "../shared/intersections.js"; import { $ark } from "../shared/registry.js"; import { arkKind, hasArkKind } from "../shared/utils.js"; import { assertDefaultValueAssignability } from "../structure/optional.js"; export class BaseRoot extends BaseNode { constructor(attachments, $) { super(attachments, $); // define as a getter to avoid it being enumerable/spreadable Object.defineProperty(this, arkKind, { value: "root", enumerable: false }); } // doesn't seem possible to override this at a type-level (e.g. via declare) // without TS complaining about getters get rawIn() { return super.rawIn; } get rawOut() { return super.rawOut; } get internal() { return this; } get "~standard"() { return { vendor: "arktype", version: 1, validate: input => { const out = this(input); if (out instanceof ArkErrors) return out; return { value: out }; }, jsonSchema: { input: opts => this.rawIn.toJsonSchema({ target: validateStandardJsonSchemaTarget(opts.target), ...opts.libraryOptions }), output: opts => this.rawOut.toJsonSchema({ target: validateStandardJsonSchemaTarget(opts.target), ...opts.libraryOptions }) } }; } as() { return this; } brand(name) { if (name === "") return throwParseError(emptyBrandNameMessage); return this; } readonly() { return this; } branches = this.hasKind("union") ? this.inner.branches : [this]; distribute(mapBranch, reduceMapped) { const mappedBranches = this.branches.map(mapBranch); return reduceMapped?.(mappedBranches) ?? mappedBranches; } get shortDescription() { return this.meta.description ?? this.defaultShortDescription; } toJsonSchema(opts = {}) { const ctx = mergeToJsonSchemaConfigs(this.$.resolvedConfig.toJsonSchema, opts); ctx.useRefs ||= this.isCyclic; // ensure $schema is the first key if present const schema = typeof ctx.dialect === "string" ? { $schema: ctx.dialect } : {}; Object.assign(schema, this.toJsonSchemaRecurse(ctx)); if (ctx.useRefs) { const defs = flatMorph(this.references, (i, ref) => ref.isRoot() && !ref.alwaysExpandJsonSchema ? [ref.id, ref.toResolvedJsonSchema(ctx)] : []); // draft-2020-12 uses $defs, draft-07 uses definitions if (ctx.target === "draft-07") Object.assign(schema, { definitions: defs }); else schema.$defs = defs; } return schema; } toJsonSchemaRecurse(ctx) { if (ctx.useRefs && !this.alwaysExpandJsonSchema) { // draft-2020-12 uses $defs, draft-07 uses definitions const defsKey = ctx.target === "draft-07" ? "definitions" : "$defs"; return { $ref: `#/${defsKey}/${this.id}` }; } return this.toResolvedJsonSchema(ctx); } get alwaysExpandJsonSchema() { return (this.isBasis() || this.kind === "alias" || (this.hasKind("union") && this.isBoolean)); } toResolvedJsonSchema(ctx) { const result = this.innerToJsonSchema(ctx); return Object.assign(result, this.metaJson); } intersect(r) { const rNode = this.$.parseDefinition(r); const result = this.rawIntersect(rNode); if (result instanceof Disjoint) return result; return this.$.finalize(result); } rawIntersect(r) { return intersectNodesRoot(this, r, this.$); } toNeverIfDisjoint() { return this; } and(r) { const result = this.intersect(r); return result instanceof Disjoint ? result.throw() : result; } rawAnd(r) { const result = this.rawIntersect(r); return result instanceof Disjoint ? result.throw() : result; } or(r) { const rNode = this.$.parseDefinition(r); return this.$.finalize(this.rawOr(rNode)); } rawOr(r) { const branches = [...this.branches, ...r.branches]; return this.$.node("union", branches); } map(flatMapEntry) { return this.$.schema(this.applyStructuralOperation("map", [flatMapEntry])); } pick(...keys) { return this.$.schema(this.applyStructuralOperation("pick", keys)); } omit(...keys) { return this.$.schema(this.applyStructuralOperation("omit", keys)); } required() { return this.$.schema(this.applyStructuralOperation("required", [])); } partial() { return this.$.schema(this.applyStructuralOperation("partial", [])); } _keyof; keyof() { if (this._keyof) return this._keyof; const result = this.applyStructuralOperation("keyof", []).reduce((result, branch) => result.intersect(branch).toNeverIfDisjoint(), $ark.intrinsic.unknown.internal); if (result.branches.length === 0) { throwParseError(writeUnsatisfiableExpressionError(`keyof ${this.expression}`)); } return (this._keyof = this.$.finalize(result)); } get props() { if (this.branches.length !== 1) return throwParseError(writeLiteralUnionEntriesMessage(this.expression)); return [...this.applyStructuralOperation("props", [])[0]]; } merge(r) { const rNode = this.$.parseDefinition(r); return this.$.schema(rNode.distribute(branch => this.applyStructuralOperation("merge", [ structureOf(branch) ?? throwParseError(writeNonStructuralOperandMessage("merge", branch.expression)) ]))); } applyStructuralOperation(operation, args) { return this.distribute(branch => { if (branch.equals($ark.intrinsic.object) && operation !== "merge") // ideally this wouldn't be a special case, but for now it // allows us to bypass `assertHasKeys` checks on base // instantiations of generics like Pick and Omit. Could // potentially be removed once constraints can reference each other: // https://github.com/arktypeio/arktype/issues/1053 return branch; const structure = structureOf(branch); if (!structure) { throwParseError(writeNonStructuralOperandMessage(operation, branch.expression)); } if (operation === "keyof") return structure.keyof(); if (operation === "get") return structure.get(...args); if (operation === "props") return structure.props; const structuralMethodName = operation === "required" ? "require" : operation === "partial" ? "optionalize" : operation; return this.$.node("intersection", { domain: "object", structure: structure[structuralMethodName](...args) }); }); } get(...path) { if (path[0] === undefined) return this; return this.$.schema(this.applyStructuralOperation("get", path)); } extract(r) { const rNode = this.$.parseDefinition(r); return this.$.schema(this.branches.filter(branch => branch.extends(rNode))); } exclude(r) { const rNode = this.$.parseDefinition(r); return this.$.schema(this.branches.filter(branch => !branch.extends(rNode))); } array() { return this.$.schema(this.isUnknown() ? { proto: Array } : { proto: Array, sequence: this }, { prereduced: true }); } overlaps(r) { const intersection = this.intersect(r); return !(intersection instanceof Disjoint); } extends(r) { if (this.isNever()) return true; const intersection = this.intersect(r); return (!(intersection instanceof Disjoint) && this.equals(intersection)); } ifExtends(r) { return this.extends(r) ? this : undefined; } subsumes(r) { const rNode = this.$.parseDefinition(r); return rNode.extends(this); } configure(meta, selector = "shallow") { return this.configureReferences(meta, selector); } describe(description, selector = "shallow") { return this.configure({ description }, selector); } // these should ideally be implemented in arktype since they use its syntax // https://github.com/arktypeio/arktype/issues/1223 optional() { return [this, "?"]; } // these should ideally be implemented in arktype since they use its syntax // https://github.com/arktypeio/arktype/issues/1223 default(thunkableValue) { assertDefaultValueAssignability(this, thunkableValue, null); return [this, "=", thunkableValue]; } from(input) { // ideally we might not validate here but for now we need to do determine // which morphs to apply return this.assert(input); } _pipe(...morphs) { const result = morphs.reduce((acc, morph) => acc.rawPipeOnce(morph), this); return this.$.finalize(result); } tryPipe(...morphs) { const result = morphs.reduce((acc, morph) => acc.rawPipeOnce(hasArkKind(morph, "root") ? morph : ((In, ctx) => { try { return morph(In, ctx); } catch (e) { return ctx.error({ code: "predicate", predicate: morph, actual: `aborted due to error:\n ${e}\n` }); } })), this); return this.$.finalize(result); } pipe = Object.assign(this._pipe.bind(this), { try: this.tryPipe.bind(this) }); to(def) { return this.$.finalize(this.toNode(this.$.parseDefinition(def))); } toNode(root) { const result = pipeNodesRoot(this, root, this.$); if (result instanceof Disjoint) return result.throw(); return result; } rawPipeOnce(morph) { if (hasArkKind(morph, "root")) return this.toNode(morph); return this.distribute(branch => branch.hasKind("morph") ? this.$.node("morph", { in: branch.inner.in, morphs: [...branch.morphs, morph] }) : this.$.node("morph", { in: branch, morphs: [morph] }), this.$.parseSchema); } narrow(predicate) { return this.constrainOut("predicate", predicate); } constrain(kind, schema) { return this._constrain("root", kind, schema); } constrainIn(kind, schema) { return this._constrain("in", kind, schema); } constrainOut(kind, schema) { return this._constrain("out", kind, schema); } _constrain(io, kind, schema) { const constraint = this.$.node(kind, schema); if (constraint.isRoot()) { // if the node reduces to `unknown`, nothing to do (e.g. minLength: 0) return constraint.isUnknown() ? this : (throwInternalError(`Unexpected constraint node ${constraint}`)); } const operand = io === "root" ? this : io === "in" ? this.rawIn : this.rawOut; if (operand.hasKind("morph") || (constraint.impliedBasis && !operand.extends(constraint.impliedBasis))) { return throwInvalidOperandError(kind, constraint.impliedBasis, this); } const partialIntersection = this.$.node("intersection", { // important this is constraint.kind instead of kind in case // the node was reduced during parsing [constraint.kind]: constraint }); const result = io === "out" ? pipeNodesRoot(this, partialIntersection, this.$) : intersectNodesRoot(this, partialIntersection, this.$); if (result instanceof Disjoint) result.throw(); return this.$.finalize(result); } onUndeclaredKey(cfg) { const rule = typeof cfg === "string" ? cfg : cfg.rule; const deep = typeof cfg === "string" ? false : cfg.deep; return this.$.finalize(this.transform((kind, inner) => kind === "structure" ? rule === "ignore" ? omit(inner, { undeclared: 1 }) : { ...inner, undeclared: rule } : inner, deep ? undefined : ({ shouldTransform: node => !includes(structuralKinds, node.kind) }))); } hasEqualMorphs(r) { if (!this.includesTransform && !r.includesTransform) return true; if (!arrayEquals(this.shallowMorphs, r.shallowMorphs)) return false; if (!arrayEquals(this.flatMorphs, r.flatMorphs, { isEqual: (l, r) => l.propString === r.propString && (l.node.hasKind("morph") && r.node.hasKind("morph") ? l.node.hasEqualMorphs(r.node) : l.node.hasKind("intersection") && r.node.hasKind("intersection") ? l.node.structure?.structuralMorphRef === r.node.structure?.structuralMorphRef : false) })) return false; return true; } onDeepUndeclaredKey(behavior) { return this.onUndeclaredKey({ rule: behavior, deep: true }); } filter(predicate) { return this.constrainIn("predicate", predicate); } divisibleBy(schema) { return this.constrain("divisor", schema); } matching(schema) { return this.constrain("pattern", schema); } atLeast(schema) { return this.constrain("min", schema); } atMost(schema) { return this.constrain("max", schema); } moreThan(schema) { return this.constrain("min", exclusivizeRangeSchema(schema)); } lessThan(schema) { return this.constrain("max", exclusivizeRangeSchema(schema)); } atLeastLength(schema) { return this.constrain("minLength", schema); } atMostLength(schema) { return this.constrain("maxLength", schema); } moreThanLength(schema) { return this.constrain("minLength", exclusivizeRangeSchema(schema)); } lessThanLength(schema) { return this.constrain("maxLength", exclusivizeRangeSchema(schema)); } exactlyLength(schema) { return this.constrain("exactLength", schema); } atOrAfter(schema) { return this.constrain("after", schema); } atOrBefore(schema) { return this.constrain("before", schema); } laterThan(schema) { return this.constrain("after", exclusivizeRangeSchema(schema)); } earlierThan(schema) { return this.constrain("before", exclusivizeRangeSchema(schema)); } } export const emptyBrandNameMessage = `Expected a non-empty brand name after #`; const supportedJsonSchemaTargets = [ "draft-2020-12", "draft-07" ]; export const writeInvalidJsonSchemaTargetMessage = (target) => `JSONSchema target '${target}' is not supported (must be ${supportedJsonSchemaTargets.map(t => `"${t}"`).join(" or ")})`; const validateStandardJsonSchemaTarget = (target) => { if (!includes(supportedJsonSchemaTargets, target)) throwParseError(writeInvalidJsonSchemaTargetMessage(target)); return target; }; export const exclusivizeRangeSchema = (schema) => (typeof schema === "object" && !(schema instanceof Date) ? { ...schema, exclusive: true } : { rule: schema, exclusive: true }); export const typeOrTermExtends = (t, base) => hasArkKind(base, "root") ? hasArkKind(t, "root") ? t.extends(base) : base.allows(t) : hasArkKind(t, "root") ? t.hasUnit(base) : base === t; const structureOf = (branch) => { if (branch.hasKind("morph")) return null; if (branch.hasKind("intersection")) { return (branch.inner.structure ?? (branch.basis?.domain === "object" ? branch.$.bindReference($ark.intrinsic.emptyStructure) : null)); } if (branch.isBasis() && branch.domain === "object") return branch.$.bindReference($ark.intrinsic.emptyStructure); return null; }; export const writeLiteralUnionEntriesMessage = (expression) => `Props cannot be extracted from a union. Use .distribute to extract props from each branch instead. Received: ${expression}`; export const writeNonStructuralOperandMessage = (operation, operand) => `${operation} operand must be an object (was ${operand})`;