Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions ark/attest/__tests__/assertions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { attest } from "@ark/attest"
import { MissingSnapshotError } from "@ark/attest/internal/assert/assertions.ts"
import { attestInternal } from "@ark/attest/internal/assert/attest.ts"
import { registeredReference } from "@ark/schema"
import { register } from "@ark/util"
import { type } from "arktype"
import * as assert from "node:assert/strict"

Expand Down Expand Up @@ -168,11 +170,14 @@ describe("type assertions", () => {
})

it("does not boom on Type comparison", () => {
const expectedRef = register(type.number)
const actualRef = register(type.string)

// @ts-expect-error
attest(() => attest(type.string).equals(type.number)).throws
.snap(`AssertionError [ERR_ASSERTION]: Assertion including at least one function or object was not between reference equal items
Expected: Function(fn10)
Actual: Function(fn11)`)
.equals(`AssertionError [ERR_ASSERTION]: Assertion including at least one function or object was not between reference equal items
Expected: Function(${expectedRef})
Actual: Function(${actualRef})`)
})

it("doesn't boom on ArkErrors vs plain object", () => {
Expand Down
1 change: 1 addition & 0 deletions ark/attest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@typescript/analyze-trace": "0.10.1",
"@typescript/vfs": "1.6.1",
"arktype": "workspace:*",
"@ark/schema": "workspace:*",
"prettier": "3.6.2"
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions ark/json-schema/__tests__/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ contextualize(() => {
)
})

it("minItems (0)", () => {
const tMinItems = jsonSchemaToType({
type: "array",
minItems: 0,
items: { type: "string" }
})

attest(tMinItems.expression).snap("string[]")
})

it("uniqueItems", () => {
const tUniqueItems = jsonSchemaToType({
type: "array",
Expand Down
23 changes: 23 additions & 0 deletions ark/json-schema/__tests__/string.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { attest, contextualize } from "@ark/attest"
import { jsonSchemaToType } from "@ark/json-schema"
import type { JsonSchemaOrBoolean } from "@ark/schema"

contextualize(() => {
it("type string", () => {
Expand Down Expand Up @@ -57,4 +58,26 @@ contextualize(() => {
// https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.4.3
attest(tPatternString.allows("expression")).equals(true)
})

it("string enums", () => {
const enumKeys = ["keyOne", "keyTwo"]

const stringEnums = jsonSchemaToType({
type: "string",
enum: enumKeys
})

attest(stringEnums.expression).snap('"keyOne" | "keyTwo"')
})

it("minLength (0)", () => {
const schema = {
type: "string",
minLength: 0
} satisfies JsonSchemaOrBoolean
const pattern = jsonSchemaToType(schema)

attest(() => jsonSchemaToType(schema))
attest(pattern.expression).snap("string")
})
})
2 changes: 1 addition & 1 deletion ark/json-schema/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export const parseCommonJsonSchema = (
return type.unit(jsonSchema.const)
}

if ("enum" in jsonSchema) return type.enumerated(jsonSchema.enum)
if ("enum" in jsonSchema) return type.enumerated(...jsonSchema.enum)
}
17 changes: 14 additions & 3 deletions ark/repo/scratch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { regex } from "arkregex"
import { scope, type } from "arktype"

const S = regex("^a(?<foo>b(c)d)?e\\1\\2?$")
const $ = scope({
Foo: {
"oneOf?": "Bar[]" // NB: don't get the error if this is not an array
},
Bar: "Foo"
}).export()

S
const baz = $.Bar.pipe((_: object): type.Any | undefined => {
console.log("this never gets logged since pipe isn't entered")
return type("string")
})

const r = baz({ oneOf: [{}] }) // throws "TypeError: this.Foo1Apply is not a function"
console.log(r?.toString())
5 changes: 5 additions & 0 deletions ark/schema/__tests__/bounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ contextualize(() => {
)
})

it("minLength 0 reduces to unconstrained", () => {
const T = rootSchema({ domain: "string", minLength: 0 })
attest(T.expression).snap("string")
})

for (const [min, max] of entriesOf(boundKindPairsByLower)) {
describe(`${min}/${max}`, () => {
const basis =
Expand Down
3 changes: 3 additions & 0 deletions ark/schema/__tests__/jsonSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ contextualize(() => {
{ type: "boolean" },
{ type: "null" }
]
},
union7: {
anyOf: [{ $ref: "#/$defs/intersection11" }, { type: "boolean" }]
}
}
})
Expand Down
3 changes: 3 additions & 0 deletions ark/schema/constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export const constraintKeyParser =
return nodes.sort((l, r) => (l.hash < r.hash ? -1 : 1)) as never
}
const child = ctx.$.node(kind, schema)
// If the constraint was reduced to a root node (like unknown for minLength: 0),
// omit it from the schema since it's trivially satisfied
if (child.isRoot()) return
return (child.hasOpenIntersection() ? [child] : child) as never
}

Expand Down
2 changes: 1 addition & 1 deletion ark/schema/roots/intersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ const writeIntersectionExpression = (node: Intersection.Node) => {
.map(n => n.expression)
.join(" & ")

const fullExpression = `${basisExpression}${basisExpression ? " " : ""}${refinementsExpression}`
const fullExpression = `${basisExpression}${basisExpression && refinementsExpression ? " " : ""}${refinementsExpression}`

if (fullExpression === "Array == 0") return "[]"

Expand Down
2 changes: 1 addition & 1 deletion ark/schema/roots/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ export class UnionNode extends BaseRoot<Union.Declaration> {
}

discriminate(): Discriminant | null {
if (this.branches.length < 2 || this.isCyclic) return null
if (this.branches.length < 2) return null
if (this.unitBranches.length === this.branches.length) {
const cases = flatMorph(this.unitBranches, (i, n) => [
`${(n.rawIn as Unit.Node).serializedValue}`,
Expand Down
75 changes: 75 additions & 0 deletions ark/type/__tests__/discrimination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,4 +490,79 @@ contextualize(() => {

attest(T.assert([])).equals([])
})

// https://github.com/arktypeio/arktype/issues/1547
it("discriminates cyclic union on nested path", () => {
const s = scope({
AChild: { type: "'AChild'", children: "(AParent)[] > 0" },
AParent: { type: "'AParent'", children: "(AChild)[] > 0" },
BChild: { type: "'BChild'", children: "unknown[]" },
BParent: {
type: "'BParent'",
layout: "number[]",
children: "(BChild)[] > 0"
}
})

const Thing = s.type("AParent | BParent")

attest(Thing.internal.assertHasKind("union").discriminantJson).snap({
kind: "unit",
path: ["type"],
cases: {
'"BParent"': {
required: [
{
key: "children",
value: {
sequence: {
required: [
{ key: "children", value: "Array" },
{ key: "type", value: { unit: "BChild" } }
],
domain: "object"
},
proto: "Array",
minLength: 1
}
},
{ key: "layout", value: { sequence: "number", proto: "Array" } }
]
},
'"AParent"': {
required: [
{
key: "children",
value: {
sequence: {
required: [
{
key: "children",
value: {
sequence: "$AParent",
proto: "Array",
minLength: 1
}
},
{ key: "type", value: { unit: "AChild" } }
],
domain: "object"
},
proto: "Array",
minLength: 1
}
}
]
}
}
})

attest(
Thing({
type: "BParent",
layout: "",
children: [{ type: "BChild", children: [] }]
}).toString()
).snap("layout must be an array (was string)")
})
})
6 changes: 3 additions & 3 deletions ark/type/__tests__/keywords/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ contextualize(() => {
attest(Json([])).equals([])
attest(Json(5)?.toString()).snap("must be an object (was a number)")
attest(Json({ foo: [5n] })?.toString()).snap(
'foo["0"] must be an object, a number, a string, false, null or true (was 5n) or foo must be a number, a string, false, null or true (was ["5n"])'
'foo["0"] must be an object (was a bigint)'
)
})

Expand All @@ -37,9 +37,9 @@ contextualize(() => {

attest<string>(out).snap('{"foo":"bar"}')

// this error kind of sucks, would not be sad if it was discriminated and changed
// this error kind of sucks, should have more discriminant context
attest(stringify({ foo: undefined }).toString()).snap(
"foo must be an object, a number, a string, false, null or true (was undefined)"
"foo must be an object (was undefined)"
)

// has declared out
Expand Down
2 changes: 1 addition & 1 deletion ark/type/__tests__/pipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ contextualize(() => {

attest(indiscriminable).throws
.snap(`ParseError: An unordered union of a type including a morph and a type with overlapping input is indeterminate:
Left: { foo: (In: string ) => Out<Date> | false | true }
Left: { foo: (In: string) => Out<Date> | false | true }
Right: { foo: (In: string) => Out<{ [string]: $jsonObject | number | string | false | null | true }> | false | true }`)
})

Expand Down
Loading