@atproto/lex-schema 0.0.12 → 0.0.14

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 (210) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/core/schema.d.ts +27 -36
  3. package/dist/core/schema.d.ts.map +1 -1
  4. package/dist/core/schema.js +68 -54
  5. package/dist/core/schema.js.map +1 -1
  6. package/dist/core/string-format.d.ts +1 -14
  7. package/dist/core/string-format.d.ts.map +1 -1
  8. package/dist/core/string-format.js +12 -9
  9. package/dist/core/string-format.js.map +1 -1
  10. package/dist/core/validation-error.d.ts +5 -5
  11. package/dist/core/validation-error.d.ts.map +1 -1
  12. package/dist/core/validation-error.js +8 -8
  13. package/dist/core/validation-error.js.map +1 -1
  14. package/dist/core/validation-issue.js +3 -1
  15. package/dist/core/validation-issue.js.map +1 -1
  16. package/dist/core/validator.d.ts +16 -8
  17. package/dist/core/validator.d.ts.map +1 -1
  18. package/dist/core/validator.js +24 -6
  19. package/dist/core/validator.js.map +1 -1
  20. package/dist/helpers.d.ts +10 -11
  21. package/dist/helpers.d.ts.map +1 -1
  22. package/dist/helpers.js.map +1 -1
  23. package/dist/schema/array.d.ts +1 -0
  24. package/dist/schema/array.d.ts.map +1 -1
  25. package/dist/schema/array.js +2 -1
  26. package/dist/schema/array.js.map +1 -1
  27. package/dist/schema/blob.d.ts +4 -2
  28. package/dist/schema/blob.d.ts.map +1 -1
  29. package/dist/schema/blob.js +5 -2
  30. package/dist/schema/blob.js.map +1 -1
  31. package/dist/schema/boolean.d.ts +1 -0
  32. package/dist/schema/boolean.d.ts.map +1 -1
  33. package/dist/schema/boolean.js +2 -1
  34. package/dist/schema/boolean.js.map +1 -1
  35. package/dist/schema/bytes.d.ts +1 -0
  36. package/dist/schema/bytes.d.ts.map +1 -1
  37. package/dist/schema/bytes.js +2 -1
  38. package/dist/schema/bytes.js.map +1 -1
  39. package/dist/schema/cid.d.ts +1 -0
  40. package/dist/schema/cid.d.ts.map +1 -1
  41. package/dist/schema/cid.js +2 -1
  42. package/dist/schema/cid.js.map +1 -1
  43. package/dist/schema/custom.d.ts +1 -0
  44. package/dist/schema/custom.d.ts.map +1 -1
  45. package/dist/schema/custom.js +1 -0
  46. package/dist/schema/custom.js.map +1 -1
  47. package/dist/schema/dict.d.ts +1 -0
  48. package/dist/schema/dict.d.ts.map +1 -1
  49. package/dist/schema/dict.js +2 -1
  50. package/dist/schema/dict.js.map +1 -1
  51. package/dist/schema/discriminated-union.d.ts +1 -0
  52. package/dist/schema/discriminated-union.d.ts.map +1 -1
  53. package/dist/schema/discriminated-union.js +2 -1
  54. package/dist/schema/discriminated-union.js.map +1 -1
  55. package/dist/schema/enum.d.ts +1 -0
  56. package/dist/schema/enum.d.ts.map +1 -1
  57. package/dist/schema/enum.js +1 -0
  58. package/dist/schema/enum.js.map +1 -1
  59. package/dist/schema/integer.d.ts +1 -0
  60. package/dist/schema/integer.d.ts.map +1 -1
  61. package/dist/schema/integer.js +2 -1
  62. package/dist/schema/integer.js.map +1 -1
  63. package/dist/schema/intersection.d.ts +1 -0
  64. package/dist/schema/intersection.d.ts.map +1 -1
  65. package/dist/schema/intersection.js +1 -0
  66. package/dist/schema/intersection.js.map +1 -1
  67. package/dist/schema/lex-map.d.ts +37 -0
  68. package/dist/schema/lex-map.d.ts.map +1 -0
  69. package/dist/schema/lex-map.js +60 -0
  70. package/dist/schema/lex-map.js.map +1 -0
  71. package/dist/schema/lex-value.d.ts +35 -0
  72. package/dist/schema/lex-value.d.ts.map +1 -0
  73. package/dist/schema/lex-value.js +87 -0
  74. package/dist/schema/lex-value.js.map +1 -0
  75. package/dist/schema/literal.d.ts +1 -0
  76. package/dist/schema/literal.d.ts.map +1 -1
  77. package/dist/schema/literal.js +1 -0
  78. package/dist/schema/literal.js.map +1 -1
  79. package/dist/schema/never.d.ts +1 -0
  80. package/dist/schema/never.d.ts.map +1 -1
  81. package/dist/schema/never.js +2 -1
  82. package/dist/schema/never.js.map +1 -1
  83. package/dist/schema/null.d.ts +1 -0
  84. package/dist/schema/null.d.ts.map +1 -1
  85. package/dist/schema/null.js +2 -1
  86. package/dist/schema/null.js.map +1 -1
  87. package/dist/schema/nullable.d.ts +1 -0
  88. package/dist/schema/nullable.d.ts.map +1 -1
  89. package/dist/schema/nullable.js +1 -0
  90. package/dist/schema/nullable.js.map +1 -1
  91. package/dist/schema/object.d.ts +1 -0
  92. package/dist/schema/object.d.ts.map +1 -1
  93. package/dist/schema/object.js +2 -1
  94. package/dist/schema/object.js.map +1 -1
  95. package/dist/schema/optional.d.ts +1 -0
  96. package/dist/schema/optional.d.ts.map +1 -1
  97. package/dist/schema/optional.js +1 -0
  98. package/dist/schema/optional.js.map +1 -1
  99. package/dist/schema/params.d.ts +14 -10
  100. package/dist/schema/params.d.ts.map +1 -1
  101. package/dist/schema/params.js +87 -24
  102. package/dist/schema/params.js.map +1 -1
  103. package/dist/schema/payload.d.ts.map +1 -1
  104. package/dist/schema/payload.js +3 -3
  105. package/dist/schema/payload.js.map +1 -1
  106. package/dist/schema/record.d.ts +21 -19
  107. package/dist/schema/record.d.ts.map +1 -1
  108. package/dist/schema/record.js +22 -12
  109. package/dist/schema/record.js.map +1 -1
  110. package/dist/schema/ref.d.ts +1 -0
  111. package/dist/schema/ref.d.ts.map +1 -1
  112. package/dist/schema/ref.js +1 -0
  113. package/dist/schema/ref.js.map +1 -1
  114. package/dist/schema/regexp.d.ts +1 -0
  115. package/dist/schema/regexp.d.ts.map +1 -1
  116. package/dist/schema/regexp.js +2 -1
  117. package/dist/schema/regexp.js.map +1 -1
  118. package/dist/schema/string.d.ts +22 -6
  119. package/dist/schema/string.d.ts.map +1 -1
  120. package/dist/schema/string.js +16 -9
  121. package/dist/schema/string.js.map +1 -1
  122. package/dist/schema/token.d.ts +1 -0
  123. package/dist/schema/token.d.ts.map +1 -1
  124. package/dist/schema/token.js +2 -1
  125. package/dist/schema/token.js.map +1 -1
  126. package/dist/schema/typed-object.d.ts +20 -16
  127. package/dist/schema/typed-object.d.ts.map +1 -1
  128. package/dist/schema/typed-object.js +23 -13
  129. package/dist/schema/typed-object.js.map +1 -1
  130. package/dist/schema/typed-ref.d.ts +1 -0
  131. package/dist/schema/typed-ref.d.ts.map +1 -1
  132. package/dist/schema/typed-ref.js +1 -0
  133. package/dist/schema/typed-ref.js.map +1 -1
  134. package/dist/schema/typed-union.d.ts +1 -0
  135. package/dist/schema/typed-union.d.ts.map +1 -1
  136. package/dist/schema/typed-union.js +2 -1
  137. package/dist/schema/typed-union.js.map +1 -1
  138. package/dist/schema/union.d.ts +1 -0
  139. package/dist/schema/union.d.ts.map +1 -1
  140. package/dist/schema/union.js +2 -1
  141. package/dist/schema/union.js.map +1 -1
  142. package/dist/schema/unknown.d.ts +1 -0
  143. package/dist/schema/unknown.d.ts.map +1 -1
  144. package/dist/schema/unknown.js +1 -0
  145. package/dist/schema/unknown.js.map +1 -1
  146. package/dist/schema/with-default.d.ts +1 -0
  147. package/dist/schema/with-default.d.ts.map +1 -1
  148. package/dist/schema/with-default.js +1 -0
  149. package/dist/schema/with-default.js.map +1 -1
  150. package/dist/schema.d.ts +2 -1
  151. package/dist/schema.d.ts.map +1 -1
  152. package/dist/schema.js +2 -1
  153. package/dist/schema.js.map +1 -1
  154. package/dist/util/if-any.d.ts +2 -0
  155. package/dist/util/if-any.d.ts.map +1 -0
  156. package/dist/util/if-any.js +3 -0
  157. package/dist/util/if-any.js.map +1 -0
  158. package/package.json +3 -3
  159. package/src/core/schema.ts +76 -62
  160. package/src/core/string-format.ts +14 -17
  161. package/src/core/validation-error.ts +10 -10
  162. package/src/core/validation-issue.ts +3 -2
  163. package/src/core/validator.ts +32 -12
  164. package/src/helpers.test.ts +1 -1
  165. package/src/helpers.ts +53 -19
  166. package/src/schema/array.ts +3 -1
  167. package/src/schema/blob.ts +4 -1
  168. package/src/schema/boolean.ts +3 -1
  169. package/src/schema/bytes.ts +3 -1
  170. package/src/schema/cid.ts +3 -1
  171. package/src/schema/custom.ts +2 -0
  172. package/src/schema/dict.ts +3 -1
  173. package/src/schema/discriminated-union.ts +3 -1
  174. package/src/schema/enum.ts +2 -0
  175. package/src/schema/integer.ts +3 -1
  176. package/src/schema/intersection.ts +2 -0
  177. package/src/schema/{unknown-object.test.ts → lex-map.test.ts} +9 -9
  178. package/src/schema/lex-map.ts +63 -0
  179. package/src/schema/lex-value.test.ts +81 -0
  180. package/src/schema/lex-value.ts +86 -0
  181. package/src/schema/literal.ts +2 -0
  182. package/src/schema/never.ts +3 -1
  183. package/src/schema/null.ts +3 -1
  184. package/src/schema/nullable.ts +2 -0
  185. package/src/schema/object.ts +3 -1
  186. package/src/schema/optional.ts +2 -0
  187. package/src/schema/params.test.ts +98 -43
  188. package/src/schema/params.ts +136 -39
  189. package/src/schema/payload.test.ts +2 -2
  190. package/src/schema/payload.ts +3 -4
  191. package/src/schema/record.ts +38 -22
  192. package/src/schema/ref.ts +2 -0
  193. package/src/schema/regexp.ts +3 -1
  194. package/src/schema/string.test.ts +99 -2
  195. package/src/schema/string.ts +58 -15
  196. package/src/schema/token.ts +3 -1
  197. package/src/schema/typed-object.test.ts +38 -0
  198. package/src/schema/typed-object.ts +40 -24
  199. package/src/schema/typed-ref.ts +2 -0
  200. package/src/schema/typed-union.ts +3 -1
  201. package/src/schema/union.ts +4 -2
  202. package/src/schema/unknown.ts +2 -0
  203. package/src/schema/with-default.ts +2 -0
  204. package/src/schema.ts +2 -1
  205. package/src/util/if-any.ts +3 -0
  206. package/dist/schema/unknown-object.d.ts +0 -42
  207. package/dist/schema/unknown-object.d.ts.map +0 -1
  208. package/dist/schema/unknown-object.js +0 -50
  209. package/dist/schema/unknown-object.js.map +0 -1
  210. package/src/schema/unknown-object.ts +0 -53
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { string } from './string.js'
1
+ import { describe, expect, expectTypeOf, it } from 'vitest'
2
+ import { Infer, UnknownString } from '../core.js'
3
+ import { StringSchemaOptions, string } from './string.js'
3
4
  import { token } from './token.js'
