Skip to content

Commit

Permalink
feat: undeclared and defaultable keys (#953)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad authored May 17, 2024
1 parent 08b151e commit b52212e
Show file tree
Hide file tree
Showing 183 changed files with 6,507 additions and 5,083 deletions.
5 changes: 1 addition & 4 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,12 @@ git checkout -b amazing-feature

6. Do your best to write code that is stylistically consistent with its context. The linter will help with this, but it won't catch everything. Here's a few general guidelines:

- Favor functions over classes
- Favor arrow functions outside of classes
- Favor types over interfaces
- 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())`.

We also have some unique casing rules for our TypeScript types to making writing isomorphic code easier:
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:

- Use `CapitalCase` for...

Expand Down
17 changes: 6 additions & 11 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,18 @@ assignees: "ssalbdivad"
### 🧩 Context

- ArkType version:
- TypeScript version (4.8, 4.9, or 5.0):
- Other context you think may be relevant (Node version, OS, etc.):
- TypeScript version (5.1+):
- Other context you think may be relevant (JS flavor, OS, etc.):

### 🧑‍💻 Repro

<!--
1. Update the template link below so that it reproduces the problem you're having.
2. Add comments to describe differences between actual and expected behavior.
3. Click "Fork" in the top-left corner of StackBlitz
4. Copy the new URL and use it to replace the template URL below.
5. Copy the source code you used to repro the bug and paste it into the code block below.
-->
https://stackblitz.com/edit/arktype-bug?devToolsHeight=33&file=demo.ts
Please do your best to write the simplest code you can that reproduces the issue!
```ts
import { type, scope } from "arktype"
If it requires other dependencies besides arktype, it's probably either not a minimal repro or not an arktype bug.
-->

```ts
// Paste reproduction code here
```
2 changes: 1 addition & 1 deletion .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Version | Supported |
| ------- | ------------------ |
| 1.x | :white_check_mark: |
| 2.x | :white_check_mark: |

## Reporting a Vulnerability

Expand Down
4 changes: 4 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ runs:
- name: Build
shell: bash
run: pnpm build

- name: Post-build install
shell: bash
run: pnpm install
9 changes: 0 additions & 9 deletions .github/semantic.yml

This file was deleted.

5 changes: 2 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ jobs:
include:
- os: ubuntu-latest
node: lts/-1
# https://github.com/arktypeio/arktype/issues/738
# - os: ubuntu-latest
# node: latest
- os: ubuntu-latest
node: latest
fail-fast: false

runs-on: ${{ matrix.os }}
Expand Down
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
// too many overlapping names, easy to import in schema/arktype where we don't want it
// should just import as * as ts when we need it in attest
"typescript",
"./ark/type/main.ts",
"./ark/schema/main.ts"
"./ark/type/api.ts",
"./ark/schema/api.ts"
],
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// IF YOU UPDATE THE MOCHA CONFIG HERE, PLEASE ALSO UPDATE package.json/mocha AND ark/repo/mocha.jsonc
Expand Down
27 changes: 23 additions & 4 deletions ark/attest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ export default defineConfig({
`setupVitest.ts`

```ts
import * as attest from "@arktype/attest"
import { setup, teardown } from "@arktype/attest"

// config options can be passed here
export const setup = () => attest.setup({})

export const teardown = attest.teardown
export default () => setup({})
```

### Mocha
Expand Down Expand Up @@ -124,6 +122,21 @@ describe("attest features", () => {
attest<(number | bigint)[]>(numericArray.infer)
}
})

it("integrated type performance benchmarking", () => {
const user = type({
kind: "'admin'",
"powers?": "string[]"
})
.or({
kind: "'superadmin'",
"superpowers?": "string[]"
})
.or({
kind: "'pleb'"
})
attest.instantiations([7574, "instantiations"])
})
})
```

Expand Down Expand Up @@ -155,6 +168,12 @@ bench(
.types([337, "instantiations"])
```

If you'd like to fail in CI above a threshold, you can add flags like the following (default value is 20%, but it will not throw unless `--benchErrorOnThresholdExceeded` is set):

```
tsx ./p99/within-limit/p99-tall-simple.bench.ts --benchErrorOnThresholdExceeded --benchPercentThreshold 10
```

## CLI

