@atproto/lex-schema 0.0.13 → 0.0.15

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 (81) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/core/schema.d.ts +27 -35
  3. package/dist/core/schema.d.ts.map +1 -1
  4. package/dist/core/schema.js +73 -54
  5. package/dist/core/schema.js.map +1 -1
  6. package/dist/core/standard-schema.d.ts +14 -0
  7. package/dist/core/standard-schema.d.ts.map +1 -0
  8. package/dist/core/standard-schema.js +27 -0
  9. package/dist/core/standard-schema.js.map +1 -0
  10. package/dist/core/string-format.d.ts +4 -28
  11. package/dist/core/string-format.d.ts.map +1 -1
  12. package/dist/core/string-format.js +23 -17
  13. package/dist/core/string-format.js.map +1 -1
  14. package/dist/core/validation-error.d.ts +14 -6
  15. package/dist/core/validation-error.d.ts.map +1 -1
  16. package/dist/core/validation-error.js +18 -8
  17. package/dist/core/validation-error.js.map +1 -1
  18. package/dist/core/validation-issue.d.ts +15 -15
  19. package/dist/core/validation-issue.d.ts.map +1 -1
  20. package/dist/core/validation-issue.js +33 -29
  21. package/dist/core/validation-issue.js.map +1 -1
  22. package/dist/core/validator.d.ts +21 -18
  23. package/dist/core/validator.d.ts.map +1 -1
  24. package/dist/core/validator.js +5 -4
  25. package/dist/core/validator.js.map +1 -1
  26. package/dist/core.d.ts +0 -1
  27. package/dist/core.d.ts.map +1 -1
  28. package/dist/core.js +0 -1
  29. package/dist/core.js.map +1 -1
  30. package/dist/schema/custom.d.ts +1 -1
  31. package/dist/schema/custom.d.ts.map +1 -1
  32. package/dist/schema/custom.js.map +1 -1
  33. package/dist/schema/never.d.ts +1 -1
  34. package/dist/schema/nullable.d.ts +1 -1
  35. package/dist/schema/params.d.ts.map +1 -1
  36. package/dist/schema/params.js +4 -1
  37. package/dist/schema/params.js.map +1 -1
  38. package/dist/schema/record.d.ts +12 -6
  39. package/dist/schema/record.d.ts.map +1 -1
  40. package/dist/schema/record.js +21 -12
  41. package/dist/schema/record.js.map +1 -1
  42. package/dist/schema/ref.d.ts +1 -1
  43. package/dist/schema/ref.d.ts.map +1 -1
  44. package/dist/schema/ref.js.map +1 -1
  45. package/dist/schema/refine.d.ts +1 -1
  46. package/dist/schema/refine.d.ts.map +1 -1
  47. package/dist/schema/refine.js.map +1 -1
  48. package/dist/schema/typed-object.d.ts +12 -4
  49. package/dist/schema/typed-object.d.ts.map +1 -1
  50. package/dist/schema/typed-object.js +21 -12
  51. package/dist/schema/typed-object.js.map +1 -1
  52. package/dist/schema/typed-ref.d.ts +1 -1
  53. package/dist/schema/typed-union.d.ts +1 -1
  54. package/dist/schema/union.d.ts +2 -2
  55. package/dist/schema/union.d.ts.map +1 -1
  56. package/dist/schema/union.js +1 -1
  57. package/dist/schema/union.js.map +1 -1
  58. package/package.json +4 -3
  59. package/src/core/schema.ts +78 -69
  60. package/src/core/standard-schema.test.ts +124 -0
  61. package/src/core/standard-schema.ts +31 -0
  62. package/src/core/string-format.ts +26 -33
  63. package/src/core/validation-error.ts +25 -10
  64. package/src/core/validation-issue.ts +32 -32
  65. package/src/core/validator.ts +16 -12
  66. package/src/core.ts +0 -1
  67. package/src/schema/array.test.ts +2 -2
  68. package/src/schema/custom.ts +1 -7
  69. package/src/schema/params.test.ts +18 -2
  70. package/src/schema/params.ts +5 -2
  71. package/src/schema/record.ts +27 -22
  72. package/src/schema/ref.ts +1 -5
  73. package/src/schema/refine.ts +0 -1
  74. package/src/schema/typed-object.test.ts +38 -0
  75. package/src/schema/typed-object.ts +29 -24
  76. package/src/schema/union.ts +2 -2
  77. package/dist/core/property-key.d.ts +0 -2
  78. package/dist/core/property-key.d.ts.map +0 -1
  79. package/dist/core/property-key.js +0 -3
  80. package/dist/core/property-key.js.map +0 -1
  81. package/src/core/property-key.ts +0 -1
