@atproto/lex-schema 0.0.2 → 0.0.3
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/CHANGELOG.md +68 -0
- package/dist/core/$type.d.ts +6 -3
- package/dist/core/$type.d.ts.map +1 -1
- package/dist/core/$type.js +1 -0
- package/dist/core/$type.js.map +1 -1
- package/dist/core/record-key.d.ts +3 -3
- package/dist/core/record-key.d.ts.map +1 -1
- package/dist/core/record-key.js +12 -6
- package/dist/core/record-key.js.map +1 -1
- package/dist/core/result.d.ts.map +1 -1
- package/dist/core/result.js +6 -0
- package/dist/core/result.js.map +1 -1
- package/dist/core/string-format.d.ts +30 -27
- package/dist/core/string-format.d.ts.map +1 -1
- package/dist/core/string-format.js +56 -42
- package/dist/core/string-format.js.map +1 -1
- package/dist/core/types.d.ts +9 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/external.d.ts +31 -28
- package/dist/external.d.ts.map +1 -1
- package/dist/external.js +33 -17
- package/dist/external.js.map +1 -1
- package/dist/schema/_parameters.d.ts +2 -2
- package/dist/schema/_parameters.d.ts.map +1 -1
- package/dist/schema/array.d.ts +5 -6
- package/dist/schema/array.d.ts.map +1 -1
- package/dist/schema/array.js +5 -6
- package/dist/schema/array.js.map +1 -1
- package/dist/schema/blob.d.ts +2 -3
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +1 -2
- package/dist/schema/blob.js.map +1 -1
- package/dist/schema/boolean.d.ts +4 -5
- package/dist/schema/boolean.d.ts.map +1 -1
- package/dist/schema/boolean.js +2 -3
- package/dist/schema/boolean.js.map +1 -1
- package/dist/schema/bytes.d.ts +3 -4
- package/dist/schema/bytes.d.ts.map +1 -1
- package/dist/schema/bytes.js +2 -3
- package/dist/schema/bytes.js.map +1 -1
- package/dist/schema/cid.d.ts +13 -6
- package/dist/schema/cid.d.ts.map +1 -1
- package/dist/schema/cid.js +2 -4
- package/dist/schema/cid.js.map +1 -1
- package/dist/schema/custom.d.ts +3 -4
- package/dist/schema/custom.d.ts.map +1 -1
- package/dist/schema/custom.js +4 -3
- package/dist/schema/custom.js.map +1 -1
- package/dist/schema/dict.d.ts +3 -3
- package/dist/schema/dict.d.ts.map +1 -1
- package/dist/schema/dict.js +1 -1
- package/dist/schema/dict.js.map +1 -1
- package/dist/schema/discriminated-union.d.ts +15 -24
- package/dist/schema/discriminated-union.d.ts.map +1 -1
- package/dist/schema/discriminated-union.js +40 -64
- package/dist/schema/discriminated-union.js.map +1 -1
- package/dist/schema/enum.d.ts +8 -4
- package/dist/schema/enum.d.ts.map +1 -1
- package/dist/schema/enum.js +5 -3
- package/dist/schema/enum.js.map +1 -1
- package/dist/schema/integer.d.ts +3 -4
- package/dist/schema/integer.d.ts.map +1 -1
- package/dist/schema/integer.js +3 -4
- package/dist/schema/integer.js.map +1 -1
- package/dist/schema/intersection.d.ts +22 -14
- package/dist/schema/intersection.d.ts.map +1 -1
- package/dist/schema/intersection.js +12 -22
- package/dist/schema/intersection.js.map +1 -1
- package/dist/schema/literal.d.ts +7 -3
- package/dist/schema/literal.d.ts.map +1 -1
- package/dist/schema/literal.js +5 -3
- package/dist/schema/literal.js.map +1 -1
- package/dist/schema/never.d.ts +2 -2
- package/dist/schema/never.d.ts.map +1 -1
- package/dist/schema/never.js +1 -1
- package/dist/schema/never.js.map +1 -1
- package/dist/schema/null.d.ts +2 -3
- package/dist/schema/null.d.ts.map +1 -1
- package/dist/schema/null.js +1 -2
- package/dist/schema/null.js.map +1 -1
- package/dist/schema/nullable.d.ts +7 -0
- package/dist/schema/nullable.d.ts.map +1 -0
- package/dist/schema/nullable.js +19 -0
- package/dist/schema/nullable.js.map +1 -0
- package/dist/schema/object.d.ts +10 -44
- package/dist/schema/object.d.ts.map +1 -1
- package/dist/schema/object.js +10 -46
- package/dist/schema/object.js.map +1 -1
- package/dist/schema/optional.d.ts +7 -0
- package/dist/schema/optional.d.ts.map +1 -0
- package/dist/schema/optional.js +25 -0
- package/dist/schema/optional.js.map +1 -0
- package/dist/schema/params.d.ts +14 -19
- package/dist/schema/params.d.ts.map +1 -1
- package/dist/schema/params.js +10 -24
- package/dist/schema/params.js.map +1 -1
- package/dist/schema/payload.d.ts +4 -4
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/permission-set.d.ts +6 -6
- package/dist/schema/permission-set.d.ts.map +1 -1
- package/dist/schema/permission-set.js +1 -2
- package/dist/schema/permission-set.js.map +1 -1
- package/dist/schema/permission.d.ts +0 -1
- package/dist/schema/permission.d.ts.map +1 -1
- package/dist/schema/permission.js +0 -1
- package/dist/schema/permission.js.map +1 -1
- package/dist/schema/procedure.d.ts +8 -9
- package/dist/schema/procedure.d.ts.map +1 -1
- package/dist/schema/procedure.js +0 -1
- package/dist/schema/procedure.js.map +1 -1
- package/dist/schema/query.d.ts +7 -8
- package/dist/schema/query.d.ts.map +1 -1
- package/dist/schema/query.js +0 -1
- package/dist/schema/query.js.map +1 -1
- package/dist/schema/record.d.ts +34 -28
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +1 -2
- package/dist/schema/record.js.map +1 -1
- package/dist/schema/ref.d.ts +2 -3
- package/dist/schema/ref.d.ts.map +1 -1
- package/dist/schema/ref.js +1 -2
- package/dist/schema/ref.js.map +1 -1
- package/dist/schema/refine.d.ts +18 -0
- package/dist/schema/refine.d.ts.map +1 -0
- package/dist/schema/refine.js +33 -0
- package/dist/schema/refine.js.map +1 -0
- package/dist/schema/regexp.d.ts +7 -0
- package/dist/schema/regexp.d.ts.map +1 -0
- package/dist/schema/regexp.js +22 -0
- package/dist/schema/regexp.js.map +1 -0
- package/dist/schema/string.d.ts +4 -8
- package/dist/schema/string.d.ts.map +1 -1
- package/dist/schema/string.js +6 -3
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/subscription.d.ts +7 -6
- package/dist/schema/subscription.d.ts.map +1 -1
- package/dist/schema/subscription.js.map +1 -1
- package/dist/schema/token.d.ts +2 -3
- package/dist/schema/token.d.ts.map +1 -1
- package/dist/schema/token.js +1 -2
- package/dist/schema/token.js.map +1 -1
- package/dist/schema/typed-object.d.ts +29 -27
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +1 -2
- package/dist/schema/typed-object.js.map +1 -1
- package/dist/schema/typed-ref.d.ts +2 -2
- package/dist/schema/typed-ref.d.ts.map +1 -1
- package/dist/schema/typed-ref.js +1 -1
- package/dist/schema/typed-ref.js.map +1 -1
- package/dist/schema/typed-union.d.ts +3 -4
- package/dist/schema/typed-union.d.ts.map +1 -1
- package/dist/schema/typed-union.js +3 -10
- package/dist/schema/typed-union.js.map +1 -1
- package/dist/schema/union.d.ts +2 -2
- package/dist/schema/union.d.ts.map +1 -1
- package/dist/schema/union.js +1 -1
- package/dist/schema/union.js.map +1 -1
- package/dist/schema/unknown-object.d.ts +2 -3
- package/dist/schema/unknown-object.d.ts.map +1 -1
- package/dist/schema/unknown-object.js +1 -2
- package/dist/schema/unknown-object.js.map +1 -1
- package/dist/schema/unknown.d.ts +2 -2
- package/dist/schema/unknown.d.ts.map +1 -1
- package/dist/schema/unknown.js +1 -1
- package/dist/schema/unknown.js.map +1 -1
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -1
- package/dist/schema.js.map +1 -1
- package/dist/util/array-agg.d.ts.map +1 -1
- package/dist/util/array-agg.js +1 -0
- package/dist/util/array-agg.js.map +1 -1
- package/dist/util/lazy-property.d.ts +2 -0
- package/dist/util/lazy-property.d.ts.map +1 -0
- package/dist/util/lazy-property.js +14 -0
- package/dist/util/lazy-property.js.map +1 -0
- package/dist/validation/schema.d.ts +24 -0
- package/dist/validation/schema.d.ts.map +1 -0
- package/dist/validation/schema.js +57 -0
- package/dist/validation/schema.js.map +1 -0
- package/dist/validation/validation-error.d.ts +3 -3
- package/dist/validation/validation-error.d.ts.map +1 -1
- package/dist/validation/validation-error.js +32 -4
- package/dist/validation/validation-error.js.map +1 -1
- package/dist/validation/validation-issue.d.ts +32 -24
- package/dist/validation/validation-issue.d.ts.map +1 -1
- package/dist/validation/validation-issue.js +136 -92
- package/dist/validation/validation-issue.js.map +1 -1
- package/dist/validation/validator.d.ts +20 -50
- package/dist/validation/validator.d.ts.map +1 -1
- package/dist/validation/validator.js +40 -134
- package/dist/validation/validator.js.map +1 -1
- package/dist/validation.d.ts +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -0
- package/dist/validation.js.map +1 -1
- package/package.json +8 -4
- package/src/core/$type.ts +7 -4
- package/src/core/record-key.ts +12 -5
- package/src/core/result.ts +6 -0
- package/src/core/string-format.ts +97 -61
- package/src/core/types.ts +12 -6
- package/src/external.ts +92 -70
- package/src/schema/_parameters.test.ts +416 -0
- package/src/schema/array.test.ts +237 -0
- package/src/schema/array.ts +17 -11
- package/src/schema/blob.test.ts +506 -0
- package/src/schema/blob.ts +3 -5
- package/src/schema/boolean.test.ts +116 -0
- package/src/schema/boolean.ts +5 -7
- package/src/schema/bytes.test.ts +226 -0
- package/src/schema/bytes.ts +4 -6
- package/src/schema/cid.test.ts +155 -0
- package/src/schema/cid.ts +14 -8
- package/src/schema/custom.test.ts +413 -0
- package/src/schema/custom.ts +10 -8
- package/src/schema/dict.test.ts +198 -0
- package/src/schema/dict.ts +6 -8
- package/src/schema/discriminated-union.test.ts +675 -0
- package/src/schema/discriminated-union.ts +68 -95
- package/src/schema/enum.test.ts +396 -0
- package/src/schema/enum.ts +12 -5
- package/src/schema/integer.test.ts +312 -0
- package/src/schema/integer.ts +5 -7
- package/src/schema/intersection.test.ts +32 -0
- package/src/schema/intersection.ts +37 -40
- package/src/schema/literal.test.ts +531 -0
- package/src/schema/literal.ts +12 -5
- package/src/schema/never.test.ts +174 -0
- package/src/schema/never.ts +3 -10
- package/src/schema/null.test.ts +79 -0
- package/src/schema/null.ts +3 -5
- package/src/schema/nullable.test.ts +480 -0
- package/src/schema/nullable.ts +23 -0
- package/src/schema/object.test.ts +47 -115
- package/src/schema/object.ts +19 -123
- package/src/schema/optional.test.ts +485 -0
- package/src/schema/optional.ts +31 -0
- package/src/schema/params.test.ts +582 -0
- package/src/schema/params.ts +37 -55
- package/src/schema/payload.test.ts +345 -0
- package/src/schema/payload.ts +5 -5
- package/src/schema/permission-set.test.ts +679 -0
- package/src/schema/permission-set.ts +6 -8
- package/src/schema/permission.test.ts +536 -0
- package/src/schema/permission.ts +0 -2
- package/src/schema/procedure.test.ts +443 -0
- package/src/schema/procedure.ts +11 -13
- package/src/schema/query.test.ts +408 -0
- package/src/schema/query.ts +9 -11
- package/src/schema/record.test.ts +694 -0
- package/src/schema/record.ts +38 -36
- package/src/schema/ref.test.ts +365 -0
- package/src/schema/ref.ts +8 -5
- package/src/schema/refine.test.ts +578 -0
- package/src/schema/refine.ts +85 -0
- package/src/schema/regexp.test.ts +580 -0
- package/src/schema/regexp.ts +22 -0
- package/src/schema/string.test.ts +612 -0
- package/src/schema/string.ts +11 -17
- package/src/schema/subscription.test.ts +689 -0
- package/src/schema/subscription.ts +13 -8
- package/src/schema/token.test.ts +428 -0
- package/src/schema/token.ts +3 -5
- package/src/schema/typed-object.test.ts +612 -0
- package/src/schema/typed-object.ts +23 -20
- package/src/schema/typed-ref.test.ts +823 -0
- package/src/schema/typed-ref.ts +10 -5
- package/src/schema/typed-union.test.ts +378 -0
- package/src/schema/typed-union.ts +6 -15
- package/src/schema/union.test.ts +200 -0
- package/src/schema/union.ts +5 -4
- package/src/schema/unknown-object.test.ts +592 -0
- package/src/schema/unknown-object.ts +3 -5
- package/src/schema/unknown.test.ts +312 -0
- package/src/schema/unknown.ts +3 -3
- package/src/schema.ts +7 -1
- package/src/util/array-agg.ts +1 -0
- package/src/util/lazy-property.ts +14 -0
- package/src/validation/schema.ts +92 -0
- package/src/validation/validation-error.ts +60 -9
- package/src/validation/validation-issue.ts +141 -144
- package/src/validation/validator.ts +67 -206
- package/src/validation.ts +1 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tests.json +9 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { IntegerSchema } from './integer.js'
|
|
2
|
+
import { ObjectSchema } from './object.js'
|
|
3
|
+
import { refine } from './refine.js'
|
|
4
|
+
import { StringSchema } from './string.js'
|
|
5
|
+
|
|
6
|
+
describe('refine', () => {
|
|
7
|
+
describe('basic refinement checks', () => {
|
|
8
|
+
const schema = refine(new IntegerSchema({}), {
|
|
9
|
+
check: (value) => value > 0,
|
|
10
|
+
message: 'Value must be positive',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('validates values that pass the refinement check', () => {
|
|
14
|
+
const result = schema.safeParse(42)
|
|
15
|
+
expect(result.success).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects values that fail the refinement check', () => {
|
|
19
|
+
const result = schema.safeParse(-5)
|
|
20
|
+
expect(result.success).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects zero when check requires positive', () => {
|
|
24
|
+
const result = schema.safeParse(0)
|
|
25
|
+
expect(result.success).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('still validates base schema constraints', () => {
|
|
29
|
+
const result = schema.safeParse('not a number')
|
|
30
|
+
expect(result.success).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('refinement with type assertions', () => {
|
|
35
|
+
const schema = refine(new IntegerSchema({}), {
|
|
36
|
+
check: (value): value is 42 => value === 42,
|
|
37
|
+
message: 'Value must be 42',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('validates values that pass the type assertion', () => {
|
|
41
|
+
const result = schema.safeParse(42)
|
|
42
|
+
expect(result.success).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('rejects values that fail the type assertion', () => {
|
|
46
|
+
const result = schema.safeParse(43)
|
|
47
|
+
expect(result.success).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('refinement with string schema', () => {
|
|
52
|
+
const schema = refine(new StringSchema({}), {
|
|
53
|
+
check: (value) => value.includes('@'),
|
|
54
|
+
message: 'String must contain @ symbol',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('validates strings that pass the refinement check', () => {
|
|
58
|
+
const result = schema.safeParse('user@example.com')
|
|
59
|
+
expect(result.success).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('rejects strings that fail the refinement check', () => {
|
|
63
|
+
const result = schema.safeParse('userexample.com')
|
|
64
|
+
expect(result.success).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('rejects non-strings before refinement check', () => {
|
|
68
|
+
const result = schema.safeParse(123)
|
|
69
|
+
expect(result.success).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('refinement with base schema constraints', () => {
|
|
74
|
+
const schema = refine(new IntegerSchema({ minimum: 0, maximum: 100 }), {
|
|
75
|
+
check: (value) => value % 2 === 0,
|
|
76
|
+
message: 'Value must be even',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('validates values that pass both base constraints and refinement', () => {
|
|
80
|
+
const result = schema.safeParse(42)
|
|
81
|
+
expect(result.success).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('rejects values that fail base constraints', () => {
|
|
85
|
+
const result = schema.safeParse(150)
|
|
86
|
+
expect(result.success).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('rejects values that pass base constraints but fail refinement', () => {
|
|
90
|
+
const result = schema.safeParse(43)
|
|
91
|
+
expect(result.success).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('rejects values that fail both base constraints and refinement', () => {
|
|
95
|
+
const result = schema.safeParse(-5)
|
|
96
|
+
expect(result.success).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('multiple refinements chained', () => {
|
|
101
|
+
const schema = refine(
|
|
102
|
+
refine(new IntegerSchema({}), {
|
|
103
|
+
check: (value) => value > 0,
|
|
104
|
+
message: 'Value must be positive',
|
|
105
|
+
}),
|
|
106
|
+
{
|
|
107
|
+
check: (value) => value < 100,
|
|
108
|
+
message: 'Value must be less than 100',
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
it('validates values that pass all refinements', () => {
|
|
113
|
+
const result = schema.safeParse(42)
|
|
114
|
+
expect(result.success).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('rejects values that fail first refinement', () => {
|
|
118
|
+
const result = schema.safeParse(-5)
|
|
119
|
+
expect(result.success).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('rejects values that fail second refinement', () => {
|
|
123
|
+
const result = schema.safeParse(150)
|
|
124
|
+
expect(result.success).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('accepts values at the boundary of both refinements', () => {
|
|
128
|
+
const result1 = schema.safeParse(1)
|
|
129
|
+
expect(result1.success).toBe(true)
|
|
130
|
+
|
|
131
|
+
const result2 = schema.safeParse(99)
|
|
132
|
+
expect(result2.success).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('refinement with custom path', () => {
|
|
137
|
+
const schema = refine(new IntegerSchema({}), {
|
|
138
|
+
check: (value) => value > 0,
|
|
139
|
+
message: 'Value must be positive',
|
|
140
|
+
path: 'customField',
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('validates values that pass the refinement', () => {
|
|
144
|
+
const result = schema.safeParse(42)
|
|
145
|
+
expect(result.success).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects values that fail the refinement', () => {
|
|
149
|
+
const result = schema.safeParse(-5)
|
|
150
|
+
expect(result.success).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('refinement with array path', () => {
|
|
155
|
+
const schema = refine(new IntegerSchema({}), {
|
|
156
|
+
check: (value) => value > 0,
|
|
157
|
+
message: 'Value must be positive',
|
|
158
|
+
path: ['nested', 'field'],
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('validates values that pass the refinement', () => {
|
|
162
|
+
const result = schema.safeParse(42)
|
|
163
|
+
expect(result.success).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('rejects values that fail the refinement', () => {
|
|
167
|
+
const result = schema.safeParse(-5)
|
|
168
|
+
expect(result.success).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('refinement on object properties', () => {
|
|
173
|
+
const schema = new ObjectSchema({
|
|
174
|
+
age: refine(new IntegerSchema({}), {
|
|
175
|
+
check: (value) => value >= 18,
|
|
176
|
+
message: 'Age must be at least 18',
|
|
177
|
+
}),
|
|
178
|
+
email: refine(new StringSchema({}), {
|
|
179
|
+
check: (value) => value.includes('@'),
|
|
180
|
+
message: 'Email must contain @ symbol',
|
|
181
|
+
}),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('validates objects with properties that pass refinements', () => {
|
|
185
|
+
const result = schema.safeParse({
|
|
186
|
+
age: 25,
|
|
187
|
+
email: 'user@example.com',
|
|
188
|
+
})
|
|
189
|
+
expect(result.success).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('rejects objects with age below minimum', () => {
|
|
193
|
+
const result = schema.safeParse({
|
|
194
|
+
age: 16,
|
|
195
|
+
email: 'user@example.com',
|
|
196
|
+
})
|
|
197
|
+
expect(result.success).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('rejects objects with invalid email', () => {
|
|
201
|
+
const result = schema.safeParse({
|
|
202
|
+
age: 25,
|
|
203
|
+
email: 'userexample.com',
|
|
204
|
+
})
|
|
205
|
+
expect(result.success).toBe(false)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('rejects objects with both properties failing refinements', () => {
|
|
209
|
+
const result = schema.safeParse({
|
|
210
|
+
age: 16,
|
|
211
|
+
email: 'userexample.com',
|
|
212
|
+
})
|
|
213
|
+
expect(result.success).toBe(false)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('complex refinement logic', () => {
|
|
218
|
+
const schema = refine(new StringSchema({}), {
|
|
219
|
+
check: (value) => {
|
|
220
|
+
const hasLowerCase = /[a-z]/.test(value)
|
|
221
|
+
const hasUpperCase = /[A-Z]/.test(value)
|
|
222
|
+
const hasNumber = /[0-9]/.test(value)
|
|
223
|
+
return hasLowerCase && hasUpperCase && hasNumber
|
|
224
|
+
},
|
|
225
|
+
message: 'Password must contain lowercase, uppercase, and numbers',
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('validates strings that meet all password requirements', () => {
|
|
229
|
+
const result = schema.safeParse('Password123')
|
|
230
|
+
expect(result.success).toBe(true)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('rejects strings without lowercase', () => {
|
|
234
|
+
const result = schema.safeParse('PASSWORD123')
|
|
235
|
+
expect(result.success).toBe(false)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('rejects strings without uppercase', () => {
|
|
239
|
+
const result = schema.safeParse('password123')
|
|
240
|
+
expect(result.success).toBe(false)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('rejects strings without numbers', () => {
|
|
244
|
+
const result = schema.safeParse('Password')
|
|
245
|
+
expect(result.success).toBe(false)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('rejects empty strings', () => {
|
|
249
|
+
const result = schema.safeParse('')
|
|
250
|
+
expect(result.success).toBe(false)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('refinement with comparison logic', () => {
|
|
255
|
+
const schema = refine(new IntegerSchema({}), {
|
|
256
|
+
check: (value) => {
|
|
257
|
+
// Check if value is a prime number
|
|
258
|
+
if (value <= 1) return false
|
|
259
|
+
if (value <= 3) return true
|
|
260
|
+
if (value % 2 === 0 || value % 3 === 0) return false
|
|
261
|
+
for (let i = 5; i * i <= value; i += 6) {
|
|
262
|
+
if (value % i === 0 || value % (i + 2) === 0) return false
|
|
263
|
+
}
|
|
264
|
+
return true
|
|
265
|
+
},
|
|
266
|
+
message: 'Value must be a prime number',
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('validates prime numbers', () => {
|
|
270
|
+
const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
|
|
271
|
+
primes.forEach((prime) => {
|
|
272
|
+
const result = schema.safeParse(prime)
|
|
273
|
+
expect(result.success).toBe(true)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('rejects non-prime numbers', () => {
|
|
278
|
+
const nonPrimes = [0, 1, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20]
|
|
279
|
+
nonPrimes.forEach((nonPrime) => {
|
|
280
|
+
const result = schema.safeParse(nonPrime)
|
|
281
|
+
expect(result.success).toBe(false)
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('refinement with string length logic', () => {
|
|
287
|
+
const schema = refine(new StringSchema({ minLength: 1, maxLength: 50 }), {
|
|
288
|
+
check: (value) => value.trim().length > 0,
|
|
289
|
+
message: 'String must not be only whitespace',
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('validates non-empty trimmed strings', () => {
|
|
293
|
+
const result = schema.safeParse('hello world')
|
|
294
|
+
expect(result.success).toBe(true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('validates strings with leading/trailing whitespace', () => {
|
|
298
|
+
const result = schema.safeParse(' hello ')
|
|
299
|
+
expect(result.success).toBe(true)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('rejects strings with only whitespace', () => {
|
|
303
|
+
const result = schema.safeParse(' ')
|
|
304
|
+
expect(result.success).toBe(false)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('rejects single space', () => {
|
|
308
|
+
const result = schema.safeParse(' ')
|
|
309
|
+
expect(result.success).toBe(false)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('rejects tabs and newlines only', () => {
|
|
313
|
+
const result = schema.safeParse('\t\n ')
|
|
314
|
+
expect(result.success).toBe(false)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('refinement preserves original schema', () => {
|
|
319
|
+
const originalSchema = new IntegerSchema({ minimum: 0 })
|
|
320
|
+
const refinedSchema = refine(originalSchema, {
|
|
321
|
+
check: (value) => value % 2 === 0,
|
|
322
|
+
message: 'Value must be even',
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('original schema still works independently', () => {
|
|
326
|
+
const result = originalSchema.safeParse(5)
|
|
327
|
+
expect(result.success).toBe(true)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('refined schema has additional constraint', () => {
|
|
331
|
+
const result = refinedSchema.safeParse(5)
|
|
332
|
+
expect(result.success).toBe(false)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('refined schema inherits base constraints', () => {
|
|
336
|
+
const result1 = refinedSchema.safeParse(-2)
|
|
337
|
+
expect(result1.success).toBe(false)
|
|
338
|
+
|
|
339
|
+
const result2 = refinedSchema.safeParse(4)
|
|
340
|
+
expect(result2.success).toBe(true)
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe('refinement with boundary conditions', () => {
|
|
345
|
+
const schema = refine(new IntegerSchema({ minimum: 0, maximum: 100 }), {
|
|
346
|
+
check: (value) => value !== 50,
|
|
347
|
+
message: 'Value must not be 50',
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('validates values at lower boundary', () => {
|
|
351
|
+
const result = schema.safeParse(0)
|
|
352
|
+
expect(result.success).toBe(true)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('validates values at upper boundary', () => {
|
|
356
|
+
const result = schema.safeParse(100)
|
|
357
|
+
expect(result.success).toBe(true)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('rejects the specific excluded value', () => {
|
|
361
|
+
const result = schema.safeParse(50)
|
|
362
|
+
expect(result.success).toBe(false)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('validates values around the excluded value', () => {
|
|
366
|
+
const result1 = schema.safeParse(49)
|
|
367
|
+
expect(result1.success).toBe(true)
|
|
368
|
+
|
|
369
|
+
const result2 = schema.safeParse(51)
|
|
370
|
+
expect(result2.success).toBe(true)
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe('refinement with regex patterns', () => {
|
|
375
|
+
const schema = refine(new StringSchema({}), {
|
|
376
|
+
check: (value) => /^[A-Z][a-zA-Z0-9]*$/.test(value),
|
|
377
|
+
message: 'Must start with uppercase letter',
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('validates strings starting with uppercase', () => {
|
|
381
|
+
const result = schema.safeParse('Hello123')
|
|
382
|
+
expect(result.success).toBe(true)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('rejects strings starting with lowercase', () => {
|
|
386
|
+
const result = schema.safeParse('hello123')
|
|
387
|
+
expect(result.success).toBe(false)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('rejects strings starting with number', () => {
|
|
391
|
+
const result = schema.safeParse('123Hello')
|
|
392
|
+
expect(result.success).toBe(false)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('rejects strings starting with special character', () => {
|
|
396
|
+
const result = schema.safeParse('_Hello')
|
|
397
|
+
expect(result.success).toBe(false)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('validates single uppercase letter', () => {
|
|
401
|
+
const result = schema.safeParse('A')
|
|
402
|
+
expect(result.success).toBe(true)
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
describe('refinement with custom error messages', () => {
|
|
407
|
+
const schema = refine(new IntegerSchema({}), {
|
|
408
|
+
check: (value) => value >= 1 && value <= 10,
|
|
409
|
+
message: 'Value must be between 1 and 10 (inclusive)',
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('validates values within range', () => {
|
|
413
|
+
const result = schema.safeParse(5)
|
|
414
|
+
expect(result.success).toBe(true)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('rejects values outside range', () => {
|
|
418
|
+
const result = schema.safeParse(11)
|
|
419
|
+
expect(result.success).toBe(false)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('rejects zero', () => {
|
|
423
|
+
const result = schema.safeParse(0)
|
|
424
|
+
expect(result.success).toBe(false)
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
describe('edge cases', () => {
|
|
429
|
+
it('handles refinement that always returns true', () => {
|
|
430
|
+
const schema = refine(new IntegerSchema({}), {
|
|
431
|
+
check: () => true,
|
|
432
|
+
message: 'This should never fail',
|
|
433
|
+
})
|
|
434
|
+
const result = schema.safeParse(42)
|
|
435
|
+
expect(result.success).toBe(true)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('handles refinement that always returns false', () => {
|
|
439
|
+
const schema = refine(new IntegerSchema({}), {
|
|
440
|
+
check: () => false,
|
|
441
|
+
message: 'This always fails',
|
|
442
|
+
})
|
|
443
|
+
const result = schema.safeParse(42)
|
|
444
|
+
expect(result.success).toBe(false)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('handles empty string refinement', () => {
|
|
448
|
+
const schema = refine(new StringSchema({}), {
|
|
449
|
+
check: (value) => value === '',
|
|
450
|
+
message: 'Value must be empty string',
|
|
451
|
+
})
|
|
452
|
+
const result1 = schema.safeParse('')
|
|
453
|
+
expect(result1.success).toBe(true)
|
|
454
|
+
|
|
455
|
+
const result2 = schema.safeParse('hello')
|
|
456
|
+
expect(result2.success).toBe(false)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('handles zero value refinement', () => {
|
|
460
|
+
const schema = refine(new IntegerSchema({}), {
|
|
461
|
+
check: (value) => value === 0,
|
|
462
|
+
message: 'Value must be zero',
|
|
463
|
+
})
|
|
464
|
+
const result1 = schema.safeParse(0)
|
|
465
|
+
expect(result1.success).toBe(true)
|
|
466
|
+
|
|
467
|
+
const result2 = schema.safeParse(1)
|
|
468
|
+
expect(result2.success).toBe(false)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('handles negative value refinement', () => {
|
|
472
|
+
const schema = refine(new IntegerSchema({}), {
|
|
473
|
+
check: (value) => value < 0,
|
|
474
|
+
message: 'Value must be negative',
|
|
475
|
+
})
|
|
476
|
+
const result1 = schema.safeParse(-5)
|
|
477
|
+
expect(result1.success).toBe(true)
|
|
478
|
+
|
|
479
|
+
const result2 = schema.safeParse(5)
|
|
480
|
+
expect(result2.success).toBe(false)
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
describe('refinement with combined string constraints', () => {
|
|
485
|
+
const schema = refine(new StringSchema({ minLength: 8, maxLength: 20 }), {
|
|
486
|
+
check: (value) => {
|
|
487
|
+
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value)
|
|
488
|
+
const hasLetter = /[a-zA-Z]/.test(value)
|
|
489
|
+
return hasSpecialChar && hasLetter
|
|
490
|
+
},
|
|
491
|
+
message: 'Must contain letters and special characters',
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('validates strings meeting all requirements', () => {
|
|
495
|
+
const result = schema.safeParse('Hello@World!')
|
|
496
|
+
expect(result.success).toBe(true)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('rejects strings too short', () => {
|
|
500
|
+
const result = schema.safeParse('Hi@!')
|
|
501
|
+
expect(result.success).toBe(false)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('rejects strings without special characters', () => {
|
|
505
|
+
const result = schema.safeParse('HelloWorld')
|
|
506
|
+
expect(result.success).toBe(false)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('rejects strings without letters', () => {
|
|
510
|
+
const result = schema.safeParse('12345!@#$%')
|
|
511
|
+
expect(result.success).toBe(false)
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('validates strings at minimum length with requirements', () => {
|
|
515
|
+
const result = schema.safeParse('Hello@12')
|
|
516
|
+
expect(result.success).toBe(true)
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe('refinement inheritance', () => {
|
|
521
|
+
const baseSchema = new IntegerSchema({ minimum: 0, maximum: 1000 })
|
|
522
|
+
const refinedOnce = refine(baseSchema, {
|
|
523
|
+
check: (value) => value % 10 === 0,
|
|
524
|
+
message: 'Must be divisible by 10',
|
|
525
|
+
})
|
|
526
|
+
const refinedTwice = refine(refinedOnce, {
|
|
527
|
+
check: (value) => value % 100 === 0,
|
|
528
|
+
message: 'Must be divisible by 100',
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('validates with all inherited constraints', () => {
|
|
532
|
+
const result = refinedTwice.safeParse(500)
|
|
533
|
+
expect(result.success).toBe(true)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('rejects when failing base constraint', () => {
|
|
537
|
+
const result = refinedTwice.safeParse(1500)
|
|
538
|
+
expect(result.success).toBe(false)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('rejects when failing first refinement', () => {
|
|
542
|
+
const result = refinedTwice.safeParse(505)
|
|
543
|
+
expect(result.success).toBe(false)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('rejects when failing second refinement', () => {
|
|
547
|
+
const result = refinedTwice.safeParse(50)
|
|
548
|
+
expect(result.success).toBe(false)
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
describe('refinement with string format validation', () => {
|
|
553
|
+
const schema = refine(new StringSchema({ format: 'uri' }), {
|
|
554
|
+
check: (value) => value.startsWith('https://'),
|
|
555
|
+
message: 'Must be HTTPS URI',
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('validates HTTPS URIs', () => {
|
|
559
|
+
const result = schema.safeParse('https://example.com')
|
|
560
|
+
expect(result.success).toBe(true)
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('rejects HTTP URIs', () => {
|
|
564
|
+
const result = schema.safeParse('http://example.com')
|
|
565
|
+
expect(result.success).toBe(false)
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('rejects other valid URIs', () => {
|
|
569
|
+
const result = schema.safeParse('ftp://example.com')
|
|
570
|
+
expect(result.success).toBe(false)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('rejects invalid URIs', () => {
|
|
574
|
+
const result = schema.safeParse('not a uri')
|
|
575
|
+
expect(result.success).toBe(false)
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Infer,
|
|
3
|
+
IssueCustom,
|
|
4
|
+
PropertyKey,
|
|
5
|
+
ValidationResult,
|
|
6
|
+
Validator,
|
|
7
|
+
ValidatorContext,
|
|
8
|
+
} from '../validation.js'
|
|
9
|
+
import { CustomAssertionContext } from './custom.js'
|
|
10
|
+
|
|
11
|
+
export type RefinementCheck<T> = {
|
|
12
|
+
check: (value: T, ctx: CustomAssertionContext) => boolean
|
|
13
|
+
message: string
|
|
14
|
+
path?: PropertyKey | readonly PropertyKey[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RefinementAssertion<T, Out extends T> = {
|
|
18
|
+
check: (this: null, value: T, ctx: CustomAssertionContext) => value is Out
|
|
19
|
+
message: string
|
|
20
|
+
path?: PropertyKey | readonly PropertyKey[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type InferRefinement<R> =
|
|
24
|
+
R extends RefinementCheck<infer T>
|
|
25
|
+
? T
|
|
26
|
+
: R extends RefinementAssertion<infer T, any>
|
|
27
|
+
? T
|
|
28
|
+
: never
|
|
29
|
+
|
|
30
|
+
export type Refinement<T = any, Out extends T = T> =
|
|
31
|
+
| RefinementCheck<T>
|
|
32
|
+
| RefinementAssertion<T, Out>
|
|
33
|
+
|
|
34
|
+
export function refine<S extends Validator, Out extends Infer<S>>(
|
|
35
|
+
schema: S,
|
|
36
|
+
refinement: RefinementAssertion<Infer<S>, Out>,
|
|
37
|
+
): S & Validator<Out>
|
|
38
|
+
export function refine<S extends Validator>(
|
|
39
|
+
schema: S,
|
|
40
|
+
refinement: RefinementCheck<Infer<S>>,
|
|
41
|
+
): S
|
|
42
|
+
export function refine<
|
|
43
|
+
R extends Refinement,
|
|
44
|
+
S extends Validator<InferRefinement<R>>,
|
|
45
|
+
>(schema: S, refinement: R): S
|
|
46
|
+
/*@__NO_SIDE_EFFECTS__*/
|
|
47
|
+
export function refine<S extends Validator>(
|
|
48
|
+
schema: S,
|
|
49
|
+
refinement: Refinement<Infer<S>>,
|
|
50
|
+
): S {
|
|
51
|
+
// This is basically the same as monkey patching the "validateInContext"
|
|
52
|
+
// method to the schema, but done in a way that does not mutate the original
|
|
53
|
+
// schema. This is safe to do because Validators don't update their internal
|
|
54
|
+
// state over their lifetime.
|
|
55
|
+
return Object.create(schema, {
|
|
56
|
+
validateInContext: {
|
|
57
|
+
// We do not use an arrow function to avoid creating a closure
|
|
58
|
+
value: validateInContextUnbound.bind({ schema, refinement }),
|
|
59
|
+
enumerable: false,
|
|
60
|
+
writable: false,
|
|
61
|
+
configurable: true,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/*@__NO_SIDE_EFFECTS__*/
|
|
67
|
+
function validateInContextUnbound<S extends Validator>(
|
|
68
|
+
this: {
|
|
69
|
+
schema: S
|
|
70
|
+
refinement: Refinement<Infer<S>>
|
|
71
|
+
},
|
|
72
|
+
input: unknown,
|
|
73
|
+
ctx: ValidatorContext,
|
|
74
|
+
): ValidationResult<Infer<S>> {
|
|
75
|
+
const result = ctx.validate(input, this.schema)
|
|
76
|
+
if (!result.success) return result
|
|
77
|
+
|
|
78
|
+
const checkResult = this.refinement.check.call(null, result.value, ctx)
|
|
79
|
+
if (!checkResult) {
|
|
80
|
+
const path = ctx.concatPath(this.refinement.path)
|
|
81
|
+
return ctx.failure(new IssueCustom(path, input, this.refinement.message))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
}
|