@atproto/lex-schema 0.0.1 → 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 +8 -4
- 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 +13 -56
- 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 +23 -134
- 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,612 @@
|
|
|
1
|
+
import { StringSchema } from './string.js'
|
|
2
|
+
import { TokenSchema } from './token.js'
|
|
3
|
+
|
|
4
|
+
describe('StringSchema', () => {
|
|
5
|
+
describe('basic validation', () => {
|
|
6
|
+
const schema = new StringSchema({})
|
|
7
|
+
|
|
8
|
+
it('validates plain strings', () => {
|
|
9
|
+
const result = schema.safeParse('hello world')
|
|
10
|
+
expect(result.success).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('validates empty strings', () => {
|
|
14
|
+
const result = schema.safeParse('')
|
|
15
|
+
expect(result.success).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects non-strings', () => {
|
|
19
|
+
const result = schema.safeParse(123)
|
|
20
|
+
expect(result.success).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects null', () => {
|
|
24
|
+
const result = schema.safeParse(null)
|
|
25
|
+
expect(result.success).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects undefined', () => {
|
|
29
|
+
const result = schema.safeParse(undefined)
|
|
30
|
+
expect(result.success).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('rejects booleans', () => {
|
|
34
|
+
const result = schema.safeParse(true)
|
|
35
|
+
expect(result.success).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('rejects arrays', () => {
|
|
39
|
+
const result = schema.safeParse(['hello'])
|
|
40
|
+
expect(result.success).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects plain objects', () => {
|
|
44
|
+
const result = schema.safeParse({ value: 'hello' })
|
|
45
|
+
expect(result.success).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('default values', () => {
|
|
50
|
+
it('uses default value when no input provided', () => {
|
|
51
|
+
const schema = new StringSchema({ default: 'default value' })
|
|
52
|
+
const result = schema.safeParse(undefined)
|
|
53
|
+
expect(result.success).toBe(true)
|
|
54
|
+
if (result.success) {
|
|
55
|
+
expect(result.value).toBe('default value')
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('validates default value against constraints', () => {
|
|
60
|
+
const schema = new StringSchema({ default: 'hi', minLength: 5 })
|
|
61
|
+
const result = schema.safeParse(undefined)
|
|
62
|
+
expect(result.success).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('minLength constraint', () => {
|
|
67
|
+
const schema = new StringSchema({ minLength: 5 })
|
|
68
|
+
|
|
69
|
+
it('accepts strings meeting minimum length', () => {
|
|
70
|
+
const result = schema.safeParse('hello')
|
|
71
|
+
expect(result.success).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('accepts strings exceeding minimum length', () => {
|
|
75
|
+
const result = schema.safeParse('hello world')
|
|
76
|
+
expect(result.success).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('rejects strings below minimum length', () => {
|
|
80
|
+
const result = schema.safeParse('hi')
|
|
81
|
+
expect(result.success).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('rejects empty strings when minLength is set', () => {
|
|
85
|
+
const result = schema.safeParse('')
|
|
86
|
+
expect(result.success).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('maxLength constraint', () => {
|
|
91
|
+
const schema = new StringSchema({ maxLength: 10 })
|
|
92
|
+
|
|
93
|
+
it('accepts strings meeting maximum length', () => {
|
|
94
|
+
const result = schema.safeParse('1234567890')
|
|
95
|
+
expect(result.success).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('accepts strings below maximum length', () => {
|
|
99
|
+
const result = schema.safeParse('hello')
|
|
100
|
+
expect(result.success).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rejects strings exceeding maximum length', () => {
|
|
104
|
+
const result = schema.safeParse('hello world!')
|
|
105
|
+
expect(result.success).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('accepts empty strings', () => {
|
|
109
|
+
const result = schema.safeParse('')
|
|
110
|
+
expect(result.success).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('correctly handles UTF-8 multi-byte characters', () => {
|
|
114
|
+
// Emoji takes 4 bytes in UTF-8
|
|
115
|
+
const schema = new StringSchema({ maxLength: 4 })
|
|
116
|
+
const result = schema.safeParse('😀')
|
|
117
|
+
expect(result.success).toBe(true)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('rejects when multi-byte characters exceed maxLength', () => {
|
|
121
|
+
const schema = new StringSchema({ maxLength: 3 })
|
|
122
|
+
const result = schema.safeParse('😀')
|
|
123
|
+
expect(result.success).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('combined min and max length', () => {
|
|
128
|
+
const schema = new StringSchema({ minLength: 3, maxLength: 10 })
|
|
129
|
+
|
|
130
|
+
it('accepts strings within range', () => {
|
|
131
|
+
const result = schema.safeParse('hello')
|
|
132
|
+
expect(result.success).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('accepts strings at minimum boundary', () => {
|
|
136
|
+
const result = schema.safeParse('abc')
|
|
137
|
+
expect(result.success).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('accepts strings at maximum boundary', () => {
|
|
141
|
+
const result = schema.safeParse('1234567890')
|
|
142
|
+
expect(result.success).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('rejects strings below minimum', () => {
|
|
146
|
+
const result = schema.safeParse('hi')
|
|
147
|
+
expect(result.success).toBe(false)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('rejects strings above maximum', () => {
|
|
151
|
+
const result = schema.safeParse('hello world!')
|
|
152
|
+
expect(result.success).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('minGraphemes constraint', () => {
|
|
157
|
+
const schema = new StringSchema({ minGraphemes: 3 })
|
|
158
|
+
|
|
159
|
+
it('accepts strings meeting minimum graphemes', () => {
|
|
160
|
+
const result = schema.safeParse('abc')
|
|
161
|
+
expect(result.success).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('accepts strings exceeding minimum graphemes', () => {
|
|
165
|
+
const result = schema.safeParse('hello')
|
|
166
|
+
expect(result.success).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('rejects strings below minimum graphemes', () => {
|
|
170
|
+
const result = schema.safeParse('ab')
|
|
171
|
+
expect(result.success).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('counts emoji as single graphemes', () => {
|
|
175
|
+
const result = schema.safeParse('😀😀😀')
|
|
176
|
+
expect(result.success).toBe(true)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('rejects when emoji count is below minimum', () => {
|
|
180
|
+
const result = schema.safeParse('😀😀')
|
|
181
|
+
expect(result.success).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('maxGraphemes constraint', () => {
|
|
186
|
+
const schema = new StringSchema({ maxGraphemes: 5 })
|
|
187
|
+
|
|
188
|
+
it('accepts strings meeting maximum graphemes', () => {
|
|
189
|
+
const result = schema.safeParse('hello')
|
|
190
|
+
expect(result.success).toBe(true)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('accepts strings below maximum graphemes', () => {
|
|
194
|
+
const result = schema.safeParse('hi')
|
|
195
|
+
expect(result.success).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('rejects strings exceeding maximum graphemes', () => {
|
|
199
|
+
const result = schema.safeParse('hello world')
|
|
200
|
+
expect(result.success).toBe(false)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('counts emoji as single graphemes', () => {
|
|
204
|
+
const result = schema.safeParse('😀😀😀😀😀')
|
|
205
|
+
expect(result.success).toBe(true)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('rejects when emoji count exceeds maximum', () => {
|
|
209
|
+
const result = schema.safeParse('😀😀😀😀😀😀')
|
|
210
|
+
expect(result.success).toBe(false)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('combined grapheme constraints', () => {
|
|
215
|
+
const schema = new StringSchema({ minGraphemes: 2, maxGraphemes: 5 })
|
|
216
|
+
|
|
217
|
+
it('accepts strings within grapheme range', () => {
|
|
218
|
+
const result = schema.safeParse('hello')
|
|
219
|
+
expect(result.success).toBe(true)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('accepts strings at minimum boundary', () => {
|
|
223
|
+
const result = schema.safeParse('hi')
|
|
224
|
+
expect(result.success).toBe(true)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('accepts strings at maximum boundary', () => {
|
|
228
|
+
const result = schema.safeParse('world')
|
|
229
|
+
expect(result.success).toBe(true)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('rejects strings below minimum graphemes', () => {
|
|
233
|
+
const result = schema.safeParse('a')
|
|
234
|
+
expect(result.success).toBe(false)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('rejects strings above maximum graphemes', () => {
|
|
238
|
+
const result = schema.safeParse('hello!')
|
|
239
|
+
expect(result.success).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('format: datetime', () => {
|
|
244
|
+
const schema = new StringSchema({ format: 'datetime' })
|
|
245
|
+
|
|
246
|
+
it('accepts valid ISO datetime strings', () => {
|
|
247
|
+
const result = schema.safeParse('2023-12-25T12:00:00Z')
|
|
248
|
+
expect(result.success).toBe(true)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('accepts datetime with milliseconds', () => {
|
|
252
|
+
const result = schema.safeParse('2023-12-25T12:00:00.123Z')
|
|
253
|
+
expect(result.success).toBe(true)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('rejects invalid datetime strings', () => {
|
|
257
|
+
const result = schema.safeParse('not a date')
|
|
258
|
+
expect(result.success).toBe(false)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('rejects invalid date format', () => {
|
|
262
|
+
const result = schema.safeParse('12/25/2023')
|
|
263
|
+
expect(result.success).toBe(false)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('format: uri', () => {
|
|
268
|
+
const schema = new StringSchema({ format: 'uri' })
|
|
269
|
+
|
|
270
|
+
it('accepts valid HTTP URIs', () => {
|
|
271
|
+
const result = schema.safeParse('https://example.com')
|
|
272
|
+
expect(result.success).toBe(true)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('accepts valid URIs with paths', () => {
|
|
276
|
+
const result = schema.safeParse('https://example.com/path/to/resource')
|
|
277
|
+
expect(result.success).toBe(true)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('accepts URIs with different schemes', () => {
|
|
281
|
+
const result = schema.safeParse('ftp://files.example.com')
|
|
282
|
+
expect(result.success).toBe(true)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('rejects invalid URIs', () => {
|
|
286
|
+
const result = schema.safeParse('not a uri')
|
|
287
|
+
expect(result.success).toBe(false)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('rejects URIs without scheme', () => {
|
|
291
|
+
const result = schema.safeParse('example.com')
|
|
292
|
+
expect(result.success).toBe(false)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe('format: at-uri', () => {
|
|
297
|
+
const schema = new StringSchema({ format: 'at-uri' })
|
|
298
|
+
|
|
299
|
+
it('accepts valid AT URI', () => {
|
|
300
|
+
const result = schema.safeParse(
|
|
301
|
+
'at://did:plc:abc123/app.bsky.feed.post/xyz',
|
|
302
|
+
)
|
|
303
|
+
expect(result.success).toBe(true)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('rejects invalid AT URI', () => {
|
|
307
|
+
const result = schema.safeParse('https://example.com')
|
|
308
|
+
expect(result.success).toBe(false)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('rejects plain strings', () => {
|
|
312
|
+
const result = schema.safeParse('not an at-uri')
|
|
313
|
+
expect(result.success).toBe(false)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('format: did', () => {
|
|
318
|
+
const schema = new StringSchema({ format: 'did' })
|
|
319
|
+
|
|
320
|
+
it('accepts valid DID with plc method', () => {
|
|
321
|
+
const result = schema.safeParse('did:plc:abc123')
|
|
322
|
+
expect(result.success).toBe(true)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('accepts valid DID with web method', () => {
|
|
326
|
+
const result = schema.safeParse('did:web:example.com')
|
|
327
|
+
expect(result.success).toBe(true)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('rejects invalid DID format', () => {
|
|
331
|
+
const result = schema.safeParse('not-a-did')
|
|
332
|
+
expect(result.success).toBe(false)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('rejects DID without method', () => {
|
|
336
|
+
const result = schema.safeParse('did:')
|
|
337
|
+
expect(result.success).toBe(false)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
describe('format: handle', () => {
|
|
342
|
+
const schema = new StringSchema({ format: 'handle' })
|
|
343
|
+
|
|
344
|
+
it('accepts valid handle', () => {
|
|
345
|
+
const result = schema.safeParse('user.bsky.social')
|
|
346
|
+
expect(result.success).toBe(true)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('accepts handle with subdomain', () => {
|
|
350
|
+
const result = schema.safeParse('alice.test.example.com')
|
|
351
|
+
expect(result.success).toBe(true)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('rejects invalid handle format', () => {
|
|
355
|
+
const result = schema.safeParse('invalid handle!')
|
|
356
|
+
expect(result.success).toBe(false)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('rejects handle with spaces', () => {
|
|
360
|
+
const result = schema.safeParse('user name.bsky.social')
|
|
361
|
+
expect(result.success).toBe(false)
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('format: at-identifier', () => {
|
|
366
|
+
const schema = new StringSchema({ format: 'at-identifier' })
|
|
367
|
+
|
|
368
|
+
it('accepts valid DID as at-identifier', () => {
|
|
369
|
+
const result = schema.safeParse('did:plc:abc123')
|
|
370
|
+
expect(result.success).toBe(true)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('accepts valid handle as at-identifier', () => {
|
|
374
|
+
const result = schema.safeParse('user.bsky.social')
|
|
375
|
+
expect(result.success).toBe(true)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('rejects invalid at-identifier', () => {
|
|
379
|
+
const result = schema.safeParse('invalid!')
|
|
380
|
+
expect(result.success).toBe(false)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe('format: nsid', () => {
|
|
385
|
+
const schema = new StringSchema({ format: 'nsid' })
|
|
386
|
+
|
|
387
|
+
it('accepts valid NSID', () => {
|
|
388
|
+
const result = schema.safeParse('app.bsky.feed.post')
|
|
389
|
+
expect(result.success).toBe(true)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('accepts NSID with multiple segments', () => {
|
|
393
|
+
const result = schema.safeParse('com.example.app.feature.action')
|
|
394
|
+
expect(result.success).toBe(true)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('rejects invalid NSID format', () => {
|
|
398
|
+
const result = schema.safeParse('not-an-nsid')
|
|
399
|
+
expect(result.success).toBe(false)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('rejects NSID with invalid characters', () => {
|
|
403
|
+
const result = schema.safeParse('app.bsky.feed!')
|
|
404
|
+
expect(result.success).toBe(false)
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
describe('format: cid', () => {
|
|
409
|
+
const schema = new StringSchema({ format: 'cid' })
|
|
410
|
+
|
|
411
|
+
it('accepts valid CID v1', () => {
|
|
412
|
+
const result = schema.safeParse(
|
|
413
|
+
'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
|
|
414
|
+
)
|
|
415
|
+
expect(result.success).toBe(true)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('rejects invalid CID format', () => {
|
|
419
|
+
const result = schema.safeParse('not-a-cid')
|
|
420
|
+
expect(result.success).toBe(false)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('rejects plain strings', () => {
|
|
424
|
+
const result = schema.safeParse('abc123')
|
|
425
|
+
expect(result.success).toBe(false)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
describe('format: language', () => {
|
|
430
|
+
const schema = new StringSchema({ format: 'language' })
|
|
431
|
+
|
|
432
|
+
it('accepts valid BCP 47 language code', () => {
|
|
433
|
+
const result = schema.safeParse('en')
|
|
434
|
+
expect(result.success).toBe(true)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('accepts language code with region', () => {
|
|
438
|
+
const result = schema.safeParse('en-US')
|
|
439
|
+
expect(result.success).toBe(true)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('accepts language code with script and region', () => {
|
|
443
|
+
const result = schema.safeParse('zh-Hans-CN')
|
|
444
|
+
expect(result.success).toBe(true)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('rejects invalid language code', () => {
|
|
448
|
+
const result = schema.safeParse('not valid')
|
|
449
|
+
expect(result.success).toBe(false)
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('format: tid', () => {
|
|
454
|
+
const schema = new StringSchema({ format: 'tid' })
|
|
455
|
+
|
|
456
|
+
it('accepts valid TID', () => {
|
|
457
|
+
const result = schema.safeParse('3jzfcijpj2z2a')
|
|
458
|
+
expect(result.success).toBe(true)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('rejects invalid TID format', () => {
|
|
462
|
+
const result = schema.safeParse('not-a-tid')
|
|
463
|
+
expect(result.success).toBe(false)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('rejects TID with invalid characters', () => {
|
|
467
|
+
const result = schema.safeParse('3jzfcijpj2z2!')
|
|
468
|
+
expect(result.success).toBe(false)
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
describe('format: record-key', () => {
|
|
473
|
+
const schema = new StringSchema({ format: 'record-key' })
|
|
474
|
+
|
|
475
|
+
it('accepts valid record key', () => {
|
|
476
|
+
const result = schema.safeParse('3jzfcijpj2z2a')
|
|
477
|
+
expect(result.success).toBe(true)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('accepts alphanumeric record key', () => {
|
|
481
|
+
const result = schema.safeParse('myRecordKey123')
|
|
482
|
+
expect(result.success).toBe(true)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('rejects record key with invalid characters', () => {
|
|
486
|
+
const result = schema.safeParse('invalid/key')
|
|
487
|
+
expect(result.success).toBe(false)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('rejects record key with spaces', () => {
|
|
491
|
+
const result = schema.safeParse('invalid key')
|
|
492
|
+
expect(result.success).toBe(false)
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
describe('type coercion', () => {
|
|
497
|
+
const schema = new StringSchema({})
|
|
498
|
+
|
|
499
|
+
it('coerces Date objects to ISO strings', () => {
|
|
500
|
+
const date = new Date('2023-12-25T12:00:00Z')
|
|
501
|
+
const result = schema.safeParse(date)
|
|
502
|
+
expect(result.success).toBe(true)
|
|
503
|
+
if (result.success) {
|
|
504
|
+
expect(result.value).toBe('2023-12-25T12:00:00.000Z')
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('rejects invalid Date objects', () => {
|
|
509
|
+
const invalidDate = new Date('invalid')
|
|
510
|
+
const result = schema.safeParse(invalidDate)
|
|
511
|
+
expect(result.success).toBe(false)
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('coerces URL objects to strings', () => {
|
|
515
|
+
const url = new URL('https://example.com/path')
|
|
516
|
+
const result = schema.safeParse(url)
|
|
517
|
+
expect(result.success).toBe(true)
|
|
518
|
+
if (result.success) {
|
|
519
|
+
expect(result.value).toBe('https://example.com/path')
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('coerces String objects to primitive strings', () => {
|
|
524
|
+
const stringObj = new String('hello')
|
|
525
|
+
const result = schema.safeParse(stringObj)
|
|
526
|
+
expect(result.success).toBe(true)
|
|
527
|
+
if (result.success) {
|
|
528
|
+
expect(result.value).toBe('hello')
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('coerces TokenSchema instances to strings', () => {
|
|
533
|
+
const token = new TokenSchema('mytoken')
|
|
534
|
+
const result = schema.safeParse(token)
|
|
535
|
+
expect(result.success).toBe(true)
|
|
536
|
+
if (result.success) {
|
|
537
|
+
expect(result.value).toBe('mytoken')
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
describe('combined constraints and format', () => {
|
|
543
|
+
const schema = new StringSchema({
|
|
544
|
+
format: 'handle',
|
|
545
|
+
minLength: 5,
|
|
546
|
+
maxLength: 50,
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('validates both format and length constraints', () => {
|
|
550
|
+
const result = schema.safeParse('user.bsky.social')
|
|
551
|
+
expect(result.success).toBe(true)
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('rejects when length is valid but format is invalid', () => {
|
|
555
|
+
const result = schema.safeParse('invalid handle!')
|
|
556
|
+
expect(result.success).toBe(false)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('rejects when format is valid but length is too short', () => {
|
|
560
|
+
const result = schema.safeParse('a.bc')
|
|
561
|
+
expect(result.success).toBe(false)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('rejects when format is valid but length is too long', () => {
|
|
565
|
+
const longHandle =
|
|
566
|
+
'very.long.subdomain.name.that.exceeds.maximum.length.example.com'
|
|
567
|
+
const result = schema.safeParse(longHandle)
|
|
568
|
+
expect(result.success).toBe(false)
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
describe('edge cases', () => {
|
|
573
|
+
it('handles strings with special characters', () => {
|
|
574
|
+
const schema = new StringSchema({})
|
|
575
|
+
const result = schema.safeParse('hello\nworld\ttab')
|
|
576
|
+
expect(result.success).toBe(true)
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('handles strings with unicode characters', () => {
|
|
580
|
+
const schema = new StringSchema({})
|
|
581
|
+
const result = schema.safeParse('Hello 世界 🌍')
|
|
582
|
+
expect(result.success).toBe(true)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('handles very long strings', () => {
|
|
586
|
+
const schema = new StringSchema({ maxLength: 10000 })
|
|
587
|
+
const longString = 'a'.repeat(10000)
|
|
588
|
+
const result = schema.safeParse(longString)
|
|
589
|
+
expect(result.success).toBe(true)
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('rejects very long strings exceeding maxLength', () => {
|
|
593
|
+
const schema = new StringSchema({ maxLength: 100 })
|
|
594
|
+
const longString = 'a'.repeat(101)
|
|
595
|
+
const result = schema.safeParse(longString)
|
|
596
|
+
expect(result.success).toBe(false)
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('handles zero as minLength', () => {
|
|
600
|
+
const schema = new StringSchema({ minLength: 0 })
|
|
601
|
+
const result = schema.safeParse('')
|
|
602
|
+
expect(result.success).toBe(true)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('handles complex emoji sequences', () => {
|
|
606
|
+
const schema = new StringSchema({ maxGraphemes: 5 })
|
|
607
|
+
// Family emoji is a single grapheme cluster
|
|
608
|
+
const result = schema.safeParse('👨👩👧👦')
|
|
609
|
+
expect(result.success).toBe(true)
|
|
610
|
+
})
|
|
611
|
+
})
|
|
612
|
+
})
|
package/src/schema/string.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
StringFormat,
|
|
5
|
-
UnknownString,
|
|
6
|
-
assertStringFormat,
|
|
7
|
-
} from '../core.js'
|
|
8
|
-
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
1
|
+
import { asCid, graphemeLen, utf8Len } from '@atproto/lex-data'
|
|
2
|
+
import { InferStringFormat, StringFormat, assertStringFormat } from '../core.js'
|
|
3
|
+
import { Schema, ValidationResult, ValidatorContext } from '../validation.js'
|
|
9
4
|
import { TokenSchema } from './token.js'
|
|
10
5
|
|
|
11
6
|
export type StringSchemaOptions = {
|
|
12
7
|
default?: string
|
|
13
|
-
knownValues?: readonly string[]
|
|
14
8
|
format?: StringFormat
|
|
15
9
|
minLength?: number
|
|
16
10
|
maxLength?: number
|
|
@@ -22,20 +16,16 @@ export type StringSchemaOutput<Options> =
|
|
|
22
16
|
//
|
|
23
17
|
Options extends { format: infer F extends StringFormat }
|
|
24
18
|
? InferStringFormat<F>
|
|
25
|
-
:
|
|
26
|
-
? K | UnknownString
|
|
27
|
-
: string
|
|
19
|
+
: string
|
|
28
20
|
|
|
29
21
|
export class StringSchema<
|
|
30
22
|
const Options extends StringSchemaOptions = any,
|
|
31
|
-
> extends
|
|
32
|
-
readonly lexiconType = 'string' as const
|
|
33
|
-
|
|
23
|
+
> extends Schema<StringSchemaOutput<Options>> {
|
|
34
24
|
constructor(readonly options: Options) {
|
|
35
25
|
super()
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
validateInContext(
|
|
39
29
|
// @NOTE validation will be applied on the default value as well
|
|
40
30
|
input: unknown = this.options.default,
|
|
41
31
|
ctx: ValidatorContext,
|
|
@@ -104,6 +94,10 @@ export class StringSchema<
|
|
|
104
94
|
|
|
105
95
|
export function coerceToString(input: unknown): string | null {
|
|
106
96
|
switch (typeof input) {
|
|
97
|
+
// @NOTE We do *not* coerce numbers/booleans to strings because that can
|
|
98
|
+
// lead to them being accepted as string instead of being coerced to
|
|
99
|
+
// number/boolean when the input is a string and the expected result is
|
|
100
|
+
// number/boolean (e.g. in params).
|
|
107
101
|
case 'string':
|
|
108
102
|
return input
|
|
109
103
|
case 'object': {
|
|
@@ -124,7 +118,7 @@ export function coerceToString(input: unknown): string | null {
|
|
|
124
118
|
return input.toString()
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
const cid =
|
|
121
|
+
const cid = asCid(input)
|
|
128
122
|
if (cid) return cid.toString()
|
|
129
123
|
|
|
130
124
|
if (input instanceof String) {
|