@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,139 @@
|
|
|
1
|
+
import { CID, graphemeLen, utf8Len } from '@atproto/lex-data'
|
|
2
|
+
import {
|
|
3
|
+
InferStringFormat,
|
|
4
|
+
StringFormat,
|
|
5
|
+
UnknownString,
|
|
6
|
+
assertStringFormat,
|
|
7
|
+
} from '../core.js'
|
|
8
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
9
|
+
import { TokenSchema } from './token.js'
|
|
10
|
+
|
|
11
|
+
export type StringSchemaOptions = {
|
|
12
|
+
default?: string
|
|
13
|
+
knownValues?: readonly string[]
|
|
14
|
+
format?: StringFormat
|
|
15
|
+
minLength?: number
|
|
16
|
+
maxLength?: number
|
|
17
|
+
minGraphemes?: number
|
|
18
|
+
maxGraphemes?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type StringSchemaOutput<Options> =
|
|
22
|
+
//
|
|
23
|
+
Options extends { format: infer F extends StringFormat }
|
|
24
|
+
? InferStringFormat<F>
|
|
25
|
+
: Options extends { knownValues: readonly (infer K extends string)[] }
|
|
26
|
+
? K | UnknownString
|
|
27
|
+
: string
|
|
28
|
+
|
|
29
|
+
export class StringSchema<
|
|
30
|
+
const Options extends StringSchemaOptions = any,
|
|
31
|
+
> extends Validator<StringSchemaOutput<Options>> {
|
|
32
|
+
readonly lexiconType = 'string' as const
|
|
33
|
+
|
|
34
|
+
constructor(readonly options: Options) {
|
|
35
|
+
super()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override validateInContext(
|
|
39
|
+
// @NOTE validation will be applied on the default value as well
|
|
40
|
+
input: unknown = this.options.default,
|
|
41
|
+
ctx: ValidatorContext,
|
|
42
|
+
): ValidationResult<StringSchemaOutput<Options>> {
|
|
43
|
+
const { options } = this
|
|
44
|
+
|
|
45
|
+
const str = coerceToString(input)
|
|
46
|
+
if (str == null) {
|
|
47
|
+
return ctx.issueInvalidType(input, 'string')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let lazyUtf8Len: number
|
|
51
|
+
|
|
52
|
+
const { minLength } = options
|
|
53
|
+
if (minLength != null) {
|
|
54
|
+
if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {
|
|
55
|
+
return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { maxLength } = options
|
|
60
|
+
if (maxLength != null) {
|
|
61
|
+
// Optimization: we can avoid computing the UTF-8 length if the maximum
|
|
62
|
+
// possible length, in bytes, of the input JS string is smaller than the
|
|
63
|
+
// maxLength (in UTF-8 string bytes).
|
|
64
|
+
if (str.length * 3 <= maxLength) {
|
|
65
|
+
// Input string so small it can't possibly exceed maxLength
|
|
66
|
+
} else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) {
|
|
67
|
+
return ctx.issueTooBig(str, 'string', maxLength, lazyUtf8Len)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let lazyGraphLen: number
|
|
72
|
+
|
|
73
|
+
const { minGraphemes } = options
|
|
74
|
+
if (minGraphemes != null) {
|
|
75
|
+
// Optimization: avoid counting graphemes if the length check already fails
|
|
76
|
+
if (str.length < minGraphemes) {
|
|
77
|
+
return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)
|
|
78
|
+
} else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {
|
|
79
|
+
return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { maxGraphemes } = options
|
|
84
|
+
if (maxGraphemes != null) {
|
|
85
|
+
if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
|
|
86
|
+
return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.format !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
// @TODO optimize to avoid throw cost (requires re-writing utilities
|
|
93
|
+
// from @atproto/syntax)
|
|
94
|
+
assertStringFormat(str, options.format)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : undefined
|
|
97
|
+
return ctx.issueInvalidFormat(str, options.format, message)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return ctx.success(str as StringSchemaOutput<Options>)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function coerceToString(input: unknown): string | null {
|
|
106
|
+
switch (typeof input) {
|
|
107
|
+
case 'string':
|
|
108
|
+
return input
|
|
109
|
+
case 'object': {
|
|
110
|
+
if (input == null) return null
|
|
111
|
+
|
|
112
|
+
// @NOTE Allow using TokenSchema instances in places expecting strings,
|
|
113
|
+
// converting them to their string value.
|
|
114
|
+
if (input instanceof TokenSchema) {
|
|
115
|
+
return input.toString()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (input instanceof Date) {
|
|
119
|
+
if (Number.isNaN(input.getTime())) return null
|
|
120
|
+
return input.toISOString()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (input instanceof URL) {
|
|
124
|
+
return input.toString()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cid = CID.asCID(input)
|
|
128
|
+
if (cid) return cid.toString()
|
|
129
|
+
|
|
130
|
+
if (input instanceof String) {
|
|
131
|
+
return input.valueOf()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// falls through
|
|
136
|
+
default:
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Infer } from '../validation.js'
|
|
2
|
+
import { ObjectSchema } from './object.js'
|
|
3
|
+
import { ParamsSchema } from './params.js'
|
|
4
|
+
import { RefSchema } from './ref.js'
|
|
5
|
+
import { TypedUnionSchema } from './typed-union.js'
|
|
6
|
+
|
|
7
|
+
export type InferSubscriptionParameters<S extends Subscription> =
|
|
8
|
+
S extends Subscription<any, infer P extends ParamsSchema, any>
|
|
9
|
+
? Infer<P>
|
|
10
|
+
: never
|
|
11
|
+
|
|
12
|
+
export type InferSubscriptionMessage<S extends Subscription> =
|
|
13
|
+
S extends Subscription<
|
|
14
|
+
any,
|
|
15
|
+
any,
|
|
16
|
+
infer M extends RefSchema | TypedUnionSchema | ObjectSchema
|
|
17
|
+
>
|
|
18
|
+
? Infer<M>
|
|
19
|
+
: unknown
|
|
20
|
+
|
|
21
|
+
export class Subscription<
|
|
22
|
+
N extends string = any,
|
|
23
|
+
P extends ParamsSchema = any,
|
|
24
|
+
M extends undefined | RefSchema | TypedUnionSchema | ObjectSchema = any,
|
|
25
|
+
E extends undefined | readonly string[] = any,
|
|
26
|
+
> {
|
|
27
|
+
readonly type = 'subscription' as const
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
readonly nsid: N,
|
|
31
|
+
readonly parameters: P,
|
|
32
|
+
readonly message: M,
|
|
33
|
+
readonly errors: E,
|
|
34
|
+
) {}
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
2
|
+
|
|
3
|
+
export class TokenSchema<V extends string = any> extends Validator<V> {
|
|
4
|
+
readonly lexiconType = 'token' as const
|
|
5
|
+
|
|
6
|
+
constructor(protected readonly value: V) {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override validateInContext(
|
|
11
|
+
input: unknown,
|
|
12
|
+
ctx: ValidatorContext,
|
|
13
|
+
): ValidationResult<V> {
|
|
14
|
+
if (input === this.value) {
|
|
15
|
+
return ctx.success(this.value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// @NOTE: allow using the token instance itself (but convert to the actual
|
|
19
|
+
// token value)
|
|
20
|
+
if (input instanceof TokenSchema && input.value === this.value) {
|
|
21
|
+
return ctx.success(this.value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof input !== 'string') {
|
|
25
|
+
return ctx.issueInvalidType(input, 'token')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ctx.issueInvalidValue(input, [this.value])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// When using the TokenSchema instance as data, let's serialize it to the
|
|
32
|
+
// token value
|
|
33
|
+
|
|
34
|
+
toJSON(): string {
|
|
35
|
+
return this.value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toString(): string {
|
|
39
|
+
return this.value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import { $Type, Simplify } from '../core.js'
|
|
3
|
+
import {
|
|
4
|
+
Infer,
|
|
5
|
+
ValidationResult,
|
|
6
|
+
Validator,
|
|
7
|
+
ValidatorContext,
|
|
8
|
+
} from '../validation.js'
|
|
9
|
+
|
|
10
|
+
export class TypedObjectSchema<
|
|
11
|
+
Type extends $Type = any,
|
|
12
|
+
Schema extends Validator<Record<string, unknown>> = any,
|
|
13
|
+
Output extends Infer<Schema> & { $type?: Type } = Infer<Schema> & {
|
|
14
|
+
$type?: Type
|
|
15
|
+
},
|
|
16
|
+
> extends Validator<Output> {
|
|
17
|
+
readonly lexiconType = 'object' as const
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
readonly $type: Type,
|
|
21
|
+
readonly schema: Schema,
|
|
22
|
+
) {
|
|
23
|
+
super()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isTypeOf<X extends { $type?: unknown }>(
|
|
27
|
+
value: X,
|
|
28
|
+
): value is X extends { $type?: Type } ? X : never {
|
|
29
|
+
return value.$type === undefined || value.$type === this.$type
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
build<X extends Omit<Output, '$type'>>(
|
|
33
|
+
input: X,
|
|
34
|
+
): Simplify<Omit<X, '$type'> & { $type: Type }> {
|
|
35
|
+
return { ...input, $type: this.$type }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$isTypeOf<X extends { $type?: unknown }>(value: X) {
|
|
39
|
+
return this.isTypeOf<X>(value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
$build<X extends Omit<Output, '$type'>>(input: X) {
|
|
43
|
+
return this.build<X>(input)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override validateInContext(
|
|
47
|
+
input: unknown,
|
|
48
|
+
ctx: ValidatorContext,
|
|
49
|
+
): ValidationResult<Output> {
|
|
50
|
+
if (!isPlainObject(input)) {
|
|
51
|
+
return ctx.issueInvalidType(input, 'object')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
'$type' in input &&
|
|
56
|
+
input.$type !== undefined &&
|
|
57
|
+
input.$type !== this.$type
|
|
58
|
+
) {
|
|
59
|
+
return ctx.issueInvalidPropertyValue(input, '$type', [this.$type])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return ctx.validate(input, this.schema as Validator<Output>)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
|
|
2
|
+
|
|
3
|
+
// Basically a RecordSchema or TypedObjectSchema
|
|
4
|
+
export type TypedRefSchemaValidator<V extends { $type?: string } = any> =
|
|
5
|
+
V extends { $type?: infer T extends string }
|
|
6
|
+
? { $type: T } & Validator<V & { $type?: T }>
|
|
7
|
+
: never
|
|
8
|
+
|
|
9
|
+
export type TypedRefGetter<V extends { $type?: string } = any> =
|
|
10
|
+
() => TypedRefSchemaValidator<V>
|
|
11
|
+
|
|
12
|
+
export type TypedRefSchemaOutput<V extends { $type?: string } = any> =
|
|
13
|
+
V extends { $type?: infer T extends string } ? V & { $type: T } : never
|
|
14
|
+
|
|
15
|
+
export class TypedRefSchema<
|
|
16
|
+
V extends { $type?: string } = any,
|
|
17
|
+
> extends Validator<TypedRefSchemaOutput<V>> {
|
|
18
|
+
#getter: TypedRefGetter<V>
|
|
19
|
+
|
|
20
|
+
constructor(getter: TypedRefGetter<V>) {
|
|
21
|
+
// @NOTE In order to avoid circular dependency issues, we don't resolve
|
|
22
|
+
// the schema here. Instead, we resolve it lazily when first accessed.
|
|
23
|
+
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
this.#getter = getter
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get schema(): TypedRefSchemaValidator<V> {
|
|
30
|
+
const value = this.#getter.call(null)
|
|
31
|
+
|
|
32
|
+
// Prevents a getter from depending on itself recursively, also allows GC to
|
|
33
|
+
// clean up the getter function.
|
|
34
|
+
this.#getter = throwAlreadyCalled
|
|
35
|
+
|
|
36
|
+
// Cache the resolved schema on the instance
|
|
37
|
+
Object.defineProperty(this, 'schema', {
|
|
38
|
+
value,
|
|
39
|
+
writable: false,
|
|
40
|
+
enumerable: false,
|
|
41
|
+
configurable: true,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get $type(): TypedRefSchemaOutput<V>['$type'] {
|
|
48
|
+
return this.schema.$type
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override validateInContext(
|
|
52
|
+
input: unknown,
|
|
53
|
+
ctx: ValidatorContext,
|
|
54
|
+
): ValidationResult<TypedRefSchemaOutput<V>> {
|
|
55
|
+
const result = ctx.validate(input, this.schema)
|
|
56
|
+
if (!result.success) return result
|
|
57
|
+
|
|
58
|
+
if (result.value.$type !== this.$type) {
|
|
59
|
+
return ctx.issueInvalidPropertyValue(result.value, '$type', [this.$type])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result as ValidationResult<TypedRefSchemaOutput<V>>
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function throwAlreadyCalled(): never {
|
|
67
|
+
throw new Error('TypedRefSchema getter called multiple times')
|
|
68
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isPlainObject } from '@atproto/lex-data'
|
|
2
|
+
import { Restricted, UnknownString } from '../core.js'
|
|
3
|
+
import {
|
|
4
|
+
Infer,
|
|
5
|
+
ValidationResult,
|
|
6
|
+
Validator,
|
|
7
|
+
ValidatorContext,
|
|
8
|
+
} from '../validation.js'
|
|
9
|
+
import { TypedRefSchema, TypedRefSchemaOutput } from './typed-ref.js'
|
|
10
|
+
|
|
11
|
+
export type TypedRef<T extends { $type?: string }> = TypedRefSchemaOutput<T>
|
|
12
|
+
|
|
13
|
+
export type TypedObject = { $type: UnknownString } & {
|
|
14
|
+
// In order to prevent places that expect an open union from accepting an
|
|
15
|
+
// invalid version of the known typed objects, we need to prevent any other
|
|
16
|
+
// properties from being present.
|
|
17
|
+
//
|
|
18
|
+
// For example, if an open union expects:
|
|
19
|
+
// ```ts
|
|
20
|
+
// TypedObject | { $type: 'A'; a: number }
|
|
21
|
+
// ```
|
|
22
|
+
// we don't want it to accept:
|
|
23
|
+
// ```ts
|
|
24
|
+
// { $type: 'A' }
|
|
25
|
+
// ```
|
|
26
|
+
// Which would be the case as `{ $type: 'A' }` is a valid
|
|
27
|
+
// `TypedObject`. By adding an index signature that forbids any
|
|
28
|
+
// property, we ensure that only valid known typed objects can be used.
|
|
29
|
+
[K in string]: Restricted<'Unknown property'>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TypedRefSchemasToUnion<T extends readonly TypedRefSchema[]> = {
|
|
33
|
+
[K in keyof T]: Infer<T[K]>
|
|
34
|
+
}[number]
|
|
35
|
+
|
|
36
|
+
export type TypedUnionSchemaOutput<
|
|
37
|
+
TypedRefs extends readonly TypedRefSchema[],
|
|
38
|
+
Closed extends boolean,
|
|
39
|
+
> = Closed extends true
|
|
40
|
+
? TypedRefSchemasToUnion<TypedRefs>
|
|
41
|
+
: TypedRefSchemasToUnion<TypedRefs> | TypedObject
|
|
42
|
+
|
|
43
|
+
export class TypedUnionSchema<
|
|
44
|
+
TypedRefs extends readonly TypedRefSchema[] = any,
|
|
45
|
+
Closed extends boolean = any,
|
|
46
|
+
> extends Validator<TypedUnionSchemaOutput<TypedRefs, Closed>> {
|
|
47
|
+
readonly lexiconType = 'union' as const
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
protected readonly refs: TypedRefs,
|
|
51
|
+
public readonly closed: Closed,
|
|
52
|
+
) {
|
|
53
|
+
// @NOTE In order to avoid circular dependency issues, we don't access the
|
|
54
|
+
// refs's schema (or $type) here. Instead, we access them lazily when first
|
|
55
|
+
// needed.
|
|
56
|
+
|
|
57
|
+
super()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected get refsMap() {
|
|
61
|
+
const map = new Map<unknown, TypedRefs[number]>()
|
|
62
|
+
for (const ref of this.refs) map.set(ref.$type, ref)
|
|
63
|
+
|
|
64
|
+
// Cache the map on the instance
|
|
65
|
+
Object.defineProperty(this, 'refsMap', {
|
|
66
|
+
value: map,
|
|
67
|
+
writable: false,
|
|
68
|
+
enumerable: false,
|
|
69
|
+
configurable: true,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return map
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get $types() {
|
|
76
|
+
return Array.from(this.refsMap.keys())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override validateInContext(
|
|
80
|
+
input: unknown,
|
|
81
|
+
ctx: ValidatorContext,
|
|
82
|
+
): ValidationResult<TypedUnionSchemaOutput<TypedRefs, Closed>> {
|
|
83
|
+
if (!isPlainObject(input) || !('$type' in input)) {
|
|
84
|
+
return ctx.issueInvalidType(input, '$typed')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { $type } = input
|
|
88
|
+
|
|
89
|
+
const def = this.refsMap.get($type)
|
|
90
|
+
if (def) {
|
|
91
|
+
const result = ctx.validate(input, def)
|
|
92
|
+
return result as ValidationResult<
|
|
93
|
+
TypedUnionSchemaOutput<TypedRefs, Closed>
|
|
94
|
+
>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.closed) {
|
|
98
|
+
return ctx.issueInvalidPropertyValue(input, '$type', this.$types)
|
|
99
|
+
}
|
|
100
|
+
if (typeof $type !== 'string') {
|
|
101
|
+
return ctx.issueInvalidPropertyType(input, '$type', 'string')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ctx.success(input as TypedUnionSchemaOutput<TypedRefs, Closed>)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Infer,
|
|
3
|
+
ValidationError,
|
|
4
|
+
ValidationFailure,
|
|
5
|
+
ValidationResult,
|
|
6
|
+
Validator,
|
|
7
|
+
ValidatorContext,
|
|
8
|
+
} from '../validation.js'
|
|
9
|
+
|
|
10
|
+
export type UnionSchemaValidators = readonly [Validator, ...Validator[]]
|
|
11
|
+
export type UnionSchemaOutput<V extends readonly Validator[]> = Infer<V[number]>
|
|
12
|
+
|
|
13
|
+
export class UnionSchema<
|
|
14
|
+
V extends UnionSchemaValidators = any,
|
|
15
|
+
> extends Validator<UnionSchemaOutput<V>> {
|
|
16
|
+
constructor(protected readonly validators: V) {
|
|
17
|
+
super()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override validateInContext(
|
|
21
|
+
input: unknown,
|
|
22
|
+
ctx: ValidatorContext,
|
|
23
|
+
): ValidationResult<UnionSchemaOutput<V>> {
|
|
24
|
+
const failures: ValidationFailure[] = []
|
|
25
|
+
|
|
26
|
+
for (const validator of this.validators) {
|
|
27
|
+
const result = ctx.validate(input, validator)
|
|
28
|
+
if (result.success) {
|
|
29
|
+
return result as ValidationResult<UnionSchemaOutput<V>>
|
|
30
|
+
} else {
|
|
31
|
+
failures.push(result)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
error: ValidationError.fromFailures(failures),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LexMap, isLexMap } from '@atproto/lex-data'
|
|
2
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation'
|
|
3
|
+
|
|
4
|
+
export type { LexMap }
|
|
5
|
+
export type UnknownObjectOutput = LexMap
|
|
6
|
+
|
|
7
|
+
export class UnknownObjectSchema extends Validator<UnknownObjectOutput> {
|
|
8
|
+
readonly lexiconType = 'unknown' as const
|
|
9
|
+
|
|
10
|
+
override validateInContext(
|
|
11
|
+
input: unknown,
|
|
12
|
+
ctx: ValidatorContext,
|
|
13
|
+
): ValidationResult<UnknownObjectOutput> {
|
|
14
|
+
if (isLexMap(input)) {
|
|
15
|
+
return ctx.success(input)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return ctx.issueInvalidType(input, 'unknown')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ValidationResult, Validator, ValidatorContext } from '../validation'
|
|
2
|
+
|
|
3
|
+
export class UnknownSchema extends Validator<unknown> {
|
|
4
|
+
override validateInContext(
|
|
5
|
+
input: unknown,
|
|
6
|
+
ctx: ValidatorContext,
|
|
7
|
+
): ValidationResult<unknown> {
|
|
8
|
+
return ctx.success(input)
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Utilities (that depend on *and* are used by schemas)
|
|
2
|
+
export * from './schema/_parameters.js'
|
|
3
|
+
|
|
4
|
+
// Concrete Types
|
|
5
|
+
export * from './schema/array.js'
|
|
6
|
+
export * from './schema/blob.js'
|
|
7
|
+
export * from './schema/boolean.js'
|
|
8
|
+
export * from './schema/bytes.js'
|
|
9
|
+
export * from './schema/cid.js'
|
|
10
|
+
export * from './schema/dict.js'
|
|
11
|
+
export * from './schema/enum.js'
|
|
12
|
+
export * from './schema/integer.js'
|
|
13
|
+
export * from './schema/literal.js'
|
|
14
|
+
export * from './schema/never.js'
|
|
15
|
+
export * from './schema/null.js'
|
|
16
|
+
export * from './schema/object.js'
|
|
17
|
+
export * from './schema/string.js'
|
|
18
|
+
export * from './schema/unknown-object.js'
|
|
19
|
+
export * from './schema/unknown.js'
|
|
20
|
+
|
|
21
|
+
// Composite Types
|
|
22
|
+
export * from './schema/custom.js'
|
|
23
|
+
export * from './schema/discriminated-union.js'
|
|
24
|
+
export * from './schema/intersection.js'
|
|
25
|
+
export * from './schema/ref.js'
|
|
26
|
+
export * from './schema/union.js'
|
|
27
|
+
|
|
28
|
+
// Lexicon specific Types
|
|
29
|
+
export * from './schema/params.js'
|
|
30
|
+
export * from './schema/payload.js'
|
|
31
|
+
export * from './schema/permission-set.js'
|
|
32
|
+
export * from './schema/permission.js'
|
|
33
|
+
export * from './schema/procedure.js'
|
|
34
|
+
export * from './schema/query.js'
|
|
35
|
+
export * from './schema/record.js'
|
|
36
|
+
export * from './schema/subscription.js'
|
|
37
|
+
export * from './schema/token.js'
|
|
38
|
+
export * from './schema/typed-object.js'
|
|
39
|
+
export * from './schema/typed-ref.js'
|
|
40
|
+
export * from './schema/typed-union.js'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { arrayAgg } from './array-agg.js'
|
|
2
|
+
|
|
3
|
+
describe('arrayAgg', () => {
|
|
4
|
+
it('aggregates items based on comparison and aggregation functions', () => {
|
|
5
|
+
const input = [1, 1, 2, 2, 3, 3, 3]
|
|
6
|
+
const result = arrayAgg(
|
|
7
|
+
input,
|
|
8
|
+
(a, b) => a === b,
|
|
9
|
+
(items) => ({ value: items[0], count: items.length }),
|
|
10
|
+
)
|
|
11
|
+
expect(result).toEqual([
|
|
12
|
+
{ value: 1, count: 2 },
|
|
13
|
+
{ value: 2, count: 2 },
|
|
14
|
+
{ value: 3, count: 3 },
|
|
15
|
+
])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns an empty array when input is empty', () => {
|
|
19
|
+
const input: number[] = []
|
|
20
|
+
const result = arrayAgg(
|
|
21
|
+
input,
|
|
22
|
+
(a, b) => a === b,
|
|
23
|
+
(items) => ({ value: items[0], count: items.length }),
|
|
24
|
+
)
|
|
25
|
+
expect(result).toEqual([])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('handles non-consecutive grouping', () => {
|
|
29
|
+
const input = [1, 2, 1, 2, 3, 1]
|
|
30
|
+
const result = arrayAgg(
|
|
31
|
+
input,
|
|
32
|
+
(a, b) => a === b,
|
|
33
|
+
(items) => ({ value: items[0], count: items.length }),
|
|
34
|
+
)
|
|
35
|
+
expect(result).toEqual([
|
|
36
|
+
{ value: 1, count: 3 },
|
|
37
|
+
{ value: 2, count: 2 },
|
|
38
|
+
{ value: 3, count: 1 },
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregates items in an array based on a comparison function and an aggregation function.
|
|
3
|
+
*
|
|
4
|
+
* @param arr - The input array to aggregate.
|
|
5
|
+
* @param cmp - A comparison function that determines if two items belong to the same group.
|
|
6
|
+
* @param agg - An aggregation function that combines items in a group into a single item.
|
|
7
|
+
* @returns An array of aggregated items.
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const input = [1, 1, 2, 2, 3, 3, 3]
|
|
11
|
+
* const result = arrayAgg(
|
|
12
|
+
* input,
|
|
13
|
+
* (a, b) => a === b,
|
|
14
|
+
* (items) => { value: items[0], sum: items.reduce((sum, item) => sum + item, 0) },
|
|
15
|
+
* )
|
|
16
|
+
* // result is [{ value: 1, sum: 2 }, { value: 2, sum: 4 }, { value: 3, sum: 6 }]
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function arrayAgg<T, O>(
|
|
20
|
+
arr: readonly T[],
|
|
21
|
+
cmp: (a: T, b: T) => boolean,
|
|
22
|
+
agg: (items: [T, ...T[]]) => O,
|
|
23
|
+
): O[] {
|
|
24
|
+
if (arr.length === 0) return []
|
|
25
|
+
|
|
26
|
+
const groups: [T, ...T[]][] = [[arr[0]]]
|
|
27
|
+
const skipped = Array<undefined | boolean>(arr.length)
|
|
28
|
+
|
|
29
|
+
outer: for (let i = 1; i < arr.length; i++) {
|
|
30
|
+
if (skipped[i]) continue
|
|
31
|
+
const item = arr[i]
|
|
32
|
+
for (let j = 0; j < groups.length; j++) {
|
|
33
|
+
if (cmp(item, groups[j][0])) {
|
|
34
|
+
groups[j].push(item)
|
|
35
|
+
skipped[i] = true
|
|
36
|
+
continue outer
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
groups.push([item])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return groups.map(agg)
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type PropertyKey = string | number
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ResultFailure, failureError } from '../core.js'
|
|
2
|
+
import {
|
|
3
|
+
ValidationIssue,
|
|
4
|
+
aggregateIssues,
|
|
5
|
+
stringifyIssue,
|
|
6
|
+
} from './validation-issue.js'
|
|
7
|
+
|
|
8
|
+
export class ValidationError extends Error {
|
|
9
|
+
name = 'ValidationError'
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
readonly issues: ValidationIssue[],
|
|
13
|
+
options?: ErrorOptions,
|
|
14
|
+
) {
|
|
15
|
+
super(issues.map(stringifyIssue).join(', '), options)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static fromFailures(
|
|
19
|
+
failures: ResultFailure<ValidationError>[],
|
|
20
|
+
): ValidationError {
|
|
21
|
+
if (failures.length === 1) return failures[0].error
|
|
22
|
+
const issues = failures.flatMap(extractFailureIssues)
|
|
23
|
+
return new ValidationError(aggregateIssues(issues), {
|
|
24
|
+
// Keep the original errors as the cause chain
|
|
25
|
+
cause: failures.map(failureError),
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractFailureIssues(result: ResultFailure<ValidationError>) {
|
|
31
|
+
return result.error.issues
|
|
32
|
+
}
|