@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,136 +0,0 @@
1
- import { isPlainObject } from '@atproto/lex-data'
2
- import {
3
- InferInput,
4
- InferOutput,
5
- Schema,
6
- ValidationContext,
7
- Validator,
8
- WithOptionalProperties,
9
- } from '../core.js'
10
- import { lazyProperty } from '../util/lazy-property.js'
11
-
12
- /**
13
- * Type representing the shape of an object schema.
14
- *
15
- * Maps property names to their corresponding validators.
16
- */
17
- export type ObjectSchemaShape = Record<string, Validator>
18
-
19
- /**
20
- * Schema for validating objects with a defined shape.
21
- *
22
- * Each property in the shape is validated against its corresponding schema.
23
- * Properties wrapped in `optional()` are not required.
24
- *
25
- * @template TShape - The object shape type mapping property names to validators
26
- *
27
- * @example
28
- * ```ts
29
- * const schema = new ObjectSchema({
30
- * name: l.string(),
31
- * age: l.optional(l.integer()),
32
- * })
33
- * const result = schema.validate({ name: 'Alice' })
34
- * ```
35
- */
36
- export class ObjectSchema<
37
- const TShape extends ObjectSchemaShape = any,
38
- > extends Schema<
39
- WithOptionalProperties<{
40
- [K in keyof TShape]: InferInput<TShape[K]>
41
- }>,
42
- WithOptionalProperties<{
43
- [K in keyof TShape]: InferOutput<TShape[K]>
44
- }>
45
- > {
46
- readonly type = 'object' as const
47
-
48
- constructor(readonly shape: TShape) {
49
- super()
50
- }
51
-
52
- get validatorsMap(): Map<string, Validator> {
53
- const map = new Map(Object.entries(this.shape))
54
-
55
- return lazyProperty(this, 'validatorsMap', map)
56
- }
57
-
58
- validateInContext(input: unknown, ctx: ValidationContext) {
59
- if (!isPlainObject(input)) {
60
- return ctx.issueUnexpectedType(input, 'object')
61
- }
62
-
63
- // Lazily copy value
64
- let copy: undefined | Record<string, unknown>
65
-
66
- for (const [key, propDef] of this.validatorsMap) {
67
- const result = ctx.validateChild(input, key, propDef)
68
- if (!result.success) {
69
- if (!(key in input)) {
70
- // Transform into "required key" issue
71
- return ctx.issueRequiredKey(input, key)
72
- }
73
-
74
- return result
75
- }
76
-
77
- // Skip copying if key is not present in input (and value is undefined)
78
- if (result.value === undefined && !(key in input)) {
79
- continue
80
- }
81
-
82
- if (!Object.is(result.value, input[key])) {
83
- if (ctx.options.mode === 'validate') {
84
- // In "validate" mode, we can't modify the input, so we issue an error
85
- return ctx.issueInvalidPropertyValue(input, key, [result.value])
86
- }
87
-
88
- copy ??= { ...input }
89
- copy[key] = result.value
90
- }
91
- }
92
-
93
- return ctx.success(copy ?? input)
94
- }
95
- }
96
-
97
- /**
98
- * Creates an object schema with the specified property validators.
99
- *
100
- * Validates that the input is a plain object and each property matches
101
- * its corresponding schema. Properties wrapped in `optional()` are not required.
102
- *
103
- * @param properties - Object mapping property names to their validators
104
- * @returns A new {@link ObjectSchema} instance
105
- *
106
- * @example
107
- * ```ts
108
- * // Basic object
109
- * const userSchema = l.object({
110
- * name: l.string(),
111
- * email: l.string({ format: 'uri' }),
112
- * })
113
- *
114
- * // With optional properties
115
- * const profileSchema = l.object({
116
- * displayName: l.string(),
117
- * bio: l.optional(l.string({ maxLength: 256 })),
118
- * avatar: l.optional(l.blob({ accept: ['image/*'] })),
119
- * })
120
- *
121
- * // Nested objects
122
- * const postSchema = l.object({
123
- * text: l.string(),
124
- * author: l.object({
125
- * did: l.string({ format: 'did' }),
126
- * handle: l.string({ format: 'handle' }),
127
- * }),
128
- * })
129
- * ```
130
- */
131
- /*@__NO_SIDE_EFFECTS__*/
132
- export function object<const TShape extends ObjectSchemaShape>(
133
- properties: TShape,
134
- ) {
135
- return new ObjectSchema<TShape>(properties)
136
- }
@@ -1,479 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { boolean } from './boolean.js'
3
- import { integer } from './integer.js'
4
- import { optional } from './optional.js'
5
- import { string } from './string.js'
6
- import { withDefault } from './with-default.js'
7
-
8
- describe('OptionalSchema', () => {
9
- describe('basic validation with string schema', () => {
10
- const schema = optional(string())
11
-
12
- it('validates defined string values', () => {
13
- const result = schema.safeParse('hello')
14
- expect(result.success).toBe(true)
15
- if (result.success) {
16
- expect(result.value).toBe('hello')
17
- }
18
- })
19
-
20
- it('validates empty strings', () => {
21
- const result = schema.safeParse('')
22
- expect(result.success).toBe(true)
23
- if (result.success) {
24
- expect(result.value).toBe('')
25
- }
26
- })
27
-
28
- it('validates undefined', () => {
29
- const result = schema.safeParse(undefined)
30
- expect(result.success).toBe(true)
31
- if (result.success) {
32
- expect(result.value).toBe(undefined)
33
- }
34
- })
35
-
36
- it('rejects invalid types for the inner schema', () => {
37
- const result = schema.safeParse(123)
38
- expect(result.success).toBe(false)
39
- })
40
-
41
- it('rejects null', () => {
42
- const result = schema.safeParse(null)
43
- expect(result.success).toBe(false)
44
- })
45
-
46
- it('rejects booleans', () => {
47
- const result = schema.safeParse(true)
48
- expect(result.success).toBe(false)
49
- })
50
-
51
- it('rejects objects', () => {
52
- const result = schema.safeParse({ value: 'hello' })
53
- expect(result.success).toBe(false)
54
- })
55
-
56
- it('rejects arrays', () => {
57
- const result = schema.safeParse(['hello'])
58
- expect(result.success).toBe(false)
59
- })
60
- })
61
-
62
- describe('basic validation with integer schema', () => {
63
- const schema = optional(integer())
64
-
65
- it('validates defined integer values', () => {
66
- const result = schema.safeParse(42)
67
- expect(result.success).toBe(true)
68
- if (result.success) {
69
- expect(result.value).toBe(42)
70
- }
71
- })
72
-
73
- it('validates zero', () => {
74
- const result = schema.safeParse(0)
75
- expect(result.success).toBe(true)
76
- if (result.success) {
77
- expect(result.value).toBe(0)
78
- }
79
- })
80
-
81
- it('validates negative integers', () => {
82
- const result = schema.safeParse(-42)
83
- expect(result.success).toBe(true)
84
- if (result.success) {
85
- expect(result.value).toBe(-42)
86
- }
87
- })
88
-
89
- it('validates undefined', () => {
90
- const result = schema.safeParse(undefined)
91
- expect(result.success).toBe(true)
92
- if (result.success) {
93
- expect(result.value).toBe(undefined)
94
- }
95
- })
96
-
97
- it('rejects invalid types for the inner schema', () => {
98
- const result = schema.safeParse('not a number')
99
- expect(result.success).toBe(false)
100
- })
101
-
102
- it('rejects floats', () => {
103
- const result = schema.safeParse(3.14)
104
- expect(result.success).toBe(false)
105
- })
106
-
107
- it('rejects null', () => {
108
- const result = schema.safeParse(null)
109
- expect(result.success).toBe(false)
110
- })
111
- })
112
-
113
- describe('basic validation with boolean schema', () => {
114
- const schema = optional(boolean())
115
-
116
- it('validates true', () => {
117
- const result = schema.safeParse(true)
118
- expect(result.success).toBe(true)
119
- if (result.success) {
120
- expect(result.value).toBe(true)
121
- }
122
- })
123
-
124
- it('validates false', () => {
125
- const result = schema.safeParse(false)
126
- expect(result.success).toBe(true)
127
- if (result.success) {
128
- expect(result.value).toBe(false)
129
- }
130
- })
131
-
132
- it('validates undefined', () => {
133
- const result = schema.safeParse(undefined)
134
- expect(result.success).toBe(true)
135
- if (result.success) {
136
- expect(result.value).toBe(undefined)
137
- }
138
- })
139
-
140
- it('rejects strings', () => {
141
- const result = schema.safeParse('true')
142
- expect(result.success).toBe(false)
143
- })
144
-
145
- it('rejects numbers', () => {
146
- const result = schema.safeParse(1)
147
- expect(result.success).toBe(false)
148
- })
149
-
150
- it('rejects null', () => {
151
- const result = schema.safeParse(null)
152
- expect(result.success).toBe(false)
153
- })
154
- })
155
-
156
- describe('inner schema with constraints', () => {
157
- const schema = optional(string({ minLength: 5, maxLength: 10 }))
158
-
159
- it('validates values meeting inner schema constraints', () => {
160
- const result = schema.safeParse('hello')
161
- expect(result.success).toBe(true)
162
- })
163
-
164
- it('validates values at minimum boundary', () => {
165
- const result = schema.safeParse('abcde')
166
- expect(result.success).toBe(true)
167
- })
168
-
169
- it('validates values at maximum boundary', () => {
170
- const result = schema.safeParse('1234567890')
171
- expect(result.success).toBe(true)
172
- })
173
-
174
- it('validates undefined', () => {
175
- const result = schema.safeParse(undefined)
176
- expect(result.success).toBe(true)
177
- })
178
-
179
- it('rejects values violating inner schema minimum constraint', () => {
180
- const result = schema.safeParse('hi')
181
- expect(result.success).toBe(false)
182
- })
183
-
184
- it('rejects values violating inner schema maximum constraint', () => {
185
- const result = schema.safeParse('this is too long')
186
- expect(result.success).toBe(false)
187
- })
188
-
189
- it('rejects empty strings when inner schema has minLength', () => {
190
- const result = schema.safeParse('')
191
- expect(result.success).toBe(false)
192
- })
193
- })
194
-
195
- describe('inner schema with default value', () => {
196
- const schema = optional(withDefault(string(), 'default'))
197
-
198
- it('applies default value when undefined is provided', () => {
199
- const result = schema.safeParse(undefined)
200
- expect(result.success).toBe(true)
201
- if (result.success) {
202
- expect(result.value).toBe('default')
203
- }
204
- })
205
-
206
- it('does not apply default when explicit value is provided', () => {
207
- const result = schema.safeParse('explicit')
208
- expect(result.success).toBe(true)
209
- if (result.success) {
210
- expect(result.value).toBe('explicit')
211
- }
212
- })
213
-
214
- it('does not apply default when empty string is provided', () => {
215
- const result = schema.safeParse('')
216
- expect(result.success).toBe(true)
217
- if (result.success) {
218
- expect(result.value).toBe('')
219
- }
220
- })
221
- })
222
-
223
- describe('inner schema with default value and constraints', () => {
224
- const schema = optional(withDefault(string({ minLength: 5 }), 'default'))
225
-
226
- it('applies default value when undefined is provided', () => {
227
- const result = schema.safeParse(undefined)
228
- expect(result.success).toBe(true)
229
- if (result.success) {
230
- expect(result.value).toBe('default')
231
- }
232
- })
233
-
234
- it('validates explicit values against constraints', () => {
235
- const result = schema.safeParse('hello')
236
- expect(result.success).toBe(true)
237
- })
238
-
239
- it('rejects explicit values violating constraints', () => {
240
- const result = schema.safeParse('hi')
241
- expect(result.success).toBe(false)
242
- })
243
- })
244
-
245
- describe('inner schema with invalid default value', () => {
246
- const schema = optional(string({ default: 'bad', minLength: 5 }))
247
-
248
- it('returns undefined when default value violates constraints', () => {
249
- const result = schema.safeParse(undefined)
250
- expect(result.success).toBe(true)
251
- if (result.success) {
252
- expect(result.value).toBe(undefined)
253
- }
254
- })
255
-
256
- it('still validates conforming explicit values', () => {
257
- const result = schema.safeParse('valid')
258
- expect(result.success).toBe(true)
259
- })
260
- })
261
-
262
- describe('inner schema with integer default', () => {
263
- const schema = optional(withDefault(integer(), 42))
264
-
265
- it('applies default value when undefined is provided', () => {
266
- const result = schema.safeParse(undefined)
267
- expect(result.success).toBe(true)
268
- if (result.success) {
269
- expect(result.value).toBe(42)
270
- }
271
- })
272
-
273
- it('does not apply default when explicit value is provided', () => {
274
- const result = schema.safeParse(100)
275
- expect(result.success).toBe(true)
276
- if (result.success) {
277
- expect(result.value).toBe(100)
278
- }
279
- })
280
-
281
- it('does not apply default when zero is provided', () => {
282
- const result = schema.safeParse(0)
283
- expect(result.success).toBe(true)
284
- if (result.success) {
285
- expect(result.value).toBe(0)
286
- }
287
- })
288
- })
289
-
290
- describe('inner schema with boolean default', () => {
291
- const schema = optional(withDefault(boolean(), true))
292
-
293
- it('applies default value when undefined is provided', () => {
294
- const result = schema.safeParse(undefined)
295
- expect(result.success).toBe(true)
296
- if (result.success) {
297
- expect(result.value).toBe(true)
298
- }
299
- })
300
-
301
- it('does not apply default when explicit true is provided', () => {
302
- const result = schema.safeParse(true)
303
- expect(result.success).toBe(true)
304
- if (result.success) {
305
- expect(result.value).toBe(true)
306
- }
307
- })
308
-
309
- it('does not apply default when explicit false is provided', () => {
310
- const result = schema.safeParse(false)
311
- expect(result.success).toBe(true)
312
- if (result.success) {
313
- expect(result.value).toBe(false)
314
- }
315
- })
316
- })
317
-
318
- describe('edge cases', () => {
319
- const schema = optional(string())
320
-
321
- it('handles very long strings', () => {
322
- const longString = 'a'.repeat(10000)
323
- const result = schema.safeParse(longString)
324
- expect(result.success).toBe(true)
325
- })
326
-
327
- it('handles strings with special characters', () => {
328
- const result = schema.safeParse('hello\nworld\ttab')
329
- expect(result.success).toBe(true)
330
- })
331
-
332
- it('handles strings with unicode characters', () => {
333
- const result = schema.safeParse('Hello 世界 🌍')
334
- expect(result.success).toBe(true)
335
- })
336
-
337
- it('handles empty string distinctly from undefined', () => {
338
- const emptyResult = schema.safeParse('')
339
- expect(emptyResult.success).toBe(true)
340
- if (emptyResult.success) {
341
- expect(emptyResult.value).toBe('')
342
- }
343
-
344
- const undefinedResult = schema.safeParse(undefined)
345
- expect(undefinedResult.success).toBe(true)
346
- if (undefinedResult.success) {
347
- expect(undefinedResult.value).toBe(undefined)
348
- }
349
- })
350
- })
351
-
352
- describe('type distinctions', () => {
353
- it('distinguishes between zero and undefined for integers', () => {
354
- const schema = optional(integer())
355
-
356
- const zeroResult = schema.safeParse(0)
357
- expect(zeroResult.success).toBe(true)
358
- if (zeroResult.success) {
359
- expect(zeroResult.value).toBe(0)
360
- }
361
-
362
- const undefinedResult = schema.safeParse(undefined)
363
- expect(undefinedResult.success).toBe(true)
364
- if (undefinedResult.success) {
365
- expect(undefinedResult.value).toBe(undefined)
366
- }
367
- })
368
-
369
- it('distinguishes between false and undefined for booleans', () => {
370
- const schema = optional(boolean())
371
-
372
- const falseResult = schema.safeParse(false)
373
- expect(falseResult.success).toBe(true)
374
- if (falseResult.success) {
375
- expect(falseResult.value).toBe(false)
376
- }
377
-
378
- const undefinedResult = schema.safeParse(undefined)
379
- expect(undefinedResult.success).toBe(true)
380
- if (undefinedResult.success) {
381
- expect(undefinedResult.value).toBe(undefined)
382
- }
383
- })
384
-
385
- it('distinguishes between empty string and undefined for strings', () => {
386
- const schema = optional(string())
387
-
388
- const emptyResult = schema.safeParse('')
389
- expect(emptyResult.success).toBe(true)
390
- if (emptyResult.success) {
391
- expect(emptyResult.value).toBe('')
392
- }
393
-
394
- const undefinedResult = schema.safeParse(undefined)
395
- expect(undefinedResult.success).toBe(true)
396
- if (undefinedResult.success) {
397
- expect(undefinedResult.value).toBe(undefined)
398
- }
399
- })
400
- })
401
-
402
- describe('nested optional schemas', () => {
403
- const schema = optional(optional(string()))
404
-
405
- it('validates defined values through nested optionals', () => {
406
- const result = schema.safeParse('hello')
407
- expect(result.success).toBe(true)
408
- if (result.success) {
409
- expect(result.value).toBe('hello')
410
- }
411
- })
412
-
413
- it('validates undefined through nested optionals', () => {
414
- const result = schema.safeParse(undefined)
415
- expect(result.success).toBe(true)
416
- if (result.success) {
417
- expect(result.value).toBe(undefined)
418
- }
419
- })
420
-
421
- it('rejects invalid types through nested optionals', () => {
422
- const result = schema.safeParse(123)
423
- expect(result.success).toBe(false)
424
- })
425
- })
426
-
427
- describe('inner schema format constraints', () => {
428
- const schema = optional(string({ format: 'uri' }))
429
-
430
- it('validates values meeting format constraint', () => {
431
- const result = schema.safeParse('https://example.com')
432
- expect(result.success).toBe(true)
433
- })
434
-
435
- it('validates undefined', () => {
436
- const result = schema.safeParse(undefined)
437
- expect(result.success).toBe(true)
438
- })
439
-
440
- it('rejects values violating format constraint', () => {
441
- const result = schema.safeParse('not a uri')
442
- expect(result.success).toBe(false)
443
- })
444
- })
445
-
446
- describe('integer constraint validation', () => {
447
- const schema = optional(integer({ minimum: 0, maximum: 100 }))
448
-
449
- it('validates values within range', () => {
450
- const result = schema.safeParse(50)
451
- expect(result.success).toBe(true)
452
- })
453
-
454
- it('validates values at minimum boundary', () => {
455
- const result = schema.safeParse(0)
456
- expect(result.success).toBe(true)
457
- })
458
-
459
- it('validates values at maximum boundary', () => {
460
- const result = schema.safeParse(100)
461
- expect(result.success).toBe(true)
462
- })
463
-
464
- it('validates undefined', () => {
465
- const result = schema.safeParse(undefined)
466
- expect(result.success).toBe(true)
467
- })
468
-
469
- it('rejects values below minimum', () => {
470
- const result = schema.safeParse(-1)
471
- expect(result.success).toBe(false)
472
- })
473
-
474
- it('rejects values above maximum', () => {
475
- const result = schema.safeParse(101)
476
- expect(result.success).toBe(false)
477
- })
478
- })
479
- })
@@ -1,92 +0,0 @@
1
- import {
2
- InferInput,
3
- InferOutput,
4
- Schema,
5
- UnwrapValidator,
6
- ValidationContext,
7
- Validator,
8
- } from '../core.js'
9
- import { memoizedTransformer } from '../util/memoize.js'
10
- import { WithDefaultSchema } from './with-default.js'
11
-
12
- /**
13
- * Schema wrapper that makes a value optional (allows undefined).
14
- *
15
- * When the input is `undefined`, validation succeeds without running the
16
- * inner validator. If the inner validator has a default value (via `withDefault`),
17
- * that default will be applied in parse mode.
18
- *
19
- * @template TValidator - The wrapped validator type
20
- *
21
- * @example
22
- * ```ts
23
- * const schema = new OptionalSchema(l.string())
24
- * schema.validate(undefined) // success
25
- * schema.validate('hello') // success
26
- * ```
27
- */
28
- export class OptionalSchema<TValidator extends Validator> extends Schema<
29
- InferInput<TValidator> | undefined,
30
- UnwrapValidator<TValidator> extends WithDefaultSchema<infer TValidator>
31
- ? InferOutput<TValidator>
32
- : InferOutput<TValidator> | undefined
33
- > {
34
- readonly type = 'optional' as const
35
-
36
- constructor(readonly validator: TValidator) {
37
- super()
38
- }
39
-
40
- validateInContext(input: unknown, ctx: ValidationContext) {
41
- // Optimization: No need to apply child schema defaults in validation mode
42
- if (input === undefined && ctx.options.mode === 'validate') {
43
- return ctx.success(input)
44
- }
45
-
46
- // @NOTE The inner schema might apply a default value so we need to run it
47
- // even if input is undefined.
48
- const result = ctx.validate(input, this.validator)
49
-
50
- if (result.success) {
51
- return result
52
- }
53
-
54
- if (input === undefined) {
55
- return ctx.success(input)
56
- }
57
-
58
- return result
59
- }
60
- }
61
-
62
- /**
63
- * Creates an optional schema that allows undefined values.
64
- *
65
- * Wraps another schema to make it optional. When used in an object schema,
66
- * properties with optional schemas are not required.
67
- *
68
- * @param validator - The validator to make optional
69
- * @returns A new {@link OptionalSchema} instance
70
- *
71
- * @example
72
- * ```ts
73
- * // Optional string
74
- * const optionalBio = l.optional(l.string())
75
- *
76
- * // In an object - property is not required
77
- * const userSchema = l.object({
78
- * name: l.string(),
79
- * bio: l.optional(l.string()),
80
- * })
81
- * userSchema.parse({ name: 'Alice' }) // Valid, bio is undefined
82
- *
83
- * // With default value
84
- * const countSchema = l.optional(l.withDefault(l.integer(), 0))
85
- * countSchema.parse(undefined) // Returns 0
86
- * ```
87
- */
88
- export const optional = /*#__PURE__*/ memoizedTransformer(function <
89
- const TValidator extends Validator,
90
- >(validator: TValidator) {
91
- return new OptionalSchema<TValidator>(validator)
92
- })