@atproto/lex-schema 0.0.0
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/dist/core/$type.d.ts +4 -0
- package/dist/core/$type.d.ts.map +1 -0
- package/dist/core/$type.js +7 -0
- package/dist/core/$type.js.map +1 -0
- package/dist/core/record-key.d.ts +4 -0
- package/dist/core/record-key.d.ts.map +1 -0
- package/dist/core/record-key.js +16 -0
- package/dist/core/record-key.js.map +1 -0
- package/dist/core/result.d.ts +57 -0
- package/dist/core/result.d.ts.map +1 -0
- package/dist/core/result.js +74 -0
- package/dist/core/result.js.map +1 -0
- package/dist/core/string-format.d.ts +31 -0
- package/dist/core/string-format.d.ts.map +1 -0
- package/dist/core/string-format.js +81 -0
- package/dist/core/string-format.js.map +1 -0
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +6 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +9 -0
- package/dist/core.js.map +1 -0
- package/dist/external.d.ts +86 -0
- package/dist/external.d.ts.map +1 -0
- package/dist/external.js +171 -0
- package/dist/external.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/_parameters.d.ts +17 -0
- package/dist/schema/_parameters.d.ts.map +1 -0
- package/dist/schema/_parameters.js +20 -0
- package/dist/schema/_parameters.js.map +1 -0
- package/dist/schema/array.d.ts +13 -0
- package/dist/schema/array.d.ts.map +1 -0
- package/dist/schema/array.js +40 -0
- package/dist/schema/array.js.map +1 -0
- package/dist/schema/blob.d.ts +32 -0
- package/dist/schema/blob.d.ts.map +1 -0
- package/dist/schema/blob.js +40 -0
- package/dist/schema/blob.js.map +1 -0
- package/dist/schema/boolean.d.ts +11 -0
- package/dist/schema/boolean.d.ts.map +1 -0
- package/dist/schema/boolean.js +20 -0
- package/dist/schema/boolean.js.map +1 -0
- package/dist/schema/bytes.d.ts +12 -0
- package/dist/schema/bytes.d.ts.map +1 -0
- package/dist/schema/bytes.js +31 -0
- package/dist/schema/bytes.js.map +1 -0
- package/dist/schema/cid.d.ts +13 -0
- package/dist/schema/cid.d.ts.map +1 -0
- package/dist/schema/cid.js +22 -0
- package/dist/schema/cid.js.map +1 -0
- package/dist/schema/custom.d.ts +15 -0
- package/dist/schema/custom.d.ts.map +1 -0
- package/dist/schema/custom.js +22 -0
- package/dist/schema/custom.js.map +1 -0
- package/dist/schema/dict.d.ts +18 -0
- package/dist/schema/dict.d.ts.map +1 -0
- package/dist/schema/dict.js +48 -0
- package/dist/schema/dict.js.map +1 -0
- package/dist/schema/discriminated-union.d.ts +34 -0
- package/dist/schema/discriminated-union.d.ts.map +1 -0
- package/dist/schema/discriminated-union.js +93 -0
- package/dist/schema/discriminated-union.js.map +1 -0
- package/dist/schema/enum.d.ts +7 -0
- package/dist/schema/enum.d.ts.map +1 -0
- package/dist/schema/enum.js +19 -0
- package/dist/schema/enum.js.map +1 -0
- package/dist/schema/integer.d.ts +13 -0
- package/dist/schema/integer.d.ts.map +1 -0
- package/dist/schema/integer.js +32 -0
- package/dist/schema/integer.js.map +1 -0
- package/dist/schema/intersection.d.ts +16 -0
- package/dist/schema/intersection.d.ts.map +1 -0
- package/dist/schema/intersection.js +33 -0
- package/dist/schema/intersection.js.map +1 -0
- package/dist/schema/literal.d.ts +7 -0
- package/dist/schema/literal.d.ts.map +1 -0
- package/dist/schema/literal.js +19 -0
- package/dist/schema/literal.js.map +1 -0
- package/dist/schema/never.d.ts +5 -0
- package/dist/schema/never.d.ts.map +1 -0
- package/dist/schema/never.js +11 -0
- package/dist/schema/never.js.map +1 -0
- package/dist/schema/null.d.ts +7 -0
- package/dist/schema/null.d.ts.map +1 -0
- package/dist/schema/null.js +18 -0
- package/dist/schema/null.js.map +1 -0
- package/dist/schema/object.d.ts +47 -0
- package/dist/schema/object.d.ts.map +1 -0
- package/dist/schema/object.js +89 -0
- package/dist/schema/object.js.map +1 -0
- package/dist/schema/params.d.ts +22 -0
- package/dist/schema/params.d.ts.map +1 -0
- package/dist/schema/params.js +115 -0
- package/dist/schema/params.js.map +1 -0
- package/dist/schema/payload.d.ts +19 -0
- package/dist/schema/payload.d.ts.map +1 -0
- package/dist/schema/payload.js +16 -0
- package/dist/schema/payload.js.map +1 -0
- package/dist/schema/permission-set.d.ts +15 -0
- package/dist/schema/permission-set.d.ts.map +1 -0
- package/dist/schema/permission-set.js +16 -0
- package/dist/schema/permission-set.js.map +1 -0
- package/dist/schema/permission.d.ts +9 -0
- package/dist/schema/permission.d.ts.map +1 -0
- package/dist/schema/permission.js +14 -0
- package/dist/schema/permission.js.map +1 -0
- package/dist/schema/procedure.d.ts +17 -0
- package/dist/schema/procedure.d.ts.map +1 -0
- package/dist/schema/procedure.js +20 -0
- package/dist/schema/procedure.js.map +1 -0
- package/dist/schema/query.d.ts +15 -0
- package/dist/schema/query.d.ts.map +1 -0
- package/dist/schema/query.js +18 -0
- package/dist/schema/query.js.map +1 -0
- package/dist/schema/record.d.ts +37 -0
- package/dist/schema/record.d.ts.map +1 -0
- package/dist/schema/record.js +64 -0
- package/dist/schema/record.js.map +1 -0
- package/dist/schema/ref.d.ts +10 -0
- package/dist/schema/ref.d.ts.map +1 -0
- package/dist/schema/ref.js +36 -0
- package/dist/schema/ref.js.map +1 -0
- package/dist/schema/string.d.ts +24 -0
- package/dist/schema/string.d.ts.map +1 -0
- package/dist/schema/string.js +107 -0
- package/dist/schema/string.js.map +1 -0
- package/dist/schema/subscription.d.ts +16 -0
- package/dist/schema/subscription.d.ts.map +1 -0
- package/dist/schema/subscription.js +18 -0
- package/dist/schema/subscription.js.map +1 -0
- package/dist/schema/token.d.ts +10 -0
- package/dist/schema/token.d.ts.map +1 -0
- package/dist/schema/token.js +36 -0
- package/dist/schema/token.js.map +1 -0
- package/dist/schema/typed-object.d.ts +32 -0
- package/dist/schema/typed-object.d.ts.map +1 -0
- package/dist/schema/typed-object.js +40 -0
- package/dist/schema/typed-object.js.map +1 -0
- package/dist/schema/typed-ref.d.ts +30 -0
- package/dist/schema/typed-ref.d.ts.map +1 -0
- package/dist/schema/typed-ref.js +44 -0
- package/dist/schema/typed-ref.js.map +1 -0
- package/dist/schema/typed-union.d.ts +26 -0
- package/dist/schema/typed-union.d.ts.map +1 -0
- package/dist/schema/typed-union.js +54 -0
- package/dist/schema/typed-union.js.map +1 -0
- package/dist/schema/union.d.ts +9 -0
- package/dist/schema/union.d.ts.map +1 -0
- package/dist/schema/union.js +29 -0
- package/dist/schema/union.js.map +1 -0
- package/dist/schema/unknown-object.d.ts +9 -0
- package/dist/schema/unknown-object.d.ts.map +1 -0
- package/dist/schema/unknown-object.js +16 -0
- package/dist/schema/unknown-object.js.map +1 -0
- package/dist/schema/unknown.d.ts +5 -0
- package/dist/schema/unknown.d.ts.map +1 -0
- package/dist/schema/unknown.js +11 -0
- package/dist/schema/unknown.js.map +1 -0
- package/dist/schema.d.ts +34 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +41 -0
- package/dist/schema.js.map +1 -0
- package/dist/util/array-agg.d.ts +20 -0
- package/dist/util/array-agg.d.ts.map +1 -0
- package/dist/util/array-agg.js +42 -0
- package/dist/util/array-agg.js.map +1 -0
- package/dist/validation/property-key.d.ts +2 -0
- package/dist/validation/property-key.d.ts.map +1 -0
- package/dist/validation/property-key.js +3 -0
- package/dist/validation/property-key.js.map +1 -0
- package/dist/validation/validation-error.d.ts +9 -0
- package/dist/validation/validation-error.d.ts.map +1 -0
- package/dist/validation/validation-error.js +27 -0
- package/dist/validation/validation-error.js.map +1 -0
- package/dist/validation/validation-issue.d.ts +45 -0
- package/dist/validation/validation-issue.d.ts.map +1 -0
- package/dist/validation/validation-issue.js +167 -0
- package/dist/validation/validation-issue.js.map +1 -0
- package/dist/validation/validator.d.ts +113 -0
- package/dist/validation/validator.d.ts.map +1 -0
- package/dist/validation/validator.js +209 -0
- package/dist/validation/validator.js.map +1 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +8 -0
- package/dist/validation.js.map +1 -0
- package/package.json +45 -0
- package/src/core/$type.ts +19 -0
- package/src/core/record-key.ts +15 -0
- package/src/core/result.ts +73 -0
- package/src/core/string-format.ts +124 -0
- package/src/core/types.ts +22 -0
- package/src/core.ts +5 -0
- package/src/external.ts +365 -0
- package/src/index.ts +3 -0
- package/src/schema/_parameters.ts +26 -0
- package/src/schema/array.ts +51 -0
- package/src/schema/blob.ts +82 -0
- package/src/schema/boolean.ts +24 -0
- package/src/schema/bytes.ts +38 -0
- package/src/schema/cid.ts +27 -0
- package/src/schema/custom.ts +36 -0
- package/src/schema/dict.ts +69 -0
- package/src/schema/discriminated-union.ts +144 -0
- package/src/schema/enum.ts +20 -0
- package/src/schema/integer.ts +41 -0
- package/src/schema/intersection.ts +57 -0
- package/src/schema/literal.ts +20 -0
- package/src/schema/never.ts +14 -0
- package/src/schema/null.ts +20 -0
- package/src/schema/object.test.ts +138 -0
- package/src/schema/object.ts +180 -0
- package/src/schema/params.ts +157 -0
- package/src/schema/payload.ts +53 -0
- package/src/schema/permission-set.ts +22 -0
- package/src/schema/permission.ts +15 -0
- package/src/schema/procedure.ts +35 -0
- package/src/schema/query.ts +28 -0
- package/src/schema/record.ts +106 -0
- package/src/schema/ref.ts +47 -0
- package/src/schema/string.ts +139 -0
- package/src/schema/subscription.ts +35 -0
- package/src/schema/token.ts +41 -0
- package/src/schema/typed-object.ts +64 -0
- package/src/schema/typed-ref.ts +68 -0
- package/src/schema/typed-union.ts +106 -0
- package/src/schema/union.ts +40 -0
- package/src/schema/unknown-object.ts +20 -0
- package/src/schema/unknown.ts +10 -0
- package/src/schema.ts +40 -0
- package/src/util/array-agg.test.ts +41 -0
- package/src/util/array-agg.ts +43 -0
- package/src/validation/property-key.ts +1 -0
- package/src/validation/validation-error.ts +32 -0
- package/src/validation/validation-issue.ts +231 -0
- package/src/validation/validator.ts +361 -0
- package/src/validation.ts +4 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { asUint8Array } from '@atproto/lex-data'
|
|
2
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
3
|
+
|
|
4
|
+
export type BytesSchemaOptions = {
|
|
5
|
+
minLength?: number
|
|
6
|
+
maxLength?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BytesSchema extends Validator<Uint8Array> {
|
|
10
|
+
readonly lexiconType = 'bytes' as const
|
|
11
|
+
|
|
12
|
+
constructor(readonly options: BytesSchemaOptions) {
|
|
13
|
+
super()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override validateInContext(
|
|
17
|
+
input: unknown,
|
|
18
|
+
ctx: ValidatorContext,
|
|
19
|
+
): ValidationResult<Uint8Array> {
|
|
20
|
+
// Coerce different binary formats into Uint8Array
|
|
21
|
+
const bytes = asUint8Array(input)
|
|
22
|
+
if (!bytes) {
|
|
23
|
+
return ctx.issueInvalidType(input, 'bytes')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { minLength } = this.options
|
|
27
|
+
if (minLength != null && bytes.length < minLength) {
|
|
28
|
+
return ctx.issueTooSmall(bytes, 'bytes', minLength, bytes.length)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { maxLength } = this.options
|
|
32
|
+
if (maxLength != null && bytes.length > maxLength) {
|
|
33
|
+
return ctx.issueTooBig(bytes, 'bytes', maxLength, bytes.length)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return ctx.success(bytes)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CID, isCid } from '@atproto/lex-data'
|
|
2
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
3
|
+
|
|
4
|
+
export { CID }
|
|
5
|
+
|
|
6
|
+
export type CidSchemaOptions = {
|
|
7
|
+
strict?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CidSchema extends Validator<CID> {
|
|
11
|
+
readonly lexiconType = 'cid-link' as const
|
|
12
|
+
|
|
13
|
+
constructor(readonly options: CidSchemaOptions = {}) {
|
|
14
|
+
super()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override validateInContext(
|
|
18
|
+
input: unknown,
|
|
19
|
+
ctx: ValidatorContext,
|
|
20
|
+
): ValidationResult<CID> {
|
|
21
|
+
if (!isCid(input, this.options)) {
|
|
22
|
+
return ctx.issueInvalidType(input, 'cid')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return ctx.success(input)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PropertyKey } from '../validation/property-key.js'
|
|
2
|
+
import {
|
|
3
|
+
ContextualIssue,
|
|
4
|
+
ValidationResult,
|
|
5
|
+
Validator,
|
|
6
|
+
ValidatorContext,
|
|
7
|
+
} from '../validation/validator.js'
|
|
8
|
+
|
|
9
|
+
export type CustomAssertionContext = {
|
|
10
|
+
path: PropertyKey[]
|
|
11
|
+
addIssue(issue: ContextualIssue): void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CustomAssertion<T = any> = (
|
|
15
|
+
this: null,
|
|
16
|
+
input: unknown,
|
|
17
|
+
ctx: CustomAssertionContext,
|
|
18
|
+
) => input is T
|
|
19
|
+
|
|
20
|
+
export class CustomSchema<T = unknown> extends Validator<T> {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly assertion: CustomAssertion<T>,
|
|
23
|
+
private readonly message: string,
|
|
24
|
+
private readonly path?: PropertyKey | readonly PropertyKey[],
|
|
25
|
+
) {
|
|
26
|
+
super()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override validateInContext(
|
|
30
|
+
input: unknown,
|
|
31
|
+
ctx: ValidatorContext,
|
|
32
|
+
): ValidationResult<T> {
|
|
33
|
+
if (this.assertion.call(null, input, ctx)) return ctx.success(input as T)
|
|
34
|
+
return ctx.custom(input, this.message, this.path)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import {
|
|
3
|
+
Infer,
|
|
4
|
+
ValidationResult,
|
|
5
|
+
Validator,
|
|
6
|
+
ValidatorContext,
|
|
7
|
+
} from '../validation.js'
|
|
8
|
+
|
|
9
|
+
export type DictSchemaOutput<
|
|
10
|
+
KeySchema extends Validator,
|
|
11
|
+
ValueSchema extends Validator,
|
|
12
|
+
> =
|
|
13
|
+
Infer<KeySchema> extends never
|
|
14
|
+
? Record<string, never>
|
|
15
|
+
: Record<Infer<KeySchema> & string, Infer<ValueSchema>>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @note There is no dictionary in Lexicon schemas. This is a custom extension
|
|
19
|
+
* to allow map-like objects when using the lex library programmatically (i.e.
|
|
20
|
+
* not code generated from a lexicon schema).
|
|
21
|
+
*/
|
|
22
|
+
export class DictSchema<
|
|
23
|
+
const KeySchema extends Validator = any,
|
|
24
|
+
const ValueSchema extends Validator = any,
|
|
25
|
+
> extends Validator<DictSchemaOutput<KeySchema, ValueSchema>> {
|
|
26
|
+
constructor(
|
|
27
|
+
readonly keySchema: KeySchema,
|
|
28
|
+
readonly valueSchema: ValueSchema,
|
|
29
|
+
) {
|
|
30
|
+
super()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override validateInContext(
|
|
34
|
+
input: unknown,
|
|
35
|
+
ctx: ValidatorContext,
|
|
36
|
+
options?: { ignoredKeys?: { has(k: string): boolean } },
|
|
37
|
+
): ValidationResult<DictSchemaOutput<KeySchema, ValueSchema>> {
|
|
38
|
+
if (!isPlainObject(input)) {
|
|
39
|
+
return ctx.issueInvalidType(input, 'dict')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let copy: undefined | Record<string, unknown>
|
|
43
|
+
|
|
44
|
+
for (const key in input) {
|
|
45
|
+
if (options?.ignoredKeys?.has(key)) continue
|
|
46
|
+
|
|
47
|
+
const keyResult = ctx.validate(key, this.keySchema)
|
|
48
|
+
if (!keyResult.success) return keyResult
|
|
49
|
+
if (keyResult.value !== key) {
|
|
50
|
+
// We can't safely "move" the key to a different name in the output
|
|
51
|
+
// object (because there may already be something there), so we issue a
|
|
52
|
+
// "required key" error if the key validation changes the key
|
|
53
|
+
return ctx.issueRequiredKey(input, key)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const valueResult = ctx.validateChild(input, key, this.valueSchema)
|
|
57
|
+
if (!valueResult.success) return valueResult
|
|
58
|
+
|
|
59
|
+
if (valueResult.value !== input[key]) {
|
|
60
|
+
copy ??= { ...input }
|
|
61
|
+
copy[key] = valueResult.value
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return ctx.success(
|
|
66
|
+
(copy ?? input) as DictSchemaOutput<KeySchema, ValueSchema>,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import { ArrayContaining } from '../core.js'
|
|
3
|
+
import {
|
|
4
|
+
ValidationError,
|
|
5
|
+
ValidationFailure,
|
|
6
|
+
ValidationResult,
|
|
7
|
+
Validator,
|
|
8
|
+
ValidatorContext,
|
|
9
|
+
} from '../validation.js'
|
|
10
|
+
import { EnumSchema } from './enum.js'
|
|
11
|
+
import { LiteralSchema } from './literal.js'
|
|
12
|
+
import { ObjectSchema } from './object.js'
|
|
13
|
+
|
|
14
|
+
export type DiscriminatedUnionSchemaVariant<Discriminator extends string> =
|
|
15
|
+
ObjectSchema<
|
|
16
|
+
{ [_ in Discriminator]: Validator },
|
|
17
|
+
{ required: ArrayContaining<Discriminator, string> }
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
export type DiscriminatedUnionSchemaVariants<Discriminator extends string> =
|
|
21
|
+
readonly [
|
|
22
|
+
DiscriminatedUnionSchemaVariant<Discriminator>,
|
|
23
|
+
...DiscriminatedUnionSchemaVariant<Discriminator>[],
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export type DiscriminatedUnionSchemaOutput<
|
|
27
|
+
Options extends readonly Validator[],
|
|
28
|
+
> = Options extends readonly [Validator<infer V>]
|
|
29
|
+
? V
|
|
30
|
+
: Options extends readonly [
|
|
31
|
+
Validator<infer V>,
|
|
32
|
+
...infer Rest extends Validator[],
|
|
33
|
+
]
|
|
34
|
+
? V | DiscriminatedUnionSchemaOutput<Rest>
|
|
35
|
+
: never
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @note There is no discriminated union in Lexicon schemas. This is a custom
|
|
39
|
+
* extension to allow optimized validation of union of objects when using the
|
|
40
|
+
* lex library programmatically (i.e. not code generated from a lexicon schema).
|
|
41
|
+
*/
|
|
42
|
+
export class DiscriminatedUnionSchema<
|
|
43
|
+
const Discriminator extends string = any,
|
|
44
|
+
const Options extends DiscriminatedUnionSchemaVariants<Discriminator> = any,
|
|
45
|
+
> extends Validator<DiscriminatedUnionSchemaOutput<Options>> {
|
|
46
|
+
constructor(
|
|
47
|
+
readonly discriminator: Discriminator,
|
|
48
|
+
readonly variants: Options,
|
|
49
|
+
) {
|
|
50
|
+
super()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* If all variants have a literal or enum for the discriminator property,
|
|
55
|
+
* and there are no overlapping values, returns a map of discriminator values
|
|
56
|
+
* to variants. Otherwise, returns null.
|
|
57
|
+
*/
|
|
58
|
+
protected get variantsMap() {
|
|
59
|
+
const map = new Map<
|
|
60
|
+
unknown,
|
|
61
|
+
DiscriminatedUnionSchemaVariant<Discriminator>
|
|
62
|
+
>()
|
|
63
|
+
for (const variant of this.variants) {
|
|
64
|
+
const schema = variant.validators[this.discriminator]
|
|
65
|
+
if (schema instanceof LiteralSchema) {
|
|
66
|
+
if (map.has(schema.value)) return null // overlapping value
|
|
67
|
+
map.set(schema.value, variant)
|
|
68
|
+
} else if (schema instanceof EnumSchema) {
|
|
69
|
+
for (const val of schema.values) {
|
|
70
|
+
if (map.has(val)) return null // overlapping value
|
|
71
|
+
map.set(val, variant)
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
return null // not a literal or enum
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Cache the map on the instance (to avoid re-computing)
|
|
79
|
+
Object.defineProperty(this, 'variantsMap', {
|
|
80
|
+
value: map,
|
|
81
|
+
writable: false,
|
|
82
|
+
enumerable: false,
|
|
83
|
+
configurable: true,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return map
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override validateInContext(
|
|
90
|
+
input: unknown,
|
|
91
|
+
ctx: ValidatorContext,
|
|
92
|
+
): ValidationResult<DiscriminatedUnionSchemaOutput<Options>> {
|
|
93
|
+
if (!isPlainObject(input)) {
|
|
94
|
+
return ctx.issueInvalidType(input, 'object')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!Object.hasOwn(input, this.discriminator)) {
|
|
98
|
+
return ctx.issueRequiredKey(input, this.discriminator)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fast path: if we have a mapping of discriminator values to variants,
|
|
102
|
+
// we can directly select the correct variant to validate against. This also
|
|
103
|
+
// outputs a better error (with a single failure issue) when the discriminator.
|
|
104
|
+
if (this.variantsMap) {
|
|
105
|
+
const variant = this.variantsMap.get(input[this.discriminator])
|
|
106
|
+
if (!variant) {
|
|
107
|
+
return ctx.issueInvalidPropertyValue(input, this.discriminator, [
|
|
108
|
+
...this.variantsMap.keys(),
|
|
109
|
+
])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return ctx.validate(input, variant) as ValidationResult<
|
|
113
|
+
DiscriminatedUnionSchemaOutput<Options>
|
|
114
|
+
>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Slow path: try validating against each variant and return the first
|
|
118
|
+
// successful one (or aggregate all failures if none match).
|
|
119
|
+
const failures: ValidationFailure[] = []
|
|
120
|
+
|
|
121
|
+
for (const variant of this.variants) {
|
|
122
|
+
const discSchema = variant.validators[this.discriminator]
|
|
123
|
+
const discResult = ctx.validateChild(
|
|
124
|
+
input,
|
|
125
|
+
this.discriminator,
|
|
126
|
+
discSchema,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (!discResult.success) {
|
|
130
|
+
failures.push(discResult)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return ctx.validate(input, variant) as ValidationResult<
|
|
135
|
+
DiscriminatedUnionSchemaOutput<Options>
|
|
136
|
+
>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: ValidationError.fromFailures(failures),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
2
|
+
|
|
3
|
+
export class EnumSchema<
|
|
4
|
+
Output extends null | string | number | boolean = any,
|
|
5
|
+
> extends Validator<Output> {
|
|
6
|
+
constructor(readonly values: readonly Output[]) {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override validateInContext(
|
|
11
|
+
input: unknown,
|
|
12
|
+
ctx: ValidatorContext,
|
|
13
|
+
): ValidationResult<Output> {
|
|
14
|
+
if (!(this.values as readonly unknown[]).includes(input)) {
|
|
15
|
+
return ctx.issueInvalidValue(input, this.values)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return ctx.success(input as Output)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
2
|
+
|
|
3
|
+
export type IntegerSchemaOptions = {
|
|
4
|
+
default?: number
|
|
5
|
+
minimum?: number
|
|
6
|
+
maximum?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class IntegerSchema extends Validator<number> {
|
|
10
|
+
readonly lexiconType = 'integer' as const
|
|
11
|
+
|
|
12
|
+
constructor(readonly options: IntegerSchemaOptions) {
|
|
13
|
+
super()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override validateInContext(
|
|
17
|
+
input: unknown = this.options.default,
|
|
18
|
+
ctx: ValidatorContext,
|
|
19
|
+
): ValidationResult<number> {
|
|
20
|
+
if (!isInteger(input)) {
|
|
21
|
+
return ctx.issueInvalidType(input, 'integer')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (this.options.minimum !== undefined && input < this.options.minimum) {
|
|
25
|
+
return ctx.issueTooSmall(input, 'integer', this.options.minimum, input)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.options.maximum !== undefined && input > this.options.maximum) {
|
|
29
|
+
return ctx.issueTooBig(input, 'integer', this.options.maximum, input)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ctx.success(input)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Simple wrapper around {@link Number.isInteger} that acts as a type guard.
|
|
38
|
+
*/
|
|
39
|
+
function isInteger(input: unknown): input is number {
|
|
40
|
+
return Number.isInteger(input)
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Infer,
|
|
3
|
+
ValidationResult,
|
|
4
|
+
Validator,
|
|
5
|
+
ValidatorContext,
|
|
6
|
+
} from '../validation.js'
|
|
7
|
+
|
|
8
|
+
export type IntersectionSchemaValidators = readonly [
|
|
9
|
+
Validator,
|
|
10
|
+
Validator,
|
|
11
|
+
...Validator[],
|
|
12
|
+
]
|
|
13
|
+
export type IntersectionSchemaOutput<
|
|
14
|
+
V extends readonly Validator[],
|
|
15
|
+
Base = unknown,
|
|
16
|
+
> = V extends readonly [
|
|
17
|
+
infer First extends Validator,
|
|
18
|
+
...infer Rest extends Validator[],
|
|
19
|
+
]
|
|
20
|
+
? IntersectionSchemaOutput<Rest, Base & Infer<First>>
|
|
21
|
+
: Base
|
|
22
|
+
|
|
23
|
+
export class IntersectionSchema<
|
|
24
|
+
V extends IntersectionSchemaValidators = any,
|
|
25
|
+
> extends Validator<IntersectionSchemaOutput<V>> {
|
|
26
|
+
constructor(protected readonly validators: V) {
|
|
27
|
+
super()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override validateInContext(
|
|
31
|
+
input: unknown,
|
|
32
|
+
ctx: ValidatorContext,
|
|
33
|
+
): ValidationResult<IntersectionSchemaOutput<V>> {
|
|
34
|
+
for (let i = 0; i < this.validators.length; i++) {
|
|
35
|
+
const result = ctx.validate(input, this.validators[i])
|
|
36
|
+
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// @NOTE because transforming the value could make it invalid for previous
|
|
42
|
+
// validators, we need to ensure the input remains unchanged only gets
|
|
43
|
+
// transformed by the first validator.
|
|
44
|
+
if (i !== 0 && input !== result.value) {
|
|
45
|
+
// The alternative would be to allow transforms on a first pass
|
|
46
|
+
// (ignoring errors) and then re-validate the final value against all
|
|
47
|
+
// validators (without allowing further transforms). This would be way
|
|
48
|
+
// less efficient (we could make this optional).
|
|
49
|
+
return ctx.issueInvalidValue(input, [result.value])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
input = result.value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ctx.success(input as IntersectionSchemaOutput<V>)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
2
|
+
|
|
3
|
+
export class LiteralSchema<
|
|
4
|
+
Output extends null | string | number | boolean = any,
|
|
5
|
+
> extends Validator<Output> {
|
|
6
|
+
constructor(readonly value: Output) {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override validateInContext(
|
|
11
|
+
input: unknown,
|
|
12
|
+
ctx: ValidatorContext,
|
|
13
|
+
): ValidationResult<Output> {
|
|
14
|
+
if (input !== this.value) {
|
|
15
|
+
return ctx.issueInvalidValue(input, [this.value])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return ctx.success(this.value)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ValidationFailure,
|
|
3
|
+
Validator,
|
|
4
|
+
ValidatorContext,
|
|
5
|
+
} from '../validation.js'
|
|
6
|
+
|
|
7
|
+
export class NeverSchema extends Validator<never> {
|
|
8
|
+
override validateInContext(
|
|
9
|
+
input: unknown,
|
|
10
|
+
ctx: ValidatorContext,
|
|
11
|
+
): ValidationFailure {
|
|
12
|
+
return ctx.issueInvalidType(input, 'never')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
2
|
+
|
|
3
|
+
export class NullSchema extends Validator<null> {
|
|
4
|
+
readonly lexiconType = 'null' as const
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override validateInContext(
|
|
11
|
+
input: unknown,
|
|
12
|
+
ctx: ValidatorContext,
|
|
13
|
+
): ValidationResult<null> {
|
|
14
|
+
if (input !== null) {
|
|
15
|
+
return ctx.issueInvalidType(input, 'null')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return ctx.success(null)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { BooleanSchema } from './boolean.js'
|
|
2
|
+
import { DictSchema } from './dict.js'
|
|
3
|
+
import { EnumSchema } from './enum.js'
|
|
4
|
+
import { IntegerSchema } from './integer.js'
|
|
5
|
+
import { ObjectSchema } from './object.js'
|
|
6
|
+
import { StringSchema } from './string.js'
|
|
7
|
+
|
|
8
|
+
describe('ObjectSchema', () => {
|
|
9
|
+
describe('simple schema', () => {
|
|
10
|
+
const schema = new ObjectSchema(
|
|
11
|
+
{
|
|
12
|
+
name: new StringSchema({}),
|
|
13
|
+
age: new IntegerSchema({}),
|
|
14
|
+
gender: new EnumSchema(['male', 'female']),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
required: ['name'],
|
|
18
|
+
nullable: ['gender'],
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
it('validates plain objects', () => {
|
|
23
|
+
const result = schema.validate({
|
|
24
|
+
name: 'Alice',
|
|
25
|
+
age: 30,
|
|
26
|
+
gender: 'female',
|
|
27
|
+
})
|
|
28
|
+
expect(result.success).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('rejects non-objects', () => {
|
|
32
|
+
const result = schema.validate('not an object')
|
|
33
|
+
expect(result.success).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('rejects missing properties', () => {
|
|
37
|
+
const result = schema.validate({
|
|
38
|
+
age: 30,
|
|
39
|
+
gender: 'female',
|
|
40
|
+
})
|
|
41
|
+
expect(result.success).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('validates optional properties', () => {
|
|
45
|
+
const result = schema.validate({
|
|
46
|
+
name: 'Alice',
|
|
47
|
+
})
|
|
48
|
+
expect(result.success).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('validates nullable properties', () => {
|
|
52
|
+
const result = schema.validate({
|
|
53
|
+
name: 'Alice',
|
|
54
|
+
gender: null,
|
|
55
|
+
})
|
|
56
|
+
expect(result.success).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects invalid property types', () => {
|
|
60
|
+
const result = schema.validate({
|
|
61
|
+
name: 'Alice',
|
|
62
|
+
age: 'thirty',
|
|
63
|
+
})
|
|
64
|
+
expect(result.success).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('ignores extra properties', () => {
|
|
68
|
+
const result = schema.validate({
|
|
69
|
+
name: 'Alice',
|
|
70
|
+
age: 30,
|
|
71
|
+
extra: 'value',
|
|
72
|
+
})
|
|
73
|
+
expect(result.success).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('strict schema', () => {
|
|
78
|
+
const schema = new ObjectSchema(
|
|
79
|
+
{
|
|
80
|
+
id: new StringSchema({}),
|
|
81
|
+
score: new IntegerSchema({}),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
required: ['id', 'score'],
|
|
85
|
+
unknownProperties: 'strict',
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
it('rejects extra properties in strict mode', () => {
|
|
90
|
+
const result = schema.validate({
|
|
91
|
+
id: 'item1',
|
|
92
|
+
score: 100,
|
|
93
|
+
extra: 'not allowed',
|
|
94
|
+
})
|
|
95
|
+
expect(result.success).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('accepts only defined properties in strict mode', () => {
|
|
99
|
+
const result = schema.validate({
|
|
100
|
+
id: 'item1',
|
|
101
|
+
score: 100,
|
|
102
|
+
})
|
|
103
|
+
expect(result.success).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('schema with unknownProperties validator', () => {
|
|
108
|
+
const schema = new ObjectSchema(
|
|
109
|
+
{
|
|
110
|
+
title: new StringSchema({}),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
required: ['title'],
|
|
114
|
+
unknownProperties: new DictSchema(
|
|
115
|
+
new EnumSchema(['tag1', 'tag2']),
|
|
116
|
+
new BooleanSchema({}),
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
it('validates extra properties with the provided validator', () => {
|
|
122
|
+
const result = schema.validate({
|
|
123
|
+
title: 'My Post',
|
|
124
|
+
tag1: true,
|
|
125
|
+
tag2: false,
|
|
126
|
+
})
|
|
127
|
+
expect(result.success).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('rejects extra properties that fail the provided validator', () => {
|
|
131
|
+
const result = schema.validate({
|
|
132
|
+
title: 'My Post',
|
|
133
|
+
tag1: 'not a boolean',
|
|
134
|
+
})
|
|
135
|
+
expect(result.success).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
})
|