@atproto/lex-schema 0.1.5 → 0.1.6

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.
Files changed (263) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/core/$type.d.ts +2 -2
  3. package/dist/core/$type.d.ts.map +1 -1
  4. package/dist/core/$type.js.map +1 -1
  5. package/dist/core/record-key.d.ts +1 -1
  6. package/dist/core/record-key.d.ts.map +1 -1
  7. package/dist/core/record-key.js.map +1 -1
  8. package/dist/core/schema.d.ts +3 -2
  9. package/dist/core/schema.d.ts.map +1 -1
  10. package/dist/core/schema.js +1 -1
  11. package/dist/core/schema.js.map +1 -1
  12. package/dist/core/standard-schema.d.ts +2 -2
  13. package/dist/core/standard-schema.d.ts.map +1 -1
  14. package/dist/core/standard-schema.js.map +1 -1
  15. package/dist/core/string-format.d.ts +2 -2
  16. package/dist/core/string-format.d.ts.map +1 -1
  17. package/dist/core/string-format.js.map +1 -1
  18. package/dist/core/validation-error.d.ts +1 -1
  19. package/dist/core/validation-error.d.ts.map +1 -1
  20. package/dist/core/validation-error.js +1 -1
  21. package/dist/core/validation-error.js.map +1 -1
  22. package/dist/core/validator.d.ts +1 -1
  23. package/dist/core/validator.d.ts.map +1 -1
  24. package/dist/core/validator.js +1 -1
  25. package/dist/core/validator.js.map +1 -1
  26. package/dist/helpers.d.ts +2 -2
  27. package/dist/helpers.d.ts.map +1 -1
  28. package/dist/helpers.js +2 -2
  29. package/dist/helpers.js.map +1 -1
  30. package/dist/schema/array.d.ts +1 -1
  31. package/dist/schema/array.d.ts.map +1 -1
  32. package/dist/schema/array.js +1 -1
  33. package/dist/schema/array.js.map +1 -1
  34. package/dist/schema/blob.d.ts +1 -1
  35. package/dist/schema/blob.d.ts.map +1 -1
  36. package/dist/schema/blob.js +2 -2
  37. package/dist/schema/blob.js.map +1 -1
  38. package/dist/schema/boolean.js +1 -1
  39. package/dist/schema/boolean.js.map +1 -1
  40. package/dist/schema/bytes.js +1 -1
  41. package/dist/schema/bytes.js.map +1 -1
  42. package/dist/schema/cid.d.ts +1 -1
  43. package/dist/schema/cid.d.ts.map +1 -1
  44. package/dist/schema/cid.js +3 -3
  45. package/dist/schema/cid.js.map +1 -1
  46. package/dist/schema/custom.js +1 -1
  47. package/dist/schema/custom.js.map +1 -1
  48. package/dist/schema/dict.d.ts +1 -1
  49. package/dist/schema/dict.d.ts.map +1 -1
  50. package/dist/schema/dict.js +1 -1
  51. package/dist/schema/dict.js.map +1 -1
  52. package/dist/schema/discriminated-union.d.ts +1 -1
  53. package/dist/schema/discriminated-union.d.ts.map +1 -1
  54. package/dist/schema/discriminated-union.js +2 -1
  55. package/dist/schema/discriminated-union.js.map +1 -1
  56. package/dist/schema/enum.js +1 -1
  57. package/dist/schema/enum.js.map +1 -1
  58. package/dist/schema/integer.js +1 -1
  59. package/dist/schema/integer.js.map +1 -1
  60. package/dist/schema/intersection.d.ts +1 -1
  61. package/dist/schema/intersection.d.ts.map +1 -1
  62. package/dist/schema/intersection.js +3 -1
  63. package/dist/schema/intersection.js.map +1 -1
  64. package/dist/schema/lex-map.d.ts +1 -1
  65. package/dist/schema/lex-map.d.ts.map +1 -1
  66. package/dist/schema/lex-map.js +1 -1
  67. package/dist/schema/lex-map.js.map +1 -1
  68. package/dist/schema/lex-value.d.ts +1 -1
  69. package/dist/schema/lex-value.d.ts.map +1 -1
  70. package/dist/schema/lex-value.js +1 -1
  71. package/dist/schema/lex-value.js.map +1 -1
  72. package/dist/schema/literal.js +1 -1
  73. package/dist/schema/literal.js.map +1 -1
  74. package/dist/schema/never.js +1 -1
  75. package/dist/schema/never.js.map +1 -1
  76. package/dist/schema/null.js +1 -1
  77. package/dist/schema/null.js.map +1 -1
  78. package/dist/schema/nullable.d.ts +1 -1
  79. package/dist/schema/nullable.d.ts.map +1 -1
  80. package/dist/schema/nullable.js +1 -1
  81. package/dist/schema/nullable.js.map +1 -1
  82. package/dist/schema/object.d.ts +2 -1
  83. package/dist/schema/object.d.ts.map +1 -1
  84. package/dist/schema/object.js +1 -1
  85. package/dist/schema/object.js.map +1 -1
  86. package/dist/schema/optional.d.ts +2 -1
  87. package/dist/schema/optional.d.ts.map +1 -1
  88. package/dist/schema/optional.js +2 -1
  89. package/dist/schema/optional.js.map +1 -1
  90. package/dist/schema/params.d.ts +1 -1
  91. package/dist/schema/params.d.ts.map +1 -1
  92. package/dist/schema/params.js +1 -1
  93. package/dist/schema/params.js.map +1 -1
  94. package/dist/schema/payload.d.ts +3 -2
  95. package/dist/schema/payload.d.ts.map +1 -1
  96. package/dist/schema/payload.js +2 -1
  97. package/dist/schema/payload.js.map +1 -1
  98. package/dist/schema/permission-set.d.ts +1 -1
  99. package/dist/schema/permission-set.d.ts.map +1 -1
  100. package/dist/schema/permission-set.js +1 -0
  101. package/dist/schema/permission-set.js.map +1 -1
  102. package/dist/schema/permission.d.ts +1 -1
  103. package/dist/schema/permission.d.ts.map +1 -1
  104. package/dist/schema/permission.js.map +1 -1
  105. package/dist/schema/procedure.d.ts +1 -1
  106. package/dist/schema/procedure.d.ts.map +1 -1
  107. package/dist/schema/procedure.js +2 -0
  108. package/dist/schema/procedure.js.map +1 -1
  109. package/dist/schema/query.d.ts +1 -1
  110. package/dist/schema/query.d.ts.map +1 -1
  111. package/dist/schema/query.js +2 -0
  112. package/dist/schema/query.js.map +1 -1
  113. package/dist/schema/record.d.ts +2 -2
  114. package/dist/schema/record.d.ts.map +1 -1
  115. package/dist/schema/record.js +1 -1
  116. package/dist/schema/record.js.map +1 -1
  117. package/dist/schema/ref.d.ts +1 -1
  118. package/dist/schema/ref.d.ts.map +1 -1
  119. package/dist/schema/ref.js +1 -1
  120. package/dist/schema/ref.js.map +1 -1
  121. package/dist/schema/refine.d.ts +2 -2
  122. package/dist/schema/refine.d.ts.map +1 -1
  123. package/dist/schema/refine.js +1 -1
  124. package/dist/schema/refine.js.map +1 -1
  125. package/dist/schema/regexp.js +1 -1
  126. package/dist/schema/regexp.js.map +1 -1
  127. package/dist/schema/string.d.ts +2 -2
  128. package/dist/schema/string.d.ts.map +1 -1
  129. package/dist/schema/string.js +1 -1
  130. package/dist/schema/string.js.map +1 -1
  131. package/dist/schema/subscription.d.ts +3 -2
  132. package/dist/schema/subscription.d.ts.map +1 -1
  133. package/dist/schema/subscription.js +2 -0
  134. package/dist/schema/subscription.js.map +1 -1
  135. package/dist/schema/token.d.ts +1 -1
  136. package/dist/schema/token.d.ts.map +1 -1
  137. package/dist/schema/token.js +1 -1
  138. package/dist/schema/token.js.map +1 -1
  139. package/dist/schema/typed-object.d.ts +2 -2
  140. package/dist/schema/typed-object.d.ts.map +1 -1
  141. package/dist/schema/typed-object.js +1 -1
  142. package/dist/schema/typed-object.js.map +1 -1
  143. package/dist/schema/typed-ref.d.ts +1 -1
  144. package/dist/schema/typed-ref.d.ts.map +1 -1
  145. package/dist/schema/typed-ref.js +1 -1
  146. package/dist/schema/typed-ref.js.map +1 -1
  147. package/dist/schema/typed-union.d.ts +1 -1
  148. package/dist/schema/typed-union.d.ts.map +1 -1
  149. package/dist/schema/typed-union.js +3 -1
  150. package/dist/schema/typed-union.js.map +1 -1
  151. package/dist/schema/union.d.ts +1 -1
  152. package/dist/schema/union.d.ts.map +1 -1
  153. package/dist/schema/union.js +1 -1
  154. package/dist/schema/union.js.map +1 -1
  155. package/dist/schema/unknown.js +1 -1
  156. package/dist/schema/unknown.js.map +1 -1
  157. package/dist/schema/with-default.d.ts +1 -1
  158. package/dist/schema/with-default.d.ts.map +1 -1
  159. package/dist/schema/with-default.js +1 -1
  160. package/dist/schema/with-default.js.map +1 -1
  161. package/package.json +6 -10
  162. package/src/core/$type.test.ts +0 -24
  163. package/src/core/$type.ts +0 -199
  164. package/src/core/record-key.ts +0 -85
  165. package/src/core/result.ts +0 -15
  166. package/src/core/schema.ts +0 -412
  167. package/src/core/standard-schema.test.ts +0 -124
  168. package/src/core/standard-schema.ts +0 -31
  169. package/src/core/string-format.ts +0 -411
  170. package/src/core/types.ts +0 -120
  171. package/src/core/validation-error.ts +0 -134
  172. package/src/core/validation-issue.ts +0 -340
  173. package/src/core/validator.ts +0 -636
  174. package/src/core.ts +0 -9
  175. package/src/external.ts +0 -3
  176. package/src/helpers.test.ts +0 -694
  177. package/src/helpers.ts +0 -222
  178. package/src/index.ts +0 -3
  179. package/src/schema/array.test.ts +0 -251
  180. package/src/schema/array.ts +0 -126
  181. package/src/schema/blob.test.ts +0 -733
  182. package/src/schema/blob.ts +0 -150
  183. package/src/schema/boolean.test.ts +0 -118
  184. package/src/schema/boolean.ts +0 -46
  185. package/src/schema/bytes.test.ts +0 -227
  186. package/src/schema/bytes.ts +0 -81
  187. package/src/schema/cid.test.ts +0 -125
  188. package/src/schema/cid.ts +0 -69
  189. package/src/schema/custom.test.ts +0 -414
  190. package/src/schema/custom.ts +0 -106
  191. package/src/schema/dict.test.ts +0 -181
  192. package/src/schema/dict.ts +0 -122
  193. package/src/schema/discriminated-union.test.ts +0 -676
  194. package/src/schema/discriminated-union.ts +0 -196
  195. package/src/schema/enum.test.ts +0 -398
  196. package/src/schema/enum.ts +0 -77
  197. package/src/schema/integer.test.ts +0 -314
  198. package/src/schema/integer.ts +0 -86
  199. package/src/schema/intersection.test.ts +0 -33
  200. package/src/schema/intersection.ts +0 -113
  201. package/src/schema/lex-map.test.ts +0 -593
  202. package/src/schema/lex-map.ts +0 -63
  203. package/src/schema/lex-value.test.ts +0 -81
  204. package/src/schema/lex-value.ts +0 -86
  205. package/src/schema/literal.test.ts +0 -533
  206. package/src/schema/literal.ts +0 -70
  207. package/src/schema/never.test.ts +0 -175
  208. package/src/schema/never.ts +0 -56
  209. package/src/schema/null.test.ts +0 -80
  210. package/src/schema/null.ts +0 -49
  211. package/src/schema/nullable.test.ts +0 -470
  212. package/src/schema/nullable.ts +0 -74
  213. package/src/schema/object.test.ts +0 -69
  214. package/src/schema/object.ts +0 -136
  215. package/src/schema/optional.test.ts +0 -479
  216. package/src/schema/optional.ts +0 -92
  217. package/src/schema/params.test.ts +0 -1118
  218. package/src/schema/params.ts +0 -371
  219. package/src/schema/payload.test.ts +0 -340
  220. package/src/schema/payload.ts +0 -204
  221. package/src/schema/permission-set.test.ts +0 -613
  222. package/src/schema/permission-set.ts +0 -86
  223. package/src/schema/permission.test.ts +0 -537
  224. package/src/schema/permission.ts +0 -63
  225. package/src/schema/procedure.test.ts +0 -324
  226. package/src/schema/procedure.ts +0 -98
  227. package/src/schema/query.test.ts +0 -348
  228. package/src/schema/query.ts +0 -86
  229. package/src/schema/record.test.ts +0 -812
  230. package/src/schema/record.ts +0 -217
  231. package/src/schema/ref.test.ts +0 -349
  232. package/src/schema/ref.ts +0 -103
  233. package/src/schema/refine.test.ts +0 -579
  234. package/src/schema/refine.ts +0 -153
  235. package/src/schema/regexp.test.ts +0 -577
  236. package/src/schema/regexp.ts +0 -82
  237. package/src/schema/string.test.ts +0 -773
  238. package/src/schema/string.ts +0 -229
  239. package/src/schema/subscription.test.ts +0 -499
  240. package/src/schema/subscription.ts +0 -108
  241. package/src/schema/token.test.ts +0 -152
  242. package/src/schema/token.ts +0 -103
  243. package/src/schema/typed-object.test.ts +0 -745
  244. package/src/schema/typed-object.ts +0 -181
  245. package/src/schema/typed-ref.test.ts +0 -796
  246. package/src/schema/typed-ref.ts +0 -126
  247. package/src/schema/typed-union.test.ts +0 -355
  248. package/src/schema/typed-union.ts +0 -130
  249. package/src/schema/union.test.ts +0 -191
  250. package/src/schema/union.ts +0 -89
  251. package/src/schema/unknown.test.ts +0 -313
  252. package/src/schema/unknown.ts +0 -47
  253. package/src/schema/with-default.ts +0 -81
  254. package/src/schema.ts +0 -43
  255. package/src/util/array-agg.test.ts +0 -42
  256. package/src/util/array-agg.ts +0 -44
  257. package/src/util/assertion-util.ts +0 -1
  258. package/src/util/if-any.ts +0 -3
  259. package/src/util/lazy-property.ts +0 -14
  260. package/src/util/memoize.ts +0 -37
  261. package/tsconfig.build.json +0 -12
  262. package/tsconfig.json +0 -7
  263. package/tsconfig.tests.json +0 -8