4
5
  import { withDefault } from './with-default.js'
5
6
 
@@ -610,4 +611,100 @@ describe('StringSchema', () => {
610
611
  expect(result.success).toBe(true)
611
612
  })
612
613
  })
614
+
615
+ describe('knownValues option', () => {
616
+ it('allows omitting knownValues at runtime', () => {
617
+ string<{ knownValues: ['active', 'inactive'] }>()
618
+
619
+ // @ts-expect-error format requires options to be set
620
+ string<{ knownValues: ['active', 'inactive']; format: 'did' }>()
621
+
622
+ // @ts-expect-error any options, besides knownValues, must be provided
623
+ string<{ knownValues: ['active', 'inactive']; minLength: 5 }>()
624
+
625
+ string<{
626
+ knownValues: ['john.doe', 'someone.else']
627
+ format: 'handle'
628
+ }>({
629
+ format: 'handle',
630
+ })
631
+
632
+ string<{
633
+ knownValues: ['john.doe', 'someone.else']
634
+ }>({
635
+ // Being *more* precise than the generic if fine
636
+ format: 'handle',
637
+ })
638
+
639
+ string<{
640
+ knownValues: ['did', 'inactive']
641
+ format: 'did'
642
+ }>({
643
+ // @ts-expect-error does not match format form generic constraint
644
+ format: 'handle',
645
+ })
646
+
647
+ string<{
648
+ knownValues: ['active', 'inactive']
649
+ minLength: 10
650
+ }>({
651
+ minLength: 10,
652
+ })
653
+
654
+ string<{
655
+ knownValues: ['active', 'inactive']
656
+ minLength: 5
657
+ }>({
658
+ // @ts-expect-error mismatch
659
+ minLength: 10,
660
+ })
661
+ })
662
+ })
663
+
664
+ it('properly types knownValues in parameters', () => {
665
+ const schema = string({
666
+ knownValues: ['active', 'inactive'],
667
+ })
668
+ type SchemaType = Infer<typeof schema>
669
+ expectTypeOf<{
670
+ foo: SchemaType
671
+ }>().toMatchObjectType<{
672
+ foo: 'active' | 'inactive' | UnknownString
673
+ }>()
674
+ expectTypeOf<{
675
+ foo: SchemaType
676
+ }>().not.toMatchObjectType<{
677
+ foo: string
678
+ }>()
679
+ expectTypeOf<{
680
+ foo: SchemaType
681
+ }>().not.toMatchObjectType<{
682
+ foo: 'active' | 'inactive'
683
+ }>()
684
+ expectTypeOf<{
685
+ foo: SchemaType
686
+ }>().not.toMatchObjectType<{
687
+ foo: UnknownString
688
+ }>()
689
+ })
690
+
691
+ it('type string<any>() as string', () => {
692
+ const schema = string<any>()
693
+ type SchemaType = Infer<typeof schema>
694
+ expectTypeOf<{
695
+ foo: SchemaType
696
+ }>().toMatchObjectType<{
697
+ foo: string
698
+ }>()
699
+ })
700
+
701
+ it('type string<StringSchemaOptions>({}) as string', () => {
702
+ const schema = string<StringSchemaOptions>({})
703
+ type SchemaType = Infer<typeof schema>
704
+ expectTypeOf<{
705
+ foo: SchemaType
706
+ }>().toMatchObjectType<{
707
+ foo: string
708
+ }>()
709
+ })
613
710
  })