Attest also includes a builtin `attest` CLI including the following commands:
Expand Down
14 changes: 12 additions & 2 deletions ark/attest/__tests__/instantiations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { it } from "mocha"

contextualize(() => {
it("Inline instantiations", () => {
type("string")
attest.instantiations([1968, "instantiations"])
const user = type({
kind: "'admin'",
"powers?": "string[]"
})
.or({
kind: "'superadmin'",
"superpowers?": "string[]"
})
.or({
kind: "'pleb'"
})
attest.instantiations([7574, "instantiations"])
})
})
4 changes: 3 additions & 1 deletion ark/attest/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { dirName, readFile, shell } from "@arktype/fs"
import { dirName, fromHere, readFile, shell } from "@arktype/fs"
import { copyFileSync, rmSync } from "node:fs"

export const runThenGetContents = (templatePath: string): string => {
rmSync(fromHere(".attest"), { force: true, recursive: true })

const tempPath = templatePath + ".temp.ts"
copyFileSync(templatePath, tempPath)
try {
Expand Down
File renamed without changes.
37 changes: 20 additions & 17 deletions ark/attest/assert/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,27 +89,30 @@ const unversionedAssertEquals: AssertFn = (expected, actual, ctx) => {
}
}

export const assertEquals = versionableAssertion(unversionedAssertEquals)
export const assertEquals: AssertFn = versionableAssertion(
unversionedAssertEquals
)

export const typeEqualityMapping = new TypeAssertionMapping(data => {
const expected = data.typeArgs[0]
const actual = data.typeArgs[1] ?? data.args[0]
if (!expected || !actual)
throwInternalError(`Unexpected type data ${printable(data)}`)
export const typeEqualityMapping: TypeAssertionMapping =
new TypeAssertionMapping(data => {
const expected = data.typeArgs[0]
const actual = data.typeArgs[1] ?? data.args[0]
if (!expected || !actual)
throwInternalError(`Unexpected type data ${printable(data)}`)

if (actual.relationships.typeArgs[0] !== "equality") {
return {
expected: expected.type,
actual:
expected.type === actual.type ?
"(serializes to same value)"
: actual.type
if (actual.relationships.typeArgs[0] !== "equality") {
return {
expected: expected.type,
actual:
expected.type === actual.type ?
"(serializes to same value)"
: actual.type
}
}
}
return null
})
return null
})

export const assertEqualOrMatching = versionableAssertion(
export const assertEqualOrMatching: AssertFn = versionableAssertion(
(expected, actual, ctx) => {
const assertionArgs = { actual, expected, ctx }
if (typeof actual !== "string") {
Expand Down
24 changes: 11 additions & 13 deletions ark/attest/assert/attest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,15 @@ export const attestInternal = (
return new ChainableAssertions(ctx)
}

attestInternal.instantiations = (
args: Measure<"instantiations"> | undefined
) => {
const attestConfig = getConfig()
if (attestConfig.skipInlineInstantiations) return
export const attest: AttestFn = Object.assign(attestInternal, {
instantiations: (args: Measure<"instantiations"> | undefined) => {
const attestConfig = getConfig()
if (attestConfig.skipInlineInstantiations) return

const calledFrom = caller()
const ctx = getBenchCtx([calledFrom.file])
ctx.benchCallPosition = calledFrom
ctx.lastSnapCallPosition = calledFrom
instantiationDataHandler({ ...ctx, kind: "instantiations" }, args, false)
}

export const attest: AttestFn = attestInternal as AttestFn
const calledFrom = caller()
const ctx = getBenchCtx([calledFrom.file])
ctx.benchCallPosition = calledFrom
ctx.lastSnapCallPosition = calledFrom
instantiationDataHandler({ ...ctx, kind: "instantiations" }, args, false)
}
})
54 changes: 17 additions & 37 deletions ark/attest/assert/chainableAssertions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { caller } from "@arktype/fs"
import {
printable,
snapshot,
type Constructor,
type Guardable
} from "@arktype/util"
import { printable, snapshot, type Constructor } from "@arktype/util"
import * as assert from "node:assert/strict"
import { isDeepStrictEqual } from "node:util"
import {
Expand Down Expand Up @@ -39,7 +34,7 @@ export class ChainableAssertions implements AssertionRecord {
return snapshot(value)
}

private get actual() {
private get unversionedActual() {
if (this.ctx.actual instanceof TypeAssertionMapping) {
return this.ctx.actual.fn(
this.ctx.typeRelationshipAssertionEntries![0][1],
Expand All @@ -50,53 +45,39 @@ export class ChainableAssertions implements AssertionRecord {
}

private get serializedActual() {
return this.serialize(this.actual)
return this.serialize(this.unversionedActual)
}

private snapRequiresUpdate(expectedSerialized: unknown) {
return (
!isDeepStrictEqual(this.serializedActual, expectedSerialized) ||
// If actual is undefined, we still need to write the "undefined" literal
// to the snap even though it will serialize to the same value as the (nonexistent) first arg
this.actual === undefined
this.unversionedActual === undefined
)
}

narrow(predicate: Guardable, messageOnError?: string): never {
if (!predicate(this.actual)) {
throwAssertionError({
ctx: this.ctx,
message:
messageOnError ??
`${this.serializedActual} failed to satisfy predicate${
predicate.name ? ` ${predicate.name}` : ""
}`
})
}
return this.actual as never
}

get unknown(): this {
return this
}

is(expected: unknown): this {
assert.equal(this.actual, expected)
assert.equal(this.unversionedActual, expected)
return this
}

equals(expected: unknown): this {
assertEquals(expected, this.actual, this.ctx)
assertEquals(expected, this.ctx.actual, this.ctx)
return this
}

instanceOf(expected: Constructor): this {
if (!(this.actual instanceof expected)) {
if (!(this.ctx.actual instanceof expected)) {
throwAssertionError({
ctx: this.ctx,
message: `Expected an instance of ${expected.name} (was ${
typeof this.actual === "object" && this.actual !== null ?
this.actual.constructor.name
typeof this.ctx.actual === "object" && this.ctx.actual !== null ?
this.ctx.actual.constructor.name
: this.serializedActual
})`
})
Expand All @@ -123,7 +104,7 @@ export class ChainableAssertions implements AssertionRecord {
// to give a clearer error message. This avoid problems with objects
// like subtypes of array that do not pass node's deep equality test
// but serialize to the same value.
if (printable(args[0]) !== printable(this.actual))
if (printable(args[0]) !== printable(this.unversionedActual))
assertEquals(expectedSerialized, this.serializedActual, this.ctx)
}
return this
Expand Down Expand Up @@ -163,8 +144,8 @@ export class ChainableAssertions implements AssertionRecord {
}
}
if (this.ctx.allowRegex)
assertEqualOrMatching(expected, this.actual, this.ctx)
else assertEquals(expected, this.actual, this.ctx)
assertEqualOrMatching(expected, this.ctx.actual, this.ctx)
else assertEquals(expected, this.ctx.actual, this.ctx)

return this
}
Expand All @@ -174,7 +155,7 @@ export class ChainableAssertions implements AssertionRecord {
}

get throws(): unknown {
const result = callAssertedFunction(this.actual as Function)
const result = callAssertedFunction(this.unversionedActual as Function)
this.ctx.actual = getThrownMessage(result, this.ctx)
this.ctx.allowRegex = true
this.ctx.defaultExpected = ""
Expand All @@ -184,7 +165,10 @@ export class ChainableAssertions implements AssertionRecord {
throwsAndHasTypeError(matchValue: string | RegExp): void {
assertEqualOrMatching(
matchValue,
getThrownMessage(callAssertedFunction(this.actual as Function), this.ctx),
getThrownMessage(
callAssertedFunction(this.unversionedActual as Function),
this.ctx
),
this.ctx
)
if (!this.ctx.cfg.skipTypes) {
Expand Down Expand Up @@ -294,10 +278,6 @@ export type comparableValueAssertion<expected, kind extends AssertionKind> = {
instanceOf: (constructor: Constructor) => nextAssertions<kind>
is: (value: expected) => nextAssertions<kind>
completions: (value?: Completions) => void
narrow<narrowed>(
predicate: (data: unknown) => data is narrowed,
messageOnError?: string
): narrowed
// This can be used to assert values without type constraints
unknown: Omit<comparableValueAssertion<unknown, kind>, "unknown">
}
Expand Down
Loading

0 comments on commit b52212e

Please sign in to comment.