@atcute/lexicons 1.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/LICENSE +17 -0
- package/README.md +11 -0
- package/dist/ambient.d.ts +8 -0
- package/dist/ambient.js +2 -0
- package/dist/ambient.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/blob.d.ts +20 -0
- package/dist/interfaces/blob.js +20 -0
- package/dist/interfaces/blob.js.map +1 -0
- package/dist/interfaces/bytes.d.ts +23 -0
- package/dist/interfaces/bytes.js +21 -0
- package/dist/interfaces/bytes.js.map +1 -0
- package/dist/interfaces/cid-link.d.ts +24 -0
- package/dist/interfaces/cid-link.js +13 -0
- package/dist/interfaces/cid-link.js.map +1 -0
- package/dist/interfaces/index.d.ts +3 -0
- package/dist/interfaces/index.js +4 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/syntax/at-identifier.d.ts +8 -0
- package/dist/syntax/at-identifier.js +7 -0
- package/dist/syntax/at-identifier.js.map +1 -0
- package/dist/syntax/at-uri.d.ts +46 -0
- package/dist/syntax/at-uri.js +64 -0
- package/dist/syntax/at-uri.js.map +1 -0
- package/dist/syntax/cid.d.ts +5 -0
- package/dist/syntax/cid.js +10 -0
- package/dist/syntax/cid.js.map +1 -0
- package/dist/syntax/datetime.d.ts +2 -0
- package/dist/syntax/datetime.js +6 -0
- package/dist/syntax/datetime.js.map +1 -0
- package/dist/syntax/did.d.ts +9 -0
- package/dist/syntax/did.js +6 -0
- package/dist/syntax/did.js.map +1 -0
- package/dist/syntax/handle.d.ts +6 -0
- package/dist/syntax/handle.js +6 -0
- package/dist/syntax/handle.js.map +1 -0
- package/dist/syntax/index.d.ts +11 -0
- package/dist/syntax/index.js +12 -0
- package/dist/syntax/index.js.map +1 -0
- package/dist/syntax/language.d.ts +2 -0
- package/dist/syntax/language.js +6 -0
- package/dist/syntax/language.js.map +1 -0
- package/dist/syntax/nsid.d.ts +5 -0
- package/dist/syntax/nsid.js +6 -0
- package/dist/syntax/nsid.js.map +1 -0
- package/dist/syntax/record-key.d.ts +5 -0
- package/dist/syntax/record-key.js +6 -0
- package/dist/syntax/record-key.js.map +1 -0
- package/dist/syntax/tid.d.ts +5 -0
- package/dist/syntax/tid.js +6 -0
- package/dist/syntax/tid.js.map +1 -0
- package/dist/syntax/uri.d.ts +5 -0
- package/dist/syntax/uri.js +6 -0
- package/dist/syntax/uri.js.map +1 -0
- package/dist/types/brand.d.ts +13 -0
- package/dist/types/brand.js +2 -0
- package/dist/types/brand.js.map +1 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +12 -0
- package/dist/utils.js.map +1 -0
- package/dist/validations/index.d.ts +409 -0
- package/dist/validations/index.js +1003 -0
- package/dist/validations/index.js.map +1 -0
- package/dist/validations/utils.d.ts +8 -0
- package/dist/validations/utils.js +54 -0
- package/dist/validations/utils.js.map +1 -0
- package/lib/ambient.ts +7 -0
- package/lib/index.ts +38 -0
- package/lib/interfaces/blob.ts +47 -0
- package/lib/interfaces/bytes.ts +45 -0
- package/lib/interfaces/cid-link.ts +36 -0
- package/lib/interfaces/index.ts +3 -0
- package/lib/syntax/at-identifier.ts +13 -0
- package/lib/syntax/at-uri.ts +117 -0
- package/lib/syntax/cid.ts +16 -0
- package/lib/syntax/datetime.ts +9 -0
- package/lib/syntax/did.ts +16 -0
- package/lib/syntax/handle.ts +13 -0
- package/lib/syntax/index.ts +11 -0
- package/lib/syntax/language.ts +9 -0
- package/lib/syntax/nsid.ts +12 -0
- package/lib/syntax/record-key.ts +11 -0
- package/lib/syntax/tid.ts +11 -0
- package/lib/syntax/uri.ts +11 -0
- package/lib/types/brand.ts +11 -0
- package/lib/utils.ts +15 -0
- package/lib/validations/index.ts +1749 -0
- package/lib/validations/utils.ts +62 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1749 @@
|
|
|
1
|
+
import * as syntax from '../syntax/index.js';
|
|
2
|
+
|
|
3
|
+
import { _isBytesWrapper } from '../interfaces/bytes.js';
|
|
4
|
+
import * as interfaces from '../interfaces/index.js';
|
|
5
|
+
|
|
6
|
+
import type { $type } from '../types/brand.js';
|
|
7
|
+
|
|
8
|
+
import { assert } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
import { getGraphemeLength, getUtf8Length, isArray, isObject, lazy, lazyProperty } from './utils.js';
|
|
11
|
+
|
|
12
|
+
type Identity<T> = T;
|
|
13
|
+
type Flatten<T> = Identity<{ [K in keyof T]: T[K] }>;
|
|
14
|
+
|
|
15
|
+
type InputType =
|
|
16
|
+
| 'unknown'
|
|
17
|
+
| 'null'
|
|
18
|
+
| 'undefined'
|
|
19
|
+
| 'string'
|
|
20
|
+
| 'integer'
|
|
21
|
+
| 'boolean'
|
|
22
|
+
| 'blob'
|
|
23
|
+
| 'bytes'
|
|
24
|
+
| 'cid-link'
|
|
25
|
+
| 'object'
|
|
26
|
+
| 'array';
|
|
27
|
+
|
|
28
|
+
type StringFormatMap = {
|
|
29
|
+
'at-identifier': syntax.ActorIdentifier;
|
|
30
|
+
'at-uri': syntax.ResourceUri;
|
|
31
|
+
cid: syntax.Cid;
|
|
32
|
+
datetime: syntax.Datetime;
|
|
33
|
+
did: syntax.Did;
|
|
34
|
+
handle: syntax.Handle;
|
|
35
|
+
language: syntax.LanguageCode;
|
|
36
|
+
nsid: syntax.Nsid;
|
|
37
|
+
'record-key': syntax.RecordKey;
|
|
38
|
+
tid: syntax.Tid;
|
|
39
|
+
uri: syntax.GenericUri;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type StringFormat = keyof StringFormatMap;
|
|
43
|
+
|
|
44
|
+
type Literal = string | number | boolean;
|
|
45
|
+
type Key = string | number;
|
|
46
|
+
|
|
47
|
+
// #region Schema issue types
|
|
48
|
+
export type IssueLeaf =
|
|
49
|
+
| { ok: false; code: 'missing_value' }
|
|
50
|
+
| { ok: false; code: 'invalid_literal'; expected: readonly Literal[] }
|
|
51
|
+
| { ok: false; code: 'invalid_type'; expected: InputType }
|
|
52
|
+
| { ok: false; code: 'invalid_variant'; expected: string[] }
|
|
53
|
+
| { ok: false; code: 'invalid_integer_range'; min: number; max: number }
|
|
54
|
+
| { ok: false; code: 'invalid_string_format'; expected: StringFormat }
|
|
55
|
+
| { ok: false; code: 'invalid_string_graphemes'; minGraphemes: number; maxGraphemes: number }
|
|
56
|
+
| { ok: false; code: 'invalid_string_length'; minLength: number; maxLength: number }
|
|
57
|
+
| { ok: false; code: 'invalid_array_length'; minLength: number; maxLength: number }
|
|
58
|
+
| { ok: false; code: 'invalid_bytes_size'; minSize: number; maxSize: number };
|
|
59
|
+
|
|
60
|
+
export type IssueTree =
|
|
61
|
+
| IssueLeaf
|
|
62
|
+
| { ok: false; code: 'prepend'; key: Key; tree: IssueTree }
|
|
63
|
+
| { ok: false; code: 'join'; left: IssueTree; right: IssueTree };
|
|
64
|
+
|
|
65
|
+
export type Issue =
|
|
66
|
+
| { code: 'missing_value'; path: Key[] }
|
|
67
|
+
| { code: 'invalid_literal'; path: Key[]; expected: readonly Literal[] }
|
|
68
|
+
| { code: 'invalid_type'; path: Key[]; expected: InputType }
|
|
69
|
+
| { code: 'invalid_variant'; path: Key[]; expected: string[] }
|
|
70
|
+
| { code: 'invalid_integer_range'; path: Key[]; min: number; max: number }
|
|
71
|
+
| { code: 'invalid_string_format'; path: Key[]; expected: StringFormat }
|
|
72
|
+
| { code: 'invalid_string_graphemes'; path: Key[]; minGraphemes: number; maxGraphemes: number }
|
|
73
|
+
| { code: 'invalid_string_length'; path: Key[]; minLength: number; maxLength: number }
|
|
74
|
+
| { code: 'invalid_array_length'; path: Key[]; minLength: number; maxLength: number }
|
|
75
|
+
| { code: 'invalid_bytes_size'; path: Key[]; minSize: number; maxSize: number };
|
|
76
|
+
|
|
77
|
+
// #__NO_SIDE_EFFECTS__
|
|
78
|
+
const joinIssues = (left: IssueTree | undefined, right: IssueTree): IssueTree => {
|
|
79
|
+
return left ? { ok: false, code: 'join', left, right } : right;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// #__NO_SIDE_EFFECTS__
|
|
83
|
+
const prependPath = (key: Key, tree: IssueTree): IssueTree => {
|
|
84
|
+
return { ok: false, code: 'prepend', key, tree };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// #region Schema result types
|
|
88
|
+
|
|
89
|
+
export type Ok<T> = {
|
|
90
|
+
ok: true;
|
|
91
|
+
value: T;
|
|
92
|
+
};
|
|
93
|
+
export type Err = {
|
|
94
|
+
ok: false;
|
|
95
|
+
readonly message: string;
|
|
96
|
+
readonly issues: readonly Issue[];
|
|
97
|
+
throw(): never;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type ValidationResult<T> = Ok<T> | Err;
|
|
101
|
+
|
|
102
|
+
// #region Base schema
|
|
103
|
+
|
|
104
|
+
// Private symbols meant to hold types
|
|
105
|
+
declare const kType: unique symbol;
|
|
106
|
+
type kType = typeof kType;
|
|
107
|
+
|
|
108
|
+
// We need a special symbol to hold the types for objects due to their
|
|
109
|
+
// recursive nature.
|
|
110
|
+
declare const kObjectType: unique symbol;
|
|
111
|
+
type kObjectType = typeof kObjectType;
|
|
112
|
+
|
|
113
|
+
// None set
|
|
114
|
+
export const FLAG_EMPTY = 0;
|
|
115
|
+
// Don't continue validation if an error is encountered
|
|
116
|
+
export const FLAG_ABORT_EARLY = 1 << 0;
|
|
117
|
+
|
|
118
|
+
type MatcherResult = undefined | Ok<unknown> | IssueTree;
|
|
119
|
+
type Matcher = (this: void, input: unknown, flags: number) => MatcherResult;
|
|
120
|
+
|
|
121
|
+
export interface BaseSchema<TInput = unknown, TOutput = TInput> {
|
|
122
|
+
readonly kind: 'schema';
|
|
123
|
+
readonly type: string;
|
|
124
|
+
readonly '~run': Matcher;
|
|
125
|
+
|
|
126
|
+
readonly [kType]?: { in: TInput; out: TOutput };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type InferInput<T extends BaseSchema> = T extends { [kObjectType]?: any }
|
|
130
|
+
? NonNullable<T[kObjectType]>['in']
|
|
131
|
+
: NonNullable<T[kType]>['in'];
|
|
132
|
+
|
|
133
|
+
export type InferOutput<T extends BaseSchema> = T extends { [kObjectType]?: any }
|
|
134
|
+
? NonNullable<T[kObjectType]>['out']
|
|
135
|
+
: NonNullable<T[kType]>['out'];
|
|
136
|
+
|
|
137
|
+
// #region Schema runner
|
|
138
|
+
const cloneIssueWithPath = (issue: IssueLeaf, path: Key[]): Issue => {
|
|
139
|
+
const { ok: _ok, ...clone } = issue;
|
|
140
|
+
|
|
141
|
+
return { ...clone, path };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const collectIssues = (tree: IssueTree, path: Key[] = [], issues: Issue[] = []): Issue[] => {
|
|
145
|
+
for (;;) {
|
|
146
|
+
switch (tree.code) {
|
|
147
|
+
case 'join': {
|
|
148
|
+
collectIssues(tree.left, path.slice(), issues);
|
|
149
|
+
tree = tree.right;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
case 'prepend': {
|
|
153
|
+
path.push(tree.key);
|
|
154
|
+
tree = tree.tree;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
issues.push(cloneIssueWithPath(tree, path));
|
|
159
|
+
return issues;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const countIssues = (tree: IssueTree): number => {
|
|
166
|
+
let count = 0;
|
|
167
|
+
for (;;) {
|
|
168
|
+
switch (tree.code) {
|
|
169
|
+
case 'join': {
|
|
170
|
+
count += countIssues(tree.left);
|
|
171
|
+
tree = tree.right;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
case 'prepend': {
|
|
175
|
+
tree = tree.tree;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
default: {
|
|
179
|
+
return count + 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const separatedList = (list: string[], sep: 'or' | 'and'): string => {
|
|
186
|
+
switch (list.length) {
|
|
187
|
+
case 0: {
|
|
188
|
+
return `nothing`;
|
|
189
|
+
}
|
|
190
|
+
case 1: {
|
|
191
|
+
return list[0];
|
|
192
|
+
}
|
|
193
|
+
default: {
|
|
194
|
+
return `${list.slice(0, -1).join(', ')} ${sep} ${list[list.length - 1]}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const formatLiteral = (value: Literal): string => {
|
|
200
|
+
return JSON.stringify(value);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const formatRangeMessage = (
|
|
204
|
+
type: 'a string' | 'an array' | 'a byte array',
|
|
205
|
+
unit: 'character' | 'grapheme' | 'item' | 'byte',
|
|
206
|
+
min: number,
|
|
207
|
+
max: number,
|
|
208
|
+
): string => {
|
|
209
|
+
let message = `${type} `;
|
|
210
|
+
|
|
211
|
+
if (min > 0) {
|
|
212
|
+
if (max === min) {
|
|
213
|
+
message += `${min}`;
|
|
214
|
+
} else if (max !== Infinity) {
|
|
215
|
+
message += `between ${min} and ${max}`;
|
|
216
|
+
} else {
|
|
217
|
+
message += `at least ${min}`;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
message += `at most ${max}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
message += ` ${unit}(s)`;
|
|
224
|
+
return message;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const formatIssueTree = (tree: IssueTree): string => {
|
|
228
|
+
let path = '';
|
|
229
|
+
let count = 0;
|
|
230
|
+
for (;;) {
|
|
231
|
+
switch (tree.code) {
|
|
232
|
+
case 'join': {
|
|
233
|
+
count += countIssues(tree.right);
|
|
234
|
+
tree = tree.left;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
case 'prepend': {
|
|
238
|
+
path += `.${tree.key}`;
|
|
239
|
+
tree = tree.tree;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let message: string;
|
|
248
|
+
switch (tree.code) {
|
|
249
|
+
case 'missing_value': {
|
|
250
|
+
message = `missing value`;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'invalid_literal': {
|
|
254
|
+
message = `expected ${separatedList(tree.expected.map(formatLiteral), 'or')}`;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'invalid_type': {
|
|
258
|
+
message = `expected ${tree.expected}`;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case 'invalid_variant': {
|
|
262
|
+
message = `expected ${separatedList(tree.expected, 'or')}`;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case 'invalid_integer_range': {
|
|
266
|
+
const min = tree.min;
|
|
267
|
+
const max = tree.max;
|
|
268
|
+
|
|
269
|
+
message = `expected an integer `;
|
|
270
|
+
|
|
271
|
+
if (min > 0) {
|
|
272
|
+
if (max === min) {
|
|
273
|
+
message += `of exactly ${min}`;
|
|
274
|
+
} else if (max !== Infinity) {
|
|
275
|
+
message += `between ${min} and ${max}`;
|
|
276
|
+
} else {
|
|
277
|
+
message += `of at least ${min}`;
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
message += `of at most ${max}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case 'invalid_string_format': {
|
|
286
|
+
message = `expected a ${tree.expected} formatted string`;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case 'invalid_string_graphemes': {
|
|
290
|
+
message = formatRangeMessage('a string', 'grapheme', tree.minGraphemes, tree.maxGraphemes);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'invalid_string_length': {
|
|
294
|
+
message = formatRangeMessage('a string', 'character', tree.minLength, tree.maxLength);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'invalid_array_length': {
|
|
298
|
+
message = formatRangeMessage('an array', 'item', tree.minLength, tree.maxLength);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 'invalid_bytes_size': {
|
|
302
|
+
message = formatRangeMessage('a byte array', 'byte', tree.minSize, tree.maxSize);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let msg = `${tree.code} at ${path ?? '.'} (${message})`;
|
|
308
|
+
if (count > 0) {
|
|
309
|
+
msg += ` (+${count} other issue(s))`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return msg;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export class ValidationError extends Error {
|
|
316
|
+
override readonly name = 'ValidationError';
|
|
317
|
+
|
|
318
|
+
#issueTree: IssueTree;
|
|
319
|
+
|
|
320
|
+
constructor(issueTree: IssueTree) {
|
|
321
|
+
super();
|
|
322
|
+
|
|
323
|
+
this.#issueTree = issueTree;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
override get message(): string {
|
|
327
|
+
return formatIssueTree(this.#issueTree);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
get issues(): readonly Issue[] {
|
|
331
|
+
return collectIssues(this.#issueTree);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
class ErrImpl implements Err {
|
|
336
|
+
readonly ok = false;
|
|
337
|
+
|
|
338
|
+
#issueTree: IssueTree;
|
|
339
|
+
|
|
340
|
+
constructor(issueTree: IssueTree) {
|
|
341
|
+
this.#issueTree = issueTree;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
get message(): string {
|
|
345
|
+
return formatIssueTree(this.#issueTree);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
get issues(): readonly Issue[] {
|
|
349
|
+
return collectIssues(this.#issueTree);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
throw(): never {
|
|
353
|
+
throw new ValidationError(this.#issueTree);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// #__NO_SIDE_EFFECTS__
|
|
358
|
+
export const is = <const TSchema extends BaseSchema>(
|
|
359
|
+
schema: TSchema,
|
|
360
|
+
input: unknown,
|
|
361
|
+
): input is InferInput<TSchema> => {
|
|
362
|
+
const r = schema['~run'](input, FLAG_ABORT_EARLY);
|
|
363
|
+
return r === undefined || r.ok;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// #__NO_SIDE_EFFECTS__
|
|
367
|
+
export const safeParse = <const TSchema extends BaseSchema>(
|
|
368
|
+
schema: TSchema,
|
|
369
|
+
input: unknown,
|
|
370
|
+
): ValidationResult<InferOutput<TSchema>> => {
|
|
371
|
+
const r = schema['~run'](input, FLAG_EMPTY);
|
|
372
|
+
|
|
373
|
+
if (r === undefined) {
|
|
374
|
+
return { ok: true, value: input as InferOutput<TSchema> };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (r.ok) {
|
|
378
|
+
return r as Ok<InferOutput<TSchema>>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return new ErrImpl(r);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export const parse = <const TSchema extends BaseSchema>(
|
|
385
|
+
schema: TSchema,
|
|
386
|
+
input: unknown,
|
|
387
|
+
): InferOutput<TSchema> => {
|
|
388
|
+
const r = schema['~run'](input, FLAG_EMPTY);
|
|
389
|
+
|
|
390
|
+
if (r === undefined) {
|
|
391
|
+
return input as InferOutput<TSchema>;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (r.ok) {
|
|
395
|
+
return r.value as InferOutput<TSchema>;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
throw new ValidationError(r);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// #region Base constraint
|
|
402
|
+
|
|
403
|
+
export interface BaseConstraint<TType = unknown> {
|
|
404
|
+
readonly kind: 'constraint';
|
|
405
|
+
readonly type: string;
|
|
406
|
+
readonly '~run': (input: TType, flags: number) => MatcherResult;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
type ConstraintTuple<T> = readonly [BaseConstraint<T>, ...BaseConstraint<T>[]];
|
|
410
|
+
|
|
411
|
+
export type SchemaWithConstraint<
|
|
412
|
+
TItem extends BaseSchema,
|
|
413
|
+
TConstraints extends ConstraintTuple<InferOutput<TItem>>,
|
|
414
|
+
> = TItem & {
|
|
415
|
+
readonly constraints: TConstraints;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export const constrain = <
|
|
419
|
+
TItem extends BaseSchema,
|
|
420
|
+
const TConstraints extends ConstraintTuple<InferOutput<TItem>>,
|
|
421
|
+
>(
|
|
422
|
+
base: TItem,
|
|
423
|
+
constraints: TConstraints,
|
|
424
|
+
): SchemaWithConstraint<TItem, TConstraints> => {
|
|
425
|
+
const len = constraints.length;
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
...base,
|
|
429
|
+
constraints: constraints,
|
|
430
|
+
'~run'(input, flags) {
|
|
431
|
+
let result = base['~run'](input, flags);
|
|
432
|
+
let current: any;
|
|
433
|
+
|
|
434
|
+
if (result === undefined) {
|
|
435
|
+
current = input;
|
|
436
|
+
} else if (result.ok) {
|
|
437
|
+
current = result.value;
|
|
438
|
+
} else {
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (let idx = 0; idx < len; idx++) {
|
|
443
|
+
const r = constraints[idx]['~run'](current, flags);
|
|
444
|
+
|
|
445
|
+
if (r !== undefined) {
|
|
446
|
+
if (r.ok) {
|
|
447
|
+
current = r.value;
|
|
448
|
+
|
|
449
|
+
if (result === undefined || result.ok) {
|
|
450
|
+
result = r;
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
if (flags & FLAG_ABORT_EARLY) {
|
|
454
|
+
return r;
|
|
455
|
+
} else if (result === undefined || result.ok) {
|
|
456
|
+
result = r;
|
|
457
|
+
} else {
|
|
458
|
+
result = joinIssues(result, r);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return result;
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// #region Base metadata
|
|
470
|
+
|
|
471
|
+
export interface BaseMetadata {
|
|
472
|
+
readonly kind: 'metadata';
|
|
473
|
+
readonly type: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// #region Literal schema
|
|
477
|
+
|
|
478
|
+
export interface LiteralSchema<T extends Literal = Literal> extends BaseSchema<T> {
|
|
479
|
+
readonly type: 'literal';
|
|
480
|
+
readonly expected: T;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// #__NO_SIDE_EFFECTS__
|
|
484
|
+
export const literal = <T extends Literal>(value: T): LiteralSchema<T> => {
|
|
485
|
+
const issue: IssueLeaf = {
|
|
486
|
+
ok: false,
|
|
487
|
+
code: 'invalid_literal',
|
|
488
|
+
expected: [value],
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
kind: 'schema',
|
|
493
|
+
type: 'literal',
|
|
494
|
+
expected: value,
|
|
495
|
+
'~run'(input, _flags) {
|
|
496
|
+
if (input !== value) {
|
|
497
|
+
return issue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return undefined;
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export interface LiteralEnumSchema<TEnums extends readonly Literal[] = []>
|
|
506
|
+
extends BaseSchema<TEnums[number]> {
|
|
507
|
+
readonly type: 'literal_enum';
|
|
508
|
+
readonly expected: TEnums;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// #__NO_SIDE_EFFECTS__
|
|
512
|
+
export const literalEnum = <const TEnums extends readonly Literal[]>(
|
|
513
|
+
values: TEnums,
|
|
514
|
+
): LiteralEnumSchema<TEnums> => {
|
|
515
|
+
const issue: IssueLeaf = {
|
|
516
|
+
ok: false,
|
|
517
|
+
code: 'invalid_literal',
|
|
518
|
+
expected: values,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
kind: 'schema',
|
|
523
|
+
type: 'literal_enum',
|
|
524
|
+
expected: values,
|
|
525
|
+
'~run'(input, _flags) {
|
|
526
|
+
if (!values.includes(input as any)) {
|
|
527
|
+
return issue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return undefined;
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// #region Boolean schema
|
|
536
|
+
|
|
537
|
+
export interface BooleanSchema extends BaseSchema<boolean> {
|
|
538
|
+
readonly type: 'boolean';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const ISSUE_TYPE_BOOLEAN: IssueLeaf = {
|
|
542
|
+
ok: false,
|
|
543
|
+
code: 'invalid_type',
|
|
544
|
+
expected: 'boolean',
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const BOOLEAN_SCHEMA: BooleanSchema = {
|
|
548
|
+
kind: 'schema',
|
|
549
|
+
type: 'boolean',
|
|
550
|
+
'~run'(input, _flags) {
|
|
551
|
+
if (typeof input !== 'boolean') {
|
|
552
|
+
return ISSUE_TYPE_BOOLEAN;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return undefined;
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// #__NO_SIDE_EFFECTS__
|
|
560
|
+
export const boolean = (): BooleanSchema => {
|
|
561
|
+
return BOOLEAN_SCHEMA;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// #region Integer schema
|
|
565
|
+
|
|
566
|
+
export interface IntegerSchema extends BaseSchema<number> {
|
|
567
|
+
readonly type: 'integer';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const ISSUE_TYPE_INTEGER: IssueLeaf = {
|
|
571
|
+
ok: false,
|
|
572
|
+
code: 'invalid_type',
|
|
573
|
+
expected: 'integer',
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const INTEGER_SCHEMA: IntegerSchema = {
|
|
577
|
+
kind: 'schema',
|
|
578
|
+
type: 'integer',
|
|
579
|
+
'~run'(input, _flags) {
|
|
580
|
+
if (typeof input !== 'number') {
|
|
581
|
+
return ISSUE_TYPE_INTEGER;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (input < 0 || !Number.isSafeInteger(input)) {
|
|
585
|
+
return ISSUE_TYPE_INTEGER;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return undefined;
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// #__NO_SIDE_EFFECTS__
|
|
593
|
+
export const integer = (): IntegerSchema => {
|
|
594
|
+
return INTEGER_SCHEMA;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// #region Integer constraints
|
|
598
|
+
|
|
599
|
+
export interface IntegerRangeConstraint<TMin extends number = number, TMax extends number = number>
|
|
600
|
+
extends BaseConstraint<number> {
|
|
601
|
+
readonly type: 'integer_range';
|
|
602
|
+
readonly min: TMin;
|
|
603
|
+
readonly max: TMax;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// #__NO_SIDE_EFFECTS__
|
|
607
|
+
export const integerRange: {
|
|
608
|
+
<const TMin extends number>(min: TMin): IntegerRangeConstraint<TMin>;
|
|
609
|
+
<const TMin extends number, const TMax extends number>(
|
|
610
|
+
min: TMin,
|
|
611
|
+
max: TMax,
|
|
612
|
+
): IntegerRangeConstraint<TMin, TMax>;
|
|
613
|
+
} = (min: number, max: number = Infinity): IntegerRangeConstraint => {
|
|
614
|
+
const issue: IssueLeaf = {
|
|
615
|
+
ok: false,
|
|
616
|
+
code: 'invalid_integer_range',
|
|
617
|
+
min: min,
|
|
618
|
+
max: max,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
kind: 'constraint',
|
|
623
|
+
type: 'integer_range',
|
|
624
|
+
min: min,
|
|
625
|
+
max: max,
|
|
626
|
+
'~run'(input, _flags) {
|
|
627
|
+
if (input < min) {
|
|
628
|
+
return issue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (input > max) {
|
|
632
|
+
return issue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return undefined;
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// #region String schema
|
|
641
|
+
|
|
642
|
+
export interface StringSchema<T extends string = string> extends BaseSchema<T> {
|
|
643
|
+
readonly type: 'string';
|
|
644
|
+
readonly format: null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export interface FormattedStringSchema<TFormat extends keyof StringFormatMap = keyof StringFormatMap>
|
|
648
|
+
extends BaseSchema<StringFormatMap[TFormat]> {
|
|
649
|
+
readonly type: 'string';
|
|
650
|
+
readonly format: TFormat;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const ISSUE_TYPE_STRING: IssueLeaf = {
|
|
654
|
+
ok: false,
|
|
655
|
+
code: 'invalid_type',
|
|
656
|
+
expected: 'string',
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const STRING_SINGLETON: StringSchema = {
|
|
660
|
+
kind: 'schema',
|
|
661
|
+
type: 'string',
|
|
662
|
+
format: null,
|
|
663
|
+
'~run'(input, _flags) {
|
|
664
|
+
if (typeof input !== 'string') {
|
|
665
|
+
return ISSUE_TYPE_STRING;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return undefined;
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// #__NO_SIDE_EFFECTS__
|
|
673
|
+
export const string = <T extends string = string>(): StringSchema<T> => {
|
|
674
|
+
return STRING_SINGLETON as StringSchema<T>;
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// #__NO_SIDE_EFFECTS__
|
|
678
|
+
const _formattedString = <TFormat extends keyof StringFormatMap>(
|
|
679
|
+
format: TFormat,
|
|
680
|
+
validate: (input: string) => boolean,
|
|
681
|
+
) => {
|
|
682
|
+
const issue: IssueLeaf = {
|
|
683
|
+
ok: false,
|
|
684
|
+
code: 'invalid_string_format',
|
|
685
|
+
expected: format,
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const schema: FormattedStringSchema<TFormat> = {
|
|
689
|
+
kind: 'schema',
|
|
690
|
+
type: 'string',
|
|
691
|
+
format: format,
|
|
692
|
+
'~run'(input, _flags) {
|
|
693
|
+
if (typeof input !== 'string') {
|
|
694
|
+
return ISSUE_TYPE_STRING;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!validate(input)) {
|
|
698
|
+
return issue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return undefined;
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return () => schema;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// prettier-ignore
|
|
709
|
+
export const actorIdentifierString = /*#__PURE__*/ _formattedString('at-identifier', syntax.isActorIdentifier);
|
|
710
|
+
export const resourceUriString = /*#__PURE__*/ _formattedString('at-uri', syntax.isResourceUri);
|
|
711
|
+
export const cidString = /*#__PURE__*/ _formattedString('cid', syntax.isCid);
|
|
712
|
+
export const datetimeString = /*#__PURE__*/ _formattedString('datetime', syntax.isDatetime);
|
|
713
|
+
export const didString = /*#__PURE__*/ _formattedString('did', syntax.isDid);
|
|
714
|
+
export const handleString = /*#__PURE__*/ _formattedString('handle', syntax.isHandle);
|
|
715
|
+
export const languageCodeString = /*#__PURE__*/ _formattedString('language', syntax.isLanguageCode);
|
|
716
|
+
export const nsidString = /*#__PURE__*/ _formattedString('nsid', syntax.isNsid);
|
|
717
|
+
export const recordKeyString = /*#__PURE__*/ _formattedString('record-key', syntax.isRecordKey);
|
|
718
|
+
export const tidString = /*#__PURE__*/ _formattedString('tid', syntax.isTid);
|
|
719
|
+
export const genericUriString = /*#__PURE__*/ _formattedString('uri', syntax.isGenericUri);
|
|
720
|
+
|
|
721
|
+
// #region String constraints
|
|
722
|
+
|
|
723
|
+
export interface StringLengthConstraint<
|
|
724
|
+
TMinLength extends number = number,
|
|
725
|
+
TMaxLength extends number = number,
|
|
726
|
+
> extends BaseConstraint<string> {
|
|
727
|
+
readonly type: 'string_length';
|
|
728
|
+
readonly minLength: TMinLength;
|
|
729
|
+
readonly maxLength: TMaxLength;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// #__NO_SIDE_EFFECTS__
|
|
733
|
+
export const stringLength: {
|
|
734
|
+
<const TMinLength extends number>(min: TMinLength): StringLengthConstraint<TMinLength>;
|
|
735
|
+
<const TMinLength extends number, const TMaxLength extends number>(
|
|
736
|
+
min: TMinLength,
|
|
737
|
+
max: TMaxLength,
|
|
738
|
+
): StringLengthConstraint<TMinLength, TMaxLength>;
|
|
739
|
+
} = (minLength: number, maxLength: number = Infinity): StringLengthConstraint => {
|
|
740
|
+
const issue: IssueLeaf = {
|
|
741
|
+
ok: false,
|
|
742
|
+
code: 'invalid_string_length',
|
|
743
|
+
minLength: minLength,
|
|
744
|
+
maxLength: maxLength,
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
kind: 'constraint',
|
|
749
|
+
type: 'string_length',
|
|
750
|
+
minLength: minLength,
|
|
751
|
+
maxLength: maxLength,
|
|
752
|
+
'~run'(input, _flags) {
|
|
753
|
+
// UTF-8 conversion can be expensive, so we're going to do some safe naive
|
|
754
|
+
// checks where we assume an upper-bound of the UTF-16 to UTF-8 conversion
|
|
755
|
+
|
|
756
|
+
const maybeUtf8Len = input.length * 3;
|
|
757
|
+
|
|
758
|
+
// fail early if we're still less than minimum length
|
|
759
|
+
if (maybeUtf8Len < minLength) {
|
|
760
|
+
return issue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// skip if we're still within maximum length
|
|
764
|
+
if (maybeUtf8Len <= maxLength) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const utf8Len = getUtf8Length(input);
|
|
769
|
+
|
|
770
|
+
if (utf8Len < minLength) {
|
|
771
|
+
return issue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (utf8Len > maxLength) {
|
|
775
|
+
return issue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return undefined;
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
export interface StringGraphemesConstraint<
|
|
784
|
+
TMinGraphemes extends number = number,
|
|
785
|
+
TMaxGraphemes extends number = number,
|
|
786
|
+
> extends BaseConstraint<string> {
|
|
787
|
+
readonly type: 'string_graphemes';
|
|
788
|
+
readonly minGraphemes: TMinGraphemes;
|
|
789
|
+
readonly maxGraphemes: TMaxGraphemes;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export const stringGraphemes: {
|
|
793
|
+
<const TMinGraphemes extends number>(min: TMinGraphemes): StringGraphemesConstraint<TMinGraphemes>;
|
|
794
|
+
<const TMinGraphemes extends number, const TMaxGraphemes extends number>(
|
|
795
|
+
min: TMinGraphemes,
|
|
796
|
+
max: TMaxGraphemes,
|
|
797
|
+
): StringGraphemesConstraint<TMinGraphemes, TMaxGraphemes>;
|
|
798
|
+
} = (minGraphemes: number, maxGraphemes: number = Infinity): StringGraphemesConstraint => {
|
|
799
|
+
const issue: IssueLeaf = {
|
|
800
|
+
ok: false,
|
|
801
|
+
code: 'invalid_string_graphemes',
|
|
802
|
+
minGraphemes: minGraphemes,
|
|
803
|
+
maxGraphemes: maxGraphemes,
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
kind: 'constraint',
|
|
808
|
+
type: 'string_graphemes',
|
|
809
|
+
minGraphemes: minGraphemes,
|
|
810
|
+
maxGraphemes: maxGraphemes,
|
|
811
|
+
'~run'(input, _flags) {
|
|
812
|
+
// grapheme conversion is expensive, so we're going to do some safe naive
|
|
813
|
+
// checks where we assume 1 UTF-16 character = 1 grapheme.
|
|
814
|
+
|
|
815
|
+
const utf16Len = input.length;
|
|
816
|
+
|
|
817
|
+
// fail early if UTF-16 length is less than grapheme length
|
|
818
|
+
if (utf16Len < minGraphemes) {
|
|
819
|
+
return issue;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// skip if we're still within maximum constraint
|
|
823
|
+
if (utf16Len <= maxGraphemes) {
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const graphemeLen = getGraphemeLength(input);
|
|
828
|
+
|
|
829
|
+
if (graphemeLen < minGraphemes) {
|
|
830
|
+
return issue;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (graphemeLen > maxGraphemes) {
|
|
834
|
+
return issue;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return undefined;
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// #region Blob schema
|
|
843
|
+
|
|
844
|
+
export interface BlobSchema extends BaseSchema<interfaces.Blob | interfaces.LegacyBlob, interfaces.Blob> {
|
|
845
|
+
readonly type: 'blob';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const ISSUE_EXPECTED_BLOB: IssueLeaf = {
|
|
849
|
+
ok: false,
|
|
850
|
+
code: 'invalid_type',
|
|
851
|
+
expected: 'blob',
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const BLOB_SCHEMA: BlobSchema = {
|
|
855
|
+
kind: 'schema',
|
|
856
|
+
type: 'blob',
|
|
857
|
+
'~run'(input, _flags) {
|
|
858
|
+
if (typeof input !== 'object' || input === null) {
|
|
859
|
+
return ISSUE_EXPECTED_BLOB;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (interfaces.isBlob(input)) {
|
|
863
|
+
return undefined;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (interfaces.isLegacyBlob(input)) {
|
|
867
|
+
const blob: interfaces.Blob = {
|
|
868
|
+
$type: 'blob',
|
|
869
|
+
mimeType: input.mimeType,
|
|
870
|
+
ref: { $link: input.cid },
|
|
871
|
+
size: -1,
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
return { ok: true, value: blob };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return ISSUE_EXPECTED_BLOB;
|
|
878
|
+
},
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// #__NO_SIDE_EFFECTS__
|
|
882
|
+
export const blob = (): BlobSchema => {
|
|
883
|
+
return BLOB_SCHEMA;
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// #region IPLD bytes schema
|
|
887
|
+
|
|
888
|
+
export interface BytesSchema extends BaseSchema<interfaces.Bytes, interfaces.Bytes> {
|
|
889
|
+
readonly type: 'bytes';
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const ISSUE_EXPECTED_BYTES: IssueLeaf = {
|
|
893
|
+
ok: false,
|
|
894
|
+
code: 'invalid_type',
|
|
895
|
+
expected: 'bytes',
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const BYTES_SCHEMA: BytesSchema = {
|
|
899
|
+
kind: 'schema',
|
|
900
|
+
type: 'bytes',
|
|
901
|
+
'~run'(input, _flags) {
|
|
902
|
+
if (!interfaces.isBytes(input)) {
|
|
903
|
+
return ISSUE_EXPECTED_BYTES;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return undefined;
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// #__NO_SIDE_EFFECTS__
|
|
911
|
+
export const bytes = (): BytesSchema => {
|
|
912
|
+
return BYTES_SCHEMA;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// #region IPLD bytes constraint
|
|
916
|
+
export interface BytesSizeConstraint<TMinLength extends number = number, TMaxLength extends number = number>
|
|
917
|
+
extends BaseConstraint<interfaces.Bytes> {
|
|
918
|
+
readonly type: 'bytes_size';
|
|
919
|
+
readonly minSize: TMinLength;
|
|
920
|
+
readonly maxSize: TMaxLength;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// #__NO_SIDE_EFFECTS__
|
|
924
|
+
export const bytesSize: {
|
|
925
|
+
<const TMinLength extends number>(min: TMinLength): BytesSizeConstraint<TMinLength>;
|
|
926
|
+
<const TMinLength extends number, const TMaxLength extends number>(
|
|
927
|
+
min: TMinLength,
|
|
928
|
+
max: TMaxLength,
|
|
929
|
+
): BytesSizeConstraint<TMinLength, TMaxLength>;
|
|
930
|
+
} = (minSize: number, maxSize: number = Infinity): BytesSizeConstraint => {
|
|
931
|
+
const issue: IssueLeaf = {
|
|
932
|
+
ok: false,
|
|
933
|
+
code: 'invalid_bytes_size',
|
|
934
|
+
minSize: minSize,
|
|
935
|
+
maxSize: maxSize,
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
kind: 'constraint',
|
|
940
|
+
type: 'bytes_size',
|
|
941
|
+
minSize: minSize,
|
|
942
|
+
maxSize: maxSize,
|
|
943
|
+
'~run'(input, _flags) {
|
|
944
|
+
let size: number;
|
|
945
|
+
|
|
946
|
+
if (_isBytesWrapper(input)) {
|
|
947
|
+
size = input.buf.length;
|
|
948
|
+
} else {
|
|
949
|
+
const str = input.$bytes;
|
|
950
|
+
let bytes = str.length;
|
|
951
|
+
|
|
952
|
+
if (str.charCodeAt(bytes - 1) === 0x3d) {
|
|
953
|
+
bytes--;
|
|
954
|
+
}
|
|
955
|
+
if (bytes > 1 && str.charCodeAt(bytes - 1) === 0x3d) {
|
|
956
|
+
bytes--;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
size = (bytes * 3) >>> 2;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (size < minSize) {
|
|
963
|
+
return issue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (size > maxSize) {
|
|
967
|
+
return issue;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return undefined;
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// #region IPLD CID type schema
|
|
976
|
+
|
|
977
|
+
export interface CidLinkSchema extends BaseSchema<interfaces.CidLink, interfaces.CidLink> {
|
|
978
|
+
readonly type: 'cid_link';
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const ISSUE_EXPECTED_CID_LINK: IssueLeaf = {
|
|
982
|
+
ok: false,
|
|
983
|
+
code: 'invalid_type',
|
|
984
|
+
expected: 'cid-link',
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const CID_LINK_SCHEMA: CidLinkSchema = {
|
|
988
|
+
kind: 'schema',
|
|
989
|
+
type: 'cid_link',
|
|
990
|
+
'~run'(input, _flags) {
|
|
991
|
+
if (!interfaces.isCidLink(input)) {
|
|
992
|
+
return ISSUE_EXPECTED_CID_LINK;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return undefined;
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// #__NO_SIDE_EFFECTS__
|
|
1000
|
+
export const cidLink = (): CidLinkSchema => {
|
|
1001
|
+
return CID_LINK_SCHEMA;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// #region Nullable schema
|
|
1005
|
+
|
|
1006
|
+
export interface NullableSchema<TItem extends BaseSchema>
|
|
1007
|
+
extends BaseSchema<InferInput<TItem> | null, InferOutput<TItem> | null> {
|
|
1008
|
+
readonly type: 'nullable';
|
|
1009
|
+
readonly wrapped: TItem;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// #__NO_SIDE_EFFECTS__
|
|
1013
|
+
export const nullable = <TItem extends BaseSchema>(wrapped: TItem): NullableSchema<TItem> => {
|
|
1014
|
+
return {
|
|
1015
|
+
kind: 'schema',
|
|
1016
|
+
type: 'nullable',
|
|
1017
|
+
wrapped: wrapped,
|
|
1018
|
+
'~run'(input, flags) {
|
|
1019
|
+
if (input === null) {
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return wrapped['~run'](input, flags);
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// #region Optional schema
|
|
1029
|
+
|
|
1030
|
+
export type DefaultValue<TItem extends BaseSchema> =
|
|
1031
|
+
| InferOutput<TItem>
|
|
1032
|
+
| (() => InferOutput<TItem>)
|
|
1033
|
+
| undefined;
|
|
1034
|
+
|
|
1035
|
+
export type InferOptionalOutput<
|
|
1036
|
+
TItem extends BaseSchema,
|
|
1037
|
+
TDefault extends DefaultValue<TItem>,
|
|
1038
|
+
> = undefined extends TDefault ? InferOutput<TItem> | undefined : InferOutput<TItem>;
|
|
1039
|
+
|
|
1040
|
+
export interface OptionalSchema<TItem extends BaseSchema, TDefault extends DefaultValue<TItem>>
|
|
1041
|
+
extends BaseSchema<InferInput<TItem> | undefined, InferOptionalOutput<TItem, TDefault>> {
|
|
1042
|
+
readonly type: 'optional';
|
|
1043
|
+
readonly wrapped: TItem;
|
|
1044
|
+
readonly default: TDefault;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
type MaybeOptional<TItem extends BaseSchema> = TItem | OptionalSchema<TItem, undefined>;
|
|
1048
|
+
|
|
1049
|
+
// #__NO_SIDE_EFFECTS__
|
|
1050
|
+
export const optional: {
|
|
1051
|
+
<TItem extends BaseSchema>(wrapped: TItem): OptionalSchema<TItem, undefined>;
|
|
1052
|
+
<TItem extends BaseSchema, TDefault extends DefaultValue<TItem>>(
|
|
1053
|
+
wrapped: TItem,
|
|
1054
|
+
defaultValue: TDefault,
|
|
1055
|
+
): OptionalSchema<TItem, TDefault>;
|
|
1056
|
+
} = (wrapped: BaseSchema, defaultValue?: any): OptionalSchema<any, any> => {
|
|
1057
|
+
return {
|
|
1058
|
+
kind: 'schema',
|
|
1059
|
+
type: 'optional',
|
|
1060
|
+
wrapped: wrapped,
|
|
1061
|
+
default: defaultValue,
|
|
1062
|
+
'~run'(input, flags) {
|
|
1063
|
+
if (input === undefined) {
|
|
1064
|
+
if (defaultValue === undefined) {
|
|
1065
|
+
return undefined;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const value = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
|
|
1069
|
+
|
|
1070
|
+
return { ok: true, value };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return wrapped['~run'](input, flags);
|
|
1074
|
+
},
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const isOptionalSchema = (schema: BaseSchema): schema is OptionalSchema<any, unknown> => {
|
|
1079
|
+
return schema.type === 'optional';
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
// #region Array schema
|
|
1083
|
+
|
|
1084
|
+
export interface ArraySchema<TItem extends BaseSchema> extends BaseSchema<unknown[], unknown[]> {
|
|
1085
|
+
readonly type: 'array';
|
|
1086
|
+
readonly item: TItem;
|
|
1087
|
+
|
|
1088
|
+
readonly [kObjectType]?: { in: InferInput<TItem>[]; out: InferOutput<TItem>[] };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const ISSUE_TYPE_ARRAY: IssueLeaf = {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
code: 'invalid_type',
|
|
1094
|
+
expected: 'array',
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// #__NO_SIDE_EFFECTS__
|
|
1098
|
+
export const array = <TItem extends BaseSchema>(item: TItem | (() => TItem)): ArraySchema<TItem> => {
|
|
1099
|
+
const resolvedShape = lazy(() => {
|
|
1100
|
+
return typeof item === 'function' ? item() : item;
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
return {
|
|
1104
|
+
kind: 'schema',
|
|
1105
|
+
type: 'array',
|
|
1106
|
+
get item() {
|
|
1107
|
+
return lazyProperty(this, 'item', resolvedShape.value);
|
|
1108
|
+
},
|
|
1109
|
+
get '~run'() {
|
|
1110
|
+
const shape = resolvedShape.value;
|
|
1111
|
+
|
|
1112
|
+
const matcher: Matcher = (input, flags) => {
|
|
1113
|
+
if (!isArray(input)) {
|
|
1114
|
+
return ISSUE_TYPE_ARRAY;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
let issues: IssueTree | undefined;
|
|
1118
|
+
let output: any[] | undefined;
|
|
1119
|
+
|
|
1120
|
+
for (let idx = 0, len = input.length; idx < len; idx++) {
|
|
1121
|
+
const val = input[idx];
|
|
1122
|
+
const r = shape['~run'](val, flags);
|
|
1123
|
+
|
|
1124
|
+
if (r !== undefined) {
|
|
1125
|
+
if (r.ok) {
|
|
1126
|
+
if (output === undefined) {
|
|
1127
|
+
output = input.slice();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
output[idx] = r.value;
|
|
1131
|
+
} else {
|
|
1132
|
+
issues = joinIssues(issues, prependPath(idx, r));
|
|
1133
|
+
|
|
1134
|
+
if (flags & FLAG_ABORT_EARLY) {
|
|
1135
|
+
return issues;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (issues !== undefined) {
|
|
1142
|
+
return issues;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (output !== undefined) {
|
|
1146
|
+
return { ok: true, value: output };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return undefined;
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
return lazyProperty(this, '~run', matcher);
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// #region Array constraints
|
|
1158
|
+
|
|
1159
|
+
export interface ArrayLengthConstraint<TMinLength extends number = number, TMaxLength extends number = number>
|
|
1160
|
+
extends BaseConstraint<unknown[]> {
|
|
1161
|
+
readonly type: 'array_length';
|
|
1162
|
+
readonly minLength: TMinLength;
|
|
1163
|
+
readonly maxLength: TMaxLength;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// #__NO_SIDE_EFFECTS__
|
|
1167
|
+
export const arrayLength: {
|
|
1168
|
+
<const TMinLength extends number>(min: TMinLength): ArrayLengthConstraint<TMinLength>;
|
|
1169
|
+
<const TMinLength extends number, const TMaxLength extends number>(
|
|
1170
|
+
min: TMinLength,
|
|
1171
|
+
max: TMaxLength,
|
|
1172
|
+
): ArrayLengthConstraint<TMinLength, TMaxLength>;
|
|
1173
|
+
} = (minLength: number, maxLength: number = Infinity): ArrayLengthConstraint => {
|
|
1174
|
+
const issue: IssueLeaf = {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
code: 'invalid_array_length',
|
|
1177
|
+
minLength: minLength,
|
|
1178
|
+
maxLength: maxLength,
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
return {
|
|
1182
|
+
kind: 'constraint',
|
|
1183
|
+
type: 'array_length',
|
|
1184
|
+
minLength: minLength,
|
|
1185
|
+
maxLength: maxLength,
|
|
1186
|
+
'~run'(input, _flags) {
|
|
1187
|
+
const length = input.length;
|
|
1188
|
+
|
|
1189
|
+
if (length < minLength) {
|
|
1190
|
+
return issue;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (length > maxLength) {
|
|
1194
|
+
return issue;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return undefined;
|
|
1198
|
+
},
|
|
1199
|
+
};
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// #region Object schema
|
|
1203
|
+
|
|
1204
|
+
export type LooseObjectShape = Record<string, any>;
|
|
1205
|
+
export type ObjectShape = Record<string, BaseSchema>;
|
|
1206
|
+
|
|
1207
|
+
export type OptionalObjectInputKeys<TShape extends ObjectShape> = {
|
|
1208
|
+
[Key in keyof TShape]: TShape[Key] extends OptionalSchema<any, any> ? Key : never;
|
|
1209
|
+
}[keyof TShape];
|
|
1210
|
+
|
|
1211
|
+
export type OptionalObjectOutputKeys<TShape extends ObjectShape> = {
|
|
1212
|
+
[Key in keyof TShape]: TShape[Key] extends OptionalSchema<any, infer Default>
|
|
1213
|
+
? undefined extends Default
|
|
1214
|
+
? Key
|
|
1215
|
+
: never
|
|
1216
|
+
: never;
|
|
1217
|
+
}[keyof TShape];
|
|
1218
|
+
|
|
1219
|
+
type InferObjectInput<TShape extends ObjectShape> = Flatten<
|
|
1220
|
+
{
|
|
1221
|
+
-readonly [Key in Exclude<keyof TShape, OptionalObjectInputKeys<TShape>>]: InferInput<TShape[Key]>;
|
|
1222
|
+
} & {
|
|
1223
|
+
-readonly [Key in OptionalObjectInputKeys<TShape>]?: InferInput<TShape[Key]>;
|
|
1224
|
+
}
|
|
1225
|
+
>;
|
|
1226
|
+
|
|
1227
|
+
type InferObjectOutput<TShape extends ObjectShape> = Flatten<
|
|
1228
|
+
{
|
|
1229
|
+
-readonly [Key in Exclude<keyof TShape, OptionalObjectOutputKeys<TShape>>]: InferOutput<TShape[Key]>;
|
|
1230
|
+
} & {
|
|
1231
|
+
-readonly [Key in OptionalObjectOutputKeys<TShape>]?: InferOutput<TShape[Key]>;
|
|
1232
|
+
}
|
|
1233
|
+
>;
|
|
1234
|
+
|
|
1235
|
+
export interface ObjectSchema<TShape extends LooseObjectShape = LooseObjectShape>
|
|
1236
|
+
extends BaseSchema<Record<string, unknown>> {
|
|
1237
|
+
readonly type: 'object';
|
|
1238
|
+
readonly shape: Readonly<TShape>;
|
|
1239
|
+
|
|
1240
|
+
readonly [kObjectType]?: { in: InferObjectInput<TShape>; out: InferObjectOutput<TShape> };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
interface ObjectEntry {
|
|
1244
|
+
key: string;
|
|
1245
|
+
schema: BaseSchema;
|
|
1246
|
+
optional: boolean;
|
|
1247
|
+
missing: IssueTree;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const ISSUE_TYPE_OBJECT: IssueLeaf = {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
code: 'invalid_type',
|
|
1253
|
+
expected: 'object',
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const ISSUE_MISSING: IssueLeaf = {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
code: 'missing_value',
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
const set = (obj: Record<string, unknown>, key: string, value: unknown): void => {
|
|
1262
|
+
if (key === '__proto__') {
|
|
1263
|
+
Object.defineProperty(obj, key, { value });
|
|
1264
|
+
} else {
|
|
1265
|
+
obj[key] = value;
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// #__NO_SIDE_EFFECTS__
|
|
1270
|
+
export const object = <TShape extends LooseObjectShape>(shape: TShape): ObjectSchema<TShape> => {
|
|
1271
|
+
const resolvedEntries = lazy(() => {
|
|
1272
|
+
const resolved: ObjectEntry[] = [];
|
|
1273
|
+
|
|
1274
|
+
for (const key in shape) {
|
|
1275
|
+
const schema = shape[key];
|
|
1276
|
+
|
|
1277
|
+
resolved.push({
|
|
1278
|
+
key: key,
|
|
1279
|
+
schema: schema,
|
|
1280
|
+
optional: isOptionalSchema(schema),
|
|
1281
|
+
missing: prependPath(key, ISSUE_MISSING),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return resolved;
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
kind: 'schema',
|
|
1290
|
+
type: 'object',
|
|
1291
|
+
get shape() {
|
|
1292
|
+
// if we just return the shape as is then it wouldn't be the same exact
|
|
1293
|
+
// shape when getters are present.
|
|
1294
|
+
const resolved = resolvedEntries.value;
|
|
1295
|
+
const obj: any = {};
|
|
1296
|
+
|
|
1297
|
+
for (const entry of resolved) {
|
|
1298
|
+
obj[entry.key] = entry.schema;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return lazyProperty(this, 'shape', obj as TShape);
|
|
1302
|
+
},
|
|
1303
|
+
get '~run'() {
|
|
1304
|
+
const shape = resolvedEntries.value;
|
|
1305
|
+
const len = shape.length;
|
|
1306
|
+
|
|
1307
|
+
const matcher: Matcher = (input, flags) => {
|
|
1308
|
+
if (!isObject(input)) {
|
|
1309
|
+
return ISSUE_TYPE_OBJECT;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
let issues: IssueTree | undefined;
|
|
1313
|
+
let output: Record<string, unknown> | undefined;
|
|
1314
|
+
|
|
1315
|
+
for (let idx = 0; idx < len; idx++) {
|
|
1316
|
+
const entry = shape[idx];
|
|
1317
|
+
|
|
1318
|
+
const key = entry.key;
|
|
1319
|
+
const value = input[key];
|
|
1320
|
+
|
|
1321
|
+
if (value === undefined && !(key in input)) {
|
|
1322
|
+
if (!entry.optional) {
|
|
1323
|
+
issues = joinIssues(issues, entry.missing);
|
|
1324
|
+
|
|
1325
|
+
if (flags & FLAG_ABORT_EARLY) {
|
|
1326
|
+
return issues;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const r = entry.schema['~run'](value, flags);
|
|
1334
|
+
|
|
1335
|
+
if (r === undefined) {
|
|
1336
|
+
if (output !== undefined) {
|
|
1337
|
+
/*#__INLINE__*/ set(output, key, value);
|
|
1338
|
+
}
|
|
1339
|
+
} else if (r.ok) {
|
|
1340
|
+
if (output === undefined) {
|
|
1341
|
+
output = { ...input };
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/*#__INLINE__*/ set(output, key, value);
|
|
1345
|
+
} else {
|
|
1346
|
+
issues = joinIssues(issues, prependPath(key, r));
|
|
1347
|
+
|
|
1348
|
+
if (flags & FLAG_ABORT_EARLY) {
|
|
1349
|
+
return issues;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (issues !== undefined) {
|
|
1355
|
+
return issues;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (output !== undefined) {
|
|
1359
|
+
return { ok: true, value: output };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return undefined;
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
return lazyProperty(this, '~run', matcher);
|
|
1366
|
+
},
|
|
1367
|
+
};
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
// #region Record schema
|
|
1371
|
+
//
|
|
1372
|
+
// unfortunately, adapting for circular references has meant that we can't have
|
|
1373
|
+
// TypeScript check the object against a particular shape ($type field required)
|
|
1374
|
+
|
|
1375
|
+
export type RecordObjectShape = {
|
|
1376
|
+
$type: LiteralSchema<syntax.Nsid>;
|
|
1377
|
+
[key: string]: BaseSchema;
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
export type RecordKeySchema = StringSchema | FormattedStringSchema | LiteralSchema<string>;
|
|
1381
|
+
export type RecordObjectSchema = ObjectSchema<RecordObjectShape>;
|
|
1382
|
+
|
|
1383
|
+
export interface RecordSchema<TObject extends ObjectSchema, TKey extends RecordKeySchema>
|
|
1384
|
+
extends BaseSchema<Record<string, unknown>> {
|
|
1385
|
+
readonly type: 'record';
|
|
1386
|
+
readonly key: TKey;
|
|
1387
|
+
readonly object: TObject;
|
|
1388
|
+
|
|
1389
|
+
readonly [kObjectType]?: { in: InferInput<TObject>; out: InferOutput<TObject> };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// #__NO_SIDE_EFFECTS__
|
|
1393
|
+
export const record = <TKey extends RecordKeySchema, TObject extends ObjectSchema>(
|
|
1394
|
+
key: TKey,
|
|
1395
|
+
object: TObject,
|
|
1396
|
+
): RecordSchema<TObject, TKey> => {
|
|
1397
|
+
const validatedObject = lazy((): TObject => {
|
|
1398
|
+
const shape = object.shape;
|
|
1399
|
+
|
|
1400
|
+
let t = shape.$type as MaybeOptional<LiteralSchema<syntax.Nsid>> | undefined;
|
|
1401
|
+
|
|
1402
|
+
assert(t !== undefined, `expected $type in record to be defined`);
|
|
1403
|
+
if (t.type === 'optional') {
|
|
1404
|
+
t = t.wrapped;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
assert(t.type === 'literal' && typeof t.expected === 'string', `expected $type to be a string literal`);
|
|
1408
|
+
|
|
1409
|
+
return object;
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
return {
|
|
1413
|
+
kind: 'schema',
|
|
1414
|
+
type: 'record',
|
|
1415
|
+
key: key,
|
|
1416
|
+
get object() {
|
|
1417
|
+
return lazyProperty(this, 'object', validatedObject.value);
|
|
1418
|
+
},
|
|
1419
|
+
get '~run'() {
|
|
1420
|
+
const object = validatedObject.value;
|
|
1421
|
+
|
|
1422
|
+
const matcher: Matcher = (input, flags) => {
|
|
1423
|
+
return object['~run'](input, flags);
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
return lazyProperty(this, '~run', matcher);
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// #region Variant schema
|
|
1432
|
+
|
|
1433
|
+
type VariantTuple = readonly ObjectSchema<any>[];
|
|
1434
|
+
|
|
1435
|
+
type InferVariantInput<TMembers extends VariantTuple> = $type.enforce<InferInput<TMembers[number]>>;
|
|
1436
|
+
|
|
1437
|
+
type InferVariantOutput<TMembers extends VariantTuple> = $type.enforce<InferOutput<TMembers[number]>>;
|
|
1438
|
+
|
|
1439
|
+
export interface VariantSchema<
|
|
1440
|
+
TMembers extends VariantTuple = VariantTuple,
|
|
1441
|
+
TClosed extends boolean = boolean,
|
|
1442
|
+
> extends BaseSchema<Record<string, unknown>> {
|
|
1443
|
+
readonly type: 'variant';
|
|
1444
|
+
readonly members: TMembers;
|
|
1445
|
+
readonly closed: TClosed;
|
|
1446
|
+
|
|
1447
|
+
readonly [kObjectType]?: { in: InferVariantInput<TMembers>; out: InferVariantOutput<TMembers> };
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const ISSUE_VARIANT_MISSING = /*#__PURE__*/ prependPath('$type', {
|
|
1451
|
+
ok: false,
|
|
1452
|
+
code: 'missing_value',
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
const ISSUE_VARIANT_TYPE = /*#__PURE__*/ prependPath('$type', {
|
|
1456
|
+
ok: false,
|
|
1457
|
+
code: 'invalid_type',
|
|
1458
|
+
expected: 'string',
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
// #__NO_SIDE_EFFECTS__
|
|
1462
|
+
export const variant: {
|
|
1463
|
+
<const TMembers extends VariantTuple>(members: TMembers): VariantSchema<TMembers>;
|
|
1464
|
+
<const TMembers extends VariantTuple, TClosed extends boolean>(
|
|
1465
|
+
members: TMembers,
|
|
1466
|
+
closed: TClosed,
|
|
1467
|
+
): VariantSchema<TMembers, TClosed>;
|
|
1468
|
+
} = (members: ObjectSchema[], closed: boolean = false): VariantSchema<any, any> => {
|
|
1469
|
+
return {
|
|
1470
|
+
kind: 'schema',
|
|
1471
|
+
type: 'variant',
|
|
1472
|
+
members: members,
|
|
1473
|
+
closed: closed,
|
|
1474
|
+
get '~run'() {
|
|
1475
|
+
const map = Object.fromEntries(
|
|
1476
|
+
members.map((member, idx) => {
|
|
1477
|
+
const shape = member.shape;
|
|
1478
|
+
|
|
1479
|
+
let t = shape.$type as MaybeOptional<LiteralSchema<syntax.Nsid>> | undefined;
|
|
1480
|
+
|
|
1481
|
+
assert(t !== undefined, `expected $type in variant member #${idx} to be defined`);
|
|
1482
|
+
if (t.type === 'optional') {
|
|
1483
|
+
t = t.wrapped;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
assert(
|
|
1487
|
+
t.type === 'literal' && typeof t.expected === 'string',
|
|
1488
|
+
`expected $type in variant member #${idx} to be a string literal`,
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
return [t.expected, member];
|
|
1492
|
+
}),
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
const issue: IssueLeaf = {
|
|
1496
|
+
ok: false,
|
|
1497
|
+
code: 'invalid_variant',
|
|
1498
|
+
expected: Object.keys(map),
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const matcher: Matcher = (input, flags) => {
|
|
1502
|
+
if (!isObject(input)) {
|
|
1503
|
+
return ISSUE_TYPE_OBJECT;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (!('$type' in input)) {
|
|
1507
|
+
return ISSUE_VARIANT_MISSING;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const type = input.$type;
|
|
1511
|
+
if (typeof type !== 'string') {
|
|
1512
|
+
return ISSUE_VARIANT_TYPE;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (!(type in map)) {
|
|
1516
|
+
if (closed) {
|
|
1517
|
+
return issue;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return undefined;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const schema = map[type];
|
|
1524
|
+
|
|
1525
|
+
return schema['~run'](input, flags);
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
return lazyProperty(this, '~run', matcher);
|
|
1529
|
+
},
|
|
1530
|
+
};
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// #region Unknown schema
|
|
1534
|
+
|
|
1535
|
+
export interface UnknownSchema extends BaseSchema<Record<string, unknown>> {
|
|
1536
|
+
readonly type: 'unknown';
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const ISSUE_TYPE_UNKNOWN: IssueLeaf = {
|
|
1540
|
+
ok: false,
|
|
1541
|
+
code: 'invalid_type',
|
|
1542
|
+
expected: 'unknown',
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
const UNKNOWN_SCHEMA: UnknownSchema = {
|
|
1546
|
+
kind: 'schema',
|
|
1547
|
+
type: 'unknown',
|
|
1548
|
+
'~run'(input, _flags) {
|
|
1549
|
+
if (typeof input !== 'object' || input === null) {
|
|
1550
|
+
return ISSUE_TYPE_UNKNOWN;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return undefined;
|
|
1554
|
+
},
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
// #__NO_SIDE_EFFECTS__
|
|
1558
|
+
export const unknown = (): UnknownSchema => {
|
|
1559
|
+
return UNKNOWN_SCHEMA;
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// #region XRPC types
|
|
1563
|
+
|
|
1564
|
+
export interface XRPCLexBodyParam<
|
|
1565
|
+
TSchema extends ObjectSchema | VariantSchema = ObjectSchema | VariantSchema,
|
|
1566
|
+
> {
|
|
1567
|
+
readonly type: 'lex';
|
|
1568
|
+
readonly schema: TSchema;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export interface XRPCBlobBodyParam {
|
|
1572
|
+
readonly type: 'blob';
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
export type XRPCBodyParam = XRPCLexBodyParam | XRPCBlobBodyParam | null;
|
|
1576
|
+
|
|
1577
|
+
export type InferXRPCBodyInput<T extends XRPCBodyParam> =
|
|
1578
|
+
T extends XRPCLexBodyParam<infer Schema>
|
|
1579
|
+
? InferInput<Schema>
|
|
1580
|
+
: T extends XRPCBlobBodyParam
|
|
1581
|
+
? Blob
|
|
1582
|
+
: T extends null
|
|
1583
|
+
? void
|
|
1584
|
+
: never;
|
|
1585
|
+
|
|
1586
|
+
export type InferXRPCBodyOutput<T extends XRPCBodyParam> =
|
|
1587
|
+
T extends XRPCLexBodyParam<infer Schema>
|
|
1588
|
+
? InferOutput<Schema>
|
|
1589
|
+
: T extends XRPCBlobBodyParam
|
|
1590
|
+
? Blob
|
|
1591
|
+
: T extends null
|
|
1592
|
+
? void
|
|
1593
|
+
: never;
|
|
1594
|
+
|
|
1595
|
+
// #region XRPC procedure metadata
|
|
1596
|
+
|
|
1597
|
+
export interface XRPCProcedureMetadata<
|
|
1598
|
+
TParams extends ObjectSchema | null,
|
|
1599
|
+
TInput extends XRPCBodyParam,
|
|
1600
|
+
TOutput extends XRPCBodyParam,
|
|
1601
|
+
TNsid extends syntax.Nsid,
|
|
1602
|
+
> extends BaseMetadata {
|
|
1603
|
+
readonly type: 'xrpc_procedure';
|
|
1604
|
+
readonly nsid: TNsid;
|
|
1605
|
+
readonly params: TParams;
|
|
1606
|
+
readonly input: TInput;
|
|
1607
|
+
readonly output: TOutput;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// #__NO_SIDE_EFFECTS__
|
|
1611
|
+
export const procedure = <
|
|
1612
|
+
TNsid extends syntax.Nsid,
|
|
1613
|
+
TParams extends ObjectSchema | null,
|
|
1614
|
+
TInput extends XRPCBodyParam,
|
|
1615
|
+
TOutput extends XRPCBodyParam,
|
|
1616
|
+
>(
|
|
1617
|
+
nsid: TNsid,
|
|
1618
|
+
options: {
|
|
1619
|
+
params: TParams;
|
|
1620
|
+
input: TInput;
|
|
1621
|
+
output: TOutput;
|
|
1622
|
+
},
|
|
1623
|
+
): XRPCProcedureMetadata<TParams, TInput, TOutput, TNsid> => {
|
|
1624
|
+
// `schema` can be a getter, and we'd have to resolve that getter.
|
|
1625
|
+
|
|
1626
|
+
return {
|
|
1627
|
+
kind: 'metadata',
|
|
1628
|
+
type: 'xrpc_procedure',
|
|
1629
|
+
nsid: nsid,
|
|
1630
|
+
params: options.params,
|
|
1631
|
+
get input() {
|
|
1632
|
+
let val = options.input;
|
|
1633
|
+
|
|
1634
|
+
switch (val?.type) {
|
|
1635
|
+
case 'lex': {
|
|
1636
|
+
val = {
|
|
1637
|
+
type: 'lex',
|
|
1638
|
+
schema: val.schema,
|
|
1639
|
+
} as TInput;
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return lazyProperty(this, 'input', val);
|
|
1645
|
+
},
|
|
1646
|
+
get output() {
|
|
1647
|
+
let val = options.output;
|
|
1648
|
+
|
|
1649
|
+
switch (val?.type) {
|
|
1650
|
+
case 'lex': {
|
|
1651
|
+
val = {
|
|
1652
|
+
type: 'lex',
|
|
1653
|
+
schema: val.schema,
|
|
1654
|
+
} as TOutput;
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return lazyProperty(this, 'output', val);
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// #region XRPC query metadata
|
|
1665
|
+
|
|
1666
|
+
export interface XRPCQueryMetadata<
|
|
1667
|
+
TParams extends ObjectSchema | null,
|
|
1668
|
+
TOutput extends XRPCBodyParam,
|
|
1669
|
+
TNsid extends syntax.Nsid,
|
|
1670
|
+
> extends BaseMetadata {
|
|
1671
|
+
readonly type: 'xrpc_query';
|
|
1672
|
+
readonly nsid: TNsid;
|
|
1673
|
+
readonly params: TParams;
|
|
1674
|
+
readonly output: TOutput;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// #__NO_SIDE_EFFECTS__
|
|
1678
|
+
export const query = <
|
|
1679
|
+
TNsid extends syntax.Nsid,
|
|
1680
|
+
TParams extends ObjectSchema | null,
|
|
1681
|
+
TOutput extends XRPCBodyParam,
|
|
1682
|
+
>(
|
|
1683
|
+
nsid: TNsid,
|
|
1684
|
+
options: {
|
|
1685
|
+
params: TParams;
|
|
1686
|
+
output: TOutput;
|
|
1687
|
+
},
|
|
1688
|
+
): XRPCQueryMetadata<TParams, TOutput, TNsid> => {
|
|
1689
|
+
// `schema` can be a getter, and we'd have to resolve that getter.
|
|
1690
|
+
|
|
1691
|
+
return {
|
|
1692
|
+
kind: 'metadata',
|
|
1693
|
+
type: 'xrpc_query',
|
|
1694
|
+
nsid: nsid,
|
|
1695
|
+
params: options.params,
|
|
1696
|
+
get output() {
|
|
1697
|
+
let val = options.output;
|
|
1698
|
+
|
|
1699
|
+
switch (val?.type) {
|
|
1700
|
+
case 'lex': {
|
|
1701
|
+
val = {
|
|
1702
|
+
type: 'lex',
|
|
1703
|
+
schema: val.schema,
|
|
1704
|
+
} as TOutput;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
return lazyProperty(this, 'output', val);
|
|
1709
|
+
},
|
|
1710
|
+
};
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
// #region XRPC subscription metadata
|
|
1714
|
+
|
|
1715
|
+
export interface XRPCSubscriptionMetadata<
|
|
1716
|
+
TParams extends ObjectSchema | null,
|
|
1717
|
+
TMessage extends ObjectSchema<any> | VariantSchema<any, any> | null,
|
|
1718
|
+
TNsid extends syntax.Nsid,
|
|
1719
|
+
> extends BaseMetadata {
|
|
1720
|
+
readonly type: 'xrpc_subscription';
|
|
1721
|
+
readonly nsid: TNsid;
|
|
1722
|
+
readonly params: TParams;
|
|
1723
|
+
readonly message: TMessage;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// #__NO_SIDE_EFFECTS__
|
|
1727
|
+
export const subscription = <
|
|
1728
|
+
TNsid extends syntax.Nsid,
|
|
1729
|
+
TParams extends ObjectSchema | null,
|
|
1730
|
+
TMessage extends ObjectSchema<any> | VariantSchema<any, any> | null,
|
|
1731
|
+
>(
|
|
1732
|
+
nsid: TNsid,
|
|
1733
|
+
options: {
|
|
1734
|
+
params: TParams;
|
|
1735
|
+
readonly message: TMessage;
|
|
1736
|
+
},
|
|
1737
|
+
): XRPCSubscriptionMetadata<TParams, TMessage, TNsid> => {
|
|
1738
|
+
// `message` can be a getter, and we'd have to resolve that getter.
|
|
1739
|
+
|
|
1740
|
+
return {
|
|
1741
|
+
kind: 'metadata',
|
|
1742
|
+
type: 'xrpc_subscription',
|
|
1743
|
+
nsid: nsid,
|
|
1744
|
+
params: options.params,
|
|
1745
|
+
get message() {
|
|
1746
|
+
return lazyProperty(this, 'message', options.message);
|
|
1747
|
+
},
|
|
1748
|
+
};
|
|
1749
|
+
};
|