@@ -1,11 +1,14 @@
1
1
  import { graphemeLen, ifCid, utf8Len } from '@atproto/lex-data'
2
2
  import {
3
3
  InferStringFormat,
4
+ Restricted,
4
5
  Schema,
5
6
  StringFormat,
7
+ UnknownString,
6
8
  ValidationContext,
7
9
  isStringFormat,
8
10
  } from '../core.js'
11
+ import { IfAny } from '../util/if-any.js'
9
12
  import { memoizedOptions } from '../util/memoize.js'
10
13
  import { TokenSchema } from './token.js'
11
14
 
@@ -13,6 +16,7 @@ import { TokenSchema } from './token.js'
13
16
  * Configuration options for string schema validation.
14
17
  *
15
18
  * @property format - Expected string format (e.g., 'datetime', 'uri', 'at-uri', 'did', 'handle', 'nsid', 'cid', 'tid', 'record-key', 'at-identifier', 'language')
19
+ * @property knownValues - Known string literal values for type narrowing
16
20
  * @property minLength - Minimum length in UTF-8 bytes
17
21
  * @property maxLength - Maximum length in UTF-8 bytes
18
22
  * @property minGraphemes - Minimum number of grapheme clusters
@@ -20,6 +24,7 @@ import { TokenSchema } from './token.js'
20
24
  */