@@ -1,3 +1,6 @@
1
+ import { StandardSchemaV1 } from '@standard-schema/spec'
2
+ import { lazyProperty } from '../util/lazy-property.js'
3
+ import { StandardSchemaAdapter } from './standard-schema.js'
1
4
  import {
2
5
  InferInput,
3
6
  InferOutput,
@@ -51,7 +54,6 @@ export interface SchemaInternals<out TInput = unknown, out TOutput = TInput> {
51
54
  *
52
55
  * @typeParam TInput - The type accepted as valid input during validation
53
56
  * @typeParam TOutput - The type returned after parsing (may include transformations)
54
- * @typeParam TInternals - Internal type structure for type inference
55
57
  *
56
58
  * @example
57
59
  * ```typescript
@@ -66,19 +68,13 @@ export interface SchemaInternals<out TInput = unknown, out TOutput = TInput> {
66
68
  *
67
69
  * const schema = new MySchema()
68
70
  * schema.assert('hello') // OK
69
- * schema.assert(123) // Throws ValidationError
71
+ * schema.assert(123) // Throws LexValidationError
70
72
  * schema.matches('hello') // true
71
73
  * schema.matches(123) // false
72
74
  * ```
73
75
  */
74
- export abstract class Schema<
75
- out TInput = unknown,
76
- out TOutput = TInput,
77
- out TInternals extends SchemaInternals<TInput, TOutput> = SchemaInternals<
78
- TInput,
79
- TOutput
80
- >,
81
- > implements Validator<TInternals['input'], TInternals['output']>
76
+ export abstract class Schema<out TInput = unknown, out TOutput = TInput>
77
+ implements Validator<TInput, TOutput>, StandardSchemaV1<TInput, TOutput>
82
78
  {
83
79
  /**
84
80
  * Internal phantom property for type inference.
@@ -86,7 +82,13 @@ export abstract class Schema<
86
82
  *
87
83
  * @internal
88
84
  */
89
- declare readonly ['__lex']: TInternals
85
+ declare readonly ['__lex']: SchemaInternals<TInput, TOutput>
86
+
87
+ get '~standard'(): StandardSchemaV1.Props<TInput, TOutput> {
88
+ // Lazily create, and cache, the Standard Schema adapter for this schema
89
+ // instance.
90
+ return lazyProperty(this, '~standard', new StandardSchemaAdapter(this))
91
+ }
90
92
 
91
93
  // Needed to discriminate multiple schema types when used in unions. Without
92
94
  // this, Typescript could allow an EnumSchema<"foo" | "bar"> to be used where
@@ -190,12 +192,12 @@ export abstract class Schema<
190
192
  *
191
193
  * Unlike {@link validate}, this method allows the schema to transform
192
194
  * the input value (e.g., applying default values, type coercion).
193
- * Throws a {@link ValidationError} if the input is invalid.
195
+ * Throws a {@link LexValidationError} if the input is invalid.
194
196
  *
195
197
  * @param input - The value to parse
196
198
  * @param options - Optional parsing configuration
197
199
  * @returns The parsed and potentially transformed value
198
- * @throws {ValidationError} If the input fails validation
200
+ * @throws {LexValidationError} If the input fails validation
199
201
  *
200
202
  * @example
201
203
  * ```typescript
@@ -244,13 +246,13 @@ export abstract class Schema<
244
246
  *
245
247
  * Unlike {@link parse}, this method requires the input to exactly match
246
248
  * the schema without any transformations (no defaults applied, no coercion).
247
- * Throws a {@link ValidationError} if the input is invalid or would require transformation.
249
+ * Throws a {@link LexValidationError} if the input is invalid or would require transformation.
248
250
  *
249
251
  * @typeParam I - The input type (preserved in the return type)
250
252
  * @param input - The value to validate
251
253
  * @param options - Optional validation configuration
252
254
  * @returns The validated input with narrowed type
253
- * @throws {ValidationError} If the input fails validation or requires transformation
255
+ * @throws {LexValidationError} If the input fails validation or requires transformation
254
256
  *
255
257
  * @example
256
258
  * ```typescript
@@ -297,104 +299,111 @@ export abstract class Schema<
297
299
 
298
300
  // @NOTE Dollar-prefixed aliases
299
301
  //
300
- // The built lexicons namespaces export utility functions that allow accessing
301
- // the schema's methods without the need to specify ".main." as part of the
302
- // namespace. This way, a utility for a particular record type can be called
303
- // like "app.bsky.feed.post.<utility>()" instead of
304
- // "app.bsky.feed.post.main.<utility>()". Because those utilities could
305
- // conflict with other schemas (e.g. if there is a lexicon definition at
306
- // "#<utility>"), those exported utilities will be prefixed with "$". In order
307
- // to be able to consistently call the utilities, when using the "main" and
308
- // non "main" definitions, we also expose the same methods with a "$" prefix.
309
- // Thanks to this, both of the following call will be possible:
302
+ // The `lex-builder` lib generates namespaced utility functions that allow
303
+ // accessing the schema's methods without the need to specify the ".main."
304
+ // part of the namespace. This allows utilities for a particular record type
305
+ // to be called like "app.bsky.feed.post.<utility>()" instead of
306
+ // "app.bsky.feed.post.main.<utility>()".
307
+ //
308
+ // Because those utilities could conflict with other schemas (e.g. if there is
309
+ // a lexicon definition with the same name as the "<utility>"), those exported
310
+ // utilities will be prefixed with "$".
311
+ //
312
+ // Similarly, since those utilities are defined as simple "const", they are
313
+ // also bound (using JS's .bind) to the schema instance, so that they can be
314
+ // used without worrying about the context (e.g. "app.bsky.feed.post.$parse()"
315
+ // will work regardless of how it is imported or called).
316
+ //
317
+ // In order to provide the same functionalities for non-main definitions, we
318
+ // also define those aliases directly on the schema instance, so that they can
319
+ // be used in the same way as the utilities generated by "lex-builder". For
320
+ // example, if there is a non-main definition "app.bsky.feed.defs.postView",
321
+ // it will also be possible to call "app.bsky.feed.defs.postView.$parse()".
322
+ //
323
+ // These methods are also "bound" to the instance so that they can be used
324
+ // exactly like the utilities generated by "lex-builder", without worrying
325
+ // about the context.
310
326
  //
311
- // - "app.bsky.feed.post.$parse(...)" // calls a utility function created by "lex build"
312
- // - "app.bsky.feed.defs.postView.$parse(...)" // uses the alias defined below on the schema instance
327
+ // There are two ways we could "bind" those methods to the instance:
328
+ // 1. Define them as getters that return the bound method (e.g. get $parse() {
329
+ // return this.parse.bind(this) })
330
+ // 2. Define them as properties that are initialized in the constructor (e.g.
331
+ // this.$parse = this.parse.bind(this))
332
+ //
333
+ // Since a **lot** of those methods would end-up being created in systems that
334
+ // contains many schemas (e.g. the appview), we choose the first approach
335
+ // (getters) in order to avoid the overhead of creating all those bound
336
+ // functions upfront when instantiating the schemas.
313
337
 
314
338
  /**
315
- * Alias for {@link assert} with `$` prefix for namespace compatibility.
316
- *
339
+ * Bound alias for {@link assert} for compatibility with generated utilities.
317
340
  * @see {@link assert}
318
341
  */
319
- $assert(input: unknown): asserts input is InferInput<this> {
320
- return this.assert(input)
342
+ get $assert(): typeof this.assert {
343
+ return lazyProperty(this, '$assert', this.assert.bind(this))
321
344
  }
322
345
 
323
346
  /**
324
- * Alias for {@link check} with `$` prefix for namespace compatibility.
325
- *
347
+ * Bound alias for {@link check} for compatibility with generated utilities.
326
348
  * @see {@link check}
327
349
  */
328
- $check(input: unknown): void {
329
- return this.check(input)
350
+ get $check(): typeof this.check {
351
+ return lazyProperty(this, '$check', this.check.bind(this))
330
352
  }
331
353
 
332
354
  /**
333
- * Alias for {@link cast} with `$` prefix for namespace compatibility.
334
- *
355
+ * Bound alias for {@link cast} for compatibility with generated utilities.
335
356
  * @see {@link cast}
336
357
  */
337
- $cast<I>(input: I): I & InferInput<this> {
338
- return this.cast(input)
358
+ get $cast(): typeof this.cast {
359
+ return lazyProperty(this, '$cast', this.cast.bind(this))
339
360
  }
340
361
 
341
362
  /**
342
- * Alias for {@link matches} with `$` prefix for namespace compatibility.
343
- *
363
+ * Bound alias for {@link matches} for compatibility with generated utilities.
344
364
  * @see {@link matches}
345
365
  */
346
- $matches(input: unknown): input is InferInput<this> {
347
- return this.matches(input)
366
+ get $matches(): typeof this.matches {
367
+ return lazyProperty(this, '$matches', this.matches.bind(this))
348
368
  }
349
369
 
350
370
  /**
351
- * Alias for {@link ifMatches} with `$` prefix for namespace compatibility.
352
- *
371
+ * Bound alias for {@link ifMatches} for compatibility with generated utilities.
353
372
  * @see {@link ifMatches}
354
373
  */
355
- $ifMatches<I>(input: I): (I & InferInput<this>) | undefined {
356
- return this.ifMatches(input)
374
+ get $ifMatches(): typeof this.ifMatches {
375
+ return lazyProperty(this, '$ifMatches', this.ifMatches.bind(this))
357
376
  }
358
377
 
359
378
  /**
360
- * Alias for {@link parse} with `$` prefix for namespace compatibility.
361
- *
379
+ * Bound alias for {@link parse} for compatibility with generated utilities.
362
380
  * @see {@link parse}
363
381
  */
364
- $parse(input: unknown, options?: ValidateOptions): InferOutput<this> {
365
- return this.parse(input, options)
382
+ get $parse(): typeof this.parse {
383
+ return lazyProperty(this, '$parse', this.parse.bind(this))
366
384
  }
367
385
 
368
386
  /**
369
- * Alias for {@link safeParse} with `$` prefix for namespace compatibility.
370
- *
387
+ * Bound alias for {@link safeParse} for compatibility with generated utilities.
371
388
  * @see {@link safeParse}
372
389
  */
373
- $safeParse(
374
- input: unknown,
375
- options?: ValidateOptions,
376
- ): ValidationResult<InferOutput<this>> {
377
- return this.safeParse(input, options)
390
+ get $safeParse(): typeof this.safeParse {
391
+ return lazyProperty(this, '$safeParse', this.safeParse.bind(this))
378
392
  }
379
393
 
380
394
  /**
381
- * Alias for {@link validate} with `$` prefix for namespace compatibility.
382
- *
395
+ * Bound alias for {@link validate} for compatibility with generated utilities.
383
396
  * @see {@link validate}
384
397
  */
385
- $validate<I>(input: I, options?: ValidateOptions): I & InferInput<this> {
386
- return this.validate(input, options)
398
+ get $validate(): typeof this.validate {
399
+ return lazyProperty(this, '$validate', this.validate.bind(this))
387
400
  }
388
401
 
389
402
  /**
390
- * Alias for {@link safeValidate} with `$` prefix for namespace compatibility.
391
- *
403
+ * Bound alias for {@link safeValidate} for compatibility with generated utilities.
392
404
  * @see {@link safeValidate}
393
405
  */
394
- $safeValidate<I>(
395
- input: I,
396
- options?: ValidateOptions,
397
- ): ValidationResult<I & InferInput<this>> {
398
- return this.safeValidate(input, options)
406
+ get $safeValidate(): typeof this.safeValidate {
407
+ return lazyProperty(this, '$safeValidate', this.safeValidate.bind(this))
399
408
  }
400
409
  }
@@ -0,0 +1,124 @@
1
+ import { assert, describe, expect, it } from 'vitest'
2
+ import { array } from '../schema/array.js'
3
+ import { integer } from '../schema/integer.js'
4
+ import { object } from '../schema/object.js'
5
+ import { optional } from '../schema/optional.js'
6
+ import { string } from '../schema/string.js'
7
+ import { withDefault } from '../schema/with-default.js'
8
+ import { LexValidationError } from './validation-error.js'
9
+
10
+ describe('StandardSchemaAdapter', () => {
11
+ describe('metadata', () => {
12
+ const schema = integer()
13
+
14
+ it('has version 1', () => {
15
+ expect(schema['~standard'].version).toBe(1)
16
+ })
17
+
18
+ it('has vendor @atproto/lex-schema', () => {
19
+ expect(schema['~standard'].vendor).toBe('@atproto/lex-schema')
20
+ })
21
+ })
22
+
23
+ describe('lazy caching', () => {
24
+ it('returns the same adapter instance on repeated accesses', () => {
25
+ const schema = integer()
26
+ const first = schema['~standard']
27
+ const second = schema['~standard']
28
+ expect(first).toBe(second)
29
+ })
30
+ })
31
+
32
+ describe('validate() result shape on success', () => {
33
+ it('returns a value property for a valid integer', () => {
34
+ const result = integer()['~standard'].validate(42)
35
+ expect(result).toMatchObject({ value: 42 })
36
+ })
37
+
38
+ it('returns a value property for a valid string', () => {
39
+ const result = string()['~standard'].validate('hello')
40
+ expect(result).toMatchObject({ value: 'hello' })
41
+ })
42
+
43
+ it('does not include an issues property on success', () => {
44
+ const result = integer()['~standard'].validate(1)
45
+ expect(result).not.toHaveProperty('issues')
46
+ })
47
+ })
48
+
49
+ describe('validate() result shape on failure', () => {
50
+ it('returns a LexValidationError with issues for an invalid value', () => {
51
+ const result = integer()['~standard'].validate('not-a-number')
52
+ assert(result instanceof LexValidationError)
53
+ expect(Array.isArray(result.issues)).toBe(true)
54
+ expect(result.issues.length).toBeGreaterThan(0)
55
+ })
56
+
57
+ it('does not include a value property on failure', () => {
58
+ const result = integer()['~standard'].validate('not-a-number')
59
+ expect(result).not.toHaveProperty('value')
60
+ })
61
+
62
+ describe('issues[].message', () => {
63
+ it('is a non-empty string', () => {
64
+ const result = integer()['~standard'].validate('not-a-number')
65
+ assert(result instanceof LexValidationError)
66
+ for (const issue of result.issues) {
67
+ expect(typeof issue.message).toBe('string')
68
+ expect(issue.message.length).toBeGreaterThan(0)
69
+ }
70
+ })
71
+
72
+ it('describes the type mismatch', () => {
73
+ const result = integer()['~standard'].validate('not-a-number')
74
+ assert(result instanceof LexValidationError)
75
+ expect(result.issues[0].message).toContain('integer')
76
+ })
77
+ })
78
+
79
+ describe('issues[].path', () => {
80
+ it('is an empty array for a root-level failure', () => {
81
+ const result = integer()['~standard'].validate('not-a-number')
82
+ assert(result instanceof LexValidationError)
83
+ expect(result.issues[0].path).toEqual([])
84
+ })
85
+
86
+ it('reflects the property key for a nested object failure', () => {
87
+ const schema = object({ age: integer() })
88
+ const result = schema['~standard'].validate({ age: 'not-a-number' })
89
+ assert(result instanceof LexValidationError)
90
+ expect(result.issues[0].path).toContain('age')
91
+ })
92
+
93
+ it('reflects the index for an array element failure', () => {
94
+ const schema = array(integer())
95
+ const result = schema['~standard'].validate([1, 'bad', 3])
96
+ assert(result instanceof LexValidationError)
97
+ expect(result.issues[0].path).toContain(1)
98
+ })
99
+ })
100
+ })
101
+
102
+ describe('parse mode (default value application)', () => {
103
+ it('applies default values when input is undefined', () => {
104
+ const schema = withDefault(integer(), 10)
105
+ const result = schema['~standard'].validate(undefined)
106
+ expect(result).toMatchObject({ value: 10 })
107
+ })
108
+
109
+ it('uses the provided value instead of default when input is present', () => {
110
+ const schema = withDefault(integer(), 10)
111
+ const result = schema['~standard'].validate(42)
112
+ expect(result).toMatchObject({ value: 42 })
113
+ })
114
+
115
+ it('applies defaults for optional object properties in parse mode', () => {
116
+ const schema = object({
117
+ name: string(),
118
+ count: optional(withDefault(integer(), 0)),
119
+ })
120
+ const result = schema['~standard'].validate({ name: 'Alice' })
121
+ expect(result).toMatchObject({ value: { name: 'Alice', count: 0 } })
122
+ })
123
+ })
124
+ })
@@ -0,0 +1,31 @@
1
+ import { StandardSchemaV1 } from '@standard-schema/spec'
2
+ import { ValidationContext, Validator } from './validator.js'
3
+
4
+ /**
5
+ * The Standard Schema adapter for {@link Validator} instances.
6
+ */
7
+ export class StandardSchemaAdapter<TInput, TOutput>
8
+ implements StandardSchemaV1.Props<TInput, TOutput>
9
+ {
10
+ readonly version = 1
11
+
12
+ readonly vendor = '@atproto/lex-schema'
13
+
14
+ declare readonly types: StandardSchemaV1.Types<TInput, TOutput>
15
+
16
+ constructor(private readonly validator: Validator<TInput, TOutput>) {}
17
+
18
+ validate(
19
+ value: unknown,
20
+ options?: StandardSchemaV1.Options,
21
+ ): StandardSchemaV1.Result<TOutput> {
22
+ // Perform validation in "parse" mode to ensure transformations (defaults,
23
+ // coercions, etc.) are applied. Also ensures that the output type is
24
+ // returned. Note that ValidationResult is compatible with
25
+ // StandardSchemaV1.Result :-)
26
+ return ValidationContext.validate(value, this.validator, {
27
+ ...options?.libraryOptions,
28
+ mode: 'parse',
29
+ })
30
+ }
31
+ }
@@ -9,9 +9,9 @@ import {
9
9
  RecordKeyString,
10
10
  TidString,
11
11
  UriString,
12
- isValidAtIdentifier as isValidAtId,
12
+ isAtIdentifierString,
13
+ isDatetimeString,
13
14
  isValidAtUri,
14
- isValidDatetime,
15
15
  isValidDid,
16
16
  isValidHandle,
17
17
  isValidLanguage,
@@ -26,21 +26,30 @@ import { CheckFn } from '../util/assertion-util.js'
26
26
  // Individual string format types and type guards
27
27
  // -----------------------------------------------------------------------------
28
28
 
29
- /**
30
- * Type guard that checks if a value is a valid AT identifier (DID or handle).
31
- *
32
- * @param value - The value to check
33
- * @returns `true` if the value is a valid AT identifier
34
- */
35
- export const isAtIdentifierString: CheckFn<AtIdentifierString> = isValidAtId
36
- export type {
37
- /**
38
- * An AT identifier string - either a DID or a handle.
39
- *
40
- * @example `"did:plc:1234..."` or `"alice.bsky.social"`
41
- */
42
- AtIdentifierString,
43
- }
29
+ // Re-exporting from @atproto/syntax without modification to preserve types and
30
+ // documentation for types and utilities that are already well-defined there.
31
+ // @TODO rework other string formats in @atproto/syntax to follow this pattern
32
+ // and re-export here, e.g. language tags, NSIDs, record keys, etc.
33
+
34
+ export {
35
+ type AtIdentifierString,
36
+ asAtIdentifierString,
37
+ ifAtIdentifierString,
38
+ isAtIdentifierString,
39
+ } from '@atproto/syntax'
40
+
41
+ // AtIdentifierString utilities
42
+ export { isDidIdentifier, isHandleIdentifier } from '@atproto/syntax'
43
+
44
+ export {
45
+ type DatetimeString,
46
+ asDatetimeString,
47
+ ifDatetimeString,
48
+ isDatetimeString,
49
+ } from '@atproto/syntax'
50
+
51
+ // DatetimeString utilities
52
+ export { currentDatetimeString, toDatetimeString } from '@atproto/syntax'
44
53
 
45
54
  /**
46
55
  * Type guard that checks if a value is a valid AT URI.
@@ -74,22 +83,6 @@ export const isCidString = ((v) => validateCidString(v)) as CheckFn<CidString>
74
83
  */
75
84
  export type CidString = string
76
85
 
77
- /**
78
- * Type guard that checks if a value is a valid datetime string.
79
- *
80
- * @param value - The value to check
81
- * @returns `true` if the value is a valid datetime string
82
- */
83
- export const isDatetimeString: CheckFn<DatetimeString> = isValidDatetime
84
- export type {
85
- /**
86
- * An ISO 8601 datetime string.
87
- *
88
- * @example `"2024-01-15T12:30:00.000Z"`
89
- */
90
- DatetimeString,
91
- }
92
-
93
86
  /**
94
87
  * Type guard that checks if a value is a valid DID string.
95
88
  *
@@ -19,7 +19,7 @@ import {
19
19
  *
20
20
  * @example
21
21
  * ```typescript
22
- * const error = new ValidationError([
22
+ * const error = new LexValidationError([
23
23
  * new IssueInvalidType(['user', 'age'], 'hello', ['number'])
24
24
  * ])
25
25
  * console.log(error.message)
@@ -29,9 +29,16 @@ import {
29
29
  * console.log(error.toJSON())
30
30
  * // { error: 'InvalidRequest', message: '...', issues: [...] }
31
31
  * ```
32
+ *
33
+ * @note this class implements {@link ResultFailure} to allow it to be used
34
+ * directly as a failure reason in validation results, avoiding the need for
35
+ * wrapping it in an additional object.
32
36
  */
33
- export class ValidationError extends LexError {
34
- name = 'ValidationError'
37
+ export class LexValidationError
38
+ extends LexError<'InvalidRequest'>
39
+ implements ResultFailure<LexValidationError>
40
+ {
41
+ name = 'LexValidationError'
35
42
 
36
43
  /**
37
44
  * The list of validation issues that caused this error.
@@ -56,12 +63,20 @@ export class ValidationError extends LexError {
56
63
  this.issues = issuesAgg
57
64
  }
58
65
 
66
+ /** @see {ResultFailure.success} */
67
+ readonly success = false as const
68
+
69
+ /** @see {ResultFailure.reason} */
70
+ get reason() {
71
+ return this
72
+ }
73
+
59
74
  /**
60
75
  * Converts the error to a JSON-serializable object.
61
76
  *
62
- * @returns An object containing the error details and all issues in JSON format
77
+ * @returns An object containing the error details and issues details
63
78
  */
64
- toJSON() {
79
+ override toJSON() {
65
80
  return {
66
81
  ...super.toJSON(),
67
82
  issues: this.issues.map((issue) => issue.toJSON()),
@@ -81,23 +96,23 @@ export class ValidationError extends LexError {
81
96
  * ```typescript
82
97
  * const failures = schemas.map(s => s.safeValidate(data)).filter(r => !r.success)
83
98
  * if (failures.length === schemas.length) {
84
- * throw ValidationError.fromFailures(failures)
99
+ * throw LexValidationError.fromFailures(failures)
85
100
  * }
86
101
  * ```
87
102
  */
88
103
  static fromFailures(
89
- failures: ResultFailure<ValidationError>[],
90
- ): ValidationError {
104
+ failures: readonly ResultFailure<LexValidationError>[],
105
+ ): LexValidationError {
91
106
  if (failures.length === 1) return failureReason(failures[0])
92
107
  const issues = failures.flatMap(extractFailureIssues)
93
- return new ValidationError(issues, {
108
+ return new LexValidationError(issues, {
94
109
  // Keep the original errors as the cause chain
95
110
  cause: failures.map(failureReason),
96
111
  })
97
112
  }
98
113
  }
99
114
 
100
- function extractFailureIssues(result: ResultFailure<ValidationError>) {
115
+ function extractFailureIssues(result: ResultFailure<LexValidationError>) {
101
116
  return result.reason.issues
102
117
  }
103
118