schematox 1.2.0 → 1.2.2-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +22 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +5 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +328 -0
- package/dist/parse.js.map +1 -0
- package/dist/struct.d.ts +48 -0
- package/dist/struct.d.ts.map +1 -0
- package/dist/struct.js +111 -0
- package/dist/struct.js.map +1 -0
- package/dist/types/extensions.d.ts +13 -0
- package/dist/types/extensions.d.ts.map +1 -0
- package/dist/types/extensions.js +2 -0
- package/dist/types/extensions.js.map +1 -0
- package/dist/types/infer.d.ts +35 -0
- package/dist/types/infer.d.ts.map +1 -0
- package/dist/types/infer.js +2 -0
- package/dist/types/infer.js.map +1 -0
- package/dist/types/schema.d.ts +93 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +2 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/standard-schema.d.ts +35 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/types/struct.d.ts +52 -0
- package/dist/types/struct.d.ts.map +1 -0
- package/dist/types/struct.js +2 -0
- package/dist/types/struct.js.map +1 -0
- package/dist/types/utils.d.ts +41 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/utils.js +2 -0
- package/dist/types/utils.js.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -0
- package/package.json +16 -4
- package/src/tests/README.md +390 -0
- package/src/tests/by-struct/array.test.ts +1684 -0
- package/src/tests/by-struct/boolean.test.ts +741 -0
- package/src/tests/by-struct/literal.test.ts +755 -0
- package/src/tests/by-struct/number.test.ts +1234 -0
- package/src/tests/by-struct/object.test.ts +1484 -0
- package/src/tests/by-struct/record.test.ts +1802 -0
- package/src/tests/by-struct/string.test.ts +1252 -0
- package/src/tests/by-struct/tuple.test.ts +1341 -0
- package/src/tests/by-struct/union.test.ts +1284 -0
- package/src/tests/fixtures.ts +52 -0
- package/src/tests/fold-constants.ts +247 -0
- package/src/tests/fold-morph.ts +49 -0
- package/src/tests/type.ts +1 -0
- package/src/tests/types/extensions.test.ts +117 -0
- package/src/tests/types/infer.test.ts +1410 -0
- package/src/tests/utils.test.ts +191 -0
- package/CHANGELOG.md +0 -48
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
The testing strategy for schematox uses a **fold-based approach** to ensure consistency across all data type tests. The system tests three equivalent ways of creating and using schemas:
|
|
2
|
+
|
|
3
|
+
1. **Raw schema objects** (e.g., `{ type: 'boolean' }`)
|
|
4
|
+
2. **Constructs** created via `x.makeStruct(schema)`
|
|
5
|
+
3. **Structs** created via fluent API (e.g., `x.boolean()`)
|
|
6
|
+
|
|
7
|
+
The key principle is that all three approaches should behave identically in terms of:
|
|
8
|
+
- Type inference
|
|
9
|
+
- Runtime parsing
|
|
10
|
+
- Error handling
|
|
11
|
+
- Schema immutability
|
|
12
|
+
|
|
13
|
+
**Folds** are reusable code blocks marked with comments like `foldA:`, `foldB:`, etc. These are automatically synchronized across all type tests using the `npm run morph` script with AST manipulation, ensuring consistency and reducing maintenance overhead.
|
|
14
|
+
|
|
15
|
+
# By struct scaffold
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import * as x from '../../'
|
|
19
|
+
import * as fixture from '../fixtures'
|
|
20
|
+
|
|
21
|
+
describe('Type inference and parse by schema/construct/struct (foldA)', () => {
|
|
22
|
+
it.todo('required')
|
|
23
|
+
it.todo('optional')
|
|
24
|
+
it.todo('nullable')
|
|
25
|
+
it.todo('min')
|
|
26
|
+
it.todo('max')
|
|
27
|
+
it.todo('optional + nullable + minLength + maxLength')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('Struct parameter keys reduction and schema immutability (foldB)', () => {
|
|
31
|
+
it.todo('optional')
|
|
32
|
+
it.todo('optional + nullable')
|
|
33
|
+
it.todo('optional + nullable + brand')
|
|
34
|
+
it.todo('optional + nullable + brand + min')
|
|
35
|
+
it.todo('optional + nullable + brand + min')
|
|
36
|
+
it.todo('optional + nullable + brand + min + max')
|
|
37
|
+
it.todo('optional + nullable + brand + min + max + description')
|
|
38
|
+
it.todo('description + max + min + brand + nullable + optional')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('ERROR_CODE.invalidType (foldC, foldE)', () => {
|
|
42
|
+
it.todo('iterate over fixture.DATA_TYPE')
|
|
43
|
+
it.todo('InvalidSubject error of nested schema should have correct path/schema/subject')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('ERROR_CODE.invalidRange (foldD)', () => {
|
|
47
|
+
it.todo('min')
|
|
48
|
+
it.todo('max')
|
|
49
|
+
it.todo('min + max')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('Compound schema specifics (foldA)', () => {
|
|
53
|
+
it.todo('nested primitive schema: optional + nullable + brand')
|
|
54
|
+
it.todo('nested compound schema: optional + nullable')
|
|
55
|
+
it.todo('nested by itself (schema depth: 4)')
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
# Fold A: Type Inference and Parse Consistency
|
|
60
|
+
|
|
61
|
+
Purpose**: Validates that all three approaches (schema, construct, struct) produce identical TypeScript types and runtime behavior for successful parsing scenarios.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
it('required', () => {
|
|
65
|
+
const schema = { type: 'boolean' } as const satisfies x.Schema
|
|
66
|
+
const struct = x.boolean()
|
|
67
|
+
|
|
68
|
+
type ExpectedSubj = boolean
|
|
69
|
+
|
|
70
|
+
const subjects: Array<ExpectedSubj> = [true, false]
|
|
71
|
+
|
|
72
|
+
foldA: {
|
|
73
|
+
const construct = x.makeStruct(schema)
|
|
74
|
+
|
|
75
|
+
/* ensure that schema/construct/struct/~standard subject types are identical */
|
|
76
|
+
|
|
77
|
+
type ConstructSchemaSubj = x.Infer<typeof construct.__schema>
|
|
78
|
+
|
|
79
|
+
x.tCh<ConstructSchemaSubj, ExpectedSubj>()
|
|
80
|
+
x.tCh<ExpectedSubj, ConstructSchemaSubj>()
|
|
81
|
+
|
|
82
|
+
type SchemaSubj = x.Infer<typeof schema>
|
|
83
|
+
|
|
84
|
+
x.tCh<SchemaSubj, ExpectedSubj>()
|
|
85
|
+
x.tCh<ExpectedSubj, SchemaSubj>()
|
|
86
|
+
|
|
87
|
+
type StructSubj = x.Infer<typeof struct.__schema>
|
|
88
|
+
|
|
89
|
+
x.tCh<StructSubj, ExpectedSubj>()
|
|
90
|
+
x.tCh<ExpectedSubj, StructSubj>()
|
|
91
|
+
|
|
92
|
+
type StandardSubj = NonNullable<
|
|
93
|
+
(typeof struct)['~standard']['types']
|
|
94
|
+
>['output']
|
|
95
|
+
|
|
96
|
+
x.tCh<StandardSubj, ExpectedSubj>()
|
|
97
|
+
x.tCh<ExpectedSubj, StandardSubj>()
|
|
98
|
+
|
|
99
|
+
/* parsed either type check */
|
|
100
|
+
|
|
101
|
+
type ExpectedParsed = x.ParseResult<ExpectedSubj>
|
|
102
|
+
|
|
103
|
+
const parsed = x.parse(schema, undefined)
|
|
104
|
+
|
|
105
|
+
type SchemaParsed = typeof parsed
|
|
106
|
+
|
|
107
|
+
x.tCh<SchemaParsed, ExpectedParsed>()
|
|
108
|
+
x.tCh<ExpectedParsed, SchemaParsed>()
|
|
109
|
+
|
|
110
|
+
type ConstructParsed = ReturnType<typeof construct.parse>
|
|
111
|
+
|
|
112
|
+
x.tCh<ConstructParsed, ExpectedParsed>()
|
|
113
|
+
x.tCh<ExpectedParsed, ConstructParsed>()
|
|
114
|
+
|
|
115
|
+
type StructParsed = ReturnType<typeof struct.parse>
|
|
116
|
+
|
|
117
|
+
x.tCh<StructParsed, ExpectedParsed>()
|
|
118
|
+
x.tCh<ExpectedParsed, StructParsed>()
|
|
119
|
+
|
|
120
|
+
type StandardParsed = Extract<
|
|
121
|
+
ReturnType<(typeof struct)['~standard']['validate']>,
|
|
122
|
+
{ value: unknown }
|
|
123
|
+
>['value']
|
|
124
|
+
|
|
125
|
+
x.tCh<StandardParsed, ExpectedSubj>()
|
|
126
|
+
x.tCh<ExpectedSubj, StandardParsed>()
|
|
127
|
+
|
|
128
|
+
/* runtime schema check */
|
|
129
|
+
|
|
130
|
+
expect(struct.__schema).toStrictEqual(schema)
|
|
131
|
+
expect(construct.__schema).toStrictEqual(schema)
|
|
132
|
+
expect(construct.__schema === schema).toBe(false)
|
|
133
|
+
|
|
134
|
+
/* parse result check */
|
|
135
|
+
|
|
136
|
+
for (const subj of subjects) {
|
|
137
|
+
const schemaParsed = x.parse(schema, subj)
|
|
138
|
+
|
|
139
|
+
expect(schemaParsed.error).toBe(undefined)
|
|
140
|
+
expect(schemaParsed.data).toStrictEqual(subj)
|
|
141
|
+
|
|
142
|
+
const constructParsed = construct.parse(subj)
|
|
143
|
+
|
|
144
|
+
expect(constructParsed.error).toBe(undefined)
|
|
145
|
+
expect(constructParsed.data).toStrictEqual(subj)
|
|
146
|
+
|
|
147
|
+
const structParsed = struct.parse(subj)
|
|
148
|
+
|
|
149
|
+
expect(structParsed.error).toBe(undefined)
|
|
150
|
+
expect(structParsed.data).toStrictEqual(subj)
|
|
151
|
+
|
|
152
|
+
const standardParsed = struct['~standard'].validate(subj)
|
|
153
|
+
|
|
154
|
+
if (standardParsed instanceof Promise) {
|
|
155
|
+
throw Error('Not expected')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (standardParsed.issues !== undefined) {
|
|
159
|
+
throw Error('not expected')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expect(standardParsed.value).toStrictEqual(subj)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
# Fold B: Parameter Keys Reduction and Schema Immutability
|
|
169
|
+
|
|
170
|
+
Purpose**: Tests that fluent API methods correctly reduce available keys (removing applied methods) and ensure schema objects are immutable when parameters are applied.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
it('optional', () => {
|
|
174
|
+
const schema = {
|
|
175
|
+
type: 'boolean',
|
|
176
|
+
optional: true,
|
|
177
|
+
} as const satisfies x.Schema
|
|
178
|
+
|
|
179
|
+
const prevStruct = x.boolean()
|
|
180
|
+
const struct = prevStruct.optional()
|
|
181
|
+
|
|
182
|
+
type ExpectedKeys =
|
|
183
|
+
| 'brand'
|
|
184
|
+
| 'nullable'
|
|
185
|
+
| 'description'
|
|
186
|
+
| '__schema'
|
|
187
|
+
| 'parse'
|
|
188
|
+
|
|
189
|
+
foldB: {
|
|
190
|
+
const construct = x.makeStruct(schema)
|
|
191
|
+
|
|
192
|
+
/* ensure that struct keys are reduced after application */
|
|
193
|
+
|
|
194
|
+
type StructKeys = keyof typeof struct
|
|
195
|
+
|
|
196
|
+
x.tCh<StructKeys, ExpectedKeys>()
|
|
197
|
+
x.tCh<ExpectedKeys, StructKeys>()
|
|
198
|
+
|
|
199
|
+
type ConstructKeys = keyof typeof construct
|
|
200
|
+
|
|
201
|
+
x.tCh<ConstructKeys, ExpectedKeys>()
|
|
202
|
+
x.tCh<ExpectedKeys, ConstructKeys>()
|
|
203
|
+
|
|
204
|
+
/* ensure that construct/struct schema types are identical */
|
|
205
|
+
|
|
206
|
+
type ExpectedSchema = typeof schema
|
|
207
|
+
type StructSchema = typeof struct.__schema
|
|
208
|
+
|
|
209
|
+
x.tCh<StructSchema, ExpectedSchema>()
|
|
210
|
+
x.tCh<ExpectedSchema, StructSchema>()
|
|
211
|
+
|
|
212
|
+
type ConstructSchema = typeof struct.__schema
|
|
213
|
+
|
|
214
|
+
x.tCh<ConstructSchema, ExpectedSchema>()
|
|
215
|
+
x.tCh<ExpectedSchema, ConstructSchema>()
|
|
216
|
+
|
|
217
|
+
/* runtime schema check */
|
|
218
|
+
|
|
219
|
+
expect(struct.__schema).toStrictEqual(schema)
|
|
220
|
+
expect(construct.__schema).toStrictEqual(schema)
|
|
221
|
+
expect(construct.__schema === schema).toBe(false)
|
|
222
|
+
|
|
223
|
+
/* runtime schema parameter application immutability check */
|
|
224
|
+
|
|
225
|
+
expect(prevStruct.__schema === struct.__schema).toBe(false)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
# Fold C: Invalid Type Error Handling
|
|
231
|
+
|
|
232
|
+
Purpose**: Systematically tests type validation errors by feeding incompatible data types and ensuring consistent error responses across all three approaches.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
it('iterate over fixture.DATA_TYPE', () => {
|
|
236
|
+
const schema = { type: 'boolean' } satisfies x.Schema
|
|
237
|
+
const struct = x.boolean()
|
|
238
|
+
const source = fixture.DATA_TYPE
|
|
239
|
+
|
|
240
|
+
foldC: {
|
|
241
|
+
const construct = x.makeStruct(schema)
|
|
242
|
+
|
|
243
|
+
for (const [kind, types] of source) {
|
|
244
|
+
if (kind === schema.type) {
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const subject of types) {
|
|
249
|
+
const expectedError = [
|
|
250
|
+
{
|
|
251
|
+
code: x.ERROR_CODE.invalidType,
|
|
252
|
+
schema: schema,
|
|
253
|
+
subject: subject,
|
|
254
|
+
path: [],
|
|
255
|
+
},
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
const parsedSchema = x.parse(schema, subject)
|
|
259
|
+
const parsedConstruct = construct.parse(subject)
|
|
260
|
+
const parsedStruct = struct.parse(subject)
|
|
261
|
+
|
|
262
|
+
expect(parsedSchema.error).toStrictEqual(expectedError)
|
|
263
|
+
expect(parsedConstruct.error).toStrictEqual(expectedError)
|
|
264
|
+
expect(parsedStruct.error).toStrictEqual(expectedError)
|
|
265
|
+
|
|
266
|
+
const parsedStandard = struct['~standard'].validate(subject)
|
|
267
|
+
|
|
268
|
+
if (parsedStandard instanceof Promise) {
|
|
269
|
+
throw Error('Not expected')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
expect(parsedStandard.issues).toStrictEqual([
|
|
273
|
+
{ message: x.ERROR_CODE.invalidType, path: [] },
|
|
274
|
+
])
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Fold D: Range Validation Error Handling
|
|
282
|
+
|
|
283
|
+
Purpose**: Tests range/constraint validation errors (like `min`, `max`, `minLength`, `maxLength`) ensuring consistent error handling.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
it('min', () => {
|
|
287
|
+
const schema = { type: 'number', min: 0 } satisfies x.Schema
|
|
288
|
+
const struct = x.number().min(schema.min)
|
|
289
|
+
const subjects = [-1, -2, -3]
|
|
290
|
+
|
|
291
|
+
foldD: {
|
|
292
|
+
const construct = x.makeStruct(schema)
|
|
293
|
+
|
|
294
|
+
for (const subject of subjects) {
|
|
295
|
+
const expectedError = [
|
|
296
|
+
{
|
|
297
|
+
code: x.ERROR_CODE.invalidRange,
|
|
298
|
+
schema: schema,
|
|
299
|
+
subject: subject,
|
|
300
|
+
path: [],
|
|
301
|
+
},
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
const parsedSchema = x.parse(schema, subject)
|
|
305
|
+
const parsedConstruct = construct.parse(subject)
|
|
306
|
+
const parsedStruct = struct.parse(subject)
|
|
307
|
+
|
|
308
|
+
expect(parsedSchema.error).toStrictEqual(expectedError)
|
|
309
|
+
expect(parsedConstruct.error).toStrictEqual(expectedError)
|
|
310
|
+
expect(parsedStruct.error).toStrictEqual(expectedError)
|
|
311
|
+
|
|
312
|
+
const parsedStandard = struct['~standard'].validate(subject)
|
|
313
|
+
|
|
314
|
+
if (parsedStandard instanceof Promise) {
|
|
315
|
+
throw Error('Not expected')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
expect(parsedStandard.issues).toStrictEqual([
|
|
319
|
+
{ message: x.ERROR_CODE.invalidRange, path: [] },
|
|
320
|
+
])
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
# Fold E: Nested Schema Error Path Tracking
|
|
327
|
+
|
|
328
|
+
Purpose**: Tests that error reporting correctly tracks the path to invalid data in nested structures, ensuring the `path`, `schema`, and `subject` properties in errors point to the exact location and nature of validation failures.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
it('InvalidSubject error of nested schema should have correct path/schema/subject', () => {
|
|
332
|
+
const schema = {
|
|
333
|
+
type: 'array',
|
|
334
|
+
of: { type: 'array', of: { type: 'boolean' } },
|
|
335
|
+
} as const satisfies x.Schema
|
|
336
|
+
|
|
337
|
+
const struct = x.array(x.array(x.boolean()))
|
|
338
|
+
const samples: Array<
|
|
339
|
+
[
|
|
340
|
+
subj: unknown[],
|
|
341
|
+
invalidSubj: unknown,
|
|
342
|
+
invalidSubjSchema: x.Schema,
|
|
343
|
+
errorPath: x.ErrorPath,
|
|
344
|
+
]
|
|
345
|
+
> = [
|
|
346
|
+
[[[null, false, true]], null, schema.of.of, [0, 0]],
|
|
347
|
+
[[[], [null, false, true]], null, schema.of.of, [1, 0]],
|
|
348
|
+
[[[], [], [null, false, true]], null, schema.of.of, [2, 0]],
|
|
349
|
+
[[[true, 'str', true]], 'str', schema.of.of, [0, 1]],
|
|
350
|
+
[[[], [true, 'str', true]], 'str', schema.of.of, [1, 1]],
|
|
351
|
+
[[[], [], [true, 'str', true]], 'str', schema.of.of, [2, 1]],
|
|
352
|
+
[[[true, false, 69]], 69, schema.of.of, [0, 2]],
|
|
353
|
+
[[[], [true, false, 69]], 69, schema.of.of, [1, 2]],
|
|
354
|
+
[[[], [], [true, false, 69]], 69, schema.of.of, [2, 2]],
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
foldE: {
|
|
358
|
+
const construct = x.makeStruct(schema)
|
|
359
|
+
|
|
360
|
+
for (const [subject, invalidSubj, invalidSubjSchema, path] of samples) {
|
|
361
|
+
const expectedError = [
|
|
362
|
+
{
|
|
363
|
+
path,
|
|
364
|
+
code: x.ERROR_CODE.invalidType,
|
|
365
|
+
schema: invalidSubjSchema,
|
|
366
|
+
subject: invalidSubj,
|
|
367
|
+
},
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
const parsedSchema = x.parse(schema, subject)
|
|
371
|
+
const parsedConstruct = construct.parse(subject)
|
|
372
|
+
const parsedStruct = struct.parse(subject)
|
|
373
|
+
|
|
374
|
+
expect(parsedSchema.error).toStrictEqual(expectedError)
|
|
375
|
+
expect(parsedConstruct.error).toStrictEqual(expectedError)
|
|
376
|
+
expect(parsedStruct.error).toStrictEqual(expectedError)
|
|
377
|
+
|
|
378
|
+
const parsedStandard = struct['~standard'].validate(subject)
|
|
379
|
+
|
|
380
|
+
if (parsedStandard instanceof Promise) {
|
|
381
|
+
throw Error('Not expected')
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
expect(parsedStandard.issues).toStrictEqual([
|
|
385
|
+
{ path, message: x.ERROR_CODE.invalidType },
|
|
386
|
+
])
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
```
|