@atproto/lex-schema 0.0.11 → 0.0.13

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 (261) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/core/$type.d.ts +149 -0
  3. package/dist/core/$type.d.ts.map +1 -1
  4. package/dist/core/$type.js +44 -0
  5. package/dist/core/$type.js.map +1 -1
  6. package/dist/core/record-key.d.ts +44 -0
  7. package/dist/core/record-key.d.ts.map +1 -1
  8. package/dist/core/record-key.js +30 -0
  9. package/dist/core/record-key.js.map +1 -1
  10. package/dist/core/result.d.ts +85 -4
  11. package/dist/core/result.d.ts.map +1 -1
  12. package/dist/core/result.js +60 -4
  13. package/dist/core/result.js.map +1 -1
  14. package/dist/core/schema.d.ts +232 -5
  15. package/dist/core/schema.d.ts.map +1 -1
  16. package/dist/core/schema.js +197 -4
  17. package/dist/core/schema.js.map +1 -1
  18. package/dist/core/string-format.d.ts +244 -11
  19. package/dist/core/string-format.d.ts.map +1 -1
  20. package/dist/core/string-format.js +150 -0
  21. package/dist/core/string-format.js.map +1 -1
  22. package/dist/core/types.d.ts +90 -3
  23. package/dist/core/types.d.ts.map +1 -1
  24. package/dist/core/types.js.map +1 -1
  25. package/dist/core/validation-error.d.ts +60 -0
  26. package/dist/core/validation-error.d.ts.map +1 -1
  27. package/dist/core/validation-error.js +60 -0
  28. package/dist/core/validation-error.js.map +1 -1
  29. package/dist/core/validation-issue.d.ts +61 -0
  30. package/dist/core/validation-issue.d.ts.map +1 -1
  31. package/dist/core/validation-issue.js +54 -1
  32. package/dist/core/validation-issue.js.map +1 -1
  33. package/dist/core/validator.d.ts +356 -11
  34. package/dist/core/validator.d.ts.map +1 -1
  35. package/dist/core/validator.js +203 -4
  36. package/dist/core/validator.js.map +1 -1
  37. package/dist/helpers.d.ts +12 -28
  38. package/dist/helpers.d.ts.map +1 -1
  39. package/dist/helpers.js.map +1 -1
  40. package/dist/schema/array.d.ts +46 -0
  41. package/dist/schema/array.d.ts.map +1 -1
  42. package/dist/schema/array.js +16 -1
  43. package/dist/schema/array.js.map +1 -1
  44. package/dist/schema/blob.d.ts +50 -2
  45. package/dist/schema/blob.d.ts.map +1 -1
  46. package/dist/schema/blob.js +44 -2
  47. package/dist/schema/blob.js.map +1 -1
  48. package/dist/schema/boolean.d.ts +29 -0
  49. package/dist/schema/boolean.d.ts.map +1 -1
  50. package/dist/schema/boolean.js +30 -1
  51. package/dist/schema/boolean.js.map +1 -1
  52. package/dist/schema/bytes.d.ts +39 -0
  53. package/dist/schema/bytes.d.ts.map +1 -1
  54. package/dist/schema/bytes.js +34 -1
  55. package/dist/schema/bytes.js.map +1 -1
  56. package/dist/schema/cid.d.ts +39 -0
  57. package/dist/schema/cid.d.ts.map +1 -1
  58. package/dist/schema/cid.js +35 -1
  59. package/dist/schema/cid.js.map +1 -1
  60. package/dist/schema/custom.d.ts +67 -1
  61. package/dist/schema/custom.d.ts.map +1 -1
  62. package/dist/schema/custom.js +55 -0
  63. package/dist/schema/custom.js.map +1 -1
  64. package/dist/schema/dict.d.ts +45 -0
  65. package/dist/schema/dict.d.ts.map +1 -1
  66. package/dist/schema/dict.js +46 -1
  67. package/dist/schema/dict.js.map +1 -1
  68. package/dist/schema/discriminated-union.d.ts +59 -0
  69. package/dist/schema/discriminated-union.d.ts.map +1 -1
  70. package/dist/schema/discriminated-union.js +47 -1
  71. package/dist/schema/discriminated-union.js.map +1 -1
  72. package/dist/schema/enum.d.ts +49 -0
  73. package/dist/schema/enum.d.ts.map +1 -1
  74. package/dist/schema/enum.js +49 -0
  75. package/dist/schema/enum.js.map +1 -1
  76. package/dist/schema/integer.d.ts +43 -0
  77. package/dist/schema/integer.d.ts.map +1 -1
  78. package/dist/schema/integer.js +38 -1
  79. package/dist/schema/integer.js.map +1 -1
  80. package/dist/schema/intersection.d.ts +55 -0
  81. package/dist/schema/intersection.d.ts.map +1 -1
  82. package/dist/schema/intersection.js +50 -0
  83. package/dist/schema/intersection.js.map +1 -1
  84. package/dist/schema/lex-map.d.ts +37 -0
  85. package/dist/schema/lex-map.d.ts.map +1 -0
  86. package/dist/schema/lex-map.js +60 -0
  87. package/dist/schema/lex-map.js.map +1 -0
  88. package/dist/schema/lex-value.d.ts +35 -0
  89. package/dist/schema/lex-value.d.ts.map +1 -0
  90. package/dist/schema/lex-value.js +87 -0
  91. package/dist/schema/lex-value.js.map +1 -0
  92. package/dist/schema/literal.d.ts +45 -0
  93. package/dist/schema/literal.d.ts.map +1 -1
  94. package/dist/schema/literal.js +45 -0
  95. package/dist/schema/literal.js.map +1 -1
  96. package/dist/schema/never.d.ts +43 -0
  97. package/dist/schema/never.d.ts.map +1 -1
  98. package/dist/schema/never.js +44 -1
  99. package/dist/schema/never.js.map +1 -1
  100. package/dist/schema/null.d.ts +30 -0
  101. package/dist/schema/null.d.ts.map +1 -1
  102. package/dist/schema/null.js +31 -1
  103. package/dist/schema/null.js.map +1 -1
  104. package/dist/schema/nullable.d.ts +42 -0
  105. package/dist/schema/nullable.d.ts.map +1 -1
  106. package/dist/schema/nullable.js +42 -0
  107. package/dist/schema/nullable.js.map +1 -1
  108. package/dist/schema/object.d.ts +57 -0
  109. package/dist/schema/object.d.ts.map +1 -1
  110. package/dist/schema/object.js +53 -1
  111. package/dist/schema/object.js.map +1 -1
  112. package/dist/schema/optional.d.ts +43 -0
  113. package/dist/schema/optional.d.ts.map +1 -1
  114. package/dist/schema/optional.js +43 -0
  115. package/dist/schema/optional.js.map +1 -1
  116. package/dist/schema/params.d.ts +96 -12
  117. package/dist/schema/params.d.ts.map +1 -1
  118. package/dist/schema/params.js +155 -21
  119. package/dist/schema/params.js.map +1 -1
  120. package/dist/schema/payload.d.ts +111 -15
  121. package/dist/schema/payload.d.ts.map +1 -1
  122. package/dist/schema/payload.js +73 -3
  123. package/dist/schema/payload.js.map +1 -1
  124. package/dist/schema/permission-set.d.ts +58 -0
  125. package/dist/schema/permission-set.d.ts.map +1 -1
  126. package/dist/schema/permission-set.js +50 -0
  127. package/dist/schema/permission-set.js.map +1 -1
  128. package/dist/schema/permission.d.ts +42 -0
  129. package/dist/schema/permission.d.ts.map +1 -1
  130. package/dist/schema/permission.js +39 -0
  131. package/dist/schema/permission.js.map +1 -1
  132. package/dist/schema/procedure.d.ts +64 -0
  133. package/dist/schema/procedure.d.ts.map +1 -1
  134. package/dist/schema/procedure.js +64 -0
  135. package/dist/schema/procedure.js.map +1 -1
  136. package/dist/schema/query.d.ts +55 -0
  137. package/dist/schema/query.d.ts.map +1 -1
  138. package/dist/schema/query.js +55 -0
  139. package/dist/schema/query.js.map +1 -1
  140. package/dist/schema/record.d.ts +76 -25
  141. package/dist/schema/record.d.ts.map +1 -1
  142. package/dist/schema/record.js +21 -0
  143. package/dist/schema/record.js.map +1 -1
  144. package/dist/schema/ref.d.ts +51 -0
  145. package/dist/schema/ref.d.ts.map +1 -1
  146. package/dist/schema/ref.js +18 -0
  147. package/dist/schema/ref.js.map +1 -1
  148. package/dist/schema/refine.d.ts +58 -9
  149. package/dist/schema/refine.d.ts.map +1 -1
  150. package/dist/schema/refine.js.map +1 -1
  151. package/dist/schema/regexp.d.ts +45 -0
  152. package/dist/schema/regexp.d.ts.map +1 -1
  153. package/dist/schema/regexp.js +46 -1
  154. package/dist/schema/regexp.js.map +1 -1
  155. package/dist/schema/string.d.ts +72 -6
  156. package/dist/schema/string.d.ts.map +1 -1
  157. package/dist/schema/string.js +56 -8
  158. package/dist/schema/string.js.map +1 -1
  159. package/dist/schema/subscription.d.ts +72 -2
  160. package/dist/schema/subscription.d.ts.map +1 -1
  161. package/dist/schema/subscription.js +59 -0
  162. package/dist/schema/subscription.js.map +1 -1
  163. package/dist/schema/token.d.ts +48 -0
  164. package/dist/schema/token.d.ts.map +1 -1
  165. package/dist/schema/token.js +49 -1
  166. package/dist/schema/token.js.map +1 -1
  167. package/dist/schema/typed-object.d.ts +73 -23
  168. package/dist/schema/typed-object.d.ts.map +1 -1
  169. package/dist/schema/typed-object.js +20 -1
  170. package/dist/schema/typed-object.js.map +1 -1
  171. package/dist/schema/typed-ref.d.ts +54 -0
  172. package/dist/schema/typed-ref.d.ts.map +1 -1
  173. package/dist/schema/typed-ref.js +16 -0
  174. package/dist/schema/typed-ref.js.map +1 -1
  175. package/dist/schema/typed-union.d.ts +51 -1
  176. package/dist/schema/typed-union.d.ts.map +1 -1
  177. package/dist/schema/typed-union.js +52 -2
  178. package/dist/schema/typed-union.js.map +1 -1
  179. package/dist/schema/union.d.ts +46 -0
  180. package/dist/schema/union.d.ts.map +1 -1
  181. package/dist/schema/union.js +41 -0
  182. package/dist/schema/union.js.map +1 -1
  183. package/dist/schema/unknown.d.ts +34 -0
  184. package/dist/schema/unknown.d.ts.map +1 -1
  185. package/dist/schema/unknown.js +34 -0
  186. package/dist/schema/unknown.js.map +1 -1
  187. package/dist/schema/with-default.d.ts +45 -0
  188. package/dist/schema/with-default.d.ts.map +1 -1
  189. package/dist/schema/with-default.js +45 -0
  190. package/dist/schema/with-default.js.map +1 -1
  191. package/dist/schema.d.ts +2 -1
  192. package/dist/schema.d.ts.map +1 -1
  193. package/dist/schema.js +2 -1
  194. package/dist/schema.js.map +1 -1
  195. package/dist/util/if-any.d.ts +2 -0
  196. package/dist/util/if-any.d.ts.map +1 -0
  197. package/dist/util/if-any.js +3 -0
  198. package/dist/util/if-any.js.map +1 -0
  199. package/package.json +3 -3
  200. package/src/core/$type.ts +150 -18
  201. package/src/core/record-key.ts +44 -0
  202. package/src/core/result.ts +86 -4
  203. package/src/core/schema.ts +244 -9
  204. package/src/core/string-format.ts +259 -13
  205. package/src/core/types.ts +91 -3
  206. package/src/core/validation-error.ts +60 -0
  207. package/src/core/validation-issue.ts +68 -2
  208. package/src/core/validator.ts +373 -12
  209. package/src/helpers.test.ts +110 -29
  210. package/src/helpers.ts +54 -25
  211. package/src/schema/array.test.ts +94 -79
  212. package/src/schema/array.ts +48 -1
  213. package/src/schema/blob.ts +50 -1
  214. package/src/schema/boolean.ts +31 -1
  215. package/src/schema/bytes.ts +41 -1
  216. package/src/schema/cid.ts +41 -1
  217. package/src/schema/custom.ts +68 -1
  218. package/src/schema/dict.ts +47 -1
  219. package/src/schema/discriminated-union.ts +61 -1
  220. package/src/schema/enum.ts +50 -0
  221. package/src/schema/integer.ts +45 -1
  222. package/src/schema/intersection.ts +56 -0
  223. package/src/schema/{unknown-object.test.ts → lex-map.test.ts} +9 -9
  224. package/src/schema/lex-map.ts +63 -0
  225. package/src/schema/lex-value.test.ts +81 -0
  226. package/src/schema/lex-value.ts +86 -0
  227. package/src/schema/literal.ts +46 -0
  228. package/src/schema/never.ts +45 -1
  229. package/src/schema/null.ts +32 -1
  230. package/src/schema/nullable.ts +43 -0
  231. package/src/schema/object.ts +59 -1
  232. package/src/schema/optional.ts +44 -0
  233. package/src/schema/params.test.ts +133 -38
  234. package/src/schema/params.ts +237 -37
  235. package/src/schema/payload.test.ts +3 -3
  236. package/src/schema/payload.ts +145 -42
  237. package/src/schema/permission-set.ts +58 -0
  238. package/src/schema/permission.ts +42 -0
  239. package/src/schema/procedure.ts +64 -0
  240. package/src/schema/query.ts +55 -0
  241. package/src/schema/record.ts +82 -16
  242. package/src/schema/ref.ts +52 -0
  243. package/src/schema/refine.ts +58 -9
  244. package/src/schema/regexp.ts +47 -1
  245. package/src/schema/string.test.ts +99 -2
  246. package/src/schema/string.ts +108 -15
  247. package/src/schema/subscription.ts +72 -2
  248. package/src/schema/token.ts +50 -1
  249. package/src/schema/typed-object.ts +81 -16
  250. package/src/schema/typed-ref.ts +55 -0
  251. package/src/schema/typed-union.ts +58 -3
  252. package/src/schema/union.ts +47 -0
  253. package/src/schema/unknown.ts +35 -0
  254. package/src/schema/with-default.ts +46 -0
  255. package/src/schema.ts +2 -1
  256. package/src/util/if-any.ts +3 -0
  257. package/dist/schema/unknown-object.d.ts +0 -8
  258. package/dist/schema/unknown-object.d.ts.map +0 -1
  259. package/dist/schema/unknown-object.js +0 -19
  260. package/dist/schema/unknown-object.js.map +0 -1
  261. package/src/schema/unknown-object.ts +0 -19