@@ -1,125 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { parseCid } from '@atproto/lex-data'
3
- import { cid } from './cid.js'
4
-
5
- const cborCid = parseCid(
6
- 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
7
- { flavor: 'cbor' },
8
- )
9
-
10
- const rawCid = parseCid(
11
- 'bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4',
12
- { flavor: 'raw' },
13
- )
14
-
15
- const v0Cid = parseCid('QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB')
16
-
17
- // Using git-raw codec (0x78) instead of DAG-CBOR or raw binary
18
- const gitRawCid = parseCid(
19
- 'bafybeigvgzoolc3drupxhlevdp2ugqcrbcsqfmcek2zxiw5wctk3xjpjwy',
20
- )
21
-
22
- // Using SHA-512 (0x13) instead of SHA-256
23
- const sha512Cid = parseCid(
24
- 'bafybgqfcn3rz4mdzywp2jb6mjvpdq24rxjvbmdcmizrjdgx2ujjpvj4kxf4d62ywrzm6njk44cxhha4pj3bkvqz2esfgrm7mdkdcqcxjibf7c',
25
- )
26
-
27
- describe('CidSchema', () => {
28
- describe('default mode (non-strict)', () => {
29
- const schema = cid({})
30
-
31
- it('validates CID v1 with DAG-CBOR codec and SHA-256', () => {
32
- const result = schema.safeParse(cborCid)
33
- expect(result.success).toBe(true)
34
- })
35
-
36
- it('validates CID v1 with raw binary codec', () => {
37
- const result = schema.safeParse(rawCid)
38
- expect(result.success).toBe(true)
39
- })
40
-
41
- it('validates CID v0', () => {
42
- const result = schema.safeParse(v0Cid)
43
- expect(result.success).toBe(true)
44
- })
45
-
46
- it('rejects non-CID objects', () => {
47
- const result = schema.safeParse({ not: 'a cid' })
48
- expect(result.success).toBe(false)
49
- })
50
-
51
- it('rejects strings', () => {
52
- const result = schema.safeParse(cborCid.toString())
53
- expect(result.success).toBe(false)
54
- })
55
-
56
- it('rejects numbers', () => {
57
- const result = schema.safeParse(123)
58
- expect(result.success).toBe(false)
59
- })
60
-
61
- it('rejects null', () => {
62
- const result = schema.safeParse(null)
63
- expect(result.success).toBe(false)
64
- })
65
-
66
- it('rejects undefined', () => {
67
- const result = schema.safeParse(undefined)
68
- expect(result.success).toBe(false)
69
- })
70
-
71
- it('rejects arrays', () => {
72
- const result = schema.safeParse([])
73
- expect(result.success).toBe(false)
74
- })
75
-
76
- it('rejects booleans', () => {
77
- const result = schema.safeParse(true)
78
- expect(result.success).toBe(false)
79
- })
80
- })
81
-
82
- describe('strict mode', () => {
83
- const schema = cid({ flavor: 'dasl' })
84
-
85
- it('validates CID v1 with DAG-CBOR codec and SHA-256', () => {
86
- const result = schema.safeParse(cborCid)
87
- expect(result.success).toBe(true)
88
- })
89
-
90
- it('validates CID v1 with raw binary codec and SHA-256', () => {
91
- const result = schema.safeParse(rawCid)
92
- expect(result.success).toBe(true)
93
- })
94
-
95
- it('rejects CID v0', () => {
96
- const result = schema.safeParse(v0Cid)
97
- expect(result.success).toBe(false)
98
- })
99
-
100
- it('rejects CID v1 with non-standard codec', () => {
101
- const result = schema.safeParse(gitRawCid)
102
- expect(result.success).toBe(false)
103
- })
104
-
105
- it('rejects CID v1 with non-SHA-256 hash', () => {
106
- const result = schema.safeParse(sha512Cid)
107
- expect(result.success).toBe(false)
108
- })
109
-
110
- it('rejects non-CID objects', () => {
111
- const result = schema.safeParse({ not: 'a cid' })
112
- expect(result.success).toBe(false)
113
- })
114
-
115
- it('rejects strings', () => {
116
- const result = schema.safeParse(cborCid.toString())
117
- expect(result.success).toBe(false)
118
- })
119
-
120
- it('rejects null', () => {
121
- const result = schema.safeParse(null)
122
- expect(result.success).toBe(false)
123
- })
124
- })
125
- })
package/src/schema/cid.ts DELETED
@@ -1,69 +0,0 @@
1
- import { CheckCidOptions, Cid, InferCheckedCid, isCid } from '@atproto/lex-data'
2
- import { Schema, ValidationContext } from '../core.js'
3
- import { memoizedOptions } from '../util/memoize.js'
4
-
5
- export type { Cid }
6
-
7
- /**
8
- * Configuration options for CID schema validation.
9
- *
10
- * @see CheckCidOptions from @atproto/lex-data for available options
11
- */
12
- export type CidSchemaOptions = CheckCidOptions
13
-
14
- /**
15
- * Schema for validating Content Identifiers (CIDs).
16
- *
17
- * CIDs are self-describing content-addressed identifiers used in AT Protocol
18
- * to reference data by its cryptographic hash. This schema validates that
19
- * the input is a valid CID object.
20
- *
21
- * @template TOptions - The configuration options type
22
- *
23
- * @example
24
- * ```ts
25
- * const schema = new CidSchema()
26
- * const result = schema.validate(someCid)
27
- * ```
28
- */
29
- export class CidSchema<
30
- const TOptions extends CidSchemaOptions = { flavor: undefined },
31
- > extends Schema<InferCheckedCid<TOptions>> {
32
- readonly type = 'cid' as const
33
-
34
- constructor(readonly options?: TOptions) {
35
- super()
36
- }
37
-
38
- validateInContext(input: unknown, ctx: ValidationContext) {
39
- if (!isCid(input, this.options)) {
40
- return ctx.issueUnexpectedType(input, 'cid')
41
- }
42
-
43
- return ctx.success(input)
44
- }
45
- }
46
-
47
- /**
48
- * Creates a CID schema for validating Content Identifiers.
49
- *
50
- * CIDs are used throughout AT Protocol to reference content by its hash.
51
- * This is commonly used for referencing blobs, commits, and other data.
52
- *
53
- * @param options - Optional configuration for CID validation
54
- * @returns A new {@link CidSchema} instance
55
- *
56
- * @example
57
- * ```ts
58
- * // Basic CID validation
59
- * const cidSchema = l.cid()
60
- *
61
- * // Validate a CID from a blob reference
62
- * const result = cidSchema.validate(blobRef.ref)
63
- * ```
64
- */
65
- export const cid = /*#__PURE__*/ memoizedOptions(function <
66
- O extends CidSchemaOptions = NonNullable<unknown>,
67
- >(options?: O) {
68
- return new CidSchema(options)
69
- })
@@ -1,414 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import { IssueCustom } from '../core.js'
3
- import { custom } from './custom.js'
4
-
5
- describe('CustomSchema', () => {
6
- describe('basic validation', () => {
7
- it('validates input that passes custom assertion', () => {
8
- const schema = custom(
9
- (input): input is string => typeof input === 'string',
10
- 'Must be a string',
11
- )
12
- const result = schema.safeParse('hello')
13
- expect(result.success).toBe(true)
14
- if (result.success) {
15
- expect(result.value).toBe('hello')
16
- }
17
- })
18
-
19
- it('rejects input that fails custom assertion', () => {
20
- const schema = custom(
21
- (input): input is string => typeof input === 'string',
22
- 'Must be a string',
23
- )
24
- const result = schema.safeParse(123)
25
- expect(result.success).toBe(false)
26
- })
27
-
28
- it('includes custom message in error', () => {
29
- const schema = custom(
30
- (input): input is string => typeof input === 'string',
31
- 'Custom error message',
32
- )
33
- const result = schema.safeParse(123)
34
- expect(result.success).toBe(false)
35
- if (!result.success) {
36
- expect(result.reason.message).toContain('Custom error message')
37
- }
38
- })
39
- })
40
-
41
- describe('complex type guards', () => {
42
- it('validates objects with specific properties', () => {
43
- interface User {
44
- name: string
45
- age: number
46
- }
47
-
48
- const schema = custom((input): input is User => {
49
- return (
50
- typeof input === 'object' &&
51
- input !== null &&
52
- 'name' in input &&
53
- 'age' in input &&
54
- typeof (input as any).name === 'string' &&
55
- typeof (input as any).age === 'number'
56
- )
57
- }, 'Must be a valid User object')
58
-
59
- expect(schema.matches({ name: 'Alice', age: 30 })).toBe(true)
60
- })
61
-
62
- it('rejects objects missing required properties', () => {
63
- interface User {
64
- name: string
65
- age: number
66
- }
67
-
68
- const schema = custom((input): input is User => {
69
- return (
70
- typeof input === 'object' &&
71
- input !== null &&
72
- 'name' in input &&
73
- 'age' in input &&
74
- typeof (input as any).name === 'string' &&
75
- typeof (input as any).age === 'number'
76
- )
77
- }, 'Must be a valid User object')
78
-
79
- expect(schema.matches({ name: 'Alice' })).toBe(false)
80
- })
81
-
82
- it('validates arrays with specific element types', () => {
83
- const schema = custom((input): input is number[] => {
84
- return (
85
- Array.isArray(input) &&
86
- input.every((item) => typeof item === 'number')
87
- )
88
- }, 'Must be an array of numbers')
89
-
90
- const result = schema.safeParse([1, 2, 3, 4])
91
- expect(result.success).toBe(true)
92
- })
93
-
94
- it('rejects arrays with mixed types', () => {
95
- const schema = custom((input): input is number[] => {
96
- return (
97
- Array.isArray(input) &&
98
- input.every((item) => typeof item === 'number')
99
- )
100
- }, 'Must be an array of numbers')
101
-
102
- const result = schema.safeParse([1, 'two', 3])
103
- expect(result.success).toBe(false)
104
- })
105
- })
106
-
107
- describe('custom context usage', () => {
108
- it('can add custom issues through context', () => {
109
- const schema = custom((input, ctx): input is string => {
110
- if (typeof input !== 'string') {
111
- ctx.addIssue({
112
- code: 'invalid_type',
113
- path: ctx.path,
114
- input,
115
- expected: ['string'],
116
- } as any)
117
- return false
118
- }
119
- return true
120
- }, 'Must be a string')
121
-
122
- const result = schema.safeParse(123)
123
- expect(result.success).toBe(false)
124
- })
125
-
126
- it('accesses path from context', () => {
127
- let capturedPath: any[] = []
128
- const schema = custom((input, ctx): input is string => {
129
- capturedPath = [...ctx.path]
130
- return typeof input === 'string'
131
- }, 'Must be a string')
132
-
133
- schema.safeParse('test')
134
- expect(capturedPath).toEqual([])
135
- })
136
-
137
- it('validates with custom path', () => {
138
- const schema = custom(
139
- (input): input is string => typeof input === 'string',
140
- 'Must be a string',
141
- 'customField',
142
- )
143
-
144
- const result = schema.safeParse(123)
145
- expect(result.success).toBe(false)
146
- if (!result.success) {
147
- expect(result.reason.message).toContain('customField')
148
- }
149
- })
150
-
151
- it('validates with array of paths', () => {
152
- const schema = custom(
153
- (input): input is string => typeof input === 'string',
154
- 'Must be a string',
155
- ['nested', 'field'],
156
- )
157
-
158
- const result = schema.safeParse(123)
159
- expect(result.success).toBe(false)
160
- if (!result.success) {
161
- expect(result.reason.message).toContain('nested')
162
- expect(result.reason.message).toContain('field')
163
- }
164
- })
165
- })
166
-
167
- describe('business logic validation', () => {
168
- it('validates email format', () => {
169
- const schema = custom((input): input is string => {
170
- return (
171
- typeof input === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
172
- )
173
- }, 'Must be a valid email address')
174
-
175
- const validResult = schema.safeParse('user@example.com')
176
- expect(validResult.success).toBe(true)
177
-
178
- const invalidResult = schema.safeParse('not-an-email')
179
- expect(invalidResult.success).toBe(false)
180
- })
181
-
182
- it('validates password strength', () => {
183
- const schema = custom((input): input is string => {
184
- if (typeof input !== 'string') return false
185
- return (
186
- input.length >= 8 &&
187
- /[A-Z]/.test(input) &&
188
- /[a-z]/.test(input) &&
189
- /[0-9]/.test(input)
190
- )
191
- }, 'Password must be at least 8 characters with uppercase, lowercase, and numbers')
192
-
193
- const validResult = schema.safeParse('MyPass123')
194
- expect(validResult.success).toBe(true)
195
-
196
- const weakResult = schema.safeParse('weak')
197
- expect(weakResult.success).toBe(false)
198
- })
199
-
200
- it('validates age range', () => {
201
- const schema = custom((input): input is number => {
202
- return typeof input === 'number' && input >= 18 && input <= 120
203
- }, 'Age must be between 18 and 120')
204
-
205
- const validResult = schema.safeParse(25)
206
- expect(validResult.success).toBe(true)
207
-
208
- const tooYoungResult = schema.safeParse(15)
209
- expect(tooYoungResult.success).toBe(false)
210
-
211
- const tooOldResult = schema.safeParse(150)
212
- expect(tooOldResult.success).toBe(false)
213
- })
214
-
215
- it('validates positive numbers', () => {
216
- const schema = custom((input): input is number => {
217
- return typeof input === 'number' && input > 0
218
- }, 'Must be a positive number')
219
-
220
- const validResult = schema.safeParse(5)
221
- expect(validResult.success).toBe(true)
222
-
223
- const zeroResult = schema.safeParse(0)
224
- expect(zeroResult.success).toBe(false)
225
-
226
- const negativeResult = schema.safeParse(-5)
227
- expect(negativeResult.success).toBe(false)
228
- })
229
- })
230
-
231
- describe('edge cases', () => {
232
- it('handles null input', () => {
233
- const schema = custom(
234
- (input): input is null => input === null,
235
- 'Must be null',
236
- )
237
-
238
- const validResult = schema.safeParse(null)
239
- expect(validResult.success).toBe(true)
240
-
241
- const invalidResult = schema.safeParse(undefined)
242
- expect(invalidResult.success).toBe(false)
243
- })
244
-
245
- it('handles undefined input', () => {
246
- const schema = custom(
247
- (input): input is undefined => input === undefined,
248
- 'Must be undefined',
249
- )
250
-
251
- const validResult = schema.safeParse(undefined)
252
- expect(validResult.success).toBe(true)
253
-
254
- const invalidResult = schema.safeParse(null)
255
- expect(invalidResult.success).toBe(false)
256
- })
257
-
258
- it('handles empty string', () => {
259
- const schema = custom(
260
- (input): input is string =>
261
- typeof input === 'string' && input.length > 0,
262
- 'Must be a non-empty string',
263
- )
264
-
265
- const validResult = schema.safeParse('hello')
266
- expect(validResult.success).toBe(true)
267
-
268
- const invalidResult = schema.safeParse('')
269
- expect(invalidResult.success).toBe(false)
270
- })
271
-
272
- it('handles empty array', () => {
273
- const schema = custom(
274
- (input): input is any[] => Array.isArray(input) && input.length > 0,
275
- 'Must be a non-empty array',
276
- )
277
-
278
- const validResult = schema.safeParse([1, 2, 3])
279
- expect(validResult.success).toBe(true)
280
-
281
- const invalidResult = schema.safeParse([])
282
- expect(invalidResult.success).toBe(false)
283
- })
284
-
285
- it('handles complex nested structures', () => {
286
- interface ComplexType {
287
- users: Array<{ name: string; email: string }>
288
- metadata: { count: number }
289
- }
290
-
291
- const schema = custom((input): input is ComplexType => {
292
- if (typeof input !== 'object' || input === null) return false
293
- const obj = input as any
294
- return (
295
- Array.isArray(obj.users) &&
296
- obj.users.every(
297
- (u: any) =>
298
- typeof u === 'object' &&
299
- typeof u.name === 'string' &&
300
- typeof u.email === 'string',
301
- ) &&
302
- typeof obj.metadata === 'object' &&
303
- typeof obj.metadata.count === 'number'
304
- )
305
- }, 'Must be a valid complex structure')
306
-
307
- const validResult = schema.safeParse({
308
- users: [
309
- { name: 'Alice', email: 'alice@example.com' },
310
- { name: 'Bob', email: 'bob@example.com' },
311
- ],
312
- metadata: { count: 2 },
313
- })
314
- expect(validResult.success).toBe(true)
315
-
316
- const invalidResult = schema.safeParse({
317
- users: [{ name: 'Alice' }], // missing email
318
- metadata: { count: 1 },
319
- })
320
- expect(invalidResult.success).toBe(false)
321
- })
322
- })
323
-
324
- describe('type narrowing', () => {
325
- it('correctly narrows union types', () => {
326
- type StringOrNumber = string | number
327
-
328
- const schema = custom(
329
- (input): input is string => typeof input === 'string',
330
- 'Must be a string',
331
- )
332
-
333
- const input: StringOrNumber = 'hello'
334
-
335
- const result = schema.safeParse(input)
336
- expect(result.success).toBe(true)
337
-
338
- if (result.success) {
339
- // Type should be narrowed to string
340
- const value: string = result.value
341
- expect(typeof value).toBe('string')
342
- }
343
- })
344
-
345
- it('validates discriminated unions', () => {
346
- type Shape =
347
- | { type: 'circle'; radius: number }
348
- | { type: 'rectangle'; width: number; height: number }
349
-
350
- const circleSchema = custom((input): input is Shape => {
351
- return (
352
- typeof input === 'object' &&
353
- input !== null &&
354
- 'type' in input &&
355
- (input as any).type === 'circle' &&
356
- 'radius' in input &&
357
- typeof (input as any).radius === 'number'
358
- )
359
- }, 'Must be a valid circle')
360
-
361
- const validResult = circleSchema.safeParse({ type: 'circle', radius: 5 })
362
- expect(validResult.success).toBe(true)
363
-
364
- const invalidResult = circleSchema.safeParse({
365
- type: 'rectangle',
366
- width: 10,
367
- height: 20,
368
- })
369
- expect(invalidResult.success).toBe(false)
370
- })
371
- })
372
-
373
- describe('assertion context behavior', () => {
374
- it('calls assertion with null as this', () => {
375
- const assertion = vi.fn(function (
376
- this: unknown,
377
- input: unknown,
378
- ): input is string {
379
- expect(this).toBeNull()
380
- return typeof input === 'string'
381
- })
382
-
383
- custom(assertion as any, 'Must be a string').safeParse('test')
384
-
385
- expect(assertion).toHaveBeenCalledTimes(1)
386
- })
387
-
388
- it('provides addIssue method in context', () => {
389
- const schema = custom((input, ctx): input is string => {
390
- ctx.addIssue(new IssueCustom(ctx.path, input, 'This is a custom issue'))
391
- return false
392
- }, 'Must be a string')
393
-
394
- expect(schema.safeParse('test')).toMatchObject({
395
- success: false,
396
- reason: {
397
- issues: [
398
- { message: 'This is a custom issue' },
399
- { message: 'Must be a string' },
400
- ],
401
- },
402
- })
403
- })
404
-
405
- it('provides path array in context', () => {
406
- const schema = custom((input, ctx): input is string => {
407
- expect(Array.isArray(ctx.path)).toBe(true)
408
- return typeof input === 'string'
409
- }, 'Must be a string')
410
-
411
- schema.safeParse('test')
412
- })
413
- })
414
- })
@@ -1,106 +0,0 @@
1
- import { Issue, IssueCustom, Schema, ValidationContext } from '../core.js'
2
-
3
- /**
4
- * Context object provided to custom assertion functions.
5
- *
6
- * @property path - Current validation path as an array of property keys
7
- * @property addIssue - Function to add additional validation issues
8
- */
9
- export type CustomAssertionContext = {
10
- path: PropertyKey[]
11
- addIssue(issue: Issue): void
12
- }
13
-
14
- /**
15
- * Type guard function for custom schema validation.
16
- *
17
- * @template TValue - The type to validate/narrow to
18
- */
19
- export type CustomAssertion<TValue> = (
20
- this: null,
21
- input: unknown,
22
- ctx: CustomAssertionContext,
23
- ) => input is TValue
24
-
25
- /**
26
- * Schema with a custom validation function.
27
- *
28
- * Allows defining completely custom validation logic using a type guard
29
- * assertion function. The function receives the input and validation context,
30
- * and must return whether the input is valid.
31
- *
32
- * @template TValue - The validated output type
33
- *
34
- * @example
35
- * ```ts
36
- * const schema = new CustomSchema(
37
- * (input): input is Date => input instanceof Date,
38
- * 'Expected a Date instance'
39
- * )
40
- * ```
41
- */
42
- export class CustomSchema<out TValue = unknown> extends Schema<TValue> {
43
- readonly type = 'custom' as const
44
-
45
- constructor(
46
- private readonly assertion: CustomAssertion<TValue>,
47
- private readonly message: string,
48
- private readonly path?: PropertyKey | readonly PropertyKey[],
49
- ) {
50
- super()
51
- }
52
-
53
- validateInContext(input: unknown, ctx: ValidationContext) {
54
- if (!this.assertion.call(null, input, ctx)) {
55
- const path = ctx.concatPath(this.path)
56
- return ctx.issue(new IssueCustom(path, input, this.message))
57
- }
58
- return ctx.success(input as TValue)
59
- }
60
- }
61
-
62
- /**
63
- * Creates a custom schema with a user-defined validation function.
64
- *
65
- * Use this when the built-in schemas don't cover your validation needs.
66
- * The assertion function must be a type guard that narrows the input type.
67
- *
68
- * @param assertion - Type guard function that validates the input
69
- * @param message - Error message when validation fails
70
- * @param path - Optional path to associate with validation errors
71
- * @returns A new {@link CustomSchema} instance
72
- *
73
- * @example
74
- * ```ts
75
- * // Validate Date instances
76
- * const dateSchema = l.custom(
77
- * (input): input is Date => input instanceof Date && !isNaN(input.getTime()),
78
- * 'Expected a valid Date'
79
- * )
80
- *
81
- * // Validate specific object shape
82
- * const pointSchema = l.custom(
83
- * (input): input is { x: number; y: number } =>
84
- * typeof input === 'object' &&
85
- * input !== null &&
86
- * typeof (input as any).x === 'number' &&
87
- * typeof (input as any).y === 'number',
88
- * 'Expected a point with x and y coordinates'
89
- * )
90
- *
91
- * // With custom path
92
- * const validConfig = l.custom(
93
- * (input): input is Config => validateConfig(input),
94
- * 'Invalid configuration',
95
- * ['config']
96
- * )
97
- * ```
98
- */
99
- /*@__NO_SIDE_EFFECTS__*/
100
- export function custom<TValue>(
101
- assertion: CustomAssertion<TValue>,
102
- message: string,
103
- path?: PropertyKey | readonly PropertyKey[],
104
- ) {
105
- return new CustomSchema<TValue>(assertion, message, path)
106
- }