Skip to content

Commit d6605e4

Browse files
committed
feat(jsonschema): add preliminary type inference for jsonSchematToType
1 parent f405773 commit d6605e4

9 files changed

Lines changed: 326 additions & 76 deletions

File tree

ark/json-schema/__tests__/array.test.ts

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
contextualize(() => {
99
it("type array", () => {
1010
const t = jsonSchemaToType({ type: "array" })
11+
attest<unknown[]>(t.infer)
1112
attest(t.expression).snap("Array")
1213
})
1314

@@ -16,12 +17,14 @@ contextualize(() => {
1617
type: "array",
1718
items: { type: "string" }
1819
})
20+
attest<string[]>(tItems.infer)
1921
attest(tItems.expression).snap("string[]")
2022

2123
const tItemsArr = jsonSchemaToType({
2224
type: "array",
2325
items: [{ type: "string" }, { type: "number" }]
2426
})
27+
attest<[string, number]>(tItemsArr.infer)
2528
attest(tItemsArr.expression).snap("[string, number]")
2629
})
2730

@@ -30,17 +33,33 @@ contextualize(() => {
3033
type: "array",
3134
prefixItems: [{ type: "string" }, { type: "number" }]
3235
})
36+
attest<[string, number, ...unknown[]]>(tPrefixItems.infer)
3337
attest(tPrefixItems.expression).snap("[string, number, ...unknown[]]")
3438
})
3539

3640
it("items & prefixItems", () => {
41+
const tItemsFalseAndPrefixItems = jsonSchemaToType({
42+
type: "array",
43+
prefixItems: [{ type: "string" }, { type: "number" }],
44+
items: false
45+
})
46+
attest<[string, number]>(tItemsFalseAndPrefixItems.infer)
47+
3748
const tItemsAndPrefixItems = jsonSchemaToType({
3849
type: "array",
3950
prefixItems: [{ type: "string" }, { type: "number" }],
4051
items: { type: "boolean" }
4152
})
42-
attest(tItemsAndPrefixItems.expression).snap(
43-
"[string, number, ...boolean[]]"
53+
attest<[string, number, ...boolean[]]>(tItemsAndPrefixItems.infer)
54+
55+
const tItemsArrayAndPrefixItems = jsonSchemaToType({
56+
type: "array",
57+
prefixItems: [{ type: "string" }, { type: "number" }],
58+
items: [{ type: "boolean" }, { type: "null" }]
59+
})
60+
attest<[string, number, boolean, null]>(tItemsArrayAndPrefixItems.infer)
61+
attest(tItemsArrayAndPrefixItems.expression).snap(
62+
"[string, number, boolean, null]"
4463
)
4564
})
4665

@@ -49,6 +68,7 @@ contextualize(() => {
4968
type: "array",
5069
additionalItems: { type: "string" }
5170
})
71+
attest<string[]>(tAdditionalItems.infer)
5272
attest(tAdditionalItems.expression).snap("string[]")
5373
})
5474

@@ -58,21 +78,25 @@ contextualize(() => {
5878
additionalItems: { type: "boolean" },
5979
items: [{ type: "string" }, { type: "number" }]
6080
})
81+
attest<[string, number, ...boolean[]]>(tItemsVariadic.infer)
6182
attest(tItemsVariadic.expression).snap("[string, number, ...boolean[]]")
6283

6384
const tItemsFalseAdditional = jsonSchemaToType({
6485
type: "array",
6586
additionalItems: false,
6687
items: [{ type: "string" }]
6788
})
89+
attest<[string]>(tItemsFalseAdditional.infer)
6890
attest(tItemsFalseAdditional.expression).snap("[string]")
6991

70-
attest(() =>
71-
jsonSchemaToType({
72-
type: "array",
73-
additionalItems: { type: "string" },
74-
items: { type: "string" }
75-
})
92+
attest(
93+
() =>
94+
// @ts-expect-error
95+
jsonSchemaToType({
96+
type: "array",
97+
additionalItems: { type: "string" },
98+
items: { type: "string" }
99+
}) as never
76100
).throws(writeJsonSchemaArrayNonArrayItemsAndAdditionalItemsMessage())
77101
})
78102

@@ -88,13 +112,14 @@ contextualize(() => {
88112
})
89113

