@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.
- package/CHANGELOG.md +30 -0
- package/dist/core/schema.d.ts +5 -4
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +7 -2
- package/dist/core/schema.js.map +1 -1
- package/dist/core/standard-schema.d.ts +14 -0
- package/dist/core/standard-schema.d.ts.map +1 -0
- package/dist/core/standard-schema.js +27 -0
- package/dist/core/standard-schema.js.map +1 -0
- package/dist/core/string-format.d.ts +24 -17
- package/dist/core/string-format.d.ts.map +1 -1
- package/dist/core/string-format.js +57 -30
- package/dist/core/string-format.js.map +1 -1
- package/dist/core/validation-error.d.ts +10 -2
- package/dist/core/validation-error.d.ts.map +1 -1
- package/dist/core/validation-error.js +10 -0
- package/dist/core/validation-error.js.map +1 -1
- package/dist/core/validation-issue.d.ts +15 -15
- package/dist/core/validation-issue.d.ts.map +1 -1
- package/dist/core/validation-issue.js +33 -29
- package/dist/core/validation-issue.js.map +1 -1
- package/dist/core/validator.d.ts +29 -14
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +4 -2
- package/dist/core/validator.js.map +1 -1
- package/dist/core.d.ts +0 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +0 -1
- package/dist/core.js.map +1 -1
- package/dist/schema/blob.d.ts +10 -8
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +39 -14
- package/dist/schema/blob.js.map +1 -1
- package/dist/schema/custom.d.ts +1 -1
- package/dist/schema/custom.d.ts.map +1 -1
- package/dist/schema/custom.js.map +1 -1
- package/dist/schema/never.d.ts +1 -1
- package/dist/schema/nullable.d.ts +1 -1
- package/dist/schema/payload.d.ts +2 -2
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/record.d.ts +1 -1
- package/dist/schema/ref.d.ts +1 -1
- package/dist/schema/ref.d.ts.map +1 -1
- package/dist/schema/ref.js.map +1 -1
- package/dist/schema/refine.d.ts +1 -1
- package/dist/schema/refine.d.ts.map +1 -1
- package/dist/schema/refine.js.map +1 -1
- package/dist/schema/string.js +1 -1
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/typed-ref.d.ts +1 -1
- package/dist/schema/typed-union.d.ts +1 -1
- package/dist/schema/union.d.ts +2 -2
- package/dist/schema/union.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/core/schema.ts +12 -11
- package/src/core/standard-schema.test.ts +124 -0
- package/src/core/standard-schema.ts +31 -0
- package/src/core/string-format.ts +73 -31
- package/src/core/validation-error.ts +16 -1
- package/src/core/validation-issue.ts +32 -32
- package/src/core/validator.ts +26 -6
- package/src/core.ts +0 -1
- package/src/schema/array.test.ts +2 -2
- package/src/schema/blob.test.ts +317 -49
- package/src/schema/blob.ts +56 -23
- package/src/schema/custom.ts +1 -7
- package/src/schema/params.test.ts +2 -2
- package/src/schema/payload.ts +2 -2
- package/src/schema/ref.ts +1 -5
- package/src/schema/refine.ts +0 -1
- package/src/schema/string.test.ts +63 -0
- package/src/schema/string.ts +1 -1
- package/dist/core/property-key.d.ts +0 -2
- package/dist/core/property-key.d.ts.map +0 -1
- package/dist/core/property-key.js +0 -3
- package/dist/core/property-key.js.map +0 -1
- package/src/core/property-key.ts +0 -1
|
@@ -1 +1 @@
|
|
|
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)) {\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").
|
|
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").
|
|
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.
|
package/dist/schema/union.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InferInput, InferOutput, Schema, ValidationContext,
|
|
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):
|
|
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,
|
|
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.
|
|
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.
|
|
40
|
-
"@atproto/lex-data": "^0.0.
|
|
41
|
+
"@atproto/syntax": "^0.5.1",
|
|
42
|
+
"@atproto/lex-data": "^0.0.14"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"vitest": "^4.0.16"
|
package/src/core/schema.ts
CHANGED
|
@@ -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()` -
|
|
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
|
-
|
|
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']:
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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]:
|
|
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
|
-
|
|
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
|
|
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
|
*
|