Skip to content

Commit

Permalink
feat: improve default morphs, simplify attribute types (#1142)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad authored Sep 20, 2024
1 parent 89be3c5 commit 374c60b
Show file tree
Hide file tree
Showing 55 changed files with 805 additions and 447 deletions.
2 changes: 1 addition & 1 deletion ark/attest/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ark/attest",
"version": "0.18.4",
"version": "0.19.0",
"author": {
"name": "David Blass",
"email": "[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion ark/fs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ark/fs",
"version": "0.13.0",
"version": "0.14.0",
"author": {
"name": "David Blass",
"email": "[email protected]",
Expand Down
1 change: 1 addition & 0 deletions ark/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export * from "./shared/implement.ts"
export * from "./shared/intersections.ts"
export * from "./shared/jsonSchema.ts"
export * from "./shared/registry.ts"
export * from "./shared/traversal.ts"
export * from "./shared/utils.ts"
export * from "./structure/index.ts"
export * from "./structure/optional.ts"
Expand Down
18 changes: 11 additions & 7 deletions ark/schema/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@ export abstract class BaseNode<
/** uses -ignore rather than -expect-error because this is not an error in .d.ts
* @ts-ignore allow instantiation assignment to the base type */
out d extends BaseNodeDeclaration = BaseNodeDeclaration
> extends Callable<(data: d["prerequisite"]) => unknown, attachmentsOf<d>> {
> extends Callable<
(data: d["prerequisite"], ctx?: TraversalContext) => unknown,
attachmentsOf<d>
> {
attachments: UnknownAttachments
$: BaseScope

constructor(attachments: UnknownAttachments, $: BaseScope) {
super(
// pipedFromCtx allows us internally to reuse TraversalContext
// through pipes and keep track of piped paths. It is not exposed
(data: any, pipedFromCtx?: TraversalContext) => {
if (
!this.includesMorph &&
Expand Down Expand Up @@ -223,19 +224,22 @@ export abstract class BaseNode<

// Should be refactored to use transform
// https://github.com/arktypeio/arktype/issues/1020
getIo(kind: "in" | "out"): BaseNode {
getIo(ioKind: "in" | "out"): BaseNode {
if (!this.includesMorph) return this as never

const ioInner: Record<any, unknown> = {}
for (const [k, v] of this.innerEntries) {
const keySchemaImplementation = this.impl.keys[k]

if (keySchemaImplementation.child) {
if (keySchemaImplementation.reduceIo)
keySchemaImplementation.reduceIo(ioKind, ioInner, v)
else if (keySchemaImplementation.child) {
const childValue = v as listable<BaseNode>

ioInner[k] =
isArray(childValue) ?
childValue.map(child => child[kind])
: childValue[kind]
childValue.map(child => child[ioKind])
: childValue[ioKind]
} else ioInner[k] = v
}

Expand Down
2 changes: 1 addition & 1 deletion ark/schema/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ark/schema",
"version": "0.13.0",
"version": "0.14.0",
"license": "MIT",
"author": {
"name": "David Blass",
Expand Down
26 changes: 24 additions & 2 deletions ark/schema/roots/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
type array
} from "@ark/util"
import { throwInvalidOperandError, type Constraint } from "../constraint.ts"
import type { NodeSchema, nodeOfKind, reducibleKindOf } from "../kinds.ts"
import type {
NodeSchema,
mutableNormalizedRootOfKind,
nodeOfKind,
reducibleKindOf
} from "../kinds.ts"
import {
BaseNode,
appendUniqueFlatRefs,
Expand Down Expand Up @@ -100,6 +105,11 @@ export abstract class BaseRoot<
return this
}

brand(name: string): this {
if (name === "") return throwParseError(emptyBrandNameMessage)
return this
}

readonly(): this {
return this
}
Expand Down Expand Up @@ -389,7 +399,15 @@ export abstract class BaseRoot<
in: branch,
morphs: [morph]
}),
this.$.parseSchema
branches => {
if (!this.hasKind("union")) return this.$.parseSchema(branches[0])
const schema: mutableNormalizedRootOfKind<"union"> = {
branches
}
if ("default" in this.meta) schema.meta = { default: this.meta.default }
else if (this.meta.optional) schema.meta = { optional: true }
return this.$.parseSchema(schema)
}
)
}

Expand Down Expand Up @@ -573,6 +591,10 @@ export type UndeclaredKeyConfig = {
deep?: boolean
}

export const emptyBrandNameMessage = `Expected a non-empty brand name after #`

export type emptyBrandNameMessage = typeof emptyBrandNameMessage

export const exclusivizeRangeSchema = <schema extends UnknownRangeSchema>(
schema: schema
): schema =>
Expand Down
7 changes: 6 additions & 1 deletion ark/schema/shared/implement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import type {
BaseNormalizedSchema
} from "./declare.ts"
import type { Disjoint } from "./disjoint.ts"
import { isNode } from "./utils.ts"
import { isNode, type makeRootAndArrayPropertiesMutable } from "./utils.ts"

export const basisKinds = ["unit", "proto", "domain"] as const

Expand Down Expand Up @@ -277,6 +277,11 @@ export type NodeKeyImplementation<
preserveUndefined?: true
child?: boolean
serialize?: (schema: instantiated) => JsonData
reduceIo?: (
ioKind: "in" | "out",
inner: makeRootAndArrayPropertiesMutable<d["inner"]>,
value: d["inner"][k]
) => void
parse?: (
schema: Exclude<d["normalizedSchema"][k], undefined>,
ctx: NodeParseContext<d["kind"]>
Expand Down
41 changes: 36 additions & 5 deletions ark/schema/shared/intersections.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { PartialRecord, TypeGuard } from "@ark/util"
import type { nodeOfKind } from "../kinds.ts"
import type { mutableNormalizedRootOfKind, nodeOfKind } from "../kinds.ts"
import type { BaseNode } from "../node.ts"
import type { Morph } from "../roots/morph.ts"
import type { BaseRoot } from "../roots/root.ts"
import type { Union } from "../roots/union.ts"
import type { BaseScope } from "../scope.ts"
import type { BaseMeta } from "./declare.ts"
import { Disjoint } from "./disjoint.ts"
import {
rootKinds,
Expand Down Expand Up @@ -130,9 +131,39 @@ const pipeMorphed = (
const viableBranches = results.filter(
isNode as TypeGuard<unknown, Morph.Node>
)
return viableBranches.length === 0 ?
Disjoint.init("union", from.branches, to.branches)
: ctx.$.parseSchema(viableBranches)

if (viableBranches.length === 0)
return Disjoint.init("union", from.branches, to.branches)

if (
viableBranches.length < from.branches.length ||
!from.branches.every((branch, i) =>
branch.in.equals(viableBranches[i].in)
)
)
return ctx.$.parseSchema(viableBranches)

let meta: BaseMeta | undefined

if ("default" in from.meta) meta = { default: from.meta.default }
else if (from.meta.optional) meta = { optional: true }

if (viableBranches.length === 1) {
const onlyBranch = viableBranches[0]
if (!meta) return onlyBranch
return ctx.$.node("morph", {
...onlyBranch.inner,
in: onlyBranch.in.withMeta(meta)
})
}

const schema: mutableNormalizedRootOfKind<"union"> = {
branches: viableBranches
}

if (meta) schema.meta = meta

return ctx.$.parseSchema(schema)
}
)

Expand All @@ -154,7 +185,7 @@ const _pipeMorphed = (

return ctx.$.node("morph", {
morphs,
in: fromIsMorph ? from.in : from
in: from.in
})
}

Expand Down
98 changes: 78 additions & 20 deletions ark/schema/structure/optional.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { hasDomain, printable, throwParseError, unset } from "@ark/util"
import {
hasDomain,
omit,
printable,
throwParseError,
unset,
type keySetOf
} from "@ark/util"
import type { Morph } from "../roots/morph.ts"
import type { BaseRoot } from "../roots/root.ts"
import type { declareNode } from "../shared/declare.ts"
import type { BaseMeta, declareNode } from "../shared/declare.ts"
import { ArkErrors } from "../shared/errors.ts"
import {
implementNode,
type nodeImplementationOf
} from "../shared/implement.ts"
import { registeredReference } from "../shared/registry.ts"
import { BaseProp, intersectProps, type Prop } from "./prop.ts"

export declare namespace Optional {
Expand Down Expand Up @@ -70,31 +79,80 @@ export class OptionalNode extends BaseProp<"optional"> {
}
}

expression = `${this.compiledKey}?: ${this.value.expression}${"default" in this.inner ? ` = ${printable(this.inner.default)}` : ""}`
}
get outProp(): Prop.Node {
if (!this.hasDefault()) return this
const { default: defaultValue, ...requiredInner } = this.inner

requiredInner.value = requiredInner.value.withMeta(meta =>
omit(meta, optionalValueMetaKeys)
)

return this.cacheGetter(
"outProp",
this.$.node("required", requiredInner, { prereduced: true }) as never
)
}

expression: string = `${this.compiledKey}?: ${this.value.expression}${this.hasDefault() ? ` = ${printable(this.inner.default)}` : ""}`

defaultValueMorphs: Morph[] = this.computeDefaultValueMorphs()

defaultValueMorphsReference = registeredReference(this.defaultValueMorphs)

// if (parsedValue.meta) {
// if ("default" in parsedValue.meta) {
// return ctx.$.node("optional", {
// key: parsedKey.key,
// value: parsedValue,
// default: parsedValue.meta.default
// })
// }

// if (parsedValue.meta.optional) {
// return ctx.$.node("optional", {
// key: parsedKey.key,
// value: parsedValue
// })
// }
// }
private computeDefaultValueMorphs(): Morph[] {
if (!this.hasDefault()) return []

const defaultInput = this.default

if (typeof defaultInput === "function") {
return [
// if the value has a morph, pipe context through it
this.value.includesMorph ?
(data, ctx) => {
ctx.path.push(this.key)
this.value((data[this.key] = defaultInput()), ctx)
ctx.path.pop()
return data
}
: data => {
data[this.key] = defaultInput()
return data
}
]
}

// non-functional defaults can be safely cached as long as the morph is
// guaranteed to be pure and the output is primitive
const precomputedMorphedDefault =
this.value.includesMorph ? this.value.assert(defaultInput) : defaultInput

return [
hasDomain(precomputedMorphedDefault, "object") ?
// the type signature only allows this if the value was morphed
(data, ctx) => {
ctx.path.push(this.key)
this.value((data[this.key] = defaultInput), ctx)
ctx.path.pop()
return data
}
: data => {
data[this.key] = precomputedMorphedDefault
return data
}
]
}
}

export const Optional = {
implementation,
Node: OptionalNode
}

const optionalValueMetaKeys: keySetOf<BaseMeta> = {
default: 1,
optional: 1
}

export const assertDefaultValueAssignability = (
node: BaseRoot,
value: unknown,
Expand Down
Loading

0 comments on commit 374c60b

Please sign in to comment.