package/src/schema/ref.ts CHANGED
@@ -7,8 +7,30 @@ import {
7
7
  WrappedValidator,
8
8
  } from '../core.js'
9
9
 
10
+ /**
11
+ * Function type that returns a validator, used for lazy schema resolution.
12
+ *
13
+ * @template TValidator - The validator type that will be returned
14
+ */
10
15
  export type RefSchemaGetter<out TValidator extends Validator> = () => TValidator
11
16
 
17
+ /**
18
+ * Schema for creating references to other schemas with lazy resolution.
19
+ *
20
+ * Useful for handling circular references or breaking module dependency cycles.
21
+ * The referenced schema is resolved lazily when first needed for validation.
22
+ *
23
+ * @template TValidator - The referenced validator type
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * // Self-referential schema for tree structure
28
+ * const nodeSchema = l.object({
29
+ * value: l.string(),
30
+ * children: l.array(l.ref(() => nodeSchema)),
31
+ * })
32
+ * ```
33
+ */
12
34
  export class RefSchema<const TValidator extends Validator>
13
35
  extends Schema<
14
36
  InferInput<TValidator>,
@@ -17,6 +39,8 @@ export class RefSchema<const TValidator extends Validator>
17
39
  >
18
40
  implements WrappedValidator<TValidator>