21
25
  export type StringSchemaOptions = {
22
26
  format?: StringFormat
27
+ knownValues?: readonly string[]
23
28
  minLength?: number
24
29
  maxLength?: number
25
30
  minGraphemes?: number
@@ -43,30 +48,46 @@ export type StringSchemaOptions = {
43
48
  export class StringSchema<
44
49
  const TOptions extends StringSchemaOptions = StringSchemaOptions,
45
50
  > extends Schema<
46
- TOptions extends { format: infer F extends StringFormat }
47
- ? InferStringFormat<F>
48
- : string
51
+ IfAny<
52
+ TOptions,
53
+ string,
54
+ TOptions extends { format: infer F extends StringFormat }
55
+ ? InferStringFormat<F>
56
+ : TOptions extends { knownValues: readonly (infer V extends string)[] }
57
+ ? V | UnknownString
58
+ : string
59
+ >
49
60
  > {
50
- constructor(readonly options?: TOptions) {
61
+ readonly type = 'string' as const
62
+
63
+ // @NOTE since the _string utility allows omitting knownValues when TOptions
64
+ // *does* include it (since it's only used for typing), we cannot type options
65
+ // as TOptions directly since it may not actually include knownValues at
66
+ // runtime, making schema.options.knownValues potentially undefined even when
67
+ // TOptions includes it.
68
+ readonly options: StringSchemaOptions
69
+
70
+ constructor(options: TOptions) {
51
71
  super()
72
+ this.options = options
52
73
  }
53
74
 
54
75
  validateInContext(input: unknown, ctx: ValidationContext) {
55
76
  const str = coerceToString(input)
56
77
  if (str == null) {
57
- return ctx.issueInvalidType(input, 'string')
78
+ return ctx.issueUnexpectedType(input, 'string')
58
79
  }
59
80
 
60
81
  let lazyUtf8Len: number
61
82
 
62
- const minLength = this.options?.minLength
83
+ const minLength = this.options.minLength
63
84
  if (minLength != null) {
64
85
  if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {
65
86
  return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)
66
87
  }
67
88
  }
68
89
 
69
- const maxLength = this.options?.maxLength
90
+ const maxLength = this.options.maxLength
70
91
  if (maxLength != null) {
71
92
  // Optimization: we can avoid computing the UTF-8 length if the maximum
72
93
  // possible length, in bytes, of the input JS string is smaller than the
@@ -80,7 +101,7 @@ export class StringSchema<
80
101
 
81
102
  let lazyGraphLen: number
82
103
 
83
- const minGraphemes = this.options?.minGraphemes
104
+ const minGraphemes = this.options.minGraphemes
84
105
  if (minGraphemes != null) {
85
106
  // Optimization: avoid counting graphemes if the length check already fails
86
107
  if (str.length < minGraphemes) {
@@ -90,14 +111,14 @@ export class StringSchema<
90
111
  }
91
112
  }
92
113
 
93
- const maxGraphemes = this.options?.maxGraphemes
114
+ const maxGraphemes = this.options.maxGraphemes
94
115
  if (maxGraphemes != null) {
95
116
  if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
96
117
  return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
97
118
  }
98
119
  }
99
120
 
100
- const format = this.options?.format
121
+ const format = this.options.format
101
122
  if (format != null && !isStringFormat(str, format)) {
102
123
  return ctx.issueInvalidFormat(str, format)
103
124
  }
@@ -146,6 +167,32 @@ export function coerceToString(input: unknown): string | null {
146
167
  }
147
168
  }
148
169
 
170
+ function _string(): StringSchema<NonNullable<unknown>>
171
+ function _string<
172
+ // Allow calling `string<{ knownValues: [...] }>()` without passing an options
173
+ // object, since knownValues is only used for typing and has no runtime
174
+ // effect, so it can be safely omitted at runtime.
175
+ const TOptions extends {
176
+ knownValues: StringSchemaOptions['knownValues']
177
+ } & {
178
+ [K in Exclude<
179
+ keyof StringSchemaOptions,
180
+ 'knownValues'
181
+ >]?: Restricted<`An options argument is required when using the "${K}" option`>
182
+ },
183
+ >(): StringSchema<
184
+ IfAny<TOptions, any, { knownValues: TOptions['knownValues'] }>
185
+ >
186
+ function _string<const TOptions extends StringSchemaOptions>(
187
+ // If TOptions is explicitly provided (e.g. `string<{ ... }>({ ... })`), we
188
+ // allow the actual options argument to omit the "knownValues" property since
189
+ // it's only used for inferring the type and has no runtime effect.
190
+ options: TOptions | Omit<TOptions, 'knownValues'>,
191
+ ): StringSchema<TOptions>
192
+ function _string(options: StringSchemaOptions = {}) {
193
+ return new StringSchema(options)
194
+ }
195
+
149
196
  /**
150
197
  * Creates a string schema with optional format and length constraints.
151
198
  *
@@ -173,8 +220,4 @@ export function coerceToString(input: unknown): string | null {
173
220
  * const handleSchema = l.string({ format: 'handle', minLength: 3, maxLength: 253 })
174
221
  * ```
175
222
  */
176
- export const string = /*#__PURE__*/ memoizedOptions(function <
177
- const O extends StringSchemaOptions = NonNullable<unknown>,
178
- >(options?: StringSchemaOptions & O) {
179
- return new StringSchema<O>(options)
180
- })
223
+ export const string = /*#__PURE__*/ memoizedOptions(_string)
@@ -18,6 +18,8 @@ import { $type, NsidString, Schema, ValidationContext } from '../core.js'
18
18
  export class TokenSchema<
19
19
  const TValue extends string = string,
20
20
  > extends Schema<TValue> {
21
+ readonly type = 'token' as const
22
+
21
23
  constructor(readonly value: TValue) {
22
24
  super()
23
25
  }
@@ -34,7 +36,7 @@ export class TokenSchema<
34
36
  }
35
37
 
36
38
  if (typeof input !== 'string') {
37
- return ctx.issueInvalidType(input, 'token')
39
+ return ctx.issueUnexpectedType(input, 'token')
38
40
  }
39
41
 
40
42
  return ctx.issueInvalidValue(input, [this.value])
@@ -332,6 +332,44 @@ describe('TypedObjectSchema', () => {
332
332
  })
333
333
  })
334
334
 
335
+ describe('bound $ methods', () => {
336
+ it('$build can be used as a detached function', () => {
337
+ const { $build } = schema
338
+ const result = $build({ text: 'Hello' })
339
+ expect(result).toEqual({
340
+ text: 'Hello',
341
+ $type: 'app.bsky.feed.post',
342
+ })
343
+ })
344
+
345
+ it('$isTypeOf can be used as a detached function', () => {
346
+ const { $isTypeOf } = schema
347
+ expect($isTypeOf({ text: 'Hello' })).toBe(true)
348
+ expect($isTypeOf({ $type: 'app.bsky.feed.post', text: 'Hello' })).toBe(
349
+ true,
350
+ )
351
+ expect($isTypeOf({ $type: 'other.type', text: 'Hello' })).toBe(false)
352
+ })
353
+
354
+ it('$parse can be used as a detached function', () => {
355
+ const { $parse } = schema
356
+ const result = $parse({ text: 'Hello' })
357
+ expect(result).toEqual({ text: 'Hello' })
358
+ })
359
+
360
+ it('$matches can be used as a detached function', () => {
361
+ const { $matches } = schema
362
+ expect($matches({ text: 'Hello' })).toBe(true)
363
+ expect($matches(42)).toBe(false)
364
+ })
365
+
366
+ it('lazy property returns the same function on repeated access', () => {
367
+ const fn1 = schema.$build
368
+ const fn2 = schema.$build
369
+ expect(fn1).toBe(fn2)
370
+ })
371
+ })
372
+
335
373
  describe('with complex nested schemas', () => {
336
374
  const complexSchema = typedObject(
337
375
  'app.bsky.actor.profile',
@@ -14,6 +14,14 @@ import {
14
14
  ValidationContext,
15
15
  Validator,
16
16
  } from '../core.js'
17
+ import { lazyProperty } from '../util/lazy-property.js'
18
+
19
+ export type MaybeTypedObject<
20
+ TType extends $Type,
21
+ TValue extends { $type?: unknown } = { $type?: unknown },
22
+ > = TValue extends { $type?: TType }
23
+ ? TValue
24
+ : $TypedMaybe<Exclude<TValue, Unknown$TypedObject>, TType>
17
25
 
18
26
  /**
19
27
  * Schema for typed objects in Lexicon unions.
@@ -40,6 +48,8 @@ export class TypedObjectSchema<
40
48
  $TypedMaybe<InferInput<TShape>, TType>,
41
49
  $TypedMaybe<InferOutput<TShape>, TType>
42
50
  > {
51
+ readonly type = 'typedObject' as const
52
+
43
53
  constructor(
44
54
  readonly $type: TType,
45
55
  readonly schema: TShape,
@@ -47,12 +57,20 @@ export class TypedObjectSchema<
47
57
  super()
48
58
  }
49
59
 
50
- isTypeOf<X extends Record<string, unknown>>(
51
- value: X,
52
- ): value is X extends { $type?: TType }
53
- ? X
54
- : $TypedMaybe<Exclude<X, Unknown$TypedObject>, TType> {
55
- return value.$type === undefined || value.$type === this.$type
60
+ validateInContext(input: unknown, ctx: ValidationContext) {
61
+ if (!isPlainObject(input)) {
62
+ return ctx.issueUnexpectedType(input, 'object')
63
+ }
64
+
65
+ if (
66
+ '$type' in input &&
67
+ input.$type !== undefined &&
68
+ input.$type !== this.$type
69
+ ) {
70
+ return ctx.issueInvalidPropertyValue(input, '$type', [this.$type])
71
+ }
72
+
73
+ return ctx.validate(input, this.schema)
56
74
  }
57
75
 
58
76
  build(
@@ -64,28 +82,26 @@ export class TypedObjectSchema<
64
82
  >
65
83
  }
66
84
 
67
- $isTypeOf<X extends Record<string, unknown>>(value: X) {
68
- return this.isTypeOf(value)
85
+ isTypeOf<TValue extends Record<string, unknown>>(
86
+ value: TValue,
87
+ ): value is MaybeTypedObject<TType, TValue> {
88
+ return value.$type === undefined || value.$type === this.$type
69
89
  }
70
90
 
71
- $build(input: Omit<InferInput<this>, '$type'>) {
72
- return this.build(input)
91
+ /**
92
+ * Bound alias for {@link build} for compatibility with generated utilities.
93
+ * @see {@link build}
94
+ */
95
+ get $build(): typeof this.build {
96
+ return lazyProperty(this, '$build', this.build.bind(this))
73
97
  }
74
98
 
75
- validateInContext(input: unknown, ctx: ValidationContext) {
76
- if (!isPlainObject(input)) {
77
- return ctx.issueInvalidType(input, 'object')
78
- }
79
-
80
- if (
81
- '$type' in input &&
82
- input.$type !== undefined &&
83
- input.$type !== this.$type
84
- ) {
85
- return ctx.issueInvalidPropertyValue(input, '$type', [this.$type])
86
- }
87
-
88
- return ctx.validate(input, this.schema)
99
+ /**
100
+ * Bound alias for {@link isTypeOf} for compatibility with generated utilities.
101
+ * @see {@link isTypeOf}
102
+ */
103
+ get $isTypeOf(): typeof this.isTypeOf {
104
+ return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this))
89
105
  }
90
106
  }
91
107
 
@@ -51,6 +51,8 @@ export class TypedRefSchema<
51
51
  $Typed<InferInput<TValidator>>,
52
52
  $Typed<InferOutput<TValidator>>
53
53
  > {
54
+ readonly type = 'typedRef' as const
55
+
54
56
  #getter: TypedRefGetter<TValidator>
55
57
 
56
58
  constructor(getter: TypedRefGetter<TValidator>) {
@@ -42,6 +42,8 @@ export class TypedUnionSchema<
42
42
  ? InferOutput<TValidators[number]>
43
43
  : InferOutput<TValidators[number]> | Unknown$TypedObject
44
44
  > {
45
+ readonly type = 'typedUnion' as const
46
+
45
47
  constructor(
46
48
  protected readonly validators: TValidators,
47
49
  public readonly closed: TClosed,
@@ -67,7 +69,7 @@ export class TypedUnionSchema<
67
69
 
68
70
  validateInContext(input: unknown, ctx: ValidationContext) {
69
71
  if (!isPlainObject(input) || !('$type' in input)) {
70
- return ctx.issueInvalidType(input, '$typed')
72
+ return ctx.issueUnexpectedType(input, '$typed')
71
73
  }
72
74
 
73
75
  const { $type } = input
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  InferInput,
3
3
  InferOutput,
4
+ LexValidationError,
4
5
  Schema,
5
6
  ValidationContext,
6
- ValidationError,
7
7
  ValidationFailure,
8
8
  Validator,
9
9
  } from '../core.js'
@@ -37,6 +37,8 @@ export class UnionSchema<
37
37
  InferInput<TValidators[number]>,
38
38
  InferOutput<TValidators[number]>
39
39
  > {
40
+ readonly type = 'union' as const
41
+
40
42
  constructor(protected readonly validators: TValidators) {
41
43
  super()
42
44
  }
@@ -51,7 +53,7 @@ export class UnionSchema<
51
53
  failures.push(result)
52
54
  }
53
55
 
54
- return ctx.failure(ValidationError.fromFailures(failures))
56
+ return ctx.failure(LexValidationError.fromFailures(failures))
55
57
  }
56
58
  }
57
59
 
@@ -15,6 +15,8 @@ import { memoizedOptions } from '../util/memoize.js'
15
15
  * ```
16
16
  */
17
17
  export class UnknownSchema extends Schema<unknown> {
18
+ readonly type = 'unknown' as const
19
+
18
20
  validateInContext(input: unknown, ctx: ValidationContext) {
19
21
  return ctx.success(input)
20
22
  }
@@ -25,6 +25,8 @@ import {
25
25
  export class WithDefaultSchema<
26
26
  const TValidator extends Validator,
27
27
  > extends Schema<InferInput<TValidator>, InferOutput<TValidator>> {
28
+ readonly type = 'withDefault' as const
29
+
28
30
  constructor(
29
31
  readonly validator: TValidator,
30
32
  readonly defaultValue: InferInput<TValidator>,
package/src/schema.ts CHANGED
@@ -7,13 +7,14 @@ export * from './schema/cid.js'
7
7
  export * from './schema/dict.js'
8
8
  export * from './schema/enum.js'
9
9
  export * from './schema/integer.js'
10
+ export * from './schema/lex-map.js'
11
+ export * from './schema/lex-value.js'
10
12
  export * from './schema/literal.js'
11
13
  export * from './schema/never.js'
12
14
  export * from './schema/null.js'
13
15
  export * from './schema/object.js'
14
16
  export * from './schema/regexp.js'
15
17
  export * from './schema/string.js'
16
- export * from './schema/unknown-object.js'
17
18
  export * from './schema/unknown.js'
18
19
 
19
20
  // Composite Types
@@ -0,0 +1,3 @@
1
+ export type IfAny<T, TrueValue, FalseValue> = 0 extends 1 & T
2
+ ? TrueValue
3
+ : FalseValue
@@ -1,42 +0,0 @@
1
- import { LexMap } from '@atproto/lex-data';
2
- import { Schema, ValidationContext } from '../core.js';
3
- /**
4
- * Type alias for a plain object with unknown values.
5
- */
6
- export type UnknownObject = LexMap;
7
- /**
8
- * Schema that accepts any plain object without validating its properties.
9
- *
10
- * Validates that the input is a plain object (not an array, Date, or other
11
- * special object type), but does not validate the object's properties.
12
- *
13
- * @example
14
- * ```ts
15
- * const schema = new UnknownObjectSchema()
16
- * schema.validate({ any: 'props' }) // success
17
- * schema.validate([1, 2, 3]) // fails - arrays not accepted
18
- * ```
19
- */
20
- export declare class UnknownObjectSchema extends Schema<UnknownObject> {
21
- validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").ValidationResult<LexMap>;
22
- }
23
- /**
24
- * Creates a schema that accepts any plain object.
25
- *
26
- * Unlike `l.unknown()` which accepts any value, this validates that the input
27
- * is specifically a plain object (not an array, null, or primitive).
28
- *
29
- * @returns A new {@link UnknownObjectSchema} instance
30
- *
31
- * @example
32
- * ```ts
33
- * // Accept any object shape
34
- * const metadataSchema = l.unknownObject()
35
- *
36
- * metadataSchema.parse({ foo: 1, bar: 'baz' }) // success
37
- * metadataSchema.parse([1, 2, 3]) // throws - not a plain object
38
- * metadataSchema.parse(null) // throws
39
- * ```
40
- */
41
- export declare const unknownObject: () => UnknownObjectSchema;
42
- //# sourceMappingURL=unknown-object.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"unknown-object.d.ts","sourceRoot":"","sources":["../../src/schema/unknown-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAY,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAGtD;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAA;AAElC;;;;;;;;;;;;GAYG;AACH,qBAAa,mBAAoB,SAAQ,MAAM,CAAC,aAAa,CAAC;IAC5D,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,iBAAiB;CAOzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,aAAa,2BAExB,CAAA"}
@@ -1,50 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.unknownObject = exports.UnknownObjectSchema = void 0;
4
- const lex_data_1 = require("@atproto/lex-data");
5
- const core_js_1 = require("../core.js");
6
- const memoize_js_1 = require("../util/memoize.js");
7
- /**
8
- * Schema that accepts any plain object without validating its properties.
9
- *
10
- * Validates that the input is a plain object (not an array, Date, or other
11
- * special object type), but does not validate the object's properties.
12
- *
13
- * @example
14
- * ```ts
15
- * const schema = new UnknownObjectSchema()
16
- * schema.validate({ any: 'props' }) // success
17
- * schema.validate([1, 2, 3]) // fails - arrays not accepted
18
- * ```
19
- */
20
- class UnknownObjectSchema extends core_js_1.Schema {
21
- validateInContext(input, ctx) {
22
- if ((0, lex_data_1.isLexMap)(input)) {
23
- return ctx.success(input);
24
- }
25
- return ctx.issueInvalidType(input, 'unknown');
26
- }
27
- }
28
- exports.UnknownObjectSchema = UnknownObjectSchema;
29
- /**
30
- * Creates a schema that accepts any plain object.
31
- *
32
- * Unlike `l.unknown()` which accepts any value, this validates that the input
33
- * is specifically a plain object (not an array, null, or primitive).
34
- *
35
- * @returns A new {@link UnknownObjectSchema} instance
36
- *
37
- * @example
38
- * ```ts
39
- * // Accept any object shape
40
- * const metadataSchema = l.unknownObject()
41
- *
42
- * metadataSchema.parse({ foo: 1, bar: 'baz' }) // success
43
- * metadataSchema.parse([1, 2, 3]) // throws - not a plain object
44
- * metadataSchema.parse(null) // throws
45
- * ```
46
- */
47
- exports.unknownObject = (0, memoize_js_1.memoizedOptions)(function () {
48
- return new UnknownObjectSchema();
49
- });
50
- //# sourceMappingURL=unknown-object.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"unknown-object.js","sourceRoot":"","sources":["../../src/schema/unknown-object.ts"],"names":[],"mappings":";;;AAAA,gDAAoD;AACpD,wCAAsD;AACtD,mDAAoD;AAOpD;;;;;;;;;;;;GAYG;AACH,MAAa,mBAAoB,SAAQ,gBAAqB;IAC5D,iBAAiB,CAAC,KAAc,EAAE,GAAsB;QACtD,IAAI,IAAA,mBAAQ,EAAC,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;QAED,OAAO,GAAG,CAAC,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAC/C,CAAC;CACF;AARD,kDAQC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,aAAa,GAAiB,IAAA,4BAAe,EAAC;IACzD,OAAO,IAAI,mBAAmB,EAAE,CAAA;AAClC,CAAC,CAAC,CAAA","sourcesContent":["import { LexMap, isLexMap } from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Type alias for a plain object with unknown values.\n */\nexport type UnknownObject = LexMap\n\n/**\n * Schema that accepts any plain object without validating its properties.\n *\n * Validates that the input is a plain object (not an array, Date, or other\n * special object type), but does not validate the object's properties.\n *\n * @example\n * ```ts\n * const schema = new UnknownObjectSchema()\n * schema.validate({ any: 'props' }) // success\n * schema.validate([1, 2, 3]) // fails - arrays not accepted\n * ```\n */\nexport class UnknownObjectSchema extends Schema<UnknownObject> {\n validateInContext(input: unknown, ctx: ValidationContext) {\n if (isLexMap(input)) {\n return ctx.success(input)\n }\n\n return ctx.issueInvalidType(input, 'unknown')\n }\n}\n\n/**\n * Creates a schema that accepts any plain object.\n *\n * Unlike `l.unknown()` which accepts any value, this validates that the input\n * is specifically a plain object (not an array, null, or primitive).\n *\n * @returns A new {@link UnknownObjectSchema} instance\n *\n * @example\n * ```ts\n * // Accept any object shape\n * const metadataSchema = l.unknownObject()\n *\n * metadataSchema.parse({ foo: 1, bar: 'baz' }) // success\n * metadataSchema.parse([1, 2, 3]) // throws - not a plain object\n * metadataSchema.parse(null) // throws\n * ```\n */\nexport const unknownObject = /*#__PURE__*/ memoizedOptions(function () {\n return new UnknownObjectSchema()\n})\n"]}
@@ -1,53 +0,0 @@
1
- import { LexMap, isLexMap } from '@atproto/lex-data'
2
- import { Schema, ValidationContext } from '../core.js'
3
- import { memoizedOptions } from '../util/memoize.js'
4
-
5
- /**
6
- * Type alias for a plain object with unknown values.
7
- */
8
- export type UnknownObject = LexMap
9
-
10
- /**
11
- * Schema that accepts any plain object without validating its properties.
12
- *
13
- * Validates that the input is a plain object (not an array, Date, or other
14
- * special object type), but does not validate the object's properties.
15
- *
16
- * @example
17
- * ```ts
18
- * const schema = new UnknownObjectSchema()
19
- * schema.validate({ any: 'props' }) // success
20
- * schema.validate([1, 2, 3]) // fails - arrays not accepted
21
- * ```
22
- */
23
- export class UnknownObjectSchema extends Schema<UnknownObject> {
24
- validateInContext(input: unknown, ctx: ValidationContext) {
25
- if (isLexMap(input)) {
26
- return ctx.success(input)
27
- }
28
-
29
- return ctx.issueInvalidType(input, 'unknown')
30
- }
31
- }
32
-
33
- /**
34
- * Creates a schema that accepts any plain object.
35
- *
36
- * Unlike `l.unknown()` which accepts any value, this validates that the input
37
- * is specifically a plain object (not an array, null, or primitive).
38
- *
39
- * @returns A new {@link UnknownObjectSchema} instance
40
- *
41
- * @example
42
- * ```ts
43
- * // Accept any object shape
44
- * const metadataSchema = l.unknownObject()
45
- *
46
- * metadataSchema.parse({ foo: 1, bar: 'baz' }) // success
47
- * metadataSchema.parse([1, 2, 3]) // throws - not a plain object
48
- * metadataSchema.parse(null) // throws
49
- * ```
50
- */
51
- export const unknownObject = /*#__PURE__*/ memoizedOptions(function () {
52
- return new UnknownObjectSchema()
53
- })