@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
@@ -16,12 +16,14 @@ import { memoizedOptions } from '../util/memoize.js'
16
16
  * ```
17
17
  */
18
18
  export class BooleanSchema extends Schema<boolean> {
19
+ readonly type = 'boolean' as const
20
+
19
21
  validateInContext(input: unknown, ctx: ValidationContext) {
20
22
  if (typeof input === 'boolean') {
21
23
  return ctx.success(input)
22
24
  }
23
25
 
24
- return ctx.issueInvalidType(input, 'boolean')
26
+ return ctx.issueUnexpectedType(input, 'boolean')
25
27
  }
26
28
  }
27
29
 
@@ -26,6 +26,8 @@ export type BytesSchemaOptions = {
26
26
  * ```
27
27
  */
28
28
  export class BytesSchema extends Schema<Uint8Array> {
29
+ readonly type = 'bytes' as const
30
+
29
31
  constructor(readonly options: BytesSchemaOptions = {}) {
30
32
  super()
31
33
  }
@@ -35,7 +37,7 @@ export class BytesSchema extends Schema<Uint8Array> {
35
37
  const bytes =
36
38
  ctx.options.mode === 'parse' ? asUint8Array(input) : ifUint8Array(input)
37
39
  if (!bytes) {
38
- return ctx.issueInvalidType(input, 'bytes')
40
+ return ctx.issueUnexpectedType(input, 'bytes')
39
41
  }
40
42
 
41
43
  const { minLength } = this.options
package/src/schema/cid.ts CHANGED
@@ -29,13 +29,15 @@ export type CidSchemaOptions = CheckCidOptions
29
29
  export class CidSchema<
30
30
  const TOptions extends CidSchemaOptions = { flavor: undefined },
31
31
  > extends Schema<InferCheckedCid<TOptions>> {
32
+ readonly type = 'cid' as const
33
+
32
34
  constructor(readonly options?: TOptions) {
33
35
  super()
34
36
  }
35
37
 
36
38
  validateInContext(input: unknown, ctx: ValidationContext) {
37
39
  if (!isCid(input, this.options)) {
38
- return ctx.issueInvalidType(input, 'cid')
40
+ return ctx.issueUnexpectedType(input, 'cid')
39
41
  }
40
42
 
41
43
  return ctx.success(input)
@@ -46,6 +46,8 @@ export type CustomAssertion<TValue> = (
46
46
  * ```
47
47
  */
48
48
  export class CustomSchema<out TValue = unknown> extends Schema<TValue> {
49
+ readonly type = 'custom' as const
50
+
49
51
  constructor(
50
52
  private readonly assertion: CustomAssertion<TValue>,
51
53
  private readonly message: string,
@@ -34,6 +34,8 @@ export class DictSchema<
34
34
  Record<InferInput<TKey>, InferInput<TValue>>,
35
35
  Record<InferInput<TKey>, InferOutput<TValue>>
36
36
  > {
37
+ readonly type = 'dict' as const
38
+
37
39
  constructor(
38
40
  readonly keySchema: TKey,
39
41
  readonly valueSchema: TValue,
@@ -47,7 +49,7 @@ export class DictSchema<
47
49
  options?: { ignoredKeys?: { has(k: string): boolean } },
48
50
  ) {
49
51
  if (!isPlainObject(input)) {
50
- return ctx.issueInvalidType(input, 'dict')
52
+ return ctx.issueUnexpectedType(input, 'dict')
51
53
  }
52
54
 
53
55
  let copy: undefined | Record<string, unknown>
@@ -78,6 +78,8 @@ export class DiscriminatedUnionSchema<
78
78
  DiscriminatedUnionSchemaInput<TVariants>,
79
79
  DiscriminatedUnionSchemaOutput<TVariants>
80
80
  > {
81
+ readonly type = 'discriminatedUnion' as const
82
+
81
83
  readonly variantsMap: Map<unknown, DiscriminatedUnionVariant<TDiscriminator>>
82
84
 
83
85
  constructor(
@@ -94,7 +96,7 @@ export class DiscriminatedUnionSchema<
94
96
 
95
97
  validateInContext(input: unknown, ctx: ValidationContext) {
96
98
  if (!isPlainObject(input)) {
97
- return ctx.issueInvalidType(input, 'object')
99
+ return ctx.issueUnexpectedType(input, 'object')
98
100
  }
99
101
 
100
102
  const { discriminator } = this
@@ -18,6 +18,8 @@ import { Schema, ValidationContext } from '../core.js'
18
18
  export class EnumSchema<
19
19
  const TValue extends null | string | number | boolean,
20
20
  > extends Schema<TValue> {
21
+ readonly type = 'enum' as const
22
+
21
23
  constructor(readonly values: readonly TValue[]) {
22
24
  super()
23
25
  }
@@ -25,13 +25,15 @@ export type IntegerSchemaOptions = {
25
25
  * ```
26
26
  */
27
27
  export class IntegerSchema extends Schema<number> {
28
+ readonly type = 'integer' as const
29
+
28
30
  constructor(readonly options?: IntegerSchemaOptions) {
29
31
  super()
30
32
  }
31
33
 
32
34
  validateInContext(input: unknown, ctx: ValidationContext) {
33
35
  if (!isInteger(input)) {
34
- return ctx.issueInvalidType(input, 'integer')
36
+ return ctx.issueUnexpectedType(input, 'integer')
35
37
  }
36
38
 
37
39
  if (this.options?.minimum != null && input < this.options.minimum) {
@@ -56,6 +56,8 @@ export class IntersectionSchema<
56
56
  Simplify<Intersect<InferInput<Left>, InferInput<Right>>>,
57
57
  Simplify<Intersect<InferOutput<Left>, InferOutput<Right>>>
58
58
  > {
59
+ readonly type = 'intersection' as const
60
+
59
61
  constructor(
60
62
  protected readonly left: Left,
61
63
  protected readonly right: Right,
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { unknownObject } from './unknown-object.js'
2
+ import { lexMap } from './lex-map.js'
3
3
 
4
- describe('UnknownObjectSchema', () => {
4
+ describe(lexMap, () => {
5
5
  describe('basic validation', () => {
6
- const schema = unknownObject()
6
+ const schema = lexMap()
7
7
 
8
8
  it('accepts empty plain objects', () => {
9
9
  const result = schema.safeParse({})
@@ -106,7 +106,7 @@ describe('UnknownObjectSchema', () => {
106
106
  })
107
107
 
108
108
  describe('rejects non-plain-objects', () => {
109
- const schema = unknownObject()
109
+ const schema = lexMap()
110
110
 
111
111
  it('rejects strings', () => {
112
112
  const result = schema.safeParse('not an object')
@@ -208,7 +208,7 @@ describe('UnknownObjectSchema', () => {
208
208
  })
209
209
 
210
210
  describe('rejects invalid value types', () => {
211
- const schema = unknownObject()
211
+ const schema = lexMap()
212
212
 
213
213
  it('rejects objects with floating point numbers', () => {
214
214
  const result = schema.safeParse({ value: 3.14 })
@@ -288,7 +288,7 @@ describe('UnknownObjectSchema', () => {
288
288
  })
289
289
 
290
290
  describe('rejects invalid nested values', () => {
291
- const schema = unknownObject()
291
+ const schema = lexMap()
292
292
 
293
293
  it('rejects deeply nested invalid values', () => {
294
294
  const result = schema.safeParse({
@@ -335,7 +335,7 @@ describe('UnknownObjectSchema', () => {
335
335
  })
336
336
 
337
337
  describe('edge cases', () => {
338
- const schema = unknownObject()
338
+ const schema = lexMap()
339
339
 
340
340
  it('accepts objects with numeric string keys', () => {
341
341
  const obj = { '0': 'zero', '1': 'one', '2': 'two' }
@@ -500,7 +500,7 @@ describe('UnknownObjectSchema', () => {
500
500
  })
501
501
 
502
502
  describe('large objects', () => {
503
- const schema = unknownObject()
503
+ const schema = lexMap()
504
504
 
505
505
  it('accepts objects with many keys', () => {
506
506
  const obj: Record<string, number> = {}
@@ -538,7 +538,7 @@ describe('UnknownObjectSchema', () => {
538
538
  })
539
539
 
540
540
  describe('preservation of input', () => {
541
- const schema = unknownObject()
541
+ const schema = lexMap()
542
542
 
543
543
  it('preserves the original object reference', () => {
544
544
  const input = { key: 'value', count: 42 }
@@ -0,0 +1,63 @@
1
+ import { LexMap, isPlainObject } from '@atproto/lex-data'
2
+ import { Schema, ValidationContext } from '../core.js'
3
+ import { memoizedOptions } from '../util/memoize.js'
4
+ import { lexValue } from './lex-value.js'
5
+
6
+ const propertyValueSchema = /*#__PURE__*/ lexValue()
7
+
8
+ export type { LexMap }
9
+
10
+ /**
11
+ * AT Protocol lexicon schema definitions with "type": "unknown" are represented
12
+ * as plain objects with string keys and values that are valid AT Protocol data
13
+ * types (string, integer, boolean, null, bytes, cid, array, or object). This
14
+ * type alias corresponds to the expected structure of such "unknown" schema
15
+ * values.
16
+ */
17
+ export class LexMapSchema extends Schema<LexMap> {
18
+ readonly type = 'lexMap' as const
19
+
20
+ validateInContext(input: unknown, ctx: ValidationContext) {
21
+ if (!isPlainObject(input)) {
22
+ return ctx.issueUnexpectedType(input, 'object')
23
+ }
24
+
25
+ for (const key of Object.keys(input)) {
26
+ // @NOTE We use a lexValue() schema here to recursively validate all
27
+ // nested values, which ensures that the error reporting includes the
28
+ // correct path and type information for any invalid nested values. This
29
+ // allows for more informative error descriptions than a simple "isLexMap"
30
+ // check.
31
+ const r = ctx.validateChild(input, key, propertyValueSchema) // recursively validate all properties
32
+ if (!r.success) return r
33
+ }
34
+
35
+ return ctx.success(input)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Creates a schema that accepts any plain object with string keys and values
41
+ * that are valid AT Protocol data types (string, integer, boolean, null, bytes,
42
+ * cid, array, or object).
43
+ *
44
+ * @see {@link LexMap} from `@atproto/lex-data` for the type definition of valid AT Protocol data types
45
+ * @returns A new {@link LexMapSchema} instance
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // Accept any object shape
50
+ * const schema = l.lexMap()
51
+ *
52
+ * schema.validate({ any: 'props' }) // success
53
+ * schema.validate([1, 2, 3]) // fails - only plain objects are accepted
54
+ * schema.validate({ foo: new Date() }) // fails - Date is not a valid LexValue
55
+ * schema.validate({ foo: 1.2 }) // fails - 1.2 is not a valid LexValue (not an integer)
56
+ * ```
57
+ */
58
+ export const lexMap = /*#__PURE__*/ memoizedOptions(function () {
59
+ return new LexMapSchema()
60
+ })
61
+
62
+ /** @deprecated Use {@link lexMap} instead */
63
+ export const unknownObject = lexMap
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { parseCid } from '@atproto/lex-data'
3
+ import { lexValue } from './lex-value.js'
4
+
5
+ const schema = lexValue()
6
+
7
+ describe(lexValue, () => {
8
+ describe('valid values', () => {
9
+ for (const { note, value } of [
10
+ { note: 'string', value: 'hello' },
11
+ { note: 'boolean true', value: true },
12
+ { note: 'boolean false', value: false },
13
+ { note: 'null', value: null },
14
+ { note: 'integer', value: 42 },
15
+ { note: 'negative integer', value: -1 },
16
+ { note: 'zero', value: 0 },
17
+ { note: 'Uint8Array', value: new Uint8Array([1, 2, 3]) },
18
+ {
19
+ note: 'Cid',
20
+ value: parseCid(
21
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
22
+ ),
23
+ },
24
+ { note: 'empty plain object', value: {} },
25
+ {
26
+ note: 'object with Lex values',
27
+ value: {
28
+ a: 123,
29
+ b: 'blah',
30
+ c: true,
31
+ d: null,
32
+ e: new Uint8Array([1, 2, 3]),
33
+ f: { nested: 'value' },
34
+ g: [1, 2, 3],
35
+ },
36
+ },
37
+ { note: 'empty array', value: [] },
38
+ {
39
+ note: 'array with Lex values',
40
+ value: [
41
+ 123,
42
+ 'blah',
43
+ true,
44
+ null,
45
+ new Uint8Array([1, 2, 3]),
46
+ { nested: 'value' },
47
+ [1, 2, 3],
48
+ ],
49
+ },
50
+ ]) {
51
+ test(note, () => {
52
+ const result = schema.safeParse(value)
53
+ expect(result.success).toBe(true)
54
+ })
55
+ }
56
+ })
57
+
58
+ describe('invalid values', () => {
59
+ for (const { note, value } of [
60
+ { note: 'float', value: 42.5 },
61
+ { note: 'undefined', value: undefined },
62
+ { note: 'function', value: () => {} },
63
+ { note: 'Date object', value: new Date() },
64
+ { note: 'Map object', value: new Map() },
65
+ { note: 'Set object', value: new Set() },
66
+ { note: 'class instance', value: new (class A {})() },
67
+ { note: 'object with function value', value: { a: 123, b: () => {} } },
68
+ {
69
+ note: 'object with undefined value',
70
+ value: { a: 123, b: undefined },
71
+ },
72
+ { note: 'array with function', value: [123, 'blah', () => {}] },
73
+ { note: 'array with undefined', value: [123, 'blah', undefined] },
74
+ ]) {
75
+ test(note, () => {
76
+ const result = schema.safeParse(value)
77
+ expect(result.success).toBe(false)
78
+ })
79
+ }
80
+ })
81
+ })
@@ -0,0 +1,86 @@
1
+ import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'
2
+ import { Schema, ValidationContext } from '../core.js'
3
+ import { memoizedOptions } from '../util/memoize.js'
4
+
5
+ export type { LexValue }
6
+
7
+ const EXPECTED_TYPES = Object.freeze([
8
+ // Scalar types
9
+ 'null',
10
+ 'boolean',
11
+ 'integer',
12
+ 'string',
13
+ 'cid',
14
+ 'bytes',
15
+ // Recursive types
16
+ 'array',
17
+ 'object',
18
+ ] as const)
19
+
20
+ /**
21
+ * AT Protocol lexicon values are any valid AT Protocol data types: string,
22
+ * integer, boolean, null, bytes, cid, array, or object.
23
+ */
24
+ export class LexValueSchema extends Schema<LexValue> {
25
+ readonly type = 'lexValue' as const
26
+
27
+ validateInContext(input: unknown, ctx: ValidationContext) {
28
+ // @NOTE We are *not* using "isLexValue" here to allow for more specific
29
+ // error messages about the path and type of the invalid value. The
30
+ // "isLexValue" check is effectively performed by the recursive validation
31
+ // of child properties below.
32
+
33
+ // @NOTE There are two limitations to the fact that we are not using
34
+ // "isLexValue" here:
35
+ // 1. We cannot detect circular references in objects or arrays, which would
36
+ // cause infinite recursion. However, circular references are not valid
37
+ // AT Protocol data types, so this is not a concern for valid input. This
38
+ // could easily be addressed in the "validateChild" method by keeping
39
+ // track of "parent" objects.
40
+ // 2. We are limited in the recursion depth we can validate due to potential
41
+ // recursion depth limits in JavaScript. However, this is also not a
42
+ // concern for most valid input, as extremely deep nesting is unlikely in
43
+ // typical use cases.
44
+ if (isPlainObject(input)) {
45
+ for (const key of Object.keys(input)) {
46
+ const r = ctx.validateChild(input, key, this) // recursively validate all properties
47
+ if (!r.success) return r
48
+ }
49
+ } else if (Array.isArray(input)) {
50
+ for (let i = 0; i < input.length; i++) {
51
+ const r = ctx.validateChild(input, i, this) // recursively validate all array items
52
+ if (!r.success) return r
53
+ }
54
+ } else if (!isLexScalar(input)) {
55
+ return ctx.issueInvalidType(input, EXPECTED_TYPES)
56
+ }
57
+
58
+ return ctx.success(input)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Creates a schema that accepts any valid AT Protocol data type: string,
64
+ * integer, boolean, null, bytes, cid, array, or plain object. Arrays and
65
+ * objects are recursively validated to ensure all nested values are also valid
66
+ * AT Protocol data types.
67
+ *
68
+ * @see {@link LexValue} from `@atproto/lex-data` for the type definition of valid AT Protocol data types
69
+ * @returns A new {@link LexValueSchema} instance
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const schema = l.lexValue()
74
+ *
75
+ * schema.validate('hello') // success
76
+ * schema.validate(42) // success
77
+ * schema.validate(null) // success
78
+ * schema.validate([1, 'two', null]) // success
79
+ * schema.validate({ any: 'props' }) // success
80
+ * schema.validate(new Date()) // fails - Date is not a valid LexValue
81
+ * schema.validate({ foo: 1.2 }) // fails - 1.2 is not a valid LexValue (not an integer)
82
+ * ```
83
+ */
84
+ export const lexValue = /*#__PURE__*/ memoizedOptions(function () {
85
+ return new LexValueSchema()
86
+ })
@@ -18,6 +18,8 @@ import { Schema, ValidationContext } from '../core.js'
18
18
  export class LiteralSchema<
19
19
  const TValue extends null | string | number | boolean,
20
20
  > extends Schema<TValue> {
21
+ readonly type = 'literal' as const
22
+
21
23
  constructor(readonly value: TValue) {
22
24
  super()
23
25
  }
@@ -14,8 +14,10 @@ import { memoizedOptions } from '../util/memoize.js'
14
14
  * ```
15
15
  */
16
16
  export class NeverSchema extends Schema<never> {
17
+ readonly type = 'never' as const
18
+
17
19
  validateInContext(input: unknown, ctx: ValidationContext) {
18
- return ctx.issueInvalidType(input, 'never')
20
+ return ctx.issueUnexpectedType(input, 'never')
19
21
  }
20
22
  }
21
23
 
@@ -15,9 +15,11 @@ import { memoizedOptions } from '../util/memoize.js'
15
15
  * ```
16
16
  */
17
17
  export class NullSchema extends Schema<null> {
18
+ readonly type = 'null' as const
19
+
18
20
  validateInContext(input: unknown, ctx: ValidationContext) {
19
21
  if (input !== null) {
20
- return ctx.issueInvalidType(input, 'null')
22
+ return ctx.issueUnexpectedType(input, 'null')
21
23
  }
22
24
 
23
25
  return ctx.success(null)
@@ -26,6 +26,8 @@ export class NullableSchema<const TValidator extends Validator> extends Schema<
26
26
  InferInput<TValidator> | null,
27
27
  InferOutput<TValidator> | null
28
28
  > {
29
+ readonly type = 'nullable' as const
30
+
29
31
  constructor(readonly validator: TValidator) {
30
32
  super()
31
33
  }
@@ -43,6 +43,8 @@ export class ObjectSchema<
43
43
  [K in keyof TShape]: InferOutput<TShape[K]>
44
44
  }>
45
45
  > {
46
+ readonly type = 'object' as const
47
+
46
48
  constructor(readonly shape: TShape) {
47
49
  super()
48
50
  }
@@ -55,7 +57,7 @@ export class ObjectSchema<
55
57
 
56
58
  validateInContext(input: unknown, ctx: ValidationContext) {
57
59
  if (!isPlainObject(input)) {
58
- return ctx.issueInvalidType(input, 'object')
60
+ return ctx.issueUnexpectedType(input, 'object')
59
61
  }
60
62
 
61
63
  // Lazily copy value
@@ -31,6 +31,8 @@ export class OptionalSchema<TValidator extends Validator> extends Schema<
31
31
  ? InferOutput<TValidator>
32
32
  : InferOutput<TValidator> | undefined
33
33
  > {
34
+ readonly type = 'optional' as const
35
+
34
36
  constructor(readonly validator: TValidator) {
35
37
  super()
36
38
  }