19
41
  {
42
+ readonly type = 'ref' as const
43
+
20
44
  #getter: RefSchemaGetter<TValidator>
21
45
 
22
46
  constructor(getter: RefSchemaGetter<TValidator>) {
@@ -41,6 +65,34 @@ export class RefSchema<const TValidator extends Validator>
41
65
  }
42
66
  }
43
67
 
68
+ /**
69
+ * Creates a reference schema with lazy resolution.
70
+ *
71
+ * Allows referencing schemas that may not be defined yet, enabling
72
+ * circular references and breaking dependency cycles. The getter function
73
+ * is called lazily when validation is first performed.
74
+ *
75
+ * @param get - Function that returns the referenced validator
76
+ * @returns A new {@link RefSchema} instance
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // Circular reference - tree node that contains children of the same type
81
+ * const treeNodeSchema = l.object({
82
+ * name: l.string(),
83
+ * children: l.optional(l.array(l.ref(() => treeNodeSchema))),
84
+ * })
85
+ *
86
+ * // Cross-module reference
87
+ * const commentSchema = l.object({
88
+ * text: l.string(),
89
+ * author: l.ref(() => userSchema), // userSchema defined elsewhere
90
+ * })
91
+ *
92
+ * // Explicitly typed reference
93
+ * const itemSchema = l.ref<Item>(() => complexItemSchema)
94
+ * ```
95
+ */
44
96
  /*@__NO_SIDE_EFFECTS__*/
