Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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)")
})
})
4 changes: 2 additions & 2 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 @@ -39,7 +39,7 @@ contextualize(() => {

// this error kind of sucks, would not be sad if it was discriminated and changed
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)"
Comment thread
ssalbdivad marked this conversation as resolved.
Outdated
)

// 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
128 changes: 127 additions & 1 deletion ark/type/__tests__/realWorld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ nospace must be matched by ^\\S*$ (was "One space")`)
box: { box: { box: {} } }
})
attest(box({ box: { box: { box: "whoops" } } })?.toString()).snap(
'box.box.box must be an object (was a string) or must be null (was {"box":{"box":{"box":"whoops"}}})'
"box.box.box must be an object (was a string)"
)
})

Expand Down Expand Up @@ -1420,4 +1420,130 @@ Right: { x: number, y: number, + (undeclared): delete }`)
date2 must be a parsable date (was "")
date3 must be a parsable date (was "")`)
})

// https://github.com/arktypeio/arktype/issues/1188
it("cyclic discriminated union issue 1", () => {
let wasPiped = false

const $ = scope({
Foo: {
"oneOf?": "Bar[]" // NB: don't get the error if this is not an array
},
Bar: "Foo"
}).export()

const baz = $.Bar.pipe((_: object): type.Any | undefined => {
wasPiped = true
return type("string")
})

// preivously threw "TypeError: this.Foo1Apply is not a function"
Comment thread
ssalbdivad marked this conversation as resolved.
Outdated
const r = baz({ oneOf: [{}] })

attest(wasPiped).equals(true)
attest(r?.toString()).snap("Type<string>")
})

// https://github.com/arktypeio/arktype/issues/1367
it("cyclic discriminated union issue 2", () => {
const componentModule = type.module({
container: {
type: "'container'",
content: "component"
},

flexbox: {
type: "'flexbox'",
items: "component"
},

tabsItem: {
id: "string",
title: "component",
content: "component"
},
tabs: {
type: "'tabs'",
items: "tabsItem[]"
},

singleComponent: "string | flexbox | tabs",
component: "singleComponent | singleComponent[]"
})
const componentSchema = componentModule.component

const component: typeof componentSchema.infer = {
type: "tabs",
items: [
{
id: "tab-id",
title: "tab-title",
content: []
}
]
}

const result = componentSchema(component)

attest(result).equals(component)
})

// https://github.com/arktypeio/arktype/issues/1209
it("cyclic discriminated union issue 3", () => {
const $ = scope({
literal: '"foo"',
record: {
"[string]": "value"
},
value: "literal|literal[]|record"
}).export()

const result = $.value({})

attest(result).equals({})
})

// https://github.com/arktypeio/arktype/issues/1284
it("cyclic discriminated union issue 4", () => {
const ruleset = scope({
TypeX: {
id: "string",
"+": "reject"
},
// always worked
TypeA: {
label: "string",
id: "string",
"result?": "TypeA | TypeX",
"+": "reject"
},
// previously did not work
TypeB: {
label: "string",
id: "string",
"result?": "TypeB | TypeX | null",
"+": "reject"
},
// always worked
TypeC: {
label: "string",
id: "string",
"result?": "TypeA | TypeX | null",
"+": "reject"
}
})
const types = ruleset.export()

const data = {
label: "hi",
id: "C",
result: { label: "A", id: "B" }
}

attest(types.TypeA(data)).equals(data)
// previously resulted in error:
// result.label must be removed
attest(types.TypeB(data)).equals(data)
attest(types.TypeC(data)).equals(data)
})
})
Loading