@atproto/lex-schema 0.0.14 → 0.0.16

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 (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/core/schema.d.ts +5 -4
  3. package/dist/core/schema.d.ts.map +1 -1
  4. package/dist/core/schema.js +7 -2
  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 +24 -17
  11. package/dist/core/string-format.d.ts.map +1 -1
  12. package/dist/core/string-format.js +57 -30
  13. package/dist/core/string-format.js.map +1 -1
  14. package/dist/core/validation-error.d.ts +10 -2
  15. package/dist/core/validation-error.d.ts.map +1 -1
  16. package/dist/core/validation-error.js +10 -0
  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 +29 -14
  23. package/dist/core/validator.d.ts.map +1 -1
  24. package/dist/core/validator.js +4 -2
  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/blob.d.ts +10 -8
  31. package/dist/schema/blob.d.ts.map +1 -1
  32. package/dist/schema/blob.js +39 -14
  33. package/dist/schema/blob.js.map +1 -1
  34. package/dist/schema/custom.d.ts +1 -1
  35. package/dist/schema/custom.d.ts.map +1 -1
  36. package/dist/schema/custom.js.map +1 -1
  37. package/dist/schema/never.d.ts +1 -1
  38. package/dist/schema/nullable.d.ts +1 -1
  39. package/dist/schema/payload.d.ts +2 -2
  40. package/dist/schema/payload.d.ts.map +1 -1
  41. package/dist/schema/payload.js.map +1 -1
  42. package/dist/schema/record.d.ts +1 -1
  43. package/dist/schema/ref.d.ts +1 -1
  44. package/dist/schema/ref.d.ts.map +1 -1
  45. package/dist/schema/ref.js.map +1 -1
  46. package/dist/schema/refine.d.ts +1 -1
  47. package/dist/schema/refine.d.ts.map +1 -1
  48. package/dist/schema/refine.js.map +1 -1
  49. package/dist/schema/string.js +1 -1
  50. package/dist/schema/string.js.map +1 -1
  51. package/dist/schema/typed-ref.d.ts +1 -1
  52. package/dist/schema/typed-union.d.ts +1 -1
  53. package/dist/schema/union.d.ts +2 -2
  54. package/dist/schema/union.d.ts.map +1 -1
  55. package/package.json +5 -3
  56. package/src/core/schema.ts +12 -11
  57. package/src/core/standard-schema.test.ts +124 -0
  58. package/src/core/standard-schema.ts +31 -0
  59. package/src/core/string-format.ts +73 -31
  60. package/src/core/validation-error.ts +16 -1
  61. package/src/core/validation-issue.ts +32 -32
  62. package/src/core/validator.ts +26 -6
  63. package/src/core.ts +0 -1
  64. package/src/schema/array.test.ts +2 -2
  65. package/src/schema/blob.test.ts +317 -49
  66. package/src/schema/blob.ts +56 -23
  67. package/src/schema/custom.ts +1 -7
  68. package/src/schema/params.test.ts +2 -2
  69. package/src/schema/payload.ts +2 -2
  70. package/src/schema/ref.ts +1 -5
  71. package/src/schema/refine.ts +0 -1
  72. package/src/schema/string.test.ts +63 -0
  73. package/src/schema/string.ts +1 -1
  74. package/dist/core/property-key.d.ts +0 -2
  75. package/dist/core/property-key.d.ts.map +0 -1
  76. package/dist/core/property-key.js +0 -3
  77. package/dist/core/property-key.js.map +0 -1
  78. package/src/core/property-key.ts +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"string.js","sourceRoot":"","sources":["../../src/schema/string.ts"],"names":[],"mappings":";;;AAiIA,wCAsCC;AAvKD,gDAA+D;AAC/D,wCAQmB;AAEnB,mDAAoD;AACpD,yCAAwC;AAqBxC;;;;;;;;;;;;;GAaG;AACH,MAAa,YAEX,SAAQ,gBAUT;IACU,IAAI,GAAG,QAAiB,CAAA;IAEjC,4EAA4E;IAC5E,8EAA8E;IAC9E,wEAAwE;IACxE,6EAA6E;IAC7E,wBAAwB;IACf,OAAO,CAAqB;IAErC,YAAY,OAAiB;QAC3B,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,iBAAiB,CAAC,KAAc,EAAE,GAAsB;QACtD,MAAM,GAAG,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;QACjC,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YAChB,OAAO,GAAG,CAAC,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;QACjD,CAAC;QAED,IAAI,WAAmB,CAAA;QAEvB,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAA;QACxC,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,KAAK,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC,GAAG,SAAS,EAAE,CAAC;gBAC/C,OAAO,GAAG,CAAC,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,CAAC,CAAA;YACjE,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAA;QACxC,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YACtB,uEAAuE;YACvE,wEAAwE;YACxE,qCAAqC;YACrC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;gBAChC,2DAA2D;YAC7D,CAAC;iBAAM,IAAI,CAAC,WAAW,KAAK,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC,GAAG,SAAS,EAAE,CAAC;gBACtD,OAAO,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,CAAC,CAAA;YAC/D,CAAC;QACH,CAAC;QAED,IAAI,YAAoB,CAAA;QAExB,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAA;QAC9C,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;YACzB,2EAA2E;YAC3E,IAAI,GAAG,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;gBAC9B,OAAO,GAAG,CAAC,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;YACrE,CAAC;iBAAM,IAAI,CAAC,YAAY,KAAK,IAAA,sBAAW,EAAC,GAAG,CAAC,CAAC,GAAG,YAAY,EAAE,CAAC;gBAC9D,OAAO,GAAG,CAAC,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,CAAC,CAAA;YACvE,CAAC;QACH,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAA;QAC9C,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;YACzB,IAAI,CAAC,YAAY,KAAK,IAAA,sBAAW,EAAC,GAAG,CAAC,CAAC,GAAG,YAAY,EAAE,CAAC;gBACvD,OAAO,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,CAAC,CAAA;YACrE,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAA;QAClC,IAAI,MAAM,IAAI,IAAI,IAAI,CAAC,IAAA,wBAAc,EAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;YACnD,OAAO,GAAG,CAAC,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QAC5C,CAAC;QAED,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;CACF;AAhFD,oCAgFC;AAED,SAAgB,cAAc,CAAC,KAAc;IAC3C,QAAQ,OAAO,KAAK,EAAE,CAAC;QACrB,wEAAwE;QACxE,oEAAoE;QACpE,uEAAuE;QACvE,mCAAmC;QACnC,KAAK,QAAQ;YACX,OAAO,KAAK,CAAA;QACd,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,KAAK,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAA;YAE9B,uEAAuE;YACvE,yCAAyC;YACzC,IAAI,KAAK,YAAY,sBAAW,EAAE,CAAC;gBACjC,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;YACzB,CAAC;YAED,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;gBAC1B,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;oBAAE,OAAO,IAAI,CAAA;gBAC9C,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;YAC5B,CAAC;YAED,IAAI,KAAK,YAAY,GAAG,EAAE,CAAC;gBACzB,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;YACzB,CAAC;YAED,MAAM,GAAG,GAAG,IAAA,gBAAK,EAAC,KAAK,CAAC,CAAA;YACxB,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;YAE9B,IAAI,KAAK,YAAY,MAAM,EAAE,CAAC;gBAC5B,OAAO,KAAK,CAAC,OAAO,EAAE,CAAA;YACxB,CAAC;QACH,CAAC;QAED,gBAAgB;QAChB;YACE,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC;AAwBD,SAAS,OAAO,CAAC,UAA+B,EAAE;IAChD,OAAO,IAAI,YAAY,CAAC,OAAO,CAAC,CAAA;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACU,QAAA,MAAM,GAAiB,IAAA,4BAAe,EAAC,OAAO,CAAC,CAAA","sourcesContent":["import { graphemeLen, ifCid, utf8Len } from '@atproto/lex-data'\nimport {\n InferStringFormat,\n Restricted,\n Schema,\n StringFormat,\n UnknownString,\n ValidationContext,\n isStringFormat,\n} from '../core.js'\nimport { IfAny } from '../util/if-any.js'\nimport { memoizedOptions } from '../util/memoize.js'\nimport { TokenSchema } from './token.js'\n\n/**\n * Configuration options for string schema validation.\n *\n * @property format - Expected string format (e.g., 'datetime', 'uri', 'at-uri', 'did', 'handle', 'nsid', 'cid', 'tid', 'record-key', 'at-identifier', 'language')\n * @property knownValues - Known string literal values for type narrowing\n * @property minLength - Minimum length in UTF-8 bytes\n * @property maxLength - Maximum length in UTF-8 bytes\n * @property minGraphemes - Minimum number of grapheme clusters\n * @property maxGraphemes - Maximum number of grapheme clusters\n */\nexport type StringSchemaOptions = {\n format?: StringFormat\n knownValues?: readonly string[]\n minLength?: number\n maxLength?: number\n minGraphemes?: number\n maxGraphemes?: number\n}\n\n/**\n * Schema for validating string values with optional format and length constraints.\n *\n * Supports various string formats defined in the Lexicon specification, as well as\n * length constraints measured in UTF-8 bytes or grapheme clusters.\n *\n * @template TOptions - The configuration options type\n *\n * @example\n * ```ts\n * const schema = new StringSchema({ format: 'datetime', maxLength: 64 })\n * const result = schema.validate('2024-01-15T10:30:00Z')\n * ```\n */\nexport class StringSchema<\n const TOptions extends StringSchemaOptions = StringSchemaOptions,\n> extends Schema<\n IfAny<\n TOptions,\n string,\n TOptions extends { format: infer F extends StringFormat }\n ? InferStringFormat<F>\n : TOptions extends { knownValues: readonly (infer V extends string)[] }\n ? V | UnknownString\n : string\n >\n> {\n readonly type = 'string' as const\n\n // @NOTE since the _string utility allows omitting knownValues when TOptions\n // *does* include it (since it's only used for typing), we cannot type options\n // as TOptions directly since it may not actually include knownValues at\n // runtime, making schema.options.knownValues potentially undefined even when\n // TOptions includes it.\n readonly options: StringSchemaOptions\n\n constructor(options: TOptions) {\n super()\n this.options = options\n }\n\n validateInContext(input: unknown, ctx: ValidationContext) {\n const str = coerceToString(input)\n if (str == null) {\n return ctx.issueUnexpectedType(input, 'string')\n }\n\n let lazyUtf8Len: number\n\n const minLength = this.options.minLength\n if (minLength != null) {\n if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {\n return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)\n }\n }\n\n const maxLength = this.options.maxLength\n if (maxLength != null) {\n // Optimization: we can avoid computing the UTF-8 length if the maximum\n // possible length, in bytes, of the input JS string is smaller than the\n // maxLength (in UTF-8 string bytes).\n if (str.length * 3 <= maxLength) {\n // Input string so small it can't possibly exceed maxLength\n } else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) {\n return ctx.issueTooBig(str, 'string', maxLength, lazyUtf8Len)\n }\n }\n\n let lazyGraphLen: number\n\n const minGraphemes = this.options.minGraphemes\n if (minGraphemes != null) {\n // Optimization: avoid counting graphemes if the length check already fails\n if (str.length < minGraphemes) {\n return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)\n } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {\n return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)\n }\n }\n\n const maxGraphemes = this.options.maxGraphemes\n if (maxGraphemes != null) {\n if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {\n return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)\n }\n }\n\n const format = this.options.format\n if (format != null && !isStringFormat(str, format)) {\n return ctx.issueInvalidFormat(str, format)\n }\n\n return ctx.success(str)\n }\n}\n\nexport function coerceToString(input: unknown): string | null {\n switch (typeof input) {\n // @NOTE We do *not* coerce numbers/booleans to strings because that can\n // lead to them being accepted as string instead of being coerced to\n // number/boolean when the input is a string and the expected result is\n // number/boolean (e.g. in params).\n case 'string':\n return input\n case 'object': {\n if (input == null) return null\n\n // @NOTE Allow using TokenSchema instances in places expecting strings,\n // converting them to their string value.\n if (input instanceof TokenSchema) {\n return input.toString()\n }\n\n if (input instanceof Date) {\n if (Number.isNaN(input.getTime())) return null\n return input.toISOString()\n }\n\n if (input instanceof URL) {\n return input.toString()\n }\n\n const cid = ifCid(input)\n if (cid) return cid.toString()\n\n if (input instanceof String) {\n return input.valueOf()\n }\n }\n\n // falls through\n default:\n return null\n }\n}\n\nfunction _string(): StringSchema<NonNullable<unknown>>\nfunction _string<\n // Allow calling `string<{ knownValues: [...] }>()` without passing an options\n // object, since knownValues is only used for typing and has no runtime\n // effect, so it can be safely omitted at runtime.\n const TOptions extends {\n knownValues: StringSchemaOptions['knownValues']\n } & {\n [K in Exclude<\n keyof StringSchemaOptions,\n 'knownValues'\n >]?: Restricted<`An options argument is required when using the \"${K}\" option`>\n },\n>(): StringSchema<\n IfAny<TOptions, any, { knownValues: TOptions['knownValues'] }>\n>\nfunction _string<const TOptions extends StringSchemaOptions>(\n // If TOptions is explicitly provided (e.g. `string<{ ... }>({ ... })`), we\n // allow the actual options argument to omit the \"knownValues\" property since\n // it's only used for inferring the type and has no runtime effect.\n options: TOptions | Omit<TOptions, 'knownValues'>,\n): StringSchema<TOptions>\nfunction _string(options: StringSchemaOptions = {}) {\n return new StringSchema(options)\n}\n\n/**\n * Creates a string schema with optional format and length constraints.\n *\n * Strings can be validated against various formats (datetime, uri, did, handle, etc.)\n * and constrained by length in UTF-8 bytes or grapheme clusters.\n *\n * @param options - Optional configuration for format and length constraints\n * @returns A new {@link StringSchema} instance\n *\n * @example\n * ```ts\n * // Basic string\n * const nameSchema = l.string()\n *\n * // With format validation\n * const dateSchema = l.string({ format: 'datetime' })\n *\n * // With length constraints (UTF-8 bytes)\n * const bioSchema = l.string({ maxLength: 256 })\n *\n * // With grapheme constraints (user-perceived characters)\n * const displayNameSchema = l.string({ maxGraphemes: 64 })\n *\n * // Combining constraints\n * const handleSchema = l.string({ format: 'handle', minLength: 3, maxLength: 253 })\n * ```\n */\nexport const string = /*#__PURE__*/ memoizedOptions(_string)\n"]}