45
97
  export function ref<const TValidator extends Validator>(
46
98
  get: RefSchemaGetter<TValidator>,
@@ -8,18 +8,40 @@ import {
8
8
  } from '../core.js'
9
9
  import { CustomAssertionContext } from './custom.js'
10
10
 
11
+ /**
12
+ * Configuration for a refinement check that validates a condition.
13
+ *
14
+ * @template T - The type being validated
15
+ * @property check - Function that returns true if the value passes the check
16
+ * @property message - Error message when the check fails
17
+ * @property path - Optional path to associate with the error
18
+ */
11
19
  export type RefinementCheck<T> = {
12
20
  check: (value: T, ctx: CustomAssertionContext) => boolean
13
21
  message: string
14
22
  path?: PropertyKey | readonly PropertyKey[]
15
23
  }
16
24
 
25
+ /**
26
+ * Configuration for a refinement assertion that narrows the type.
27
+ *
28
+ * @template T - The input type being validated
29
+ * @template Out - The narrowed output type
30
+ * @property check - Type guard function that narrows the type
31
+ * @property message - Error message when the assertion fails
32
+ * @property path - Optional path to associate with the error
33
+ */
17
34
  export type RefinementAssertion<T, Out extends T> = {
18
35
  check: (this: null, value: T, ctx: CustomAssertionContext) => value is Out
19
36
  message: string
20
37
  path?: PropertyKey | readonly PropertyKey[]
21
38
  }
22
39
 
40
+ /**
41
+ * Infers the input type from a refinement configuration.
42
+ *
43
+ * @template R - The refinement type
44
+ */
23
45
  export type InferRefinement<R> =
24
46
  R extends RefinementCheck<infer T>
25
47
  ? T
@@ -27,25 +49,52 @@ export type InferRefinement<R> =
27
49
  ? T
28
50
  : never
29
51
 
52
+ /**
53
+ * Union type of refinement check or assertion.
54
+ *
55
+ * @template T - The input type being validated
56
+ * @template Out - The output type (same as T for checks, narrowed for assertions)
57
+ */
30
58
  export type Refinement<T = any, Out extends T = T> =
31
59
  | RefinementCheck<T>
32
60
  | RefinementAssertion<T, Out>
33
61
 
34
62
  /**
35
- * Create a refined schema based on an existing schema and a refinement check.
63
+ * Creates a refined schema by adding additional validation constraints.
36
64
  *
37
- * @param schema - The base schema to refine.
38
- * @param refinement - The refinement check to apply.
39
- * @returns A new schema that includes the refinement.
40
- * @example
65
+ * Wraps an existing schema with an additional check function. The base schema
66
+ * is validated first, then the refinement check is applied to the result.
41
67
  *
68
+ * @param schema - The base schema to refine
69
+ * @param refinement - The refinement check or assertion to apply
70
+ * @returns A new schema that includes the refinement
71
+ *
72
+ * @example
42
73
  * ```ts
43
- * const PositiveInt = refine(l.integer(), {
74
+ * // Simple check refinement
75
+ * const positiveInt = l.refine(l.integer(), {
44
76
  * check: (value) => value > 0,
45
- * message: 'Value must be a positive integer',
77
+ * message: 'Value must be positive',
46
78
  * })
47
- * const result = PositiveInt.validate(-5)
48
- * // result.success === false
79
+ *
80
+ * positiveInt.parse(5) // 5
81
+ * positiveInt.parse(-1) // throws
82
+ *
83
+ * // Type-narrowing assertion
84
+ * const nonEmptyString = l.refine(l.string(), {
85
+ * check: (value): value is string & { length: number } => value.length > 0,
86
+ * message: 'String must not be empty',
87
+ * })
88
+ *
89
+ * // With custom path for nested errors
90
+ * const validDateRange = l.refine(
91
+ * l.object({ start: l.string(), end: l.string() }),
92
+ * {
93
+ * check: (v) => new Date(v.start) < new Date(v.end),
94
+ * message: 'Start date must be before end date',
95
+ * path: ['end'],
96
+ * }
97
+ * )
49
98
  * ```
50
99
  */
51
100
  export function refine<
@@ -1,15 +1,32 @@
1
1
  import { Schema, ValidationContext } from '../core.js'
2
2
 
3
+ /**
4
+ * Schema for validating strings against a regular expression pattern.
5
+ *
6
+ * Validates that the input is a string and matches the provided pattern.
7
+ * The pattern is tested using RegExp.test().
8
+ *
9
+ * @template TValue - The string type (can be narrowed with branded types)
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const schema = new RegexpSchema(/^[a-z]+$/)
14
+ * schema.validate('hello') // success
15
+ * schema.validate('Hello') // fails - uppercase not allowed
16
+ * ```
17
+ */
3
18
  export class RegexpSchema<
4
19
  TValue extends string = string,
5
20
  > extends Schema<TValue> {
21
+ readonly type = 'regexp' as const
22
+
6
23
  constructor(public readonly pattern: RegExp) {
7
24
  super()
8
25
  }
9
26
 
10
27
  validateInContext(input: unknown, ctx: ValidationContext) {
11
28
  if (typeof input !== 'string') {
12
- return ctx.issueInvalidType(input, 'string')
29
+ return ctx.issueUnexpectedType(input, 'string')
13
30
  }
14
31
 
15
32
  if (!this.pattern.test(input)) {
@@ -20,6 +37,35 @@ export class RegexpSchema<
20
37
  }
21
38
  }
22
39
 
40
+ /**
41
+ * Creates a regexp schema that validates strings against a pattern.
42
+ *
43
+ * Useful for custom string formats not covered by the built-in format
44
+ * validators.
45
+ *
46
+ * @param pattern - Regular expression pattern to match against
47
+ * @returns A new {@link RegexpSchema} instance
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * // Simple pattern
52
+ * const slugSchema = l.regexp(/^[a-z0-9-]+$/)
53
+ *
54
+ * // With anchors for exact match
55
+ * const uuidSchema = l.regexp(
56
+ * /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
57
+ * )
58
+ *
59
+ * // Semantic versioning
60
+ * const semverSchema = l.regexp(/^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/)
61
+ *
62
+ * // Use in object
63
+ * const configSchema = l.object({
64
+ * name: l.regexp(/^[a-z][a-z0-9-]*$/), // kebab-case identifier
65
+ * version: semverSchema,
66
+ * })
67
+ * ```
68
+ */
23
69
  /*@__NO_SIDE_EFFECTS__*/
24
70
  export function regexp<TInput extends string = string>(pattern: RegExp) {
25
71
  return new RegexpSchema<TInput>(pattern)
@@ -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,49 +1,93 @@
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
 
15
+ /**
16
+ * Configuration options for string schema validation.
17
+ *
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
20
+ * @property minLength - Minimum length in UTF-8 bytes
21
+ * @property maxLength - Maximum length in UTF-8 bytes
22
+ * @property minGraphemes - Minimum number of grapheme clusters
23
+ * @property maxGraphemes - Maximum number of grapheme clusters
24
+ */
12
25
  export type StringSchemaOptions = {
13
26
  format?: StringFormat
27
+ knownValues?: readonly string[]
14
28
  minLength?: number
15
29
  maxLength?: number
16
30
  minGraphemes?: number
17
31
  maxGraphemes?: number
18
32
  }
19
33
 
34
+ /**
35
+ * Schema for validating string values with optional format and length constraints.
36
+ *
37
+ * Supports various string formats defined in the Lexicon specification, as well as
38
+ * length constraints measured in UTF-8 bytes or grapheme clusters.
39
+ *
40
+ * @template TOptions - The configuration options type
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const schema = new StringSchema({ format: 'datetime', maxLength: 64 })
45
+ * const result = schema.validate('2024-01-15T10:30:00Z')
46
+ * ```
47
+ */
20
48
  export class StringSchema<
21
49
  const TOptions extends StringSchemaOptions = StringSchemaOptions,
22
50
  > extends Schema<
23
- TOptions extends { format: infer F extends StringFormat }
24
- ? InferStringFormat<F>
25
- : 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
+ >
26
60
  > {
27
- 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) {
28
71
  super()
72
+ this.options = options
29
73
  }
30
74
 
31
75
  validateInContext(input: unknown, ctx: ValidationContext) {
32
76
  const str = coerceToString(input)
33
77
  if (str == null) {
34
- return ctx.issueInvalidType(input, 'string')
78
+ return ctx.issueUnexpectedType(input, 'string')
35
79
  }
36
80
 
37
81
  let lazyUtf8Len: number
38
82
 
39
- const minLength = this.options?.minLength
83
+ const minLength = this.options.minLength
40
84
  if (minLength != null) {
41
85
  if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {
42
86
  return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)
43
87
  }
44
88
  }
45
89
 
46
- const maxLength = this.options?.maxLength
90
+ const maxLength = this.options.maxLength
47
91
  if (maxLength != null) {
48
92
  // Optimization: we can avoid computing the UTF-8 length if the maximum
49
93
  // possible length, in bytes, of the input JS string is smaller than the
@@ -57,7 +101,7 @@ export class StringSchema<
57
101
 
58
102
  let lazyGraphLen: number
59
103
 
60
- const minGraphemes = this.options?.minGraphemes
104
+ const minGraphemes = this.options.minGraphemes
61
105
  if (minGraphemes != null) {
62
106
  // Optimization: avoid counting graphemes if the length check already fails
63
107
  if (str.length < minGraphemes) {
@@ -67,14 +111,14 @@ export class StringSchema<
67
111
  }
68
112
  }
69
113
 
70
- const maxGraphemes = this.options?.maxGraphemes
114
+ const maxGraphemes = this.options.maxGraphemes
71
115
  if (maxGraphemes != null) {
72
116
  if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
73
117
  return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
74
118
  }
75
119
  }
76
120
 
77
- const format = this.options?.format
121
+ const format = this.options.format
78
122
  if (format != null && !isStringFormat(str, format)) {
79
123
  return ctx.issueInvalidFormat(str, format)
80
124
  }
@@ -123,8 +167,57 @@ export function coerceToString(input: unknown): string | null {
123
167
  }
124
168
  }
125
169
 
126
- export const string = /*#__PURE__*/ memoizedOptions(function <
127
- const O extends StringSchemaOptions = NonNullable<unknown>,
128
- >(options?: StringSchemaOptions & O) {
129
- return new StringSchema<O>(options)
130
- })
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
+
196
+ /**
197
+ * Creates a string schema with optional format and length constraints.
198
+ *
199
+ * Strings can be validated against various formats (datetime, uri, did, handle, etc.)
200
+ * and constrained by length in UTF-8 bytes or grapheme clusters.
201
+ *
202
+ * @param options - Optional configuration for format and length constraints
203
+ * @returns A new {@link StringSchema} instance
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * // Basic string
208
+ * const nameSchema = l.string()
209
+ *
210
+ * // With format validation
211
+ * const dateSchema = l.string({ format: 'datetime' })
212
+ *
213
+ * // With length constraints (UTF-8 bytes)
214
+ * const bioSchema = l.string({ maxLength: 256 })
215
+ *
216
+ * // With grapheme constraints (user-perceived characters)
217
+ * const displayNameSchema = l.string({ maxGraphemes: 64 })
218
+ *
219
+ * // Combining constraints
220
+ * const handleSchema = l.string({ format: 'handle', minLength: 3, maxLength: 253 })
221
+ * ```
222
+ */
223
+ export const string = /*#__PURE__*/ memoizedOptions(_string)
@@ -1,18 +1,51 @@
1
+ import { LexValue } from '@atproto/lex-data'
1
2
  import { Infer, NsidString, Schema } from '../core.js'
2
3
  import { ParamsSchema } from './params.js'
3
4
 
5
+ /**
6
+ * Infers the parameters type from a Subscription definition.
7
+ *
8
+ * @template S - The Subscription type
9
+ */
4
10
  export type InferSubscriptionParameters<S extends Subscription> = Infer<
5
11
  S['parameters']
6
12
  >
7
13
 
14
+ /**
15
+ * Infers the message type from a Subscription definition.
16
+ *
17
+ * @template S - The Subscription type
18
+ */
8
19
  export type InferSubscriptionMessage<S extends Subscription> = Infer<
9
20
  S['message']
10
21
  >
11
22
 
23
+ /**
24
+ * Represents a Lexicon subscription (WebSocket) endpoint definition.
25
+ *
26
+ * Subscriptions are real-time event streams delivered over WebSocket.
27
+ * They have parameters for initializing the connection and a message
28
+ * schema for validating incoming events.
29
+ *
30
+ * @template TNsid - The NSID identifying this subscription
31
+ * @template TParameters - The connection parameters schema type
32
+ * @template TMessage - The message schema type
33
+ * @template TErrors - Array of error type strings, or undefined
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const firehose = new Subscription(
38
+ * 'com.atproto.sync.subscribeRepos',
39
+ * l.params({ cursor: l.optional(l.integer()) }),
40
+ * repoEventSchema,
41
+ * ['FutureCursor']
42
+ * )
43
+ * ```
44
+ */
12
45
  export class Subscription<
13
46
  const TNsid extends NsidString = NsidString,
14
47
  const TParameters extends ParamsSchema = ParamsSchema,
15
- const TMessage extends Schema = Schema,
48
+ const TMessage extends Schema<LexValue> = Schema<LexValue>,
16
49
  const TErrors extends undefined | readonly string[] =
17
50
  | undefined
18
51
  | readonly string[],
@@ -27,11 +60,48 @@ export class Subscription<
27
60
  ) {}
28
61
  }
