Skip to content

Commit

Permalink
feat: branding, implement standard-schema (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad authored Oct 16, 2024
1 parent 0426c67 commit bfbb7ad
Show file tree
Hide file tree
Showing 92 changed files with 1,107 additions and 571 deletions.
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ git checkout -b amazing-feature
- Favor mutation over copying objects in perf-sensitive contexts
- Favor clarity in naming with the following exceptions:
- Ubiquitous variables/types. For example, use `s` over `dynamicParserState` for a variable of type DynamicParserState that is used in the same way across many functions.
- Ephemeral variables whose contents can be trivially inferred from context. For example, prefer `rawKeyDefinitions.map(_ => _.trim())` to `rawKeyDefinitions.map(rawKeyDefinition => rawKeyDefinition.trim())`.
- Ephemeral variables whose contents can be trivially inferred from context. For example, prefer `rawKeyDefinitions.map(k => k.trim())` to `rawKeyDefinitions.map(rawKeyDefinition => rawKeyDefinition.trim())`.

We also have some unique casing rules for our TypeScript types to facilitate type-level code that can parallel its runtime implementation and be easily understood:

Expand Down
2 changes: 1 addition & 1 deletion ark/attest/assert/attest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type AttestFn = {
<expected, actual extends expected = never>(
...args: actual extends never ?
[
ErrorMessage<"Type-only assertion requires two explicit genreic params, e.g. attest<expected, actual>">
ErrorMessage<"Type-only assertion requires two explicit generic params, e.g. attest<expected, actual>">
]
: []
): void
Expand Down
2 changes: 1 addition & 1 deletion ark/attest/assert/chainableAssertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export type valueAssertions<
t,
kind extends AssertionKind
> = comparableValueAssertion<t, kind> &
(t extends () => unknown ? functionAssertions<kind> : {})
([t] extends [() => unknown] ? functionAssertions<kind> : {})

export type nextAssertions<kind extends AssertionKind> =
"type" extends kind ? TypeAssertionsRoot : {}
Expand Down
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.22.0",
"version": "0.23.0",
"author": {
"name": "David Blass",
"email": "[email protected]",
Expand Down
22 changes: 11 additions & 11 deletions ark/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,27 @@
"dependencies": {
"@ark/fs": "workspace:*",
"@ark/util": "workspace:*",
"@astrojs/check": "0.9.3",
"@astrojs/check": "0.9.4",
"@astrojs/react": "3.6.2",
"@astrojs/starlight": "0.28.2",
"@astrojs/starlight": "0.28.3",
"@astrojs/ts-plugin": "1.10.2",
"@shikijs/transformers": "1.18.0",
"@shikijs/twoslash": "1.18.0",
"@shikijs/transformers": "1.22.0",
"@shikijs/twoslash": "1.22.0",
"arkdark": "workspace:*",
"arktype": "workspace:*",
"astro": "4.16.1",
"astro-og-canvas": "0.5.3",
"astro": "4.16.5",
"astro-og-canvas": "0.5.4",
"canvaskit-wasm": "0.39.1",
"framer-motion": "11.7.0",
"framer-motion": "11.11.9",
"react": "18.3.1",
"react-dom": "18.3.1",
"sharp": "0.33.5",
"shiki": "1.18.0",
"twoslash": "0.2.11"
"shiki": "1.22.0",
"twoslash": "0.2.12"
},
"devDependencies": {
"@types/react": "18.3.9",
"@types/react-dom": "18.3.0",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.1",
"typescript": "catalog:"
}
}
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.17.0",
"version": "0.18.0",
"author": {
"name": "David Blass",
"email": "[email protected]",
Expand Down
13 changes: 1 addition & 12 deletions ark/repo/scratch.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import { type } from "arktype"

console.log(
type({
foo: type("string").pipe(() => 123)
})
.pipe(c => c)
.to({
foo: "123"
})({
foo: "bar"
}) + ""
)
// foo must be 123 (was "bar")
const t = type("string.numeric.parse")
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/standardSchema.ts"
export * from "./shared/traversal.ts"
export * from "./shared/utils.ts"
export * from "./structure/index.ts"
Expand Down
6 changes: 3 additions & 3 deletions ark/schema/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
isEmptyObject,
throwError,
type Dict,
type Guardable,
type GuardablePredicate,
type Json,
type Key,
type array,
Expand Down Expand Up @@ -325,13 +325,13 @@ export abstract class BaseNode<
}

firstReference<narrowed>(
filter: Guardable<BaseNode, conform<narrowed, BaseNode>>
filter: GuardablePredicate<BaseNode, conform<narrowed, BaseNode>>
): narrowed | undefined {
return this.references.find(n => n !== this && filter(n)) as never
}

firstReferenceOrThrow<narrowed extends BaseNode>(
filter: Guardable<BaseNode, narrowed>
filter: GuardablePredicate<BaseNode, narrowed>
): narrowed {
return (
this.firstReference(filter) ??
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.17.0",
"version": "0.18.0",
"license": "MIT",
"author": {
"name": "David Blass",
Expand Down
34 changes: 31 additions & 3 deletions ark/schema/roots/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import { intersectNodesRoot, pipeNodesRoot } from "../shared/intersections.ts"
import type { JsonSchema } from "../shared/jsonSchema.ts"
import { $ark } from "../shared/registry.ts"
import type { StandardSchema } from "../shared/standardSchema.ts"
import { arkKind, hasArkKind } from "../shared/utils.ts"
import { assertDefaultValueAssignability } from "../structure/optional.ts"
import type { Prop } from "../structure/prop.ts"
Expand All @@ -67,9 +68,12 @@ export interface InternalRootDeclaration extends BaseNodeDeclaration {
}

export abstract class BaseRoot<
/** @ts-ignore cast variance */
out d extends InternalRootDeclaration = InternalRootDeclaration
> extends BaseNode<d> {
/** @ts-ignore cast variance */
out d extends InternalRootDeclaration = InternalRootDeclaration
>
extends BaseNode<d>
implements StandardSchema<unknown, unknown>
{
declare readonly [arkKind]: "root"
declare readonly [inferred]: unknown

Expand All @@ -88,6 +92,22 @@ export abstract class BaseRoot<
return this
}

get "~standard"(): 1 {
return 1
}

get "~vendor"(): "arktype" {
return "arktype"
}

"~validate"(input: StandardSchema.Input): StandardSchema.Result<unknown> {
const out = this(input.value)
if (out instanceof ArkErrors) return out
return { value: out }
}

declare "~types": StandardSchema.Types<unknown, unknown>

get optionalMeta(): boolean {
return this.cacheGetter(
"optionalMeta",
Expand Down Expand Up @@ -134,6 +154,14 @@ export abstract class BaseRoot<
return this
}

brandAttributes(): this {
return this
}

unbrandAttributes(): this {
return this
}

readonly(): this {
return this
}
Expand Down
11 changes: 10 additions & 1 deletion ark/schema/shared/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { ResolvedArkConfig } from "../config.ts"
import type { Prerequisite, errorContext } from "../kinds.ts"
import type { NodeKind } from "./implement.ts"
import type { StandardSchema } from "./standardSchema.ts"
import type { TraversalContext } from "./traversal.ts"
import { arkKind, pathToPropString, type TraversalPath } from "./utils.ts"

Expand Down Expand Up @@ -74,7 +75,10 @@ export class ArkError<
}
}

export class ArkErrors extends ReadonlyArray<ArkError> {
export class ArkErrors
extends ReadonlyArray<ArkError>
implements StandardSchema.Failure
{
protected ctx: TraversalContext

constructor(ctx: TraversalContext) {
Expand Down Expand Up @@ -138,6 +142,11 @@ export class ArkErrors extends ReadonlyArray<ArkError> {
return this.toString()
}

/** Reference to this ArkErrors array (for Standard Schema compatibility) */
get issues(): this {
return this
}

toString(): string {
return this.join("\n")
}
Expand Down
39 changes: 39 additions & 0 deletions ark/schema/shared/standardSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** Subset of types from https://github.com/standard-schema/standard-schema */
export interface StandardSchema<In, Out> extends StandardSchema.ConstantProps {
readonly "~types": StandardSchema.Types<In, Out>
"~validate": StandardSchema.Validator<Out>
}

export declare namespace StandardSchema {
export interface ConstantProps {
readonly "~standard": 1
readonly "~vendor": "arktype"
}

export interface Types<In, Out> {
input: In
output: Out
}

export type Validator<Out> = (input: Input) => Result<Out>

export interface Input {
value: unknown
}

export type Result<Out> = Success<Out> | Failure

export interface Success<Out> {
value: Out
issues?: undefined
}

export interface Failure {
readonly issues: readonly Issue[]
}

export interface Issue {
readonly message: string
readonly path: readonly PropertyKey[]
}
}
10 changes: 5 additions & 5 deletions ark/type/__tests__/bounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ contextualize(() => {

it("<,<=", () => {
const t = type("-5<number<=5")
attest(t).type.toString.snap("Type<is<MoreThan<-5> & AtMost<5>>, {}>")
attest(t).type.toString.snap("Type<is<AtMost<5> & MoreThan<-5>>, {}>")
attest<number>(t.infer)
const expected = rootSchema({
domain: "number",
Expand All @@ -82,7 +82,7 @@ contextualize(() => {
it("<=,<", () => {
const t = type("-3.23<=number<4.654")
attest(t).type.toString.snap(
"Type<is<AtLeast<-3.23> & LessThan<4.654>>, {}>"
"Type<is<LessThan<4.654> & AtLeast<-3.23>>, {}>"
)
attest<number>(t.infer)
const expected = rootSchema({
Expand Down Expand Up @@ -114,7 +114,7 @@ contextualize(() => {
it("Date equality", () => {
const t = type("Date==d'2020-1-1'")
attest<Date>(t.infer)
attest(t).type.toString.snap('Type<literal<"2020-1-1">, {}>')
attest(t).type.toString.snap('Type<nominal<"2020-1-1">, {}>')
attest(t.json).snap({ unit: "2020-01-01T05:00:00.000Z" })
attest(t.allows(new Date("2020/01/01"))).equals(true)
attest(t.allows(new Date("2020/01/02"))).equals(false)
Expand All @@ -124,7 +124,7 @@ contextualize(() => {
const t = type("d'2001/10/10'< Date < d'2005/10/10'")
attest<Date>(t.infer)
attest(t.t).type.toString.snap(
'is<After<"2001/10/10"> & Before<"2005/10/10">>'
'is<Before<"2005/10/10"> & After<"2001/10/10">>'
)
attest(t.json).snap({
proto: "Date",
Expand All @@ -141,7 +141,7 @@ contextualize(() => {
const t = type(`d'2000'< Date <=d'${now.toISOString()}'`)
attest<Date>(t.infer)
attest(t).type.toString.snap(
'Type<is<After<"2000"> & AtOrBefore<string>>, {}>'
'Type<is<AtOrBefore<string> & After<"2000">>, {}>'
)
attest(t.allows(new Date(now.valueOf() - 1000))).equals(true)
attest(t.allows(now)).equals(true)
Expand Down
65 changes: 65 additions & 0 deletions ark/type/__tests__/brand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { attest, contextualize } from "@ark/attest"
import { type } from "arktype"
import type { number, string } from "arktype/internal/attributes.ts"

contextualize(() => {
it("chained", () => {
const t = type("string").brand("foo")
attest(t.t).type.toString.snap('branded<"foo">')

// no effect at runtime
attest(t.expression).equals("string")

const out = t("moo")
attest<string.branded<"foo"> | type.errors>(out)
})

it("string-embedded", () => {
const t = type("number#cool")
attest(t.t).type.toString.snap('branded<"cool">')

attest(t.expression).equals("number")

const out = t(5)
attest<number.branded<"cool"> | type.errors>(out)
})

it("brandAttributes", () => {
const unbranded = type({
age: "number.integer >= 0"
})

attest(unbranded.t).type.toString.snap(
"{ age: is<DivisibleBy<1> & AtLeast<0>> }"
)

const out = unbranded({ age: 5 })

attest<
| type.errors
| {
age: number
}
>(out).equals({ age: 5 })

const branded = unbranded.brandAttributes()

attest(branded.t).type.toString.snap(
"{ age: brand<number, DivisibleBy<1> & AtLeast<0>> }"
)

const brandedOut = branded({ age: 5 })

attest(brandedOut).type.toString.snap(` | ArkErrors
| { age: brand<number, DivisibleBy<1> & AtLeast<0>> }`)

const reunbranded = branded.unbrandAttributes()

attest(reunbranded.t).type.toString.snap(
"{ age: is<DivisibleBy<1> & AtLeast<0>> }"
)

attest<typeof unbranded, typeof reunbranded>()
attest(unbranded.json).equals(reunbranded.json)
})
})
Loading

0 comments on commit bfbb7ad

Please sign in to comment.