90114
it("additionalItems & items & prefixItems", () => {
91-
attest(() =>
92-
jsonSchemaToType({
93-
type: "array",
94-
additionalItems: { type: "boolean" },
95-
items: { type: "null" },
96-
prefixItems: [{ type: "string" }, { type: "number" }]
97-
})
115+
attest(
116+
() =>
117+
jsonSchemaToType({
118+
type: "array",
119+
additionalItems: { type: "boolean" },
120+
items: { type: "null" },
121+
prefixItems: [{ type: "string" }, { type: "number" }]
122+
}) as never
98123
).throws(writeJsonSchemaArrayAdditionalItemsAndItemsAndPrefixItemsMessage())
99124
})
100125

@@ -103,6 +128,7 @@ contextualize(() => {
103128
type: "array",
104129
contains: { type: "number" }
105130
})
131+
attest<unknown[]>(tContains.infer)
106132
attest(tContains.json).snap({
107133
proto: "Array",
108134
predicate: ["$ark.jsonSchemaArrayContainsValidator"]
@@ -117,27 +143,29 @@ contextualize(() => {
117143
type: "array",
118144
maxItems: 5
119145
})
146+
attest<unknown[]>(tMaxItems.infer)
120147
attest(tMaxItems.expression).snap("Array <= 5")
121148
})
122149

123150
it("maxItems (negative)", () => {
124-
attest(() => jsonSchemaToType({ type: "array", maxItems: -1 })).throws(
125-
"TraversalError: maxItems must be non-negative"
126-
)
151+
attest(
152+
() => jsonSchemaToType({ type: "array", maxItems: -1 }) as never
153+
).throws("TraversalError: maxItems must be non-negative")
127154
})
128155

129156
it("minItems (positive)", () => {
130157
const tMinItems = jsonSchemaToType({
131158
type: "array",
132159
minItems: 5
133160
})
161+
attest<unknown[]>(tMinItems.infer)
134162
attest(tMinItems.expression).snap("Array >= 5")
135163
})
136164

137165
it("minItems (negative)", () => {
138-
attest(() => jsonSchemaToType({ type: "array", minItems: -1 })).throws(
139-
"TraversalError: minItems must be non-negative"
140-
)
166+
attest(
167+
() => jsonSchemaToType({ type: "array", minItems: -1 }) as never
168+
).throws("TraversalError: minItems must be non-negative")
141169
})
142170

143171
it("minItems (0)", () => {
@@ -155,6 +183,7 @@ contextualize(() => {
155183
type: "array",
156184
uniqueItems: true
157185
})
186+
attest<unknown[]>(tUniqueItems.infer)
158187
attest(tUniqueItems.json).snap({
159188
proto: "Array",
160189
predicate: ["$ark.jsonSchemaArrayUniqueItemsValidator"]

ark/json-schema/__tests__/composition.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,24 @@ contextualize(() => {
99
{ type: "string", maxLength: 10 }
1010
]
1111
})
12+
attest<string>(tAllOf.infer)
1213
attest(tAllOf.expression).snap("string <= 10 & >= 1")
1314
})
1415

1516
it("anyOf", () => {
1617
const tAnyOf = jsonSchemaToType({
1718
anyOf: [
18-
{ type: "string", minLength: 1 },
19-
{ type: "string", maxLength: 10 }
19+
{ type: "string", minLength: 1, maxLength: 1 },
20+
{ type: "number", maximum: 9 }
2021
]
2122
})
22-
attest(tAnyOf.expression).snap("string <= 10 | string >= 1")
23+
attest<string | number>(tAnyOf.infer)
24+
attest(tAnyOf.expression).snap("number <= 9 | string == 1")
2325
})
2426

2527
it("not", () => {
2628
const tNot = jsonSchemaToType({ not: { type: "string", maxLength: 3 } })
29+
attest<unknown>(tNot.infer)
2730
attest(tNot.json).snap({
2831
predicate: ["$ark.jsonSchemaNotValidator"]
2932
})
@@ -39,6 +42,7 @@ contextualize(() => {
3942
const tOneOf = jsonSchemaToType({
4043
oneOf: [{ type: "string", minLength: 10 }, { const: "foo" }]
4144
})
45+
attest<string | "foo">(tOneOf.infer)
4246
attest(tOneOf.json).snap({
4347
predicate: ["$ark.jsonSchemaOneOfValidator"]
4448
})

ark/json-schema/__tests__/object.test.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {
55
writeJsonSchemaObjectNonConformingPatternAndPropertyNamesMessage
66
} from "@ark/json-schema"
77
import { writeDuplicateKeyMessage } from "@ark/schema"
8+
import type { Json } from "@ark/util"
89

910
contextualize(() => {
1011
it("type object", () => {
1112
const t = jsonSchemaToType({ type: "object" })
13+
attest<Record<string, Json>>(t.infer)
1214
attest(t.expression).snap("{}")
1315
attest(t.allows({ foo: 3 }))
1416
})
@@ -18,6 +20,7 @@ contextualize(() => {
1820
type: "object",
1921
maxProperties: 1
2022
})
23+
attest<Record<string, Json>>(tMaxProperties.infer)
2124
attest(tMaxProperties.json).snap({
2225
domain: "object",
2326
predicate: ["$ark.jsonSchemaObjectMaxPropertiesValidator"]
@@ -33,6 +36,7 @@ contextualize(() => {
3336
type: "object",
3437
minProperties: 2
3538
})
39+
attest<Record<string, Json>>(tMinProperties.infer)
3640
attest(tMinProperties.json).snap({
3741
domain: "object",
3842
predicate: ["$ark.jsonSchemaObjectMinPropertiesValidator"]
@@ -52,28 +56,31 @@ contextualize(() => {
5256
},
5357
required: ["foo"]
5458
})
59+
attest<{ foo: string; bar?: number }>(tRequired.infer)
5560
attest(tRequired.expression).snap("{ foo: string, bar?: number }")
5661

57-
attest(() =>
58-
jsonSchemaToType({ type: "object", required: ["foo"] })
62+
attest(
63+
() => jsonSchemaToType({ type: "object", required: ["foo"] }) as never
5964
).throws(
6065
"TraversalError: must be a valid object JSON Schema (was an object JSON Schema with 'required' array but no 'properties' object)"
6166
)
62-
attest(() =>
63-
jsonSchemaToType({
64-
type: "object",
65-
properties: { foo: { type: "string" } },
66-
required: ["bar"]
67-
})
67+
attest(
68+
() =>
69+
jsonSchemaToType({
70+
type: "object",
71+
properties: { foo: { type: "string" } },
72+
required: ["bar"]
73+
}) as never
6874
).throws(
6975
`TraversalError: required must be a key from the 'properties' object, i.e. foo (was bar)`
7076
)
71-
attest(() =>
72-
jsonSchemaToType({
73-
type: "object",
74-
properties: { foo: { type: "string" } },
75-
required: ["foo", "foo"]
76-
})
77+
attest(
78+
() =>
79+
jsonSchemaToType({
80+
type: "object",
81+
properties: { foo: { type: "string" } },
82+
required: ["foo", "foo"]
83+
}) as never
7784
).throws(writeDuplicateKeyMessage("foo"))
7885
})
7986

@@ -83,6 +90,7 @@ contextualize(() => {
8390
additionalProperties: { type: "number" },
8491
properties: { bar: { type: "string" } }
8592
})
93+
attest<{ bar?: string } & Record<string, Json>>(tAdditionalProperties.infer)
8694
attest(tAdditionalProperties.json).snap({
8795
domain: "object",
8896
optional: [{ key: "bar", value: "string" }],
@@ -101,6 +109,7 @@ contextualize(() => {
101109
"^[a-z]+$": { type: "string" }
102110
}
103111
})
112+
attest<Record<string, Json>>(tPatternProperties.infer)
104113
attest(tPatternProperties.expression).snap("{ [/^[a-z]+$/]: string }")
105114
attest(tPatternProperties.allows({})).equals(true)
106115
attest(tPatternProperties.allows({ foo: "bar" })).equals(true)
@@ -118,7 +127,6 @@ contextualize(() => {
118127
)
119128

120129
attest(() =>
121-
// @ts-expect-error
122130
jsonSchemaToType({
123131
type: "object",
124132
propertyNames: { type: "number" }

ark/json-schema/__tests__/string.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { JsonSchemaOrBoolean } from "@ark/schema"
55
contextualize(() => {
66
it("type string", () => {
77
const t = jsonSchemaToType({ type: "string" })
8+
attest<string>(t.infer)
89
attest(t.expression).snap("string")
910
})
1011

@@ -13,16 +14,18 @@ contextualize(() => {
1314
type: "string",
1415
maxLength: 5
1516
})
17+
attest<string>(tMaxLength.infer)
1618
attest(tMaxLength.expression).snap("string <= 5")
1719
})
1820

1921
it("maxLength (negative)", () => {
2022
const maxLength = -5
21-
attest(() =>
22-
jsonSchemaToType({
23-
type: "string",
24-
maxLength
25-
})
23+
attest(
24+
() =>
25+
jsonSchemaToType({
26+
type: "string",
27+
maxLength
28+
}) as never
2629
).throws(
2730
`TraversalError: maxLength must be non-negative (was ${maxLength})`
2831
)
@@ -33,16 +36,18 @@ contextualize(() => {
3336
type: "string",
3437
minLength: 5
3538
})
39+
attest<string>(tMinLength.infer)
3640
attest(tMinLength.expression).snap("string >= 5")
3741
})
3842

3943
it("minLength (negative)", () => {
4044
const minLength = -1
41-
attest(() =>
42-
jsonSchemaToType({
43-
type: "string",
44-
minLength
45-
})
45+
attest(
46+
() =>
47+
jsonSchemaToType({
48+
type: "string",
49+
minLength
50+
}) as never
4651
).throws(
4752
`TraversalError: minLength must be non-negative (was ${minLength})`
4853
)
@@ -53,6 +58,7 @@ contextualize(() => {
5358
type: "string",
5459
pattern: "es"
5560
})
61+
attest<string>(tPatternString.infer)
5662
attest(tPatternString.expression).snap("/es/")
5763
// JSON Schema explicitly specifies that regexes MUST NOT be implicitly anchored
5864
// https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.4.3

0 commit comments

Comments
 (0)