29
62
 
63
+ /**
64
+ * Creates a subscription definition for a Lexicon WebSocket endpoint.
65
+ *
66
+ * Subscriptions enable real-time event streaming. The connection is
67
+ * initialized with parameters, and the server sends messages matching
68
+ * the message schema.
69
+ *
70
+ * @param nsid - The NSID identifying this subscription endpoint
71
+ * @param parameters - Schema for connection parameters
72
+ * @param message - Schema for validating incoming messages
73
+ * @param errors - Optional array of error type strings
74
+ * @returns A new {@link Subscription} instance
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * // Repository event stream
79
+ * const subscribeRepos = l.subscription(
80
+ * 'com.atproto.sync.subscribeRepos',
81
+ * l.params({
82
+ * cursor: l.optional(l.integer()),
83
+ * }),
84
+ * l.typedUnion([
85
+ * l.typedRef(() => commitEventSchema),
86
+ * l.typedRef(() => handleEventSchema),
87
+ * l.typedRef(() => identityEventSchema),
88
+ * ], false),
89
+ * ['FutureCursor', 'ConsumerTooSlow'],
90
+ * )
91
+ *
92
+ * // Label stream
93
+ * const subscribeLabels = l.subscription(
94
+ * 'com.atproto.label.subscribeLabels',
95
+ * l.params({ cursor: l.optional(l.integer()) }),
96
+ * labelEventSchema,
97
+ * )
98
+ * ```
99
+ */
30
100
  /*@__NO_SIDE_EFFECTS__*/
31
101
  export function subscription<
32
102
  const N extends NsidString,
33
103
  const P extends ParamsSchema,
34
- const M extends Schema,
104
+ const M extends Schema<LexValue>,
35
105
  const E extends undefined | readonly string[] = undefined,
36
106
  >(nsid: N, parameters: P, message: M, errors: E = undefined as E) {
37
107
  return new Subscription<N, P, M, E>(nsid, parameters, message, errors)