1
+ {"version":3,"file":"string.js","sourceRoot":"","sources":["../../src/schema/string.ts"],"names":[],"mappings":"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","sourcesContent":["import { graphemeLen, ifCid, utf8Len } from '@atproto/lex-data'\nimport {\n InferStringFormat,\n Restricted,\n Schema,\n StringFormat,\n UnknownString,\n ValidationContext,\n isStringFormat,\n} from '../core.js'\nimport { IfAny } from '../util/if-any.js'\nimport { memoizedOptions } from '../util/memoize.js'\nimport { TokenSchema } from './token.js'\n\n/**\n * Configuration options for string schema validation.\n *\n * @property format - Expected string format (e.g., 'datetime', 'uri', 'at-uri', 'did', 'handle', 'nsid', 'cid', 'tid', 'record-key', 'at-identifier', 'language')\n * @property knownValues - Known string literal values for type narrowing\n * @property minLength - Minimum length in UTF-8 bytes\n * @property maxLength - Maximum length in UTF-8 bytes\n * @property minGraphemes - Minimum number of grapheme clusters\n * @property maxGraphemes - Maximum number of grapheme clusters\n */\nexport type StringSchemaOptions = {\n format?: StringFormat\n knownValues?: readonly string[]\n minLength?: number\n maxLength?: number\n minGraphemes?: number\n maxGraphemes?: number\n}\n\n/**\n * Schema for validating string values with optional format and length constraints.\n *\n * Supports various string formats defined in the Lexicon specification, as well as\n * length constraints measured in UTF-8 bytes or grapheme clusters.\n *\n * @template TOptions - The configuration options type\n *\n * @example\n * ```ts\n * const schema = new StringSchema({ format: 'datetime', maxLength: 64 })\n * const result = schema.validate('2024-01-15T10:30:00Z')\n * ```\n */\nexport class StringSchema<\n const TOptions extends StringSchemaOptions = StringSchemaOptions,\n> extends Schema<\n IfAny<\n TOptions,\n string,\n TOptions extends { format: infer F extends StringFormat }\n ? InferStringFormat<F>\n : TOptions extends { knownValues: readonly (infer V extends string)[] }\n ? V | UnknownString\n : string\n >\n> {\n readonly type = 'string' as const\n\n // @NOTE since the _string utility allows omitting knownValues when TOptions\n // *does* include it (since it's only used for typing), we cannot type options\n // as TOptions directly since it may not actually include knownValues at\n // runtime, making schema.options.knownValues potentially undefined even when\n // TOptions includes it.\n readonly options: StringSchemaOptions\n\n constructor(options: TOptions) {\n super()\n this.options = options\n }\n\n validateInContext(input: unknown, ctx: ValidationContext) {\n const str = coerceToString(input)\n if (str == null) {\n return ctx.issueUnexpectedType(input, 'string')\n }\n\n let lazyUtf8Len: number\n\n const minLength = this.options.minLength\n if (minLength != null) {\n if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {\n return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)\n }\n }\n\n const maxLength = this.options.maxLength\n if (maxLength != null) {\n // Optimization: we can avoid computing the UTF-8 length if the maximum\n // possible length, in bytes, of the input JS string is smaller than the\n // maxLength (in UTF-8 string bytes).\n if (str.length * 3 <= maxLength) {\n // Input string so small it can't possibly exceed maxLength\n } else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) {\n return ctx.issueTooBig(str, 'string', maxLength, lazyUtf8Len)\n }\n }\n\n let lazyGraphLen: number\n\n const minGraphemes = this.options.minGraphemes\n if (minGraphemes != null) {\n // Optimization: avoid counting graphemes if the length check already fails\n if (str.length < minGraphemes) {\n return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)\n } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {\n return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)\n }\n }\n\n const maxGraphemes = this.options.maxGraphemes\n if (maxGraphemes != null) {\n if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {\n return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)\n }\n }\n\n const format = this.options.format\n if (format != null && !isStringFormat(str, format, ctx.options)) {\n return ctx.issueInvalidFormat(str, format)\n }\n\n return ctx.success(str)\n }\n}\n\nexport function coerceToString(input: unknown): string | null {\n switch (typeof input) {\n // @NOTE We do *not* coerce numbers/booleans to strings because that can\n // lead to them being accepted as string instead of being coerced to\n // number/boolean when the input is a string and the expected result is\n // number/boolean (e.g. in params).\n case 'string':\n return input\n case 'object': {\n if (input == null) return null\n\n // @NOTE Allow using TokenSchema instances in places expecting strings,\n // converting them to their string value.\n if (input instanceof TokenSchema) {\n return input.toString()\n }\n\n if (input instanceof Date) {\n if (Number.isNaN(input.getTime())) return null\n return input.toISOString()\n }\n\n if (input instanceof URL) {\n return input.toString()\n }\n\n const cid = ifCid(input)\n if (cid) return cid.toString()\n\n if (input instanceof String) {\n return input.valueOf()\n }\n }\n\n // falls through\n default:\n return null\n }\n}\n\nfunction _string(): StringSchema<NonNullable<unknown>>\nfunction _string<\n // Allow calling `string<{ knownValues: [...] }>()` without passing an options\n // object, since knownValues is only used for typing and has no runtime\n // effect, so it can be safely omitted at runtime.\n const TOptions extends {\n knownValues: StringSchemaOptions['knownValues']\n } & {\n [K in Exclude<\n keyof StringSchemaOptions,\n 'knownValues'\n >]?: Restricted<`An options argument is required when using the \"${K}\" option`>\n },\n>(): StringSchema<\n IfAny<TOptions, any, { knownValues: TOptions['knownValues'] }>\n>\nfunction _string<const TOptions extends StringSchemaOptions>(\n // If TOptions is explicitly provided (e.g. `string<{ ... }>({ ... })`), we\n // allow the actual options argument to omit the \"knownValues\" property since\n // it's only used for inferring the type and has no runtime effect.\n options: TOptions | Omit<TOptions, 'knownValues'>,\n): StringSchema<TOptions>\nfunction _string(options: StringSchemaOptions = {}) {\n return new StringSchema(options)\n}\n\n/**\n * Creates a string schema with optional format and length constraints.\n *\n * Strings can be validated against various formats (datetime, uri, did, handle, etc.)\n * and constrained by length in UTF-8 bytes or grapheme clusters.\n *\n * @param options - Optional configuration for format and length constraints\n * @returns A new {@link StringSchema} instance\n *\n * @example\n * ```ts\n * // Basic string\n * const nameSchema = l.string()\n *\n * // With format validation\n * const dateSchema = l.string({ format: 'datetime' })\n *\n * // With length constraints (UTF-8 bytes)\n * const bioSchema = l.string({ maxLength: 256 })\n *\n * // With grapheme constraints (user-perceived characters)\n * const displayNameSchema = l.string({ maxGraphemes: 64 })\n *\n * // Combining constraints\n * const handleSchema = l.string({ format: 'handle', minLength: 3, maxLength: 253 })\n * ```\n */\nexport const string = /*#__PURE__*/ memoizedOptions(_string)\n"]}
@@ -41,7 +41,7 @@ export declare class TypedRefSchema<const TValidator extends TypedObjectValidato
41
41
  constructor(getter: TypedRefGetter<TValidator>);
42
42
  get validator(): TValidator;
43
43
  get $type(): TValidator['$type'];
44
- validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").ValidationFailure | import("../core.js").ValidationSuccess<InferInput<TValidator>>;
44
+ validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").LexValidationError | import("../core.js").ValidationSuccess<InferInput<TValidator>>;
45
45
  }
46
46
  /**
47
47
  * Creates a reference to a typed object schema for use in typed unions.
@@ -26,7 +26,7 @@ export declare class TypedUnionSchema<const TValidators extends readonly (TypedR
26
26
  constructor(validators: TValidators, closed: TClosed);
27
27
  get validatorsMap(): Map<unknown, TValidators[number]>;
28
28
  get $types(): unknown[];
29
- validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").ValidationFailure | import("../core.js").ValidationSuccess<Record<string, unknown>> | import("../core.js").ValidationSuccess<InferInput<TValidators[number]>>;
29
+ validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").LexValidationError | import("../core.js").ValidationSuccess<Record<string, unknown>> | import("../core.js").ValidationSuccess<InferInput<TValidators[number]>>;
30
30
  }
31
31
  /**
32
32
  * Creates a typed union schema for Lexicon unions.
@@ -1,4 +1,4 @@
1
- import { InferInput, InferOutput, Schema, ValidationContext, ValidationFailure, Validator } from '../core.js';
1
+ import { InferInput, InferOutput, LexValidationError, Schema, ValidationContext, Validator } from '../core.js';
2
2
  /**
3
3
  * Type representing a non-empty tuple of validators for union schemas.
4
4
  *
@@ -25,7 +25,7 @@ export declare class UnionSchema<const TValidators extends UnionSchemaValidators
25
25
  protected readonly validators: TValidators;
26
26
  readonly type: "union";
27
27
  constructor(validators: TValidators);
28
- validateInContext(input: unknown, ctx: ValidationContext): ValidationFailure | import("../core.js").ValidationSuccess<unknown>;
28
+ validateInContext(input: unknown, ctx: ValidationContext): LexValidationError | import("../core.js").ValidationSuccess<unknown>;
29
29
  }
30
30
  /**
31
31
  * Creates a union schema that accepts values matching any of the provided schemas.
@@ -1 +1 @@
1
- {"version":3,"file":"union.d.ts","sourceRoot":"","sources":["../../src/schema/union.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,WAAW,EAEX,MAAM,EACN,iBAAiB,EACjB,iBAAiB,EACjB,SAAS,EACV,MAAM,YAAY,CAAA;AAEnB;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAAC,SAAS,EAAE,GAAG,SAAS,EAAE,CAAC,CAAA;AAExE;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,WAAW,CACtB,KAAK,CAAC,WAAW,SAAS,qBAAqB,GAAG,GAAG,CACrD,SAAQ,MAAM,CACd,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAC/B,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CACjC;IAGa,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,WAAW;IAFtD,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;gBAED,UAAU,EAAE,WAAW;IAItD,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,iBAAiB;CAYzD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,wBAAgB,KAAK,CAAC,KAAK,CAAC,WAAW,SAAS,qBAAqB,EACnE,UAAU,EAAE,WAAW,4BAGxB"}
1
+ {"version":3,"file":"union.d.ts","sourceRoot":"","sources":["../../src/schema/union.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,MAAM,EACN,iBAAiB,EAEjB,SAAS,EACV,MAAM,YAAY,CAAA;AAEnB;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAAC,SAAS,EAAE,GAAG,SAAS,EAAE,CAAC,CAAA;AAExE;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,WAAW,CACtB,KAAK,CAAC,WAAW,SAAS,qBAAqB,GAAG,GAAG,CACrD,SAAQ,MAAM,CACd,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAC/B,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CACjC;IAGa,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,WAAW;IAFtD,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;gBAED,UAAU,EAAE,WAAW;IAItD,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,iBAAiB;CAYzD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,wBAAgB,KAAK,CAAC,KAAK,CAAC,WAAW,SAAS,qBAAqB,EACnE,UAAU,EAAE,WAAW,4BAGxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex-schema",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "license": "MIT",
5
5
  "description": "Lexicon schema system for AT Lexicons",
6
6
  "keywords": [
@@ -35,9 +35,11 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
+ "@standard-schema/spec": "^1.1.0",
39
+ "iso-datestring-validator": "^2.2.2",
38
40
  "tslib": "^2.8.1",
39
- "@atproto/syntax": "^0.5.0",
40
- "@atproto/lex-data": "^0.0.13"
41
+ "@atproto/syntax": "^0.5.1",
42
+ "@atproto/lex-data": "^0.0.14"
41
43
  },
42
44
  "devDependencies": {
43
45
  "vitest": "^4.0.16"
@@ -1,4 +1,6 @@
1
+ import { StandardSchemaV1 } from '@standard-schema/spec'
1
2
  import { lazyProperty } from '../util/lazy-property.js'
3
+ import { StandardSchemaAdapter } from './standard-schema.js'
2
4
  import {
3
5
  InferInput,
4
6
  InferOutput,
@@ -45,14 +47,13 @@ export interface SchemaInternals<out TInput = unknown, out TOutput = TInput> {
45
47
  * - **Assertion methods**: `assert()`, `check()` - throw on invalid input
46
48
  * - **Type guard methods**: `matches()`, `ifMatches()` - return boolean or optional value
47
49
  * - **Parse methods**: `parse()`, `safeParse()` - allow value transformation/coercion
48
- * - **Validate methods**: `validate()`, `safeValidate()` - strict validation without coercion
50
+ * - **Validate methods**: `validate()`, `safeValidate()` - validation without coercion
49
51
  *
50
52
  * All methods are also available with a `$` prefix (e.g., `$parse()`, `$validate()`)
51
53
  * for consistent access in generated lexicon namespaces.
52
54
  *
53
55
  * @typeParam TInput - The type accepted as valid input during validation
54
56
  * @typeParam TOutput - The type returned after parsing (may include transformations)
55
- * @typeParam TInternals - Internal type structure for type inference
56
57
  *
57
58
  * @example
58
59
  * ```typescript
@@ -72,14 +73,8 @@ export interface SchemaInternals<out TInput = unknown, out TOutput = TInput> {
72
73
  * schema.matches(123) // false
73
74
  * ```
74
75
  */
75
- export abstract class Schema<
76
- out TInput = unknown,
77
- out TOutput = TInput,
78
- out TInternals extends SchemaInternals<TInput, TOutput> = SchemaInternals<
79
- TInput,
80
- TOutput
81
- >,
82
- > 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>
83
78
  {
84
79
  /**
85
80
  * Internal phantom property for type inference.
@@ -87,7 +82,13 @@ export abstract class Schema<
87
82
  *
88
83
  * @internal
89
84
  */
90
- 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
+ }
91
92
 
92
93
  // Needed to discriminate multiple schema types when used in unions. Without
93
94
  // this, Typescript could allow an EnumSchema<"foo" | "bar"> to be used where
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import { isValidISODateString } from 'iso-datestring-validator'
1
2
  import { validateCidString } from '@atproto/lex-data'
2
3
  import {
3
4
  AtIdentifierString,
@@ -9,8 +10,8 @@ import {
9
10
  RecordKeyString,
10
11
  TidString,
11
12
  UriString,
13
+ isAtIdentifierString,
12
14
  isDatetimeString,
13
- isValidAtIdentifier as isValidAtId,
14
15
  isValidAtUri,
15
16
  isValidDid,
16
17
  isValidHandle,
@@ -30,31 +31,47 @@ import { CheckFn } from '../util/assertion-util.js'
30
31
  // documentation for types and utilities that are already well-defined there.
31
32
  // @TODO rework other string formats in @atproto/syntax to follow this pattern
32
33
  // and re-export here, e.g. language tags, NSIDs, record keys, etc.
34
+
35
+ export {
36
+ type AtIdentifierString,
37
+ asAtIdentifierString,
38
+ ifAtIdentifierString,
39
+ isAtIdentifierString,
40
+ } from '@atproto/syntax'
41
+
42
+ // AtIdentifierString utilities
43
+ export { isDidIdentifier, isHandleIdentifier } from '@atproto/syntax'
44
+
33
45
  export {
34
46
  type DatetimeString,
35
47
  asDatetimeString,
36
- currentDatetimeString,
37
48
  ifDatetimeString,
38
49
  isDatetimeString,
39
- toDatetimeString,
40
50
  } from '@atproto/syntax'
41
51
 
42
52
  /**
43
- * Type guard that checks if a value is a valid AT identifier (DID or handle).
44
- *
45
- * @param value - The value to check
46
- * @returns `true` if the value is a valid AT identifier
53
+ * Matches any ISO-ish datetime string. This is a more lenient check than
54
+ * the strict {@link isDatetimeString} guard, which only allows datetimes that
55
+ * fully conform to the AT Protocol specification (e.g. must include timezone).
47
56
  */
48
- export const isAtIdentifierString: CheckFn<AtIdentifierString> = isValidAtId
49
- export type {
50
- /**
51
- * An AT identifier string - either a DID or a handle.
52
- *
53
- * @example `"did:plc:1234..."` or `"alice.bsky.social"`
54
- */
55
- AtIdentifierString,
57
+ export function isDatetimeStringLoose<I>(
58
+ input: I,
59
+ ): input is I & DatetimeString {
60
+ // @NOTE the returned type assertion is inaccurate wrt. the DatetimeString
61
+ // type definition. A more accurate solution would be to use a branded type
62
+ // instead of a template literal for the "datetime" format
63
+ if (typeof input !== 'string') return false
64
+ try {
65
+ return isValidISODateString(input)
66
+ } catch {
67
+ // @NOTE isValidISODateString throws on some inputs
68
+ return false
69
+ }
56
70
  }
57
71
 
72
+ // DatetimeString utilities
73
+ export { currentDatetimeString, toDatetimeString } from '@atproto/syntax'
74
+
58
75
  /**
59
76
  * Type guard that checks if a value is a valid AT URI.
60
77
  *
@@ -227,23 +244,39 @@ type StringFormats = {
227
244
  export type StringFormat = Extract<keyof StringFormats, string>
228
245
 
229
246
  const stringFormatVerifiers: {
230
- readonly [K in StringFormat]: CheckFn<StringFormats[K]>
247
+ readonly [K in StringFormat]: readonly [
248
+ strict: CheckFn<StringFormats[K]>,
249
+ loose?: CheckFn<StringFormats[K]>,
250
+ ]
231
251
  } = /*#__PURE__*/ Object.freeze({
232
252
  __proto__: null,
233
253
 
234
- 'at-identifier': isAtIdentifierString,
235
- 'at-uri': isAtUriString,
236
- cid: isCidString,
237
- datetime: isDatetimeString,
238
- did: isDidString,
239
- handle: isHandleString,
240
- language: isLanguageString,
241
- nsid: isNsidString,
242
- 'record-key': isRecordKeyString,
243
- tid: isTidString,
244
- uri: isUriString,
254
+ 'at-identifier': [isAtIdentifierString],
255
+ 'at-uri': [isAtUriString],
256
+ cid: [isCidString],
257
+ datetime: [isDatetimeString, isDatetimeStringLoose],
258
+ did: [isDidString],
259
+ handle: [isHandleString],
260
+ language: [isLanguageString],
261
+ nsid: [isNsidString],
262
+ 'record-key': [isRecordKeyString],
263
+ tid: [isTidString],
264
+ uri: [isUriString],
245
265
  })
246
266
 
267
+ export type StringFormatValidationOptions = {
268
+ /**
269
+ * Allows to be more lenient in validation by using a "loose" verification
270
+ * function, if available. The behavior of the loose verifier depends on the
271
+ * specific format, but generally it may allow for a wider range of valid
272
+ * inputs, including values that are not compliant with the AT Protocol
273
+ * specification.
274
+ *
275
+ * @default true
276
+ */
277
+ strict?: boolean
278
+ }
279
+
247
280
  /**
248
281
  * Infers the string type for a given format name.
249
282
  *
@@ -281,12 +314,18 @@ export type InferStringFormat<F extends StringFormat> = F extends StringFormat
281
314
  export function isStringFormat<I extends string, F extends StringFormat>(
282
315
  input: I,
283
316
  format: F,
317
+ options?: StringFormatValidationOptions,
284
318
  ): input is I & StringFormats[F] {
285
319
  const formatVerifier = stringFormatVerifiers[format]
286
320
  // Fool-proof
287
321
  if (!formatVerifier) throw new TypeError(`Unknown string format: ${format}`)
288
322
 
289
- return formatVerifier(input)
323
+ const check: CheckFn<StringFormats[F]> =
324
+ options?.strict === false
325
+ ? formatVerifier[1] ?? formatVerifier[0]
326
+ : formatVerifier[0]
327
+
328
+ return check(input)
290
329
  }
291
330
 
292
331
  /**
@@ -308,8 +347,9 @@ export function isStringFormat<I extends string, F extends StringFormat>(
308
347
  export function assertStringFormat<I extends string, F extends StringFormat>(
309
348
  input: I,
310
349
  format: F,
350
+ options?: StringFormatValidationOptions,
311
351
  ): asserts input is I & StringFormats[F] {
312
- if (!isStringFormat(input, format)) {
352
+ if (!isStringFormat(input, format, options)) {
313
353
  throw new TypeError(`Invalid string format (${format}): ${input}`)
314
354
  }
315
355
  }
@@ -336,8 +376,9 @@ export function assertStringFormat<I extends string, F extends StringFormat>(
336
376
  export function asStringFormat<I extends string, F extends StringFormat>(
337
377
  input: I,
338
378
  format: F,
379
+ options?: StringFormatValidationOptions,
339
380
  ): I & StringFormats[F] {
340
- assertStringFormat(input, format)
381
+ assertStringFormat(input, format, options)
341
382
  return input
342
383
  }
343
384
 
@@ -365,8 +406,9 @@ export function asStringFormat<I extends string, F extends StringFormat>(
365
406
  export function ifStringFormat<I extends string, F extends StringFormat>(
366
407
  input: I,
367
408
  format: F,
409
+ options?: StringFormatValidationOptions,
368
410
  ): undefined | (I & StringFormats[F]) {
369
- return isStringFormat(input, format) ? input : undefined
411
+ return isStringFormat(input, format, options) ? input : undefined
370
412
  }
371
413
 
372
414
  /**
@@ -29,8 +29,15 @@ 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 LexValidationError extends LexError<'InvalidRequest'> {
37
+ export class LexValidationError
38
+ extends LexError<'InvalidRequest'>
39
+ implements ResultFailure<LexValidationError>
40
+ {
34
41
  name = 'LexValidationError'
35
42
 
36
43
  /**
@@ -56,6 +63,14 @@ export class LexValidationError extends LexError<'InvalidRequest'> {
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
  *