skir-client 0.0.1
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/README.md +17 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/serializer_tester.d.ts +18 -0
- package/dist/cjs/serializer_tester.d.ts.map +1 -0
- package/dist/cjs/serializer_tester.js +124 -0
- package/dist/cjs/serializer_tester.js.map +1 -0
- package/dist/cjs/skir-client.d.ts +697 -0
- package/dist/cjs/skir-client.d.ts.map +1 -0
- package/dist/cjs/skir-client.js +2392 -0
- package/dist/cjs/skir-client.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/serializer_tester.d.ts +18 -0
- package/dist/esm/serializer_tester.d.ts.map +1 -0
- package/dist/esm/serializer_tester.js +119 -0
- package/dist/esm/serializer_tester.js.map +1 -0
- package/dist/esm/skir-client.d.ts +697 -0
- package/dist/esm/skir-client.d.ts.map +1 -0
- package/dist/esm/skir-client.js +2374 -0
- package/dist/esm/skir-client.js.map +1 -0
- package/package.json +58 -0
- package/src/serializer_tester.ts +185 -0
- package/src/skir-client.ts +3492 -0
|
@@ -0,0 +1,3492 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Express as ExpressApp,
|
|
3
|
+
json as ExpressJson,
|
|
4
|
+
Request as ExpressRequest,
|
|
5
|
+
Response as ExpressResponse,
|
|
6
|
+
text as ExpressText,
|
|
7
|
+
} from "express";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A single moment in time represented in a platform-independent format, with a
|
|
11
|
+
* precision of one millisecond.
|
|
12
|
+
*
|
|
13
|
+
* A `Timestamp` object can represent a maximum of ±8,640,000,000,000,000
|
|
14
|
+
* milliseconds, or ±100,000,000 (one hundred million) days, relative to the
|
|
15
|
+
* Unix epoch. This is the range from April 20, 271821 BC to September 13,
|
|
16
|
+
* 275760 AD.
|
|
17
|
+
*
|
|
18
|
+
* Unlike the Javascript built-in `Date` type, a `Timestamp` is immutable.
|
|
19
|
+
* Like a `Date`, a `Timestamp` object does not contain a timezone.
|
|
20
|
+
*/
|
|
21
|
+
export class Timestamp {
|
|
22
|
+
/**
|
|
23
|
+
* Returns a `Timestamp` representing the same moment in time as the given
|
|
24
|
+
* Javascript `Date` object.
|
|
25
|
+
*
|
|
26
|
+
* @throws if the given `Date` object has a timestamp value of NaN
|
|
27
|
+
*/
|
|
28
|
+
static from(date: Date): Timestamp {
|
|
29
|
+
return this.fromUnixMillis(date.getTime());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a `Timestamp` object from a number of milliseconds from the Unix
|
|
34
|
+
* epoch.
|
|
35
|
+
*
|
|
36
|
+
* If the given number if outside the valid range (±8,640,000,000,000,000),
|
|
37
|
+
* this function will return `Timestamp.MAX` or `Timestamp.MIN` depending on
|
|
38
|
+
* the sign of the number.
|
|
39
|
+
*
|
|
40
|
+
* @throws if the given number is NaN
|
|
41
|
+
*/
|
|
42
|
+
static fromUnixMillis(unixMillis: number): Timestamp {
|
|
43
|
+
if (unixMillis <= this.MIN.unixMillis) {
|
|
44
|
+
return Timestamp.MIN;
|
|
45
|
+
} else if (unixMillis < Timestamp.MAX.unixMillis) {
|
|
46
|
+
return new Timestamp(Math.round(unixMillis));
|
|
47
|
+
} else if (Number.isNaN(unixMillis)) {
|
|
48
|
+
throw new Error("Cannot construct Timestamp from NaN");
|
|
49
|
+
} else {
|
|
50
|
+
return Timestamp.MAX;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a `Timestamp` object from a number of seconds from the Unix epoch.
|
|
56
|
+
*
|
|
57
|
+
* If the given number if outside the valid range (±8,640,000,000,000), this
|
|
58
|
+
* function will return `Timestamp.MAX` or `Timestamp.MIN` depending on the
|
|
59
|
+
* sign of the number.
|
|
60
|
+
*
|
|
61
|
+
* @throws if the given number is NaN
|
|
62
|
+
*/
|
|
63
|
+
static fromUnixSeconds(unixSeconds: number): Timestamp {
|
|
64
|
+
return this.fromUnixMillis(unixSeconds * 1000);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parses a date in the date time string format.
|
|
69
|
+
*
|
|
70
|
+
* @throws if the given string is not a date in the date time string format
|
|
71
|
+
* @see https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format
|
|
72
|
+
*/
|
|
73
|
+
static parse(date: string): Timestamp {
|
|
74
|
+
return this.fromUnixMillis(Date.parse(date));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns a `Timestamp` representing the current moment in time. */
|
|
78
|
+
static now(): Timestamp {
|
|
79
|
+
return this.fromUnixMillis(Date.now());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Thursday, 1 January 1970. */
|
|
83
|
+
static readonly UNIX_EPOCH = new Timestamp(0);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Earliest moment in time representable as a `Timestamp`, namely April 20,
|
|
87
|
+
* 271821 BC.
|
|
88
|
+
*
|
|
89
|
+
* @see https://262.ecma-international.org/5.1/#sec-15.9.1.1
|
|
90
|
+
*/
|
|
91
|
+
static readonly MIN = new Timestamp(-8640000000000000);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Latest moment in time representable as a `Timestamp`, namely September 13,
|
|
95
|
+
* 275760 AD.
|
|
96
|
+
*
|
|
97
|
+
* @see https://262.ecma-international.org/5.1/#sec-15.9.1.1
|
|
98
|
+
*/
|
|
99
|
+
static readonly MAX = new Timestamp(8640000000000000);
|
|
100
|
+
|
|
101
|
+
private constructor(
|
|
102
|
+
/** Number of milliseconds ellapsed since the Unix epoch. */
|
|
103
|
+
readonly unixMillis: number,
|
|
104
|
+
) {
|
|
105
|
+
Object.freeze(this);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Number of seconds ellapsed since the Unix epoch. */
|
|
109
|
+
get unixSeconds(): number {
|
|
110
|
+
return this.unixMillis / 1000.0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns a `Date` object representing the same moment in time as this
|
|
115
|
+
* `Timestamp`.
|
|
116
|
+
*/
|
|
117
|
+
toDate(): Date {
|
|
118
|
+
return new Date(this.unixMillis);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
toString(): string {
|
|
122
|
+
return this.toDate().toISOString();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An immutable array of bytes. */
|
|
127
|
+
export class ByteString {
|
|
128
|
+
/**
|
|
129
|
+
* Returns an immutable byte string containing all the bytes of `input` from
|
|
130
|
+
* `start`, inclusive, up to `end`, exclusive.
|
|
131
|
+
*
|
|
132
|
+
* If `input` is an `ArrayBuffer`, this function copies the bytes. Otherwise
|
|
133
|
+
* this function returns a sliced view of the input `ByteString`.
|
|
134
|
+
*
|
|
135
|
+
* @example <caption>Copy an array buffer into a byte string</caption>
|
|
136
|
+
* const byteString = ByteString.sliceOf(arrayBuffer);
|
|
137
|
+
*/
|
|
138
|
+
static sliceOf(
|
|
139
|
+
input: ArrayBuffer | SharedArrayBuffer | ByteString,
|
|
140
|
+
start = 0,
|
|
141
|
+
end?: number,
|
|
142
|
+
): ByteString {
|
|
143
|
+
const { byteLength } = input;
|
|
144
|
+
if (start < 0) {
|
|
145
|
+
start = 0;
|
|
146
|
+
}
|
|
147
|
+
if (end === undefined || end > byteLength) {
|
|
148
|
+
end = byteLength;
|
|
149
|
+
}
|
|
150
|
+
if (end <= start) {
|
|
151
|
+
return ByteString.EMPTY;
|
|
152
|
+
}
|
|
153
|
+
if (input instanceof ByteString) {
|
|
154
|
+
if (start <= 0 && byteLength <= end) {
|
|
155
|
+
// Don't copy the ByteString itself.
|
|
156
|
+
return input;
|
|
157
|
+
} else {
|
|
158
|
+
// Don't copy the ArrayBuffer.
|
|
159
|
+
const newByteOffset = input.byteOffset + start;
|
|
160
|
+
const newByteLength = end - start;
|
|
161
|
+
return new ByteString(input.arrayBuffer, newByteOffset, newByteLength);
|
|
162
|
+
}
|
|
163
|
+
} else if (input instanceof ArrayBuffer) {
|
|
164
|
+
return new ByteString(input.slice(start, end));
|
|
165
|
+
} else if (input instanceof SharedArrayBuffer) {
|
|
166
|
+
const slice = input.slice(start, end);
|
|
167
|
+
const newBuffer = new ArrayBuffer(slice.byteLength);
|
|
168
|
+
new Uint8Array(newBuffer).set(new Uint8Array(slice));
|
|
169
|
+
return new ByteString(newBuffer);
|
|
170
|
+
} else {
|
|
171
|
+
const _: never = input;
|
|
172
|
+
throw new TypeError(_);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Decodes a Base64 string, which can be obtained by calling `toBase64()`.
|
|
178
|
+
*
|
|
179
|
+
* @throws if the given string is not a valid Base64 string.
|
|
180
|
+
* @see https://en.wikipedia.org/wiki/Base64
|
|
181
|
+
*/
|
|
182
|
+
static fromBase64(base64: string): ByteString {
|
|
183
|
+
// See https://developer.mozilla.org/en-US/docs/Glossary/Base64
|
|
184
|
+
const binaryString: string = atob(base64);
|
|
185
|
+
const array = Uint8Array.from(binaryString, (m) => m.codePointAt(0)!);
|
|
186
|
+
return new this(array.buffer);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Decodes a hexadecimal string, which can be obtained by calling
|
|
191
|
+
* `toBase16()`.
|
|
192
|
+
*
|
|
193
|
+
* @throws if the given string is not a valid Base64 string.
|
|
194
|
+
*/
|
|
195
|
+
static fromBase16(base16: string): ByteString {
|
|
196
|
+
const bytes = new Uint8Array(base16.length / 2);
|
|
197
|
+
for (let i = 0; i < bytes.length; ++i) {
|
|
198
|
+
const byte = parseInt(base16.substring(i * 2, i * 2 + 2), 16);
|
|
199
|
+
if (Number.isNaN(byte)) {
|
|
200
|
+
throw new Error("Not a valid Base64 string");
|
|
201
|
+
}
|
|
202
|
+
bytes[i] = byte;
|
|
203
|
+
}
|
|
204
|
+
return new ByteString(bytes.buffer);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** An empty byte string. */
|
|
208
|
+
static readonly EMPTY = new ByteString(new ArrayBuffer(0));
|
|
209
|
+
|
|
210
|
+
/** Copies the contents of this byte string into the given array buffer. */
|
|
211
|
+
copyTo(target: ArrayBuffer, targetOffset = 0): void {
|
|
212
|
+
new Uint8Array(target).set(this.uint8Array, targetOffset);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Copies the contents of this byte string into a new array buffer. */
|
|
216
|
+
toBuffer(): ArrayBuffer {
|
|
217
|
+
return this.arrayBuffer.slice(
|
|
218
|
+
this.byteOffset,
|
|
219
|
+
this.byteOffset + this.byteLength,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Encodes this byte string into a Base64 string.
|
|
225
|
+
*
|
|
226
|
+
* @see https://en.wikipedia.org/wiki/Base64
|
|
227
|
+
*/
|
|
228
|
+
toBase64(): string {
|
|
229
|
+
// See https://developer.mozilla.org/en-US/docs/Glossary/Base64
|
|
230
|
+
const binaryString = Array.from(this.uint8Array, (x) =>
|
|
231
|
+
String.fromCodePoint(x),
|
|
232
|
+
).join("");
|
|
233
|
+
return btoa(binaryString);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Encodes this byte string into a hexadecimal string. */
|
|
237
|
+
toBase16(): string {
|
|
238
|
+
return [...this.uint8Array]
|
|
239
|
+
.map((x) => x.toString(16).padStart(2, "0"))
|
|
240
|
+
.join("");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
at(index: number): number | undefined {
|
|
244
|
+
return this.uint8Array[index < 0 ? index + this.byteLength : index];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
toString(): string {
|
|
248
|
+
return `ByteString(${this.byteLength})`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private constructor(
|
|
252
|
+
private readonly arrayBuffer: ArrayBuffer,
|
|
253
|
+
private readonly byteOffset = 0,
|
|
254
|
+
/** The length of this byte string. */
|
|
255
|
+
readonly byteLength = arrayBuffer.byteLength,
|
|
256
|
+
) {
|
|
257
|
+
this.uint8Array = new Uint8Array(arrayBuffer, byteOffset, byteLength);
|
|
258
|
+
Object.freeze(this);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private readonly uint8Array: Uint8Array;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A read-only JSON value. */
|
|
265
|
+
export type Json =
|
|
266
|
+
| null
|
|
267
|
+
| boolean
|
|
268
|
+
| number
|
|
269
|
+
| string
|
|
270
|
+
| readonly Json[]
|
|
271
|
+
| Readonly<{ [name: string]: Json }>;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolves to the generated mutable class for a struct.
|
|
275
|
+
* The type parameter is the generated frozen class.
|
|
276
|
+
*/
|
|
277
|
+
export type MutableForm<Frozen> = //
|
|
278
|
+
Frozen extends _FrozenBase
|
|
279
|
+
? ReturnType<Frozen["toMutable"]> & Freezable<Frozen>
|
|
280
|
+
: Freezable<Frozen>;
|
|
281
|
+
|
|
282
|
+
/** Result of encoding a struct using binary encoding format. */
|
|
283
|
+
export interface BinaryForm {
|
|
284
|
+
/** Length (in bytes) of the binary form. */
|
|
285
|
+
readonly byteLength: number;
|
|
286
|
+
/** Copies the contents of the binary form into the given array buffer. */
|
|
287
|
+
copyTo(target: ArrayBuffer, offset?: number): void;
|
|
288
|
+
/** Copies the contents of this byte string into a new array buffer. */
|
|
289
|
+
toBuffer(): ArrayBuffer;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* When using the JSON serialization format, you can chose between two flavors.
|
|
294
|
+
*
|
|
295
|
+
* The dense flavor is the default flavor and is preferred in most cases.
|
|
296
|
+
* Structs are converted to JSON arrays, where the number of each field
|
|
297
|
+
* corresponds to the index of the value in the array. This results in a more
|
|
298
|
+
* compact representation than when using JSON objects, and this makes
|
|
299
|
+
* serialization and deserialization a bit faster. Because field names are left
|
|
300
|
+
* out of the JSON, it is a representation which allows persistence: you can
|
|
301
|
+
* safely rename a field in a `.skir` file without breaking backwards
|
|
302
|
+
* compatibility.
|
|
303
|
+
* One con of this representation is that it is harder to tell, just by looking
|
|
304
|
+
* at the JSON, what field of the struct each value in the array corresponds to.
|
|
305
|
+
*
|
|
306
|
+
* When using the readable flavor, structs are converted to JSON objects. The
|
|
307
|
+
* name of each field in the `.skir` file is used as-is in the JSON. This
|
|
308
|
+
* results in a representation which is much more readable by humans, but also
|
|
309
|
+
* not suited for persistence: when you rename a field in a `.skir` file, you
|
|
310
|
+
* will no lonnger be able to deserialize old JSONs.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* const jane = Person.create({firstName: "Jane", lastName: "Doe"});
|
|
314
|
+
*
|
|
315
|
+
* console.log(Person.serializer.toJson(jane, "dense"));
|
|
316
|
+
* // Output: ["Jane","Doe"]
|
|
317
|
+
*
|
|
318
|
+
* console.log(Person.serializer.toJson(jane));
|
|
319
|
+
* // Output: ["Jane","Doe"]
|
|
320
|
+
*
|
|
321
|
+
* console.log(Person.serializer.toJson(jane, "readable"));
|
|
322
|
+
* // Output: {
|
|
323
|
+
* // "firstName": "Jane",
|
|
324
|
+
* // "lastName": "Doe"
|
|
325
|
+
* // }
|
|
326
|
+
*/
|
|
327
|
+
export type JsonFlavor = "dense" | "readable";
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Serializes and deserializes instances of `T`. Supports two serialization
|
|
331
|
+
* formats: JSON and binary.
|
|
332
|
+
*
|
|
333
|
+
* All deserialization methods return a deeply-immutable `T`. If `T` is the
|
|
334
|
+
* generated frozen class for a struct, all serialization methods accept either
|
|
335
|
+
* a `T` or a `T.Mutable`.
|
|
336
|
+
*
|
|
337
|
+
* Do NOT create your own `Serializer` implementation. Only use implementations
|
|
338
|
+
* provided by Skir.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* let jane = Person.create({firstName: "Jane", lastName: "Doe"});
|
|
342
|
+
* const json = Person.serializer.toJson(jane);
|
|
343
|
+
* jane = Person.serializer.fromJson(json);
|
|
344
|
+
* expect(jane.firstName).toBe("Jane");
|
|
345
|
+
*/
|
|
346
|
+
export interface Serializer<T> {
|
|
347
|
+
/**
|
|
348
|
+
* Converts back the given stringified JSON to `T`.
|
|
349
|
+
* Works with both [flavors]{@link JsonFlavor} of JSON.
|
|
350
|
+
*
|
|
351
|
+
* Pass in "keep-unrecognized-values" if and only if the input JSON comes
|
|
352
|
+
* from a trusted program which might have been built from more recent
|
|
353
|
+
* source files.
|
|
354
|
+
*/
|
|
355
|
+
fromJsonCode(code: string, keep?: "keep-unrecognized-values"): T;
|
|
356
|
+
/**
|
|
357
|
+
* Converts back the given JSON to `T`.
|
|
358
|
+
* Works with both [flavors]{@link JsonFlavor} of JSON.
|
|
359
|
+
*
|
|
360
|
+
* Pass in "keep-unrecognized-values" if and only if the input JSON comes
|
|
361
|
+
* from a trusted program which might have been built from more recent
|
|
362
|
+
* source files.
|
|
363
|
+
*/
|
|
364
|
+
fromJson(json: Json, keep?: "keep-unrecognized-values"): T;
|
|
365
|
+
/**
|
|
366
|
+
* Converts back the given binary form to `T`.
|
|
367
|
+
*
|
|
368
|
+
* Pass in "keep-unrecognized-values" if and only if the input JSON comes
|
|
369
|
+
* from a trusted program which might have been built from more recent
|
|
370
|
+
* source files.
|
|
371
|
+
*/
|
|
372
|
+
fromBytes(bytes: ArrayBuffer, keep?: "keep-unrecognized-values"): T;
|
|
373
|
+
/**
|
|
374
|
+
* Converts the given `T` to JSON and returns the stringified JSON. Same as
|
|
375
|
+
* calling `JSON.stringify()` on the result of `toJson()`.
|
|
376
|
+
*
|
|
377
|
+
* @param flavor dense or readable, defaults to dense
|
|
378
|
+
* @see JsonFlavor
|
|
379
|
+
*/
|
|
380
|
+
toJsonCode(input: T | MutableForm<T>, flavor?: JsonFlavor): string;
|
|
381
|
+
/**
|
|
382
|
+
* Converts the given `T` to JSON. If you only need the stringified JSON, call
|
|
383
|
+
* `toJsonCode()` instead.
|
|
384
|
+
*
|
|
385
|
+
* @param flavor dense or readable, defaults to dense
|
|
386
|
+
* @see JsonFlavor
|
|
387
|
+
*/
|
|
388
|
+
toJson(input: T | MutableForm<T>, flavor?: JsonFlavor): Json;
|
|
389
|
+
/** Converts the given `T` to binary format. */
|
|
390
|
+
toBytes(input: T | MutableForm<T>): BinaryForm;
|
|
391
|
+
/** An object describing the type `T`. Enables reflective programming. */
|
|
392
|
+
typeDescriptor: TypeDescriptorSpecialization<T>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Returns a serializer of instances of the given Skir primitive type.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* expect(
|
|
400
|
+
* primitiveSerializer("string").toJsonCode("foo")
|
|
401
|
+
* ).toBe(
|
|
402
|
+
* '"foo"'
|
|
403
|
+
* );
|
|
404
|
+
*/
|
|
405
|
+
export function primitiveSerializer<P extends keyof PrimitiveTypes>(
|
|
406
|
+
primitiveType: P,
|
|
407
|
+
): Serializer<PrimitiveTypes[P]> {
|
|
408
|
+
return primitiveSerializers[primitiveType];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Returns a serializer of arrays of `Item`s.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* expect(
|
|
416
|
+
* arraySerializer(User.serializer).toJsonCode([JANE, JOE])
|
|
417
|
+
* ).toBe(
|
|
418
|
+
* '[["jane"],["joe"]]'
|
|
419
|
+
* );
|
|
420
|
+
*/
|
|
421
|
+
export function arraySerializer<Item>(
|
|
422
|
+
item: Serializer<Item>,
|
|
423
|
+
keyChain?: string,
|
|
424
|
+
): Serializer<ReadonlyArray<Item>> {
|
|
425
|
+
if (
|
|
426
|
+
keyChain !== undefined &&
|
|
427
|
+
!/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/.test(keyChain)
|
|
428
|
+
) {
|
|
429
|
+
throw new Error(`Invalid keyChain "${keyChain}"`);
|
|
430
|
+
}
|
|
431
|
+
return new ArraySerializerImpl(item as InternalSerializer<Item>, keyChain);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Returns a serializer of nullable `T`s. */
|
|
435
|
+
export function optionalSerializer<T>(
|
|
436
|
+
other: Serializer<T>,
|
|
437
|
+
): Serializer<T | null> {
|
|
438
|
+
return other instanceof OptionalSerializerImpl
|
|
439
|
+
? other
|
|
440
|
+
: new OptionalSerializerImpl(other as InternalSerializer<T>);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Describes the type `T`, where `T` is the TypeScript equivalent of a Skir
|
|
445
|
+
* type. Enables reflective programming.
|
|
446
|
+
*
|
|
447
|
+
* Every `TypeDescriptor` instance has a `kind` field which can take one of
|
|
448
|
+
* these 5 values: `"primitive"`, `"optional"`, `"array"`, `"struct"`, `"enum"`.
|
|
449
|
+
*/
|
|
450
|
+
export type TypeDescriptor<T = unknown> =
|
|
451
|
+
| OptionalDescriptor<T>
|
|
452
|
+
| ArrayDescriptor<T>
|
|
453
|
+
| StructDescriptor<T>
|
|
454
|
+
| EnumDescriptor<T>
|
|
455
|
+
| PrimitiveDescriptor;
|
|
456
|
+
|
|
457
|
+
/** Specialization of `TypeDescriptor<T>` when `T` is known. */
|
|
458
|
+
export type TypeDescriptorSpecialization<T> = //
|
|
459
|
+
[T] extends [_FrozenBase]
|
|
460
|
+
? StructDescriptor<T>
|
|
461
|
+
: [T] extends [_EnumBase]
|
|
462
|
+
? EnumDescriptor<T>
|
|
463
|
+
: TypeDescriptor<T>;
|
|
464
|
+
|
|
465
|
+
interface TypeDescriptorBase {
|
|
466
|
+
/** Returns the JSON representation of this `TypeDescriptor`. */
|
|
467
|
+
asJson(): Json;
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Returns the JSON code representation of this `TypeDescriptor`.
|
|
471
|
+
* Same as calling `JSON.stringify()` on the result of `asJson()`.
|
|
472
|
+
*/
|
|
473
|
+
asJsonCode(): string;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Converts from one serialized form to another.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* const denseJson = User.serializer.toJson(user, "dense");
|
|
480
|
+
* expect(
|
|
481
|
+
* User.serializer.typeDescriptor.transform(denseJson, "readable")
|
|
482
|
+
* ).toMatch(
|
|
483
|
+
* User.serializer.toJson(user, "readable")
|
|
484
|
+
* );
|
|
485
|
+
*/
|
|
486
|
+
transform(json_or_bytes: Json | ArrayBuffer, out: JsonFlavor): Json;
|
|
487
|
+
transform(json: Json, out: "bytes"): ArrayBuffer;
|
|
488
|
+
transform(
|
|
489
|
+
json_or_bytes: Json | ArrayBuffer,
|
|
490
|
+
out: JsonFlavor | "bytes",
|
|
491
|
+
): Json | ArrayBuffer;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Describes a primitive Skir type. */
|
|
495
|
+
export interface PrimitiveDescriptor extends TypeDescriptorBase {
|
|
496
|
+
kind: "primitive";
|
|
497
|
+
primitive: keyof PrimitiveTypes;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* An interface mapping a primitive Skir type to the corresponding TypeScript
|
|
502
|
+
* type.
|
|
503
|
+
*/
|
|
504
|
+
export interface PrimitiveTypes {
|
|
505
|
+
bool: boolean;
|
|
506
|
+
int32: number;
|
|
507
|
+
int64: bigint;
|
|
508
|
+
uint64: bigint;
|
|
509
|
+
float32: number;
|
|
510
|
+
float64: number;
|
|
511
|
+
timestamp: Timestamp;
|
|
512
|
+
string: string;
|
|
513
|
+
bytes: ByteString;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Describes an optional type. In a `.skir` file, an optional type is
|
|
518
|
+
* represented with a question mark at the end of another type.
|
|
519
|
+
*/
|
|
520
|
+
export interface OptionalDescriptor<T> extends TypeDescriptorBase {
|
|
521
|
+
readonly kind: "optional";
|
|
522
|
+
/** Describes the other (non-optional) type. */
|
|
523
|
+
readonly otherType: TypeDescriptor<NonNullable<T>>;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Describes an array type. */
|
|
527
|
+
export interface ArrayDescriptor<T> extends TypeDescriptorBase {
|
|
528
|
+
readonly kind: "array";
|
|
529
|
+
/** Describes the type of the array items. */
|
|
530
|
+
readonly itemType: TypeDescriptor<
|
|
531
|
+
T extends ReadonlyArray<infer Item> ? Item : unknown
|
|
532
|
+
>;
|
|
533
|
+
readonly keyChain?: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Describes a Skir struct.
|
|
538
|
+
* The type parameter `T` refers to the generated frozen class for the struct.
|
|
539
|
+
*/
|
|
540
|
+
export interface StructDescriptor<T = unknown> extends TypeDescriptorBase {
|
|
541
|
+
readonly kind: "struct";
|
|
542
|
+
/** Name of the struct as specified in the `.skir` file. */
|
|
543
|
+
readonly name: string;
|
|
544
|
+
/**
|
|
545
|
+
* A string containing all the names in the hierarchic sequence above and
|
|
546
|
+
* including the struct. For example: "Foo.Bar" if "Bar" is nested within a
|
|
547
|
+
* type called "Foo", or simply "Bar" if "Bar" is defined at the top-level of
|
|
548
|
+
* the module.
|
|
549
|
+
*/
|
|
550
|
+
readonly qualifiedName: string;
|
|
551
|
+
/**
|
|
552
|
+
* Path to the module where the struct is defined, relative to the root of the
|
|
553
|
+
* project.
|
|
554
|
+
*/
|
|
555
|
+
readonly modulePath: string;
|
|
556
|
+
/**
|
|
557
|
+
* If the struct is nested within another type, the descriptor for that type.
|
|
558
|
+
* Undefined if the struct is defined at the top-level of the module.
|
|
559
|
+
*/
|
|
560
|
+
readonly parentType: StructDescriptor | EnumDescriptor | undefined;
|
|
561
|
+
/** The fields of the struct in the order they appear in the `.skir` file. */
|
|
562
|
+
readonly fields: ReadonlyArray<StructField<T>>;
|
|
563
|
+
/** The field numbers marked as removed. */
|
|
564
|
+
readonly removedNumbers: ReadonlySet<number>;
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Looks up a field. The key can be one of: the field name (e.g. "user_id");
|
|
568
|
+
* the name of the property in the generated class (e.g. "userId"), the field
|
|
569
|
+
* number.
|
|
570
|
+
*
|
|
571
|
+
* The return type is `StructField<T> | undefined` unless the key is known at
|
|
572
|
+
* compile-time to be the name of the property in the generated class, in
|
|
573
|
+
* which case it is `StructField<T>`.
|
|
574
|
+
*/
|
|
575
|
+
getField<K extends string | number>(key: K): StructFieldResult<T, K>;
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Returns a new instance of the generated mutable class for a struct.
|
|
579
|
+
* Performs a shallow copy of `initializer` if `initializer` is specified.
|
|
580
|
+
*/
|
|
581
|
+
newMutable(initializer?: T | MutableForm<T>): MutableForm<T>;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Field of a Skir struct. */
|
|
585
|
+
export interface StructField<Struct = unknown, Value = unknown> {
|
|
586
|
+
/** Field name as specified in the `.skir` file, e.g. "user_id". */
|
|
587
|
+
readonly name: string;
|
|
588
|
+
/** Name of the property in the generated class, e.g. "userId". */
|
|
589
|
+
readonly property: string;
|
|
590
|
+
/** Field number. */
|
|
591
|
+
readonly number: number;
|
|
592
|
+
/** Describes the field type. */
|
|
593
|
+
readonly type: TypeDescriptor<Value>;
|
|
594
|
+
|
|
595
|
+
/** Extracts the value of the field from the given struct. */
|
|
596
|
+
get(struct: Struct | MutableForm<Struct>): Value;
|
|
597
|
+
/** Assigns the given value to the field of the given struct. */
|
|
598
|
+
set(struct: MutableForm<Struct>, value: Value): void;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Return type of the `StructDescriptor.getField` method. If the argument is
|
|
603
|
+
* known at compile-time to be the name of a property of the generated class,
|
|
604
|
+
* resolves to `StructField<Struct>`. Otherwise, resolves to
|
|
605
|
+
* `StructField<Struct> | undefined`.
|
|
606
|
+
*
|
|
607
|
+
* @example <caption>The field is kown at compile-time</caption>
|
|
608
|
+
* const fieldNumber: number =
|
|
609
|
+
* User.serializer.typeDescriptor.getField("userId").number;
|
|
610
|
+
*
|
|
611
|
+
* @example <caption>The field is not kown at compile-time</caption>
|
|
612
|
+
* const fieldNumber: number | undefined =
|
|
613
|
+
* User.serializer.typeDescriptor.getField(variable)?.number;
|
|
614
|
+
*/
|
|
615
|
+
export type StructFieldResult<Struct, Key extends string | number> =
|
|
616
|
+
| StructField<Struct>
|
|
617
|
+
| (Struct extends _FrozenBase
|
|
618
|
+
? Key extends keyof NonNullable<Struct[typeof _INITIALIZER]>
|
|
619
|
+
? never
|
|
620
|
+
: undefined
|
|
621
|
+
: undefined);
|
|
622
|
+
|
|
623
|
+
/** Describes a Skir enum. */
|
|
624
|
+
export interface EnumDescriptor<T = unknown> extends TypeDescriptorBase {
|
|
625
|
+
readonly kind: "enum";
|
|
626
|
+
/** Name of the enum as specified in the `.skir` file. */
|
|
627
|
+
readonly name: string;
|
|
628
|
+
/**
|
|
629
|
+
* A string containing all the names in the hierarchic sequence above and
|
|
630
|
+
* including the enum. For example: "Foo.Bar" if "Bar" is nested within a type
|
|
631
|
+
* called "Foo", or simply "Bar" if "Bar" is defined at the top-level of the
|
|
632
|
+
* module.
|
|
633
|
+
*/
|
|
634
|
+
readonly qualifiedName: string;
|
|
635
|
+
/**
|
|
636
|
+
* Path to the module where the enum is defined, relative to the root of the
|
|
637
|
+
* project.
|
|
638
|
+
*/
|
|
639
|
+
readonly modulePath: string;
|
|
640
|
+
/**
|
|
641
|
+
* If the enum is nested within another type, the descriptor for that type.
|
|
642
|
+
* Undefined if the struct is defined at the top-level of the module.
|
|
643
|
+
*/
|
|
644
|
+
readonly parentType: StructDescriptor | EnumDescriptor | undefined;
|
|
645
|
+
/**
|
|
646
|
+
* Includes the UNKNOWN variant, followed by the other variants in the order
|
|
647
|
+
* they appear in the `.skir` file.
|
|
648
|
+
*/
|
|
649
|
+
readonly variants: ReadonlyArray<EnumVariant<T>>;
|
|
650
|
+
/** The variant numbers marked as removed. */
|
|
651
|
+
readonly removedNumbers: ReadonlySet<number>;
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Looks up a variant. The key can be one of the variant name or the variant
|
|
655
|
+
* number.
|
|
656
|
+
*
|
|
657
|
+
* The return type is `EnumVariant<T> | undefined` unless the key is known at
|
|
658
|
+
* compile-time to be a variant name of the enum, in which case it is
|
|
659
|
+
* `EnumVariant<T>`.
|
|
660
|
+
*/
|
|
661
|
+
getVariant<K extends string | number>(key: K): EnumVariantResult<T, K>;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Variant of a Skir enum. Variants which don't hold any value are called
|
|
666
|
+
* constant variants. Their name is always in UPPER_CASE. Variants which hold
|
|
667
|
+
* value of a given type are called wrapper variants, and their name is always
|
|
668
|
+
* in lower_case.
|
|
669
|
+
*/
|
|
670
|
+
export type EnumVariant<Enum = unknown> =
|
|
671
|
+
| EnumConstantVariant<Enum>
|
|
672
|
+
| EnumWrapperVariant<Enum, unknown>;
|
|
673
|
+
|
|
674
|
+
/** Field of a Skir enum which does not hold any value. */
|
|
675
|
+
export interface EnumConstantVariant<Enum = unknown> {
|
|
676
|
+
/**
|
|
677
|
+
* Variant name as specified in the `.skir` file, e.g. "MONDAY".
|
|
678
|
+
* Always in UPPER_CASE format.
|
|
679
|
+
*/
|
|
680
|
+
readonly name: string;
|
|
681
|
+
/** Variant number. */
|
|
682
|
+
readonly number: number;
|
|
683
|
+
/** The instance of the generated class which corresponds to this field. */
|
|
684
|
+
readonly constant: Enum;
|
|
685
|
+
/** Always undefined, unlike the `type` field of `EnumWrapperVariant`. */
|
|
686
|
+
readonly type?: undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Variant of a Skir enum which holds a value of a given type. */
|
|
690
|
+
export interface EnumWrapperVariant<Enum = unknown, Value = unknown> {
|
|
691
|
+
/**
|
|
692
|
+
* Variant name as specified in the `.skir` file, e.g. "v4".
|
|
693
|
+
* Always in lower_case format.
|
|
694
|
+
*/
|
|
695
|
+
readonly name: string;
|
|
696
|
+
/** Variant number. */
|
|
697
|
+
readonly number: number;
|
|
698
|
+
/** Describes the type of the value held by the field. */
|
|
699
|
+
readonly type: TypeDescriptor<Value>;
|
|
700
|
+
/** Always undefined, unlike the `type` field of `EnumConstantVariant`. */
|
|
701
|
+
readonly constant?: undefined;
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Extracts the value held by the given enum instance if it matches this
|
|
705
|
+
* enum variant. Returns undefined otherwise.
|
|
706
|
+
*/
|
|
707
|
+
get(e: Enum): Value | unknown;
|
|
708
|
+
/**
|
|
709
|
+
* Returns a new enum instance matching this enum variant and holding the
|
|
710
|
+
* given value.
|
|
711
|
+
*/
|
|
712
|
+
wrap(value: Value): Enum;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Return type of the `EnumDescriptor.getVariant` method. If the argument is
|
|
717
|
+
* known at compile-time to be the name of variant, resolves to
|
|
718
|
+
* `EnumVariant<Enum>`. Otherwise, resolves to
|
|
719
|
+
* `EnumVariant<Struct> | undefined`.
|
|
720
|
+
*
|
|
721
|
+
* @example <caption>The variant is known at compile-time</caption>
|
|
722
|
+
* const variantNumber: number =
|
|
723
|
+
* Weekday.serializer.typeDescriptor.getVariant("MONDAY").number;
|
|
724
|
+
*
|
|
725
|
+
* @example <caption>The variant is not known at compile-time</caption>
|
|
726
|
+
* const variantNumber: number | undefined =
|
|
727
|
+
* Weekday.serializer.typeDescriptor.getVariant(variable)?.number;
|
|
728
|
+
*/
|
|
729
|
+
export type EnumVariantResult<Enum, Key extends string | number> =
|
|
730
|
+
| EnumVariant<Enum>
|
|
731
|
+
| (Enum extends _EnumBase
|
|
732
|
+
? Key extends Enum["kind"]
|
|
733
|
+
? never
|
|
734
|
+
: undefined
|
|
735
|
+
: undefined);
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Identifies a procedure (the "P" in "RPC") on both the client side and the
|
|
739
|
+
* server side.
|
|
740
|
+
*/
|
|
741
|
+
export interface Method<Request, Response> {
|
|
742
|
+
/** Name of the procedure as specified in the `.skir` file. */
|
|
743
|
+
name: string;
|
|
744
|
+
/**
|
|
745
|
+
* A number which uniquely identifies this procedure.
|
|
746
|
+
* When it is not specified in the `.skir` file, it is obtained by hashing the
|
|
747
|
+
* procedure name.
|
|
748
|
+
*/
|
|
749
|
+
number: number;
|
|
750
|
+
/** Serializer of request objects. */
|
|
751
|
+
requestSerializer: Serializer<Request>;
|
|
752
|
+
/** Serializer of response objects. */
|
|
753
|
+
responseSerializer: Serializer<Response>;
|
|
754
|
+
/**
|
|
755
|
+
* Documentation for this procedure specified as doc comments in the `.skir`
|
|
756
|
+
* file.
|
|
757
|
+
*/
|
|
758
|
+
doc: string;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Interface implemented by both the frozen and mutable classes generated for a
|
|
763
|
+
* struct. `T` is always the generated frozen class.
|
|
764
|
+
*/
|
|
765
|
+
export interface Freezable<T> {
|
|
766
|
+
/**
|
|
767
|
+
* Returns a deeply-immutable object, either by making a copy of `this` if
|
|
768
|
+
* `this` is mutable, or by returning `this` as-is if `this` is already
|
|
769
|
+
* immutable.
|
|
770
|
+
*/
|
|
771
|
+
toFrozen(): T;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// =============================================================================
|
|
775
|
+
// Implementation of serializers and type descriptors
|
|
776
|
+
// =============================================================================
|
|
777
|
+
|
|
778
|
+
/** JSON representation of a `TypeDescriptor`. */
|
|
779
|
+
type TypeDefinition = {
|
|
780
|
+
type: TypeSignature;
|
|
781
|
+
records: readonly RecordDefinition[];
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
/** A type in the JSON representation of a `TypeDescriptor`. */
|
|
785
|
+
type TypeSignature =
|
|
786
|
+
| {
|
|
787
|
+
kind: "optional";
|
|
788
|
+
value: TypeSignature;
|
|
789
|
+
}
|
|
790
|
+
| {
|
|
791
|
+
kind: "array";
|
|
792
|
+
value: {
|
|
793
|
+
item: TypeSignature;
|
|
794
|
+
key_extractor?: string;
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
| {
|
|
798
|
+
kind: "record";
|
|
799
|
+
value: string;
|
|
800
|
+
}
|
|
801
|
+
| {
|
|
802
|
+
kind: "primitive";
|
|
803
|
+
value: keyof PrimitiveTypes;
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Definition of a struct field in the JSON representation of a
|
|
808
|
+
* `TypeDescriptor`.
|
|
809
|
+
*/
|
|
810
|
+
type FieldDefinition = {
|
|
811
|
+
name: string;
|
|
812
|
+
type: TypeSignature;
|
|
813
|
+
number: number;
|
|
814
|
+
doc?: string;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Definition of an enum variant in the JSON representation of a
|
|
819
|
+
* `TypeDescriptor`.
|
|
820
|
+
*/
|
|
821
|
+
type VariantDefinition = {
|
|
822
|
+
name: string;
|
|
823
|
+
type?: TypeSignature;
|
|
824
|
+
number: number;
|
|
825
|
+
doc?: string;
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
/** Definition of a struct in the JSON representation of a `TypeDescriptor`. */
|
|
829
|
+
type StructDefinition = {
|
|
830
|
+
kind: "struct";
|
|
831
|
+
id: string;
|
|
832
|
+
doc?: string;
|
|
833
|
+
fields: readonly FieldDefinition[];
|
|
834
|
+
removed_numbers?: readonly number[];
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
/** Definition of an enum in the JSON representation of a `TypeDescriptor`. */
|
|
838
|
+
type EnumDefinition = {
|
|
839
|
+
kind: "enum";
|
|
840
|
+
id: string;
|
|
841
|
+
doc?: string;
|
|
842
|
+
variants: readonly VariantDefinition[];
|
|
843
|
+
removed_numbers?: readonly number[];
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
type RecordDefinition = StructDefinition | EnumDefinition;
|
|
847
|
+
|
|
848
|
+
interface InternalSerializer<T = unknown> extends Serializer<T> {
|
|
849
|
+
readonly defaultValue: T;
|
|
850
|
+
isDefault(input: T): boolean;
|
|
851
|
+
decode(stream: InputStream): T;
|
|
852
|
+
encode(input: T, stream: OutputStream): void;
|
|
853
|
+
readonly typeSignature: TypeSignature;
|
|
854
|
+
addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Parameter of the {@link InternalSerializer.decode} method. */
|
|
858
|
+
class InputStream {
|
|
859
|
+
constructor(
|
|
860
|
+
readonly buffer: ArrayBuffer,
|
|
861
|
+
keep?: "keep-unrecognized-values",
|
|
862
|
+
) {
|
|
863
|
+
this.dataView = new DataView(buffer);
|
|
864
|
+
this.keepUnrecognizedValues = !!keep;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
readonly dataView: DataView;
|
|
868
|
+
readonly keepUnrecognizedValues: boolean;
|
|
869
|
+
offset = 0;
|
|
870
|
+
|
|
871
|
+
readUint8(): number {
|
|
872
|
+
return this.dataView.getUint8(this.offset++);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
type DecodeNumberFn = (stream: InputStream) => number | bigint;
|
|
877
|
+
|
|
878
|
+
// For wires [232, 241]
|
|
879
|
+
const DECODE_NUMBER_FNS: readonly DecodeNumberFn[] = [
|
|
880
|
+
(s: InputStream): number => s.dataView.getUint16((s.offset += 2) - 2, true),
|
|
881
|
+
(s: InputStream): number => s.dataView.getUint32((s.offset += 4) - 4, true),
|
|
882
|
+
(s: InputStream): bigint =>
|
|
883
|
+
s.dataView.getBigUint64((s.offset += 8) - 8, true),
|
|
884
|
+
(stream: InputStream): number => stream.readUint8() - 256,
|
|
885
|
+
(s: InputStream): number =>
|
|
886
|
+
s.dataView.getUint16((s.offset += 2) - 2, true) - 65536,
|
|
887
|
+
(s: InputStream): number => s.dataView.getInt32((s.offset += 4) - 4, true),
|
|
888
|
+
(s: InputStream): bigint => s.dataView.getBigInt64((s.offset += 8) - 8, true),
|
|
889
|
+
(s: InputStream): bigint => s.dataView.getBigInt64((s.offset += 8) - 8, true),
|
|
890
|
+
(s: InputStream): number => s.dataView.getFloat32((s.offset += 4) - 4, true),
|
|
891
|
+
(s: InputStream): number => s.dataView.getFloat64((s.offset += 8) - 8, true),
|
|
892
|
+
];
|
|
893
|
+
|
|
894
|
+
function decodeNumber(stream: InputStream): number | bigint {
|
|
895
|
+
const wire = stream.readUint8();
|
|
896
|
+
return wire < 232 ? wire : DECODE_NUMBER_FNS[wire - 232]!(stream);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function decodeBigInt(stream: InputStream): bigint {
|
|
900
|
+
const number = decodeNumber(stream);
|
|
901
|
+
return typeof number === "bigint" ? number : BigInt(Math.round(number));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Parameter of the {@link InternalSerializer.encode} method. */
|
|
905
|
+
class OutputStream implements BinaryForm {
|
|
906
|
+
writeUint8(value: number): void {
|
|
907
|
+
const dataView = this.reserve(1);
|
|
908
|
+
dataView.setUint8(++this.offset - 1, value);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
writeUint16(value: number): void {
|
|
912
|
+
const dataView = this.reserve(2);
|
|
913
|
+
dataView.setUint16((this.offset += 2) - 2, value, true);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
writeUint32(value: number): void {
|
|
917
|
+
const dataView = this.reserve(4);
|
|
918
|
+
dataView.setUint32((this.offset += 4) - 4, value, true);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
writeInt32(value: number): void {
|
|
922
|
+
const dataView = this.reserve(4);
|
|
923
|
+
dataView.setInt32((this.offset += 4) - 4, value, true);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
writeUint64(value: bigint): void {
|
|
927
|
+
const dataView = this.reserve(8);
|
|
928
|
+
dataView.setBigUint64((this.offset += 8) - 8, value, true);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
writeInt64(value: bigint): void {
|
|
932
|
+
const dataView = this.reserve(8);
|
|
933
|
+
dataView.setBigInt64((this.offset += 8) - 8, value, true);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
writeFloat32(value: number): void {
|
|
937
|
+
const dataView = this.reserve(4);
|
|
938
|
+
dataView.setFloat32((this.offset += 4) - 4, value, true);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
writeFloat64(value: number): void {
|
|
942
|
+
const dataView = this.reserve(8);
|
|
943
|
+
dataView.setFloat64((this.offset += 8) - 8, value, true);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Encodes the given string to UTF-8 and writes the bytes to this stream.
|
|
948
|
+
* Returns the number of bytes written.
|
|
949
|
+
*/
|
|
950
|
+
putUtf8String(string: string): number {
|
|
951
|
+
// We do at most 3 writes:
|
|
952
|
+
// - First, fill the current buffer as much as possible
|
|
953
|
+
// - If there is not enough room, allocate a new buffer of N bytes, where
|
|
954
|
+
// N is twice the number of remaining UTF-16 characters in the string,
|
|
955
|
+
// and write to it. This new buffer is very likely to have enough
|
|
956
|
+
// room.
|
|
957
|
+
// - If there was not enough room, try again one last time.
|
|
958
|
+
//
|
|
959
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
|
|
960
|
+
let dataView: DataView = this.dataView;
|
|
961
|
+
let result = 0;
|
|
962
|
+
while (string) {
|
|
963
|
+
const encodeResult = textEncoder.encodeInto(
|
|
964
|
+
string,
|
|
965
|
+
new Uint8Array(dataView.buffer, this.offset),
|
|
966
|
+
);
|
|
967
|
+
this.offset += encodeResult.written;
|
|
968
|
+
result += encodeResult.written;
|
|
969
|
+
string = string.substring(encodeResult.read);
|
|
970
|
+
if (string) {
|
|
971
|
+
dataView = this.reserve(string.length * 2);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
putBytes(bytes: ByteString): void {
|
|
978
|
+
// We do at most 2 writes:
|
|
979
|
+
// - First, fill the current buffer as much as possible
|
|
980
|
+
// - If there is not enough room, allocate a new buffer of N bytes, where
|
|
981
|
+
// N is at least the number of bytes left in the byte string.
|
|
982
|
+
const { buffer } = this;
|
|
983
|
+
const bytesLeftInCurrentBuffer = buffer.byteLength - this.offset;
|
|
984
|
+
const head = ByteString.sliceOf(bytes, 0, bytesLeftInCurrentBuffer);
|
|
985
|
+
head.copyTo(buffer, this.offset);
|
|
986
|
+
this.offset += head.byteLength;
|
|
987
|
+
const remainingBytes = bytes.byteLength - head.byteLength;
|
|
988
|
+
if (remainingBytes <= 0) {
|
|
989
|
+
// Everything was written.
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const tail = ByteString.sliceOf(bytes, remainingBytes);
|
|
993
|
+
this.reserve(remainingBytes);
|
|
994
|
+
tail.copyTo(buffer, this.offset);
|
|
995
|
+
this.offset += remainingBytes;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
finalize(): BinaryForm {
|
|
999
|
+
this.flush();
|
|
1000
|
+
Object.freeze(this.pieces);
|
|
1001
|
+
Object.freeze(this);
|
|
1002
|
+
return this;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
copyTo(target: ArrayBuffer, offset = 0): void {
|
|
1006
|
+
const targetArea = new Uint8Array(target);
|
|
1007
|
+
for (const piece of this.pieces) {
|
|
1008
|
+
targetArea.set(piece, offset);
|
|
1009
|
+
offset += piece.length;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
toBuffer(): ArrayBuffer {
|
|
1014
|
+
const result = new ArrayBuffer(this.byteLength);
|
|
1015
|
+
this.copyTo(result);
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** Returns a data view with enough capacity for `bytes` more bytes. */
|
|
1020
|
+
private reserve(bytes: number): DataView {
|
|
1021
|
+
if (this.offset < this.buffer.byteLength - bytes) {
|
|
1022
|
+
// Enough room in the current data view.
|
|
1023
|
+
return this.dataView;
|
|
1024
|
+
}
|
|
1025
|
+
this.flush();
|
|
1026
|
+
const lengthInBytes = Math.max(this.byteLength, bytes);
|
|
1027
|
+
this.offset = 0;
|
|
1028
|
+
this.buffer = new ArrayBuffer(lengthInBytes);
|
|
1029
|
+
return (this.dataView = new DataView(this.buffer));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** Adds the current buffer to `pieces`. Updates `byteLength` accordingly. */
|
|
1033
|
+
private flush(): void {
|
|
1034
|
+
const { offset } = this;
|
|
1035
|
+
this.pieces.push(new Uint8Array(this.dataView.buffer, 0, offset));
|
|
1036
|
+
this.byteLength += offset;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
buffer = new ArrayBuffer(128);
|
|
1040
|
+
dataView = new DataView(this.buffer);
|
|
1041
|
+
offset = 0;
|
|
1042
|
+
// The final binary form is the result of concatenating these arrays.
|
|
1043
|
+
// The length of each array is approximately twice the length of the previous
|
|
1044
|
+
// array.
|
|
1045
|
+
private readonly pieces: Uint8Array[] = [];
|
|
1046
|
+
// Updated each time `flush()` is called.
|
|
1047
|
+
byteLength = 0;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function encodeUint32(length: number, stream: OutputStream): void {
|
|
1051
|
+
if (length < 232) {
|
|
1052
|
+
stream.writeUint8(length);
|
|
1053
|
+
} else if (length < 65536) {
|
|
1054
|
+
stream.writeUint8(232);
|
|
1055
|
+
stream.writeUint16(length);
|
|
1056
|
+
} else if (length < 4294967296) {
|
|
1057
|
+
stream.writeUint8(233);
|
|
1058
|
+
stream.writeUint32(length);
|
|
1059
|
+
} else {
|
|
1060
|
+
throw new Error(`max length exceeded: ${length}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
abstract class AbstractSerializer<T> implements InternalSerializer<T> {
|
|
1065
|
+
fromJsonCode(code: string, keep?: "keep-unrecognized-values"): T {
|
|
1066
|
+
return this.fromJson(JSON.parse(code), keep);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
fromBytes(bytes: ArrayBuffer, keep?: "keep-unrecognized-values"): T {
|
|
1070
|
+
const inputStream = new InputStream(bytes, keep);
|
|
1071
|
+
inputStream.offset = 4; // Skip the "skir" header.
|
|
1072
|
+
return this.decode(inputStream);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
toJsonCode(input: T, flavor?: JsonFlavor): string {
|
|
1076
|
+
const indent = flavor === "readable" ? " " : undefined;
|
|
1077
|
+
return JSON.stringify(this.toJson(input, flavor), undefined, indent);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
toBytes(input: T): BinaryForm {
|
|
1081
|
+
const stream = new OutputStream();
|
|
1082
|
+
stream.putUtf8String("skir");
|
|
1083
|
+
this.encode(input, stream);
|
|
1084
|
+
return stream.finalize();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Default implementation; this behavior is not correct for all subclasses.
|
|
1088
|
+
isDefault(input: T): boolean {
|
|
1089
|
+
return !input;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
get typeDescriptor(): TypeDescriptorSpecialization<T> {
|
|
1093
|
+
return this as unknown as TypeDescriptorSpecialization<T>;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
abstract readonly defaultValue: T;
|
|
1097
|
+
abstract fromJson(json: Json, keep?: "keep-unrecognized-values"): T;
|
|
1098
|
+
abstract toJson(input: T, flavor?: JsonFlavor): Json;
|
|
1099
|
+
abstract decode(stream: InputStream): T;
|
|
1100
|
+
abstract encode(input: T, stream: OutputStream): void;
|
|
1101
|
+
|
|
1102
|
+
abstract readonly typeSignature: TypeSignature;
|
|
1103
|
+
abstract addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void;
|
|
1104
|
+
|
|
1105
|
+
asJson(): Json {
|
|
1106
|
+
const recordDefinitions: { [k: string]: RecordDefinition } = {};
|
|
1107
|
+
this.addRecordDefinitionsTo(recordDefinitions);
|
|
1108
|
+
const result: TypeDefinition = {
|
|
1109
|
+
type: this.typeSignature,
|
|
1110
|
+
records: Object.values(recordDefinitions),
|
|
1111
|
+
};
|
|
1112
|
+
return result;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
asJsonCode(): string {
|
|
1116
|
+
return JSON.stringify(this.asJson(), undefined, " ");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
transform(json_or_bytes: Json | ArrayBuffer, out: JsonFlavor): Json;
|
|
1120
|
+
transform(json: Json, out: "bytes"): ArrayBuffer;
|
|
1121
|
+
transform(
|
|
1122
|
+
json_or_bytes: Json | ArrayBuffer,
|
|
1123
|
+
out: JsonFlavor | "bytes",
|
|
1124
|
+
): Json | ArrayBuffer {
|
|
1125
|
+
const decoded: T =
|
|
1126
|
+
json_or_bytes instanceof ArrayBuffer
|
|
1127
|
+
? this.fromBytes(json_or_bytes)
|
|
1128
|
+
: this.fromJson(json_or_bytes);
|
|
1129
|
+
return out === "bytes"
|
|
1130
|
+
? this.toBytes(decoded).toBuffer()
|
|
1131
|
+
: this.toJson(decoded, out);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// The UNKNOWN variant is common to all enums.
|
|
1136
|
+
const UNKNOWN_VARIANT_DEFINITION: VariantDefinition = {
|
|
1137
|
+
name: "?",
|
|
1138
|
+
number: 0,
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Returns a `TypeDescriptor` from its JSON representation as returned by
|
|
1143
|
+
* `asJson()`.
|
|
1144
|
+
*/
|
|
1145
|
+
export function parseTypeDescriptorFromJson(json: Json): TypeDescriptor {
|
|
1146
|
+
const typeDefinition = json as TypeDefinition;
|
|
1147
|
+
|
|
1148
|
+
type RecordBundle = {
|
|
1149
|
+
readonly definition: RecordDefinition;
|
|
1150
|
+
readonly serializer: StructSerializerImpl<Json> | EnumSerializerImpl<Json>;
|
|
1151
|
+
};
|
|
1152
|
+
const recordBundles: { [k: string]: RecordBundle } = {};
|
|
1153
|
+
|
|
1154
|
+
// First loop: create the serializer for each record.
|
|
1155
|
+
// It's not yet initialized.
|
|
1156
|
+
for (const record of typeDefinition.records) {
|
|
1157
|
+
let serializer: StructSerializerImpl<Json> | EnumSerializerImpl<Json>;
|
|
1158
|
+
switch (record.kind) {
|
|
1159
|
+
case "struct":
|
|
1160
|
+
serializer = new StructSerializerImpl<Json>(
|
|
1161
|
+
{},
|
|
1162
|
+
(initializer: AnyRecord) => Object.freeze({ ...initializer }) as Json,
|
|
1163
|
+
(() => ({})) as NewMutableFn<Json>,
|
|
1164
|
+
);
|
|
1165
|
+
break;
|
|
1166
|
+
case "enum":
|
|
1167
|
+
serializer = new EnumSerializerImpl<Json>((o: unknown) =>
|
|
1168
|
+
o instanceof UnrecognizedEnum
|
|
1169
|
+
? Object.freeze({ kind: "?" })
|
|
1170
|
+
: (Object.freeze({
|
|
1171
|
+
kind: (o as AnyRecord).kind,
|
|
1172
|
+
value: (o as AnyRecord).value,
|
|
1173
|
+
}) as Json),
|
|
1174
|
+
);
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
const recordBundle: RecordBundle = {
|
|
1178
|
+
definition: record,
|
|
1179
|
+
serializer: serializer,
|
|
1180
|
+
};
|
|
1181
|
+
recordBundles[record.id] = recordBundle;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function parse(ts: TypeSignature): InternalSerializer {
|
|
1185
|
+
switch (ts.kind) {
|
|
1186
|
+
case "array": {
|
|
1187
|
+
const { item, key_extractor } = ts.value;
|
|
1188
|
+
return new ArraySerializerImpl(parse(item), key_extractor);
|
|
1189
|
+
}
|
|
1190
|
+
case "optional":
|
|
1191
|
+
return new OptionalSerializerImpl(parse(ts.value));
|
|
1192
|
+
case "primitive":
|
|
1193
|
+
return primitiveSerializer(ts.value) as InternalSerializer;
|
|
1194
|
+
case "record": {
|
|
1195
|
+
const recordId = ts.value;
|
|
1196
|
+
return recordBundles[recordId]!.serializer;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Second loop: initialize each serializer.
|
|
1202
|
+
const initOps: Array<() => void> = [];
|
|
1203
|
+
for (const recordBundle of Object.values(recordBundles)) {
|
|
1204
|
+
const { definition, serializer } = recordBundle;
|
|
1205
|
+
const { defaultValue } = serializer;
|
|
1206
|
+
const { id, removed_numbers } = definition;
|
|
1207
|
+
const idParts = id.split(":");
|
|
1208
|
+
const module = idParts[0]!;
|
|
1209
|
+
const qualifiedName = idParts[1]!;
|
|
1210
|
+
const nameParts = qualifiedName.split(".");
|
|
1211
|
+
const name = nameParts[nameParts.length - 1]!;
|
|
1212
|
+
const parentId = module + ":" + nameParts.slice(0, -1).join(".");
|
|
1213
|
+
const parentType = recordBundles[parentId]?.serializer;
|
|
1214
|
+
switch (definition.kind) {
|
|
1215
|
+
case "struct": {
|
|
1216
|
+
const fields: StructFieldImpl[] = [];
|
|
1217
|
+
for (const f of definition.fields) {
|
|
1218
|
+
const fieldSerializer = parse(f.type!);
|
|
1219
|
+
fields.push(
|
|
1220
|
+
new StructFieldImpl(f.name, f.name, f.number, fieldSerializer),
|
|
1221
|
+
);
|
|
1222
|
+
(defaultValue as AnyRecord)[f.name] = fieldSerializer.defaultValue;
|
|
1223
|
+
}
|
|
1224
|
+
const s = serializer as StructSerializerImpl<Json>;
|
|
1225
|
+
initOps.push(() =>
|
|
1226
|
+
s.init(name, module, parentType, fields, removed_numbers ?? []),
|
|
1227
|
+
);
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
case "enum": {
|
|
1231
|
+
const s = serializer as EnumSerializerImpl<Json>;
|
|
1232
|
+
const variants = [UNKNOWN_VARIANT_DEFINITION]
|
|
1233
|
+
.concat(definition.variants)
|
|
1234
|
+
.map((f) =>
|
|
1235
|
+
f.type
|
|
1236
|
+
? new EnumWrapperVariantImpl<Json>(
|
|
1237
|
+
f.name,
|
|
1238
|
+
f.number,
|
|
1239
|
+
parse(f.type),
|
|
1240
|
+
serializer.createFn,
|
|
1241
|
+
)
|
|
1242
|
+
: ({
|
|
1243
|
+
name: f.name,
|
|
1244
|
+
number: f.number,
|
|
1245
|
+
constant: Object.freeze({ kind: f.name }),
|
|
1246
|
+
} as EnumConstantVariant<Json>),
|
|
1247
|
+
);
|
|
1248
|
+
initOps.push(() =>
|
|
1249
|
+
s.init(name, module, parentType, variants, removed_numbers ?? []),
|
|
1250
|
+
);
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// We need to actually initialize the serializers *after* the default values
|
|
1256
|
+
// were constructed, because `init` calls `freezeDeeply` and this might result
|
|
1257
|
+
// in freezing the default of another serializer.
|
|
1258
|
+
initOps.forEach((op) => op());
|
|
1259
|
+
|
|
1260
|
+
return parse(typeDefinition.type).typeDescriptor;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Returns a `TypeDescriptor` from its JSON code representation as returned by
|
|
1265
|
+
* `asJsonCode()`.
|
|
1266
|
+
*/
|
|
1267
|
+
export function parseTypeDescriptorFromJsonCode(code: string): TypeDescriptor {
|
|
1268
|
+
return parseTypeDescriptorFromJson(JSON.parse(code));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
abstract class AbstractPrimitiveSerializer<P extends keyof PrimitiveTypes>
|
|
1272
|
+
extends AbstractSerializer<PrimitiveTypes[P]>
|
|
1273
|
+
implements PrimitiveDescriptor
|
|
1274
|
+
{
|
|
1275
|
+
readonly kind = "primitive";
|
|
1276
|
+
|
|
1277
|
+
get typeSignature(): TypeSignature {
|
|
1278
|
+
return {
|
|
1279
|
+
kind: "primitive",
|
|
1280
|
+
value: this.primitive,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
addRecordDefinitionsTo(_out: { [k: string]: RecordDefinition }): void {}
|
|
1285
|
+
|
|
1286
|
+
abstract readonly primitive: P;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
class BoolSerializer extends AbstractPrimitiveSerializer<"bool"> {
|
|
1290
|
+
readonly primitive = "bool";
|
|
1291
|
+
readonly defaultValue = false;
|
|
1292
|
+
|
|
1293
|
+
toJson(input: boolean, flavor?: JsonFlavor): boolean | number {
|
|
1294
|
+
return flavor === "readable" ? !!input : input ? 1 : 0;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
fromJson(json: Json): boolean {
|
|
1298
|
+
return !!json && json !== "0";
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
encode(input: boolean, stream: OutputStream): void {
|
|
1302
|
+
stream.writeUint8(input ? 1 : 0);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
decode(stream: InputStream): boolean {
|
|
1306
|
+
return !!decodeNumber(stream);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
class Int32Serializer extends AbstractPrimitiveSerializer<"int32"> {
|
|
1311
|
+
readonly primitive = "int32";
|
|
1312
|
+
readonly defaultValue = 0;
|
|
1313
|
+
|
|
1314
|
+
toJson(input: number): number {
|
|
1315
|
+
return input | 0;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
fromJson(json: Json): number {
|
|
1319
|
+
// `+value` will work if the input JSON value is a string, which is
|
|
1320
|
+
// what the int64 serializer produces.
|
|
1321
|
+
return +(json as number | string) | 0;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
encode(input: number, stream: OutputStream): void {
|
|
1325
|
+
if (input < 0) {
|
|
1326
|
+
if (input >= -256) {
|
|
1327
|
+
stream.writeUint8(235);
|
|
1328
|
+
stream.writeUint8(input + 256);
|
|
1329
|
+
} else if (input >= -65536) {
|
|
1330
|
+
stream.writeUint8(236);
|
|
1331
|
+
stream.writeUint16(input + 65536);
|
|
1332
|
+
} else {
|
|
1333
|
+
stream.writeUint8(237);
|
|
1334
|
+
stream.writeInt32(input >= -2147483648 ? input : -2147483648);
|
|
1335
|
+
}
|
|
1336
|
+
} else if (input < 232) {
|
|
1337
|
+
stream.writeUint8(input);
|
|
1338
|
+
} else if (input < 65536) {
|
|
1339
|
+
stream.writeUint8(232);
|
|
1340
|
+
stream.writeUint16(input);
|
|
1341
|
+
} else {
|
|
1342
|
+
stream.writeUint8(233);
|
|
1343
|
+
stream.writeUint32(input <= 2147483647 ? input : 2147483647);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
decode(stream: InputStream): number {
|
|
1348
|
+
return Number(decodeNumber(stream)) | 0;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const int32_Serializer = new Int32Serializer();
|
|
1353
|
+
|
|
1354
|
+
abstract class FloatSerializer<
|
|
1355
|
+
P extends "float32" | "float64",
|
|
1356
|
+
> extends AbstractPrimitiveSerializer<P> {
|
|
1357
|
+
readonly defaultValue = 0;
|
|
1358
|
+
|
|
1359
|
+
toJson(input: number): number | string {
|
|
1360
|
+
if (Number.isFinite(input)) {
|
|
1361
|
+
return input;
|
|
1362
|
+
} else if (typeof input === "number") {
|
|
1363
|
+
// If the number is NaN or +/- Infinity, return a JSON string.
|
|
1364
|
+
return input.toString();
|
|
1365
|
+
}
|
|
1366
|
+
throw new TypeError();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
fromJson(json: Json): number {
|
|
1370
|
+
return +(json as number | string);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
decode(stream: InputStream): number {
|
|
1374
|
+
return Number(decodeNumber(stream));
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
isDefault(input: number): boolean {
|
|
1378
|
+
// Needs to work for NaN.
|
|
1379
|
+
return input === 0;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
class Float32Serializer extends FloatSerializer<"float32"> {
|
|
1384
|
+
readonly primitive = "float32";
|
|
1385
|
+
|
|
1386
|
+
encode(input: number, stream: OutputStream): void {
|
|
1387
|
+
if (input === 0) {
|
|
1388
|
+
stream.writeUint8(0);
|
|
1389
|
+
} else {
|
|
1390
|
+
stream.writeUint8(240);
|
|
1391
|
+
stream.writeFloat32(input);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
class Float64Serializer extends FloatSerializer<"float64"> {
|
|
1397
|
+
readonly primitive = "float64";
|
|
1398
|
+
|
|
1399
|
+
encode(input: number, stream: OutputStream): void {
|
|
1400
|
+
if (input === 0) {
|
|
1401
|
+
stream.writeUint8(0);
|
|
1402
|
+
} else {
|
|
1403
|
+
stream.writeUint8(241);
|
|
1404
|
+
stream.writeFloat64(input);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
abstract class AbstractBigIntSerializer<
|
|
1410
|
+
P extends "int64" | "uint64",
|
|
1411
|
+
> extends AbstractPrimitiveSerializer<P> {
|
|
1412
|
+
readonly defaultValue = BigInt(0);
|
|
1413
|
+
|
|
1414
|
+
fromJson(json: Json): bigint {
|
|
1415
|
+
try {
|
|
1416
|
+
return BigInt(json as number | string);
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
if (typeof json === "number") {
|
|
1419
|
+
return BigInt(Math.round(json));
|
|
1420
|
+
} else {
|
|
1421
|
+
throw e;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const MIN_INT64 = BigInt("-9223372036854775808");
|
|
1428
|
+
const MAX_INT64 = BigInt("9223372036854775807");
|
|
1429
|
+
|
|
1430
|
+
class Int64Serializer extends AbstractBigIntSerializer<"int64"> {
|
|
1431
|
+
readonly primitive = "int64";
|
|
1432
|
+
|
|
1433
|
+
toJson(input: bigint): number | string {
|
|
1434
|
+
// 9007199254740991 == Number.MAX_SAFE_INTEGER
|
|
1435
|
+
if (-9007199254740991 <= input && input <= 9007199254740991) {
|
|
1436
|
+
return Number(input);
|
|
1437
|
+
}
|
|
1438
|
+
const s = BigInt(input).toString();
|
|
1439
|
+
// Clamp the number if it's out of bounds.
|
|
1440
|
+
return s.length <= 18
|
|
1441
|
+
? // Small optimization for "small" numbers. The max int64 has 19 digits.
|
|
1442
|
+
s
|
|
1443
|
+
: input < MIN_INT64
|
|
1444
|
+
? MIN_INT64.toString()
|
|
1445
|
+
: input < MAX_INT64
|
|
1446
|
+
? s
|
|
1447
|
+
: MAX_INT64.toString();
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
encode(input: bigint, stream: OutputStream): void {
|
|
1451
|
+
if (input) {
|
|
1452
|
+
if (-2147483648 <= input && input <= 2147483647) {
|
|
1453
|
+
int32_Serializer.encode(Number(input), stream);
|
|
1454
|
+
} else {
|
|
1455
|
+
stream.writeUint8(238);
|
|
1456
|
+
// Clamp the number if it's out of bounds.
|
|
1457
|
+
stream.writeInt64(
|
|
1458
|
+
input < MIN_INT64 ? MIN_INT64 : input < MAX_INT64 ? input : MAX_INT64,
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
} else {
|
|
1462
|
+
stream.writeUint8(0);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
decode(stream: InputStream): bigint {
|
|
1467
|
+
return decodeBigInt(stream);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const MAX_UINT64 = BigInt("18446744073709551615");
|
|
1472
|
+
|
|
1473
|
+
class Uint64Serializer extends AbstractBigIntSerializer<"uint64"> {
|
|
1474
|
+
readonly primitive = "uint64";
|
|
1475
|
+
|
|
1476
|
+
toJson(input: bigint): number | string {
|
|
1477
|
+
if (input <= 9007199254740991) {
|
|
1478
|
+
return input <= 0 ? 0 : Number(input);
|
|
1479
|
+
}
|
|
1480
|
+
input = BigInt(input);
|
|
1481
|
+
return MAX_UINT64 < input ? MAX_UINT64.toString() : input.toString();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
encode(input: bigint, stream: OutputStream): void {
|
|
1485
|
+
if (input < 232) {
|
|
1486
|
+
stream.writeUint8(input <= 0 ? 0 : Number(input));
|
|
1487
|
+
} else if (input < 4294967296) {
|
|
1488
|
+
if (input < 65536) {
|
|
1489
|
+
stream.writeUint8(232);
|
|
1490
|
+
stream.writeUint16(Number(input));
|
|
1491
|
+
} else {
|
|
1492
|
+
stream.writeUint8(233);
|
|
1493
|
+
stream.writeUint32(Number(input));
|
|
1494
|
+
}
|
|
1495
|
+
} else {
|
|
1496
|
+
stream.writeUint8(234);
|
|
1497
|
+
stream.writeUint64(input <= MAX_UINT64 ? input : MAX_UINT64);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
decode(stream: InputStream): bigint {
|
|
1502
|
+
return decodeBigInt(stream);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
type TimestampReadableJson = {
|
|
1507
|
+
unix_millis: number;
|
|
1508
|
+
formatted: string;
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
class TimestampSerializer extends AbstractPrimitiveSerializer<"timestamp"> {
|
|
1512
|
+
readonly primitive = "timestamp";
|
|
1513
|
+
readonly defaultValue = Timestamp.UNIX_EPOCH;
|
|
1514
|
+
|
|
1515
|
+
toJson(
|
|
1516
|
+
input: Timestamp,
|
|
1517
|
+
flavor?: JsonFlavor,
|
|
1518
|
+
): number | TimestampReadableJson {
|
|
1519
|
+
return flavor === "readable"
|
|
1520
|
+
? {
|
|
1521
|
+
unix_millis: input.unixMillis,
|
|
1522
|
+
formatted: input.toDate().toISOString(),
|
|
1523
|
+
}
|
|
1524
|
+
: input.unixMillis;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
fromJson(json: Json): Timestamp {
|
|
1528
|
+
return Timestamp.fromUnixMillis(
|
|
1529
|
+
typeof json === "number"
|
|
1530
|
+
? json
|
|
1531
|
+
: typeof json === "string"
|
|
1532
|
+
? +json
|
|
1533
|
+
: (json as TimestampReadableJson)["unix_millis"],
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
encode(input: Timestamp, stream: OutputStream): void {
|
|
1538
|
+
const { unixMillis } = input;
|
|
1539
|
+
if (unixMillis) {
|
|
1540
|
+
stream.writeUint8(239);
|
|
1541
|
+
stream.writeInt64(BigInt(unixMillis));
|
|
1542
|
+
} else {
|
|
1543
|
+
stream.writeUint8(0);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
decode(stream: InputStream): Timestamp {
|
|
1548
|
+
const unixMillis = decodeNumber(stream);
|
|
1549
|
+
return Timestamp.fromUnixMillis(Number(unixMillis));
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
isDefault(input: Timestamp): boolean {
|
|
1553
|
+
return !input.unixMillis;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
class StringSerializer extends AbstractPrimitiveSerializer<"string"> {
|
|
1558
|
+
readonly primitive = "string";
|
|
1559
|
+
readonly defaultValue = "";
|
|
1560
|
+
|
|
1561
|
+
toJson(input: string): string {
|
|
1562
|
+
if (typeof input === "string") {
|
|
1563
|
+
return input;
|
|
1564
|
+
}
|
|
1565
|
+
throw this.newTypeError(input);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
fromJson(json: Json): string {
|
|
1569
|
+
if (typeof json === "string") {
|
|
1570
|
+
return json;
|
|
1571
|
+
}
|
|
1572
|
+
if (json === 0) {
|
|
1573
|
+
return "";
|
|
1574
|
+
}
|
|
1575
|
+
throw this.newTypeError(json);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
encode(input: string, stream: OutputStream): void {
|
|
1579
|
+
if (!input) {
|
|
1580
|
+
stream.writeUint8(242);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
stream.writeUint8(243);
|
|
1584
|
+
// We don't know the length of the UTF-8 string until we actually encode the
|
|
1585
|
+
// string. We just know that it's at most 3 times the length of the input
|
|
1586
|
+
// string.
|
|
1587
|
+
const maxEncodedLength = input.length * 3;
|
|
1588
|
+
// Write zero in place of the UTF-8 sequence length. We will override this
|
|
1589
|
+
// number later.
|
|
1590
|
+
if (maxEncodedLength < 232) {
|
|
1591
|
+
stream.writeUint8(0);
|
|
1592
|
+
} else if (maxEncodedLength < 65536) {
|
|
1593
|
+
stream.writeUint8(232);
|
|
1594
|
+
stream.writeUint16(0);
|
|
1595
|
+
} else {
|
|
1596
|
+
stream.writeUint8(233);
|
|
1597
|
+
stream.writeUint32(0);
|
|
1598
|
+
}
|
|
1599
|
+
const { dataView, offset } = stream;
|
|
1600
|
+
// Write the UTF-8 string and record the number of bytes written.
|
|
1601
|
+
const encodedLength = stream.putUtf8String(input);
|
|
1602
|
+
// Write the length of the UTF-8 string where we wrote 0.
|
|
1603
|
+
if (maxEncodedLength < 232) {
|
|
1604
|
+
dataView.setUint8(offset - 1, encodedLength);
|
|
1605
|
+
} else if (maxEncodedLength < 65536) {
|
|
1606
|
+
dataView.setUint16(offset - 2, encodedLength, true);
|
|
1607
|
+
} else {
|
|
1608
|
+
dataView.setUint32(offset - 4, encodedLength, true);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
decode(stream: InputStream): string {
|
|
1613
|
+
const wire = stream.readUint8();
|
|
1614
|
+
if (wire === 0 || wire === 242) {
|
|
1615
|
+
return "";
|
|
1616
|
+
}
|
|
1617
|
+
const encodedLength = decodeNumber(stream) as number;
|
|
1618
|
+
return textDecoder.decode(
|
|
1619
|
+
new Uint8Array(
|
|
1620
|
+
stream.buffer,
|
|
1621
|
+
(stream.offset += encodedLength) - encodedLength,
|
|
1622
|
+
encodedLength,
|
|
1623
|
+
),
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
private newTypeError(actual: unknown): TypeError {
|
|
1628
|
+
return new TypeError(`expected: string; actual: ${typeof actual}`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
class ByteStringSerializer extends AbstractPrimitiveSerializer<"bytes"> {
|
|
1633
|
+
readonly primitive = "bytes";
|
|
1634
|
+
readonly defaultValue = ByteString.EMPTY;
|
|
1635
|
+
|
|
1636
|
+
toJson(input: ByteString, flavor?: JsonFlavor): string {
|
|
1637
|
+
return flavor === "readable" ? "hex:" + input.toBase16() : input.toBase64();
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
fromJson(json: Json): ByteString {
|
|
1641
|
+
if (json === 0) {
|
|
1642
|
+
return ByteString.EMPTY;
|
|
1643
|
+
}
|
|
1644
|
+
const string = json as string;
|
|
1645
|
+
return string.startsWith("hex:")
|
|
1646
|
+
? ByteString.fromBase16(string.substring(4))
|
|
1647
|
+
: ByteString.fromBase64(string);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
encode(input: ByteString, stream: OutputStream): void {
|
|
1651
|
+
const { byteLength } = input;
|
|
1652
|
+
if (byteLength) {
|
|
1653
|
+
stream.writeUint8(245);
|
|
1654
|
+
encodeUint32(byteLength, stream);
|
|
1655
|
+
stream.putBytes(input);
|
|
1656
|
+
} else {
|
|
1657
|
+
stream.writeUint8(244);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
decode(stream: InputStream): ByteString {
|
|
1662
|
+
const wire = stream.readUint8();
|
|
1663
|
+
if (wire === 0 || wire === 244) {
|
|
1664
|
+
return ByteString.EMPTY;
|
|
1665
|
+
}
|
|
1666
|
+
const lengthInBytes = decodeNumber(stream) as number;
|
|
1667
|
+
return ByteString.sliceOf(
|
|
1668
|
+
stream.buffer,
|
|
1669
|
+
stream.offset,
|
|
1670
|
+
(stream.offset += lengthInBytes),
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
isDefault(input: ByteString): boolean {
|
|
1675
|
+
return !input.byteLength;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
type AnyRecord = Record<string, unknown>;
|
|
1680
|
+
|
|
1681
|
+
class StructFieldImpl<Struct = unknown, Value = unknown>
|
|
1682
|
+
implements StructField<Struct, Value>
|
|
1683
|
+
{
|
|
1684
|
+
constructor(
|
|
1685
|
+
readonly name: string,
|
|
1686
|
+
readonly property: string,
|
|
1687
|
+
readonly number: number,
|
|
1688
|
+
readonly serializer: InternalSerializer<Value>,
|
|
1689
|
+
) {}
|
|
1690
|
+
|
|
1691
|
+
get type(): TypeDescriptor<Value> {
|
|
1692
|
+
return this.serializer.typeDescriptor;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
get(struct: Struct | MutableForm<Struct>): Value {
|
|
1696
|
+
return Reflect.get(struct as AnyRecord, this.property) as Value;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
set(struct: MutableForm<Struct>, value: Value): void {
|
|
1700
|
+
Reflect.set(struct, this.property, value);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
type EnumVariantImpl<Enum = unknown> =
|
|
1705
|
+
| EnumConstantVariantImpl<Enum>
|
|
1706
|
+
| EnumWrapperVariantImpl<Enum, unknown>;
|
|
1707
|
+
|
|
1708
|
+
const textEncoder = new TextEncoder();
|
|
1709
|
+
const textDecoder = new TextDecoder();
|
|
1710
|
+
|
|
1711
|
+
class ArraySerializerImpl<Item>
|
|
1712
|
+
extends AbstractSerializer<readonly Item[]>
|
|
1713
|
+
implements ArrayDescriptor<readonly Item[]>
|
|
1714
|
+
{
|
|
1715
|
+
constructor(
|
|
1716
|
+
readonly itemSerializer: InternalSerializer<Item>,
|
|
1717
|
+
readonly keyExtractor?: string,
|
|
1718
|
+
) {
|
|
1719
|
+
super();
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
readonly kind = "array";
|
|
1723
|
+
readonly defaultValue = _EMPTY_ARRAY;
|
|
1724
|
+
|
|
1725
|
+
toJson(input: ReadonlyArray<Item>, flavor?: JsonFlavor): Json[] {
|
|
1726
|
+
return input.map((e) => this.itemSerializer.toJson(e, flavor));
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
fromJson(json: Json, keep?: "keep-unrecognized-values"): ReadonlyArray<Item> {
|
|
1730
|
+
if (json === 0) {
|
|
1731
|
+
return _EMPTY_ARRAY;
|
|
1732
|
+
}
|
|
1733
|
+
return freezeArray(
|
|
1734
|
+
(json as readonly Json[]).map((e) =>
|
|
1735
|
+
this.itemSerializer.fromJson(e, keep),
|
|
1736
|
+
),
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
encode(input: ReadonlyArray<Item>, stream: OutputStream): void {
|
|
1741
|
+
const { length } = input;
|
|
1742
|
+
if (length <= 3) {
|
|
1743
|
+
stream.writeUint8(246 + length);
|
|
1744
|
+
} else {
|
|
1745
|
+
stream.writeUint8(250);
|
|
1746
|
+
encodeUint32(length, stream);
|
|
1747
|
+
}
|
|
1748
|
+
const { itemSerializer } = this;
|
|
1749
|
+
for (let i = 0; i < input.length; ++i) {
|
|
1750
|
+
itemSerializer.encode(input[i]!, stream);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
decode(stream: InputStream): readonly Item[] {
|
|
1755
|
+
const wire = stream.readUint8();
|
|
1756
|
+
if (wire === 0 || wire === 246) {
|
|
1757
|
+
return _EMPTY_ARRAY;
|
|
1758
|
+
}
|
|
1759
|
+
const length = wire === 250 ? (decodeNumber(stream) as number) : wire - 246;
|
|
1760
|
+
const { itemSerializer } = this;
|
|
1761
|
+
const result = new Array<Item>(length);
|
|
1762
|
+
for (let i = 0; i < length; ++i) {
|
|
1763
|
+
result[i] = itemSerializer.decode(stream);
|
|
1764
|
+
}
|
|
1765
|
+
return freezeArray(result);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
isDefault(input: ReadonlyArray<Item>): boolean {
|
|
1769
|
+
return !input.length;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
get itemType(): TypeDescriptor<Item> {
|
|
1773
|
+
return this.itemSerializer.typeDescriptor;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
get typeSignature(): TypeSignature {
|
|
1777
|
+
return {
|
|
1778
|
+
kind: "array",
|
|
1779
|
+
value: {
|
|
1780
|
+
item: this.itemSerializer.typeSignature,
|
|
1781
|
+
key_extractor: this.keyExtractor,
|
|
1782
|
+
},
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void {
|
|
1787
|
+
this.itemSerializer.addRecordDefinitionsTo(out);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
class OptionalSerializerImpl<Other>
|
|
1792
|
+
extends AbstractSerializer<Other | null>
|
|
1793
|
+
implements OptionalDescriptor<Other | null>
|
|
1794
|
+
{
|
|
1795
|
+
constructor(readonly otherSerializer: InternalSerializer<Other>) {
|
|
1796
|
+
super();
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
readonly kind = "optional";
|
|
1800
|
+
readonly defaultValue = null;
|
|
1801
|
+
|
|
1802
|
+
toJson(input: Other, flavor?: JsonFlavor): Json {
|
|
1803
|
+
return input !== null ? this.otherSerializer.toJson(input, flavor) : null;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
fromJson(json: Json, keep?: "keep-unrecognized-values"): Other | null {
|
|
1807
|
+
return json !== null ? this.otherSerializer.fromJson(json, keep) : null;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
encode(input: Other | null, stream: OutputStream): void {
|
|
1811
|
+
if (input === null) {
|
|
1812
|
+
stream.writeUint8(255);
|
|
1813
|
+
} else {
|
|
1814
|
+
this.otherSerializer.encode(input, stream);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
decode(stream: InputStream): Other | null {
|
|
1819
|
+
const wire = stream.dataView.getUint8(stream.offset);
|
|
1820
|
+
if (wire === 255) {
|
|
1821
|
+
++stream.offset;
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
return this.otherSerializer.decode(stream);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
isDefault(input: Other | null): boolean {
|
|
1828
|
+
return input === null;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
get otherType(): TypeDescriptor<NonNullable<Other>> {
|
|
1832
|
+
return this.otherSerializer.typeDescriptor as TypeDescriptor<
|
|
1833
|
+
NonNullable<Other>
|
|
1834
|
+
>;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
get typeSignature(): TypeSignature {
|
|
1838
|
+
return {
|
|
1839
|
+
kind: "optional",
|
|
1840
|
+
value: this.otherSerializer.typeSignature,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void {
|
|
1845
|
+
this.otherSerializer.addRecordDefinitionsTo(out);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const primitiveSerializers: {
|
|
1850
|
+
[P in keyof PrimitiveTypes]: Serializer<PrimitiveTypes[P]>;
|
|
1851
|
+
} = {
|
|
1852
|
+
bool: new BoolSerializer(),
|
|
1853
|
+
int32: int32_Serializer,
|
|
1854
|
+
int64: new Int64Serializer(),
|
|
1855
|
+
uint64: new Uint64Serializer(),
|
|
1856
|
+
float32: new Float32Serializer(),
|
|
1857
|
+
float64: new Float64Serializer(),
|
|
1858
|
+
timestamp: new TimestampSerializer(),
|
|
1859
|
+
string: new StringSerializer(),
|
|
1860
|
+
bytes: new ByteStringSerializer(),
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
type NewMutableFn<Frozen> = (
|
|
1864
|
+
initializer?: Frozen | MutableForm<Frozen>,
|
|
1865
|
+
) => MutableForm<Frozen>;
|
|
1866
|
+
|
|
1867
|
+
function decodeUnused(stream: InputStream): void {
|
|
1868
|
+
const wire = stream.readUint8();
|
|
1869
|
+
if (wire < 232) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
switch (wire - 232) {
|
|
1873
|
+
case 0: // uint16
|
|
1874
|
+
case 4: // uint16 - 65536
|
|
1875
|
+
stream.offset += 2;
|
|
1876
|
+
break;
|
|
1877
|
+
case 1: // uint32
|
|
1878
|
+
case 5: // int32
|
|
1879
|
+
case 8: // float32
|
|
1880
|
+
stream.offset += 4;
|
|
1881
|
+
break;
|
|
1882
|
+
case 2: // uint64
|
|
1883
|
+
case 6: // int64
|
|
1884
|
+
case 7: // uint64 timestamp
|
|
1885
|
+
case 9: // float64
|
|
1886
|
+
stream.offset += 8;
|
|
1887
|
+
break;
|
|
1888
|
+
case 3: // uint8 - 256
|
|
1889
|
+
++stream.offset;
|
|
1890
|
+
break;
|
|
1891
|
+
case 11: // string
|
|
1892
|
+
case 13: {
|
|
1893
|
+
// bytes
|
|
1894
|
+
const length = decodeNumber(stream) as number;
|
|
1895
|
+
stream.offset += length;
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
case 15: // array length==1
|
|
1899
|
+
case 19: // enum value kind==1
|
|
1900
|
+
case 20: // enum value kind==2
|
|
1901
|
+
case 21: // enum value kind==3
|
|
1902
|
+
case 22: // enum value kind==4
|
|
1903
|
+
decodeUnused(stream);
|
|
1904
|
+
break;
|
|
1905
|
+
case 16: // array length==2
|
|
1906
|
+
decodeUnused(stream);
|
|
1907
|
+
decodeUnused(stream);
|
|
1908
|
+
break;
|
|
1909
|
+
case 17: // array length==3
|
|
1910
|
+
decodeUnused(stream);
|
|
1911
|
+
decodeUnused(stream);
|
|
1912
|
+
decodeUnused(stream);
|
|
1913
|
+
break;
|
|
1914
|
+
case 18: {
|
|
1915
|
+
// array length==N
|
|
1916
|
+
const length = decodeNumber(stream);
|
|
1917
|
+
for (let i = 0; i < length; ++i) {
|
|
1918
|
+
decodeUnused(stream);
|
|
1919
|
+
}
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
abstract class AbstractRecordSerializer<T, F> extends AbstractSerializer<T> {
|
|
1926
|
+
/** Uniquely identifies this record serializer. */
|
|
1927
|
+
readonly token: symbol = Symbol();
|
|
1928
|
+
abstract kind: "struct" | "enum";
|
|
1929
|
+
name = "";
|
|
1930
|
+
modulePath = "";
|
|
1931
|
+
parentType: StructDescriptor | EnumDescriptor | undefined;
|
|
1932
|
+
removedNumbers = new Set<number>();
|
|
1933
|
+
initialized?: true;
|
|
1934
|
+
|
|
1935
|
+
init(
|
|
1936
|
+
name: string,
|
|
1937
|
+
modulePath: string,
|
|
1938
|
+
parentType: StructDescriptor | EnumDescriptor | undefined,
|
|
1939
|
+
fieldsOrVariants: readonly F[],
|
|
1940
|
+
removedNumbers: readonly number[],
|
|
1941
|
+
): void {
|
|
1942
|
+
this.name = name;
|
|
1943
|
+
this.modulePath = modulePath;
|
|
1944
|
+
this.parentType = parentType;
|
|
1945
|
+
this.removedNumbers = new Set(removedNumbers);
|
|
1946
|
+
this.registerFieldsOrVariants(fieldsOrVariants);
|
|
1947
|
+
this.initialized = true;
|
|
1948
|
+
freezeDeeply(this);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
get qualifiedName(): string {
|
|
1952
|
+
const { name, parentType } = this;
|
|
1953
|
+
return parentType ? `${parentType.name}.${name}` : name;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
abstract registerFieldsOrVariants(fieldsOrVariants: readonly F[]): void;
|
|
1957
|
+
|
|
1958
|
+
addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void {
|
|
1959
|
+
const recordId = `${this.modulePath}:${this.qualifiedName}`;
|
|
1960
|
+
if (out[recordId]) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
const recordDefinition = this.makeRecordDefinition(recordId);
|
|
1964
|
+
if (this.removedNumbers.size) {
|
|
1965
|
+
recordDefinition.removed_numbers = [...this.removedNumbers];
|
|
1966
|
+
}
|
|
1967
|
+
out[recordId] = recordDefinition;
|
|
1968
|
+
for (const dependency of this.dependencies()) {
|
|
1969
|
+
dependency.addRecordDefinitionsTo(out);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
abstract makeRecordDefinition(recordId: string): RecordDefinition;
|
|
1974
|
+
abstract dependencies(): InternalSerializer[];
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/** Unrecognized fields found when deserializing a struct. */
|
|
1978
|
+
class UnrecognizedFields {
|
|
1979
|
+
constructor(
|
|
1980
|
+
/** Uniquely identifies the struct. */
|
|
1981
|
+
readonly token: symbol,
|
|
1982
|
+
/** Total number of fields in the struct. */
|
|
1983
|
+
readonly totalSlots: number,
|
|
1984
|
+
readonly json?: ReadonlyArray<Json>,
|
|
1985
|
+
readonly bytes?: ByteString,
|
|
1986
|
+
) {
|
|
1987
|
+
Object.freeze(this);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
class StructSerializerImpl<T = unknown>
|
|
1992
|
+
extends AbstractRecordSerializer<T, StructFieldImpl<T>>
|
|
1993
|
+
implements StructDescriptor<T>
|
|
1994
|
+
{
|
|
1995
|
+
constructor(
|
|
1996
|
+
readonly defaultValue: T,
|
|
1997
|
+
readonly createFn: (initializer: AnyRecord) => T,
|
|
1998
|
+
readonly newMutableFn: NewMutableFn<T>,
|
|
1999
|
+
) {
|
|
2000
|
+
super();
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
readonly kind = "struct";
|
|
2004
|
+
// Fields in the order they appear in the `.skir` file.
|
|
2005
|
+
readonly fields: Array<StructFieldImpl<T>> = [];
|
|
2006
|
+
readonly fieldMapping: { [key: string | number]: StructFieldImpl<T> } = {};
|
|
2007
|
+
// Fields sorted by number in descending order.
|
|
2008
|
+
private reversedFields: Array<StructFieldImpl<T>> = [];
|
|
2009
|
+
// This is *not* a dense array, missing slots correspond to removed fields.
|
|
2010
|
+
private readonly slots: Array<StructFieldImpl<T>> = [];
|
|
2011
|
+
private recognizedSlots = 0;
|
|
2012
|
+
// Contains one zero for every field number.
|
|
2013
|
+
private readonly zeros: Json[] = [];
|
|
2014
|
+
private readonly initializerTemplate: Record<string, unknown> = {};
|
|
2015
|
+
|
|
2016
|
+
toJson(input: T, flavor?: JsonFlavor): Json {
|
|
2017
|
+
if (input === this.defaultValue) {
|
|
2018
|
+
return flavor === "readable" ? {} : [];
|
|
2019
|
+
}
|
|
2020
|
+
if (flavor === "readable") {
|
|
2021
|
+
const { fields } = this;
|
|
2022
|
+
const result: { [name: string]: Json } = {};
|
|
2023
|
+
for (const field of fields) {
|
|
2024
|
+
const { serializer } = field;
|
|
2025
|
+
const value = (input as AnyRecord)[field.property];
|
|
2026
|
+
if (field.serializer.isDefault(value)) {
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
result[field.name] = serializer.toJson(value, flavor);
|
|
2030
|
+
}
|
|
2031
|
+
return result;
|
|
2032
|
+
} else {
|
|
2033
|
+
// Dense flavor.
|
|
2034
|
+
const { slots } = this;
|
|
2035
|
+
let result: Json[];
|
|
2036
|
+
const unrecognizedFields = //
|
|
2037
|
+
(input as AnyRecord)["^"] as UnrecognizedFields;
|
|
2038
|
+
if (
|
|
2039
|
+
unrecognizedFields &&
|
|
2040
|
+
unrecognizedFields.json &&
|
|
2041
|
+
unrecognizedFields.token === this.token
|
|
2042
|
+
) {
|
|
2043
|
+
// We'll need to copy the unrecognized fields to the JSON.
|
|
2044
|
+
result = this.zeros.concat(unrecognizedFields.json);
|
|
2045
|
+
for (const field of this.fields) {
|
|
2046
|
+
result[field.number] = field.serializer.toJson(
|
|
2047
|
+
(input as AnyRecord)[field.property],
|
|
2048
|
+
flavor,
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
} else {
|
|
2052
|
+
result = [];
|
|
2053
|
+
const arrayLength = this.getArrayLength(input);
|
|
2054
|
+
for (let i = 0; i < arrayLength; ++i) {
|
|
2055
|
+
const field = slots[i];
|
|
2056
|
+
result[i] = field
|
|
2057
|
+
? field.serializer.toJson(
|
|
2058
|
+
(input as AnyRecord)[field.property],
|
|
2059
|
+
flavor,
|
|
2060
|
+
)
|
|
2061
|
+
: 0;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
return result;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
fromJson(json: Json, keep?: "keep-unrecognized-values"): T {
|
|
2069
|
+
if (!json) {
|
|
2070
|
+
return this.defaultValue;
|
|
2071
|
+
}
|
|
2072
|
+
const initializer = { ...this.initializerTemplate };
|
|
2073
|
+
if (json instanceof Array) {
|
|
2074
|
+
const { slots, recognizedSlots } = this;
|
|
2075
|
+
// Dense flavor.
|
|
2076
|
+
if (json.length > recognizedSlots) {
|
|
2077
|
+
// We have some unrecognized fields.
|
|
2078
|
+
if (keep) {
|
|
2079
|
+
const unrecognizedFields = new UnrecognizedFields(
|
|
2080
|
+
this.token,
|
|
2081
|
+
json.length,
|
|
2082
|
+
copyJson(json.slice(recognizedSlots)),
|
|
2083
|
+
);
|
|
2084
|
+
initializer["^"] = unrecognizedFields;
|
|
2085
|
+
}
|
|
2086
|
+
// Now that we have stored the unrecognized fields in `initializer`, we
|
|
2087
|
+
// can remove them from `json`.
|
|
2088
|
+
json = json.slice(0, recognizedSlots);
|
|
2089
|
+
}
|
|
2090
|
+
for (let i = 0; i < json.length && i < slots.length; ++i) {
|
|
2091
|
+
const field = slots[i];
|
|
2092
|
+
if (field) {
|
|
2093
|
+
initializer[field.property] = field.serializer.fromJson(
|
|
2094
|
+
json[i]!,
|
|
2095
|
+
keep,
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
// Else the field was removed.
|
|
2099
|
+
}
|
|
2100
|
+
return this.createFn(initializer);
|
|
2101
|
+
} else if (json instanceof Object) {
|
|
2102
|
+
// Readable flavor.
|
|
2103
|
+
const { fieldMapping } = this;
|
|
2104
|
+
for (const name in json) {
|
|
2105
|
+
const field = fieldMapping[name];
|
|
2106
|
+
if (field) {
|
|
2107
|
+
initializer[field.property] = field.serializer.fromJson(
|
|
2108
|
+
json[name]!,
|
|
2109
|
+
keep,
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return this.createFn(initializer);
|
|
2114
|
+
}
|
|
2115
|
+
throw TypeError();
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
encode(input: T, stream: OutputStream): void {
|
|
2119
|
+
// Total number of slots to write. Includes removed and unrecognized fields.
|
|
2120
|
+
let totalSlots: number;
|
|
2121
|
+
let recognizedSlots: number;
|
|
2122
|
+
let unrecognizedBytes: ByteString | undefined;
|
|
2123
|
+
const unrecognizedFields = (input as AnyRecord)["^"] as UnrecognizedFields;
|
|
2124
|
+
if (
|
|
2125
|
+
unrecognizedFields &&
|
|
2126
|
+
unrecognizedFields.bytes &&
|
|
2127
|
+
unrecognizedFields.token === this.token
|
|
2128
|
+
) {
|
|
2129
|
+
totalSlots = unrecognizedFields.totalSlots;
|
|
2130
|
+
recognizedSlots = this.recognizedSlots;
|
|
2131
|
+
unrecognizedBytes = unrecognizedFields.bytes;
|
|
2132
|
+
} else {
|
|
2133
|
+
// No unrecognized fields.
|
|
2134
|
+
totalSlots = recognizedSlots = this.getArrayLength(input);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
if (totalSlots <= 3) {
|
|
2138
|
+
stream.writeUint8(246 + totalSlots);
|
|
2139
|
+
} else {
|
|
2140
|
+
stream.writeUint8(250);
|
|
2141
|
+
encodeUint32(totalSlots, stream);
|
|
2142
|
+
}
|
|
2143
|
+
const { slots } = this;
|
|
2144
|
+
for (let i = 0; i < recognizedSlots; ++i) {
|
|
2145
|
+
const field = slots[i];
|
|
2146
|
+
if (field) {
|
|
2147
|
+
field.serializer.encode((input as AnyRecord)[field.property], stream);
|
|
2148
|
+
} else {
|
|
2149
|
+
// Append '0' if the field was removed.
|
|
2150
|
+
stream.writeUint8(0);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (unrecognizedBytes) {
|
|
2154
|
+
// Copy the unrecognized fields.
|
|
2155
|
+
stream.putBytes(unrecognizedBytes);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
decode(stream: InputStream): T {
|
|
2160
|
+
const wire = stream.readUint8();
|
|
2161
|
+
if (wire === 0 || wire === 246) {
|
|
2162
|
+
return this.defaultValue;
|
|
2163
|
+
}
|
|
2164
|
+
const initializer = { ...this.initializerTemplate };
|
|
2165
|
+
const encodedSlots =
|
|
2166
|
+
wire === 250 ? (decodeNumber(stream) as number) : wire - 246;
|
|
2167
|
+
const { slots, recognizedSlots } = this;
|
|
2168
|
+
// Do not read more slots than the number of recognized slots.
|
|
2169
|
+
for (let i = 0; i < encodedSlots && i < recognizedSlots; ++i) {
|
|
2170
|
+
const field = slots[i];
|
|
2171
|
+
if (field) {
|
|
2172
|
+
initializer[field.property] = field.serializer.decode(stream);
|
|
2173
|
+
} else {
|
|
2174
|
+
// The field was removed.
|
|
2175
|
+
decodeUnused(stream);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
if (encodedSlots > recognizedSlots) {
|
|
2179
|
+
// We have some unrecognized fields.
|
|
2180
|
+
const start = stream.offset;
|
|
2181
|
+
for (let i = recognizedSlots; i < encodedSlots; ++i) {
|
|
2182
|
+
decodeUnused(stream);
|
|
2183
|
+
}
|
|
2184
|
+
if (stream.keepUnrecognizedValues) {
|
|
2185
|
+
const end = stream.offset;
|
|
2186
|
+
const unrecognizedBytes = ByteString.sliceOf(stream.buffer, start, end);
|
|
2187
|
+
const unrecognizedFields = new UnrecognizedFields(
|
|
2188
|
+
this.token,
|
|
2189
|
+
encodedSlots,
|
|
2190
|
+
undefined,
|
|
2191
|
+
unrecognizedBytes,
|
|
2192
|
+
);
|
|
2193
|
+
initializer["^"] = unrecognizedFields;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return this.createFn(initializer);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Returns the length of the JSON array for the given input, which is also the
|
|
2201
|
+
* number of slots and includes removed fields.
|
|
2202
|
+
* Assumes that `input` does not contain unrecognized fields.
|
|
2203
|
+
*/
|
|
2204
|
+
private getArrayLength(input: T): number {
|
|
2205
|
+
const { reversedFields } = this;
|
|
2206
|
+
for (let i = 0; i < reversedFields.length; ++i) {
|
|
2207
|
+
const field = reversedFields[i]!;
|
|
2208
|
+
const isDefault = //
|
|
2209
|
+
field.serializer.isDefault((input as AnyRecord)[field.property]);
|
|
2210
|
+
if (!isDefault) {
|
|
2211
|
+
return field.number + 1;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return 0;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
isDefault(input: T): boolean {
|
|
2218
|
+
if (input === this.defaultValue) {
|
|
2219
|
+
return true;
|
|
2220
|
+
}
|
|
2221
|
+
// It's possible for a value of type T to be equal to T.DEFAULT but to not
|
|
2222
|
+
// be the reference to T.DEFAULT.
|
|
2223
|
+
if ((input as AnyRecord)["^"] as UnrecognizedFields) {
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
return this.fields.every((f) =>
|
|
2227
|
+
f.serializer.isDefault((input as AnyRecord)[f.property]),
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
get typeSignature(): TypeSignature {
|
|
2232
|
+
return {
|
|
2233
|
+
kind: "record",
|
|
2234
|
+
value: `${this.modulePath}:${this.qualifiedName}`,
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
getField<K extends string | number>(key: K): StructFieldResult<T, K> {
|
|
2239
|
+
return this.fieldMapping[key]!;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
newMutable(initializer?: T | MutableForm<T>): MutableForm<T> {
|
|
2243
|
+
return this.newMutableFn(initializer);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
registerFieldsOrVariants(fields: ReadonlyArray<StructFieldImpl<T>>): void {
|
|
2247
|
+
for (const field of fields) {
|
|
2248
|
+
const { name, number, property } = field;
|
|
2249
|
+
this.fields.push(field);
|
|
2250
|
+
this.slots[number] = field;
|
|
2251
|
+
this.fieldMapping[name] = field;
|
|
2252
|
+
this.fieldMapping[property] = field;
|
|
2253
|
+
this.fieldMapping[number] = field;
|
|
2254
|
+
this.initializerTemplate[property] = (this.defaultValue as AnyRecord)[
|
|
2255
|
+
field.property
|
|
2256
|
+
];
|
|
2257
|
+
}
|
|
2258
|
+
// Removed numbers count as recognized slots.
|
|
2259
|
+
this.recognizedSlots =
|
|
2260
|
+
Math.max(this.slots.length - 1, ...this.removedNumbers) + 1;
|
|
2261
|
+
this.zeros.push(...Array<Json>(this.recognizedSlots).fill(0));
|
|
2262
|
+
this.reversedFields = [...this.fields].sort((a, b) => b.number - a.number);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
makeRecordDefinition(recordId: string): StructDefinition {
|
|
2266
|
+
return {
|
|
2267
|
+
kind: "struct",
|
|
2268
|
+
id: recordId,
|
|
2269
|
+
fields: this.fields.map((f) => ({
|
|
2270
|
+
name: f.name,
|
|
2271
|
+
number: f.number,
|
|
2272
|
+
type: f.serializer.typeSignature,
|
|
2273
|
+
})),
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
dependencies(): InternalSerializer[] {
|
|
2278
|
+
return this.fields.map((f) => f.serializer);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
class UnrecognizedEnum {
|
|
2283
|
+
constructor(
|
|
2284
|
+
readonly token: symbol,
|
|
2285
|
+
readonly json?: Json,
|
|
2286
|
+
readonly bytes?: ByteString,
|
|
2287
|
+
) {
|
|
2288
|
+
Object.freeze(this);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
interface EnumConstantVariantImpl<Enum> extends EnumConstantVariant<Enum> {
|
|
2293
|
+
readonly serializer?: undefined;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
class EnumWrapperVariantImpl<Enum, Value = unknown> {
|
|
2297
|
+
constructor(
|
|
2298
|
+
readonly name: string,
|
|
2299
|
+
readonly number: number,
|
|
2300
|
+
readonly serializer: InternalSerializer<Value>,
|
|
2301
|
+
private createFn: (initializer: { kind: string; value: unknown }) => Enum,
|
|
2302
|
+
) {}
|
|
2303
|
+
|
|
2304
|
+
get type(): TypeDescriptor<Value> {
|
|
2305
|
+
return this.serializer.typeDescriptor;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
readonly constant?: undefined;
|
|
2309
|
+
|
|
2310
|
+
get(e: Enum): Value | undefined {
|
|
2311
|
+
return (e as _EnumBase).kind === this.name
|
|
2312
|
+
? ((e as AnyRecord).value as Value)
|
|
2313
|
+
: undefined;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
wrap(value: Value): Enum {
|
|
2317
|
+
return this.createFn({ kind: this.name, value: value });
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
class EnumSerializerImpl<T = unknown>
|
|
2322
|
+
extends AbstractRecordSerializer<T, EnumVariantImpl<T>>
|
|
2323
|
+
implements EnumDescriptor<T>
|
|
2324
|
+
{
|
|
2325
|
+
constructor(readonly createFn: (initializer: unknown) => T) {
|
|
2326
|
+
super();
|
|
2327
|
+
this.defaultValue = createFn("?");
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
readonly kind = "enum";
|
|
2331
|
+
readonly defaultValue: T;
|
|
2332
|
+
readonly variants: EnumVariantImpl<T>[] = [];
|
|
2333
|
+
private readonly variantMapping: {
|
|
2334
|
+
[key: string | number]: EnumVariantImpl<T>;
|
|
2335
|
+
} = {};
|
|
2336
|
+
|
|
2337
|
+
toJson(input: T, flavor?: JsonFlavor): Json {
|
|
2338
|
+
const unrecognized = (input as AnyRecord)["^"] as
|
|
2339
|
+
| UnrecognizedEnum
|
|
2340
|
+
| undefined;
|
|
2341
|
+
if (
|
|
2342
|
+
unrecognized &&
|
|
2343
|
+
unrecognized.json &&
|
|
2344
|
+
unrecognized.token === this.token
|
|
2345
|
+
) {
|
|
2346
|
+
// Unrecognized variant.
|
|
2347
|
+
return unrecognized.json;
|
|
2348
|
+
}
|
|
2349
|
+
const kind = (input as AnyRecord).kind as string;
|
|
2350
|
+
if (kind === "?") {
|
|
2351
|
+
return flavor === "readable" ? "?" : 0;
|
|
2352
|
+
}
|
|
2353
|
+
const variant = this.variantMapping[kind]!;
|
|
2354
|
+
const { serializer } = variant;
|
|
2355
|
+
if (serializer) {
|
|
2356
|
+
const value = (input as AnyRecord).value;
|
|
2357
|
+
if (flavor === "readable") {
|
|
2358
|
+
return {
|
|
2359
|
+
kind: variant.name,
|
|
2360
|
+
value: serializer.toJson(value, flavor),
|
|
2361
|
+
};
|
|
2362
|
+
} else {
|
|
2363
|
+
// Dense flavor.
|
|
2364
|
+
return [variant.number, serializer.toJson(value, flavor)];
|
|
2365
|
+
}
|
|
2366
|
+
} else {
|
|
2367
|
+
// A constant variant.
|
|
2368
|
+
return flavor === "readable" ? variant.name : variant.number;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
fromJson(json: Json, keep?: "keep-unrecognized-values"): T {
|
|
2373
|
+
const isNumber = typeof json === "number";
|
|
2374
|
+
if (isNumber || typeof json === "string") {
|
|
2375
|
+
const variant = this.variantMapping[isNumber ? json : String(json)];
|
|
2376
|
+
if (!variant) {
|
|
2377
|
+
// Check if the variant was removed, in which case we want to return
|
|
2378
|
+
// UNKNOWN, or is unrecognized.
|
|
2379
|
+
return !keep || (isNumber && this.removedNumbers.has(json))
|
|
2380
|
+
? this.defaultValue
|
|
2381
|
+
: this.createFn(new UnrecognizedEnum(this.token, copyJson(json)));
|
|
2382
|
+
}
|
|
2383
|
+
if (variant.serializer) {
|
|
2384
|
+
throw new Error(`refers to a wrapper variant: ${json}`);
|
|
2385
|
+
}
|
|
2386
|
+
return variant.constant;
|
|
2387
|
+
}
|
|
2388
|
+
let variantKey: number | string;
|
|
2389
|
+
let valueAsJson: Json;
|
|
2390
|
+
if (json instanceof Array) {
|
|
2391
|
+
variantKey = json[0] as number;
|
|
2392
|
+
valueAsJson = json[1]!;
|
|
2393
|
+
} else if (json instanceof Object) {
|
|
2394
|
+
variantKey = json["kind"] as string;
|
|
2395
|
+
valueAsJson = json["value"]!;
|
|
2396
|
+
} else {
|
|
2397
|
+
throw TypeError();
|
|
2398
|
+
}
|
|
2399
|
+
const variant = this.variantMapping[variantKey];
|
|
2400
|
+
if (!variant) {
|
|
2401
|
+
// Check if the variant was removed, in which case we want to return
|
|
2402
|
+
// UNKNOWN, or is unrecognized.
|
|
2403
|
+
return !keep ||
|
|
2404
|
+
(typeof variantKey === "number" && this.removedNumbers.has(variantKey))
|
|
2405
|
+
? this.defaultValue
|
|
2406
|
+
: this.createFn(
|
|
2407
|
+
new UnrecognizedEnum(this.token, copyJson(json), undefined),
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
const { serializer } = variant;
|
|
2411
|
+
if (!serializer) {
|
|
2412
|
+
throw new Error(`refers to a constant variant: ${json}`);
|
|
2413
|
+
}
|
|
2414
|
+
return variant.wrap(serializer.fromJson(valueAsJson, keep));
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
encode(input: T, stream: OutputStream): void {
|
|
2418
|
+
const unrecognized = //
|
|
2419
|
+
(input as AnyRecord)["^"] as UnrecognizedEnum | undefined;
|
|
2420
|
+
if (
|
|
2421
|
+
unrecognized &&
|
|
2422
|
+
unrecognized.bytes &&
|
|
2423
|
+
unrecognized.token === this.token
|
|
2424
|
+
) {
|
|
2425
|
+
// Unrecognized variant.
|
|
2426
|
+
stream.putBytes(unrecognized.bytes);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const kind = (input as AnyRecord).kind as string;
|
|
2430
|
+
if (kind === "?") {
|
|
2431
|
+
stream.writeUint8(0);
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
const variant = this.variantMapping[kind]!;
|
|
2435
|
+
const { number, serializer } = variant;
|
|
2436
|
+
if (serializer) {
|
|
2437
|
+
// A wrapper variant.
|
|
2438
|
+
const value = (input as AnyRecord).value;
|
|
2439
|
+
if (number < 5) {
|
|
2440
|
+
// The number can't be 0 or else kind == "?".
|
|
2441
|
+
stream.writeUint8(250 + number);
|
|
2442
|
+
} else {
|
|
2443
|
+
stream.writeUint8(248);
|
|
2444
|
+
encodeUint32(number, stream);
|
|
2445
|
+
}
|
|
2446
|
+
serializer.encode(value, stream);
|
|
2447
|
+
} else {
|
|
2448
|
+
// A constant field.
|
|
2449
|
+
encodeUint32(number, stream);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
decode(stream: InputStream): T {
|
|
2454
|
+
const startOffset = stream.offset;
|
|
2455
|
+
const wire = stream.dataView.getUint8(startOffset);
|
|
2456
|
+
if (wire < 242) {
|
|
2457
|
+
// A number
|
|
2458
|
+
const number = decodeNumber(stream) as number;
|
|
2459
|
+
const variant = this.variantMapping[number];
|
|
2460
|
+
if (!variant) {
|
|
2461
|
+
// Check if the variant was removed, in which case we want to return
|
|
2462
|
+
// UNKNOWN, or is unrecognized.
|
|
2463
|
+
if (!stream.keepUnrecognizedValues || this.removedNumbers.has(number)) {
|
|
2464
|
+
return this.defaultValue;
|
|
2465
|
+
} else {
|
|
2466
|
+
const { offset } = stream;
|
|
2467
|
+
const bytes = ByteString.sliceOf(stream.buffer, startOffset, offset);
|
|
2468
|
+
return this.createFn(
|
|
2469
|
+
new UnrecognizedEnum(this.token, undefined, bytes),
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
if (variant.serializer) {
|
|
2474
|
+
throw new Error(`refers to a wrapper variant: ${number}`);
|
|
2475
|
+
}
|
|
2476
|
+
return variant.constant;
|
|
2477
|
+
} else {
|
|
2478
|
+
++stream.offset;
|
|
2479
|
+
const number =
|
|
2480
|
+
wire === 248 ? (decodeNumber(stream) as number) : wire - 250;
|
|
2481
|
+
const variant = this.variantMapping[number];
|
|
2482
|
+
if (!variant) {
|
|
2483
|
+
decodeUnused(stream);
|
|
2484
|
+
// Check if the variant was removed, in which case we want to return
|
|
2485
|
+
// UNKNOWN, or is unrecognized.
|
|
2486
|
+
if (!stream.keepUnrecognizedValues || this.removedNumbers.has(number)) {
|
|
2487
|
+
return this.defaultValue;
|
|
2488
|
+
} else {
|
|
2489
|
+
const { offset } = stream;
|
|
2490
|
+
const bytes = ByteString.sliceOf(stream.buffer, startOffset, offset);
|
|
2491
|
+
return this.createFn(
|
|
2492
|
+
new UnrecognizedEnum(this.token, undefined, bytes),
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const { serializer } = variant;
|
|
2497
|
+
if (!serializer) {
|
|
2498
|
+
throw new Error(`refers to a constant variant: ${number}`);
|
|
2499
|
+
}
|
|
2500
|
+
return variant.wrap(serializer.decode(stream));
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
get typeSignature(): TypeSignature {
|
|
2505
|
+
return {
|
|
2506
|
+
kind: "record",
|
|
2507
|
+
value: `${this.modulePath}:${this.qualifiedName}`,
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
isDefault(input: T): boolean {
|
|
2512
|
+
type Kinded = { kind: string };
|
|
2513
|
+
return (input as Kinded).kind === "?" && !(input as AnyRecord)["^"];
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
getVariant<K extends string | number>(key: K): EnumVariantResult<T, K> {
|
|
2517
|
+
return this.variantMapping[key]!;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
registerFieldsOrVariants(fields: ReadonlyArray<EnumVariantImpl<T>>): void {
|
|
2521
|
+
for (const field of fields) {
|
|
2522
|
+
this.variants.push(field);
|
|
2523
|
+
this.variantMapping[field.name] = field;
|
|
2524
|
+
this.variantMapping[field.number] = field;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
makeRecordDefinition(recordId: string): EnumDefinition {
|
|
2529
|
+
return {
|
|
2530
|
+
kind: "enum",
|
|
2531
|
+
id: recordId,
|
|
2532
|
+
variants: this.variants
|
|
2533
|
+
// Skip the UNKNOWN variant.
|
|
2534
|
+
.filter((f) => f.number)
|
|
2535
|
+
.map((f) => {
|
|
2536
|
+
const result = {
|
|
2537
|
+
name: f.name,
|
|
2538
|
+
number: f.number,
|
|
2539
|
+
};
|
|
2540
|
+
const type = f?.serializer?.typeSignature;
|
|
2541
|
+
return type ? { ...result, type: type } : result;
|
|
2542
|
+
}),
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
dependencies(): InternalSerializer[] {
|
|
2547
|
+
const result: InternalSerializer[] = [];
|
|
2548
|
+
for (const f of this.variants) {
|
|
2549
|
+
if (f.serializer) {
|
|
2550
|
+
result.push(f.serializer);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return result;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function copyJson(input: readonly Json[]): Json[];
|
|
2558
|
+
function copyJson(input: Json): Json;
|
|
2559
|
+
function copyJson(input: Json): Json {
|
|
2560
|
+
if (input instanceof Array) {
|
|
2561
|
+
return Object.freeze(input.map(copyJson));
|
|
2562
|
+
} else if (input instanceof Object) {
|
|
2563
|
+
return Object.freeze(
|
|
2564
|
+
Object.fromEntries(Object.entries(input).map((k, v) => [k, copyJson(v)])),
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
// A boolean, a number, a string or null.
|
|
2568
|
+
return input;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
function freezeDeeply(o: unknown): void {
|
|
2572
|
+
if (!(o instanceof Object)) {
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (o instanceof _FrozenBase || o instanceof _EnumBase) {
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
if (o instanceof AbstractRecordSerializer && !o.initialized) {
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (Object.isFrozen(o)) {
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
Object.freeze(o);
|
|
2585
|
+
for (const v of Object.values(o)) {
|
|
2586
|
+
freezeDeeply(v);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// =============================================================================
|
|
2591
|
+
// Frozen arrays
|
|
2592
|
+
// =============================================================================
|
|
2593
|
+
|
|
2594
|
+
interface FrozenArrayInfo {
|
|
2595
|
+
keyFnToIndexing?: Map<unknown, Map<unknown, unknown>>;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const frozenArrayRegistry = new WeakMap<
|
|
2599
|
+
ReadonlyArray<unknown>,
|
|
2600
|
+
FrozenArrayInfo
|
|
2601
|
+
>();
|
|
2602
|
+
|
|
2603
|
+
function freezeArray<T>(array: readonly T[]): readonly T[] {
|
|
2604
|
+
if (!frozenArrayRegistry.has(array)) {
|
|
2605
|
+
frozenArrayRegistry.set(Object.freeze(array), {});
|
|
2606
|
+
}
|
|
2607
|
+
return array;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
export const _EMPTY_ARRAY = freezeArray([]);
|
|
2611
|
+
|
|
2612
|
+
export function _toFrozenArray<T, Initializer>(
|
|
2613
|
+
initializers: readonly Initializer[],
|
|
2614
|
+
itemToFrozenFn?: (item: Initializer) => T,
|
|
2615
|
+
): readonly T[] {
|
|
2616
|
+
if (!initializers.length) {
|
|
2617
|
+
return _EMPTY_ARRAY;
|
|
2618
|
+
}
|
|
2619
|
+
if (frozenArrayRegistry.has(initializers)) {
|
|
2620
|
+
// No need to make a copy: the given array is already deeply-frozen.
|
|
2621
|
+
return initializers as unknown as readonly T[];
|
|
2622
|
+
}
|
|
2623
|
+
const ret = Object.freeze(
|
|
2624
|
+
itemToFrozenFn
|
|
2625
|
+
? initializers.map(itemToFrozenFn)
|
|
2626
|
+
: (initializers.slice() as unknown as readonly T[]),
|
|
2627
|
+
);
|
|
2628
|
+
frozenArrayRegistry.set(ret, {});
|
|
2629
|
+
return ret;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// =============================================================================
|
|
2633
|
+
// Shared implementation of generated classes
|
|
2634
|
+
// =============================================================================
|
|
2635
|
+
|
|
2636
|
+
export declare const _INITIALIZER: unique symbol;
|
|
2637
|
+
|
|
2638
|
+
const PRIVATE_KEY: unique symbol = Symbol();
|
|
2639
|
+
|
|
2640
|
+
function forPrivateUseError(t: unknown): Error {
|
|
2641
|
+
const clazz = Object.getPrototypeOf(t).constructor as AnyRecord;
|
|
2642
|
+
const { qualifiedName } = clazz.serializer as StructDescriptor;
|
|
2643
|
+
return Error(
|
|
2644
|
+
[
|
|
2645
|
+
"Do not call the constructor directly; ",
|
|
2646
|
+
`instead, call ${qualifiedName}.create(...)`,
|
|
2647
|
+
].join(""),
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
export abstract class _FrozenBase {
|
|
2652
|
+
protected constructor(privateKey: symbol) {
|
|
2653
|
+
if (privateKey !== PRIVATE_KEY) {
|
|
2654
|
+
throw forPrivateUseError(this);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
toMutable(): unknown {
|
|
2659
|
+
return new (Object.getPrototypeOf(this).constructor.Mutable)(this);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
toFrozen(): this {
|
|
2663
|
+
return this;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
toString(): string {
|
|
2667
|
+
return toStringImpl(this);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
declare [_INITIALIZER]: unknown;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
export abstract class _EnumBase {
|
|
2674
|
+
protected constructor(
|
|
2675
|
+
privateKey: symbol,
|
|
2676
|
+
readonly kind: string,
|
|
2677
|
+
readonly value?: unknown,
|
|
2678
|
+
unrecognized?: UnrecognizedEnum,
|
|
2679
|
+
) {
|
|
2680
|
+
if (privateKey !== PRIVATE_KEY) {
|
|
2681
|
+
throw forPrivateUseError(this);
|
|
2682
|
+
}
|
|
2683
|
+
if (unrecognized) {
|
|
2684
|
+
if (!(unrecognized instanceof UnrecognizedEnum)) {
|
|
2685
|
+
throw new TypeError();
|
|
2686
|
+
}
|
|
2687
|
+
(this as AnyRecord)["^"] = unrecognized;
|
|
2688
|
+
}
|
|
2689
|
+
Object.freeze(this);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
toString(): string {
|
|
2693
|
+
return toStringImpl(this);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
// The TypeScript compiler complains if we define the property within the class.
|
|
2698
|
+
Object.defineProperty(_EnumBase.prototype, "union", {
|
|
2699
|
+
get: function () {
|
|
2700
|
+
return this;
|
|
2701
|
+
},
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
function toStringImpl<T>(value: T): string {
|
|
2705
|
+
const serializer = Object.getPrototypeOf(value).constructor
|
|
2706
|
+
.serializer as InternalSerializer<T>;
|
|
2707
|
+
return serializer.toJsonCode(value, "readable");
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// =============================================================================
|
|
2711
|
+
// Skir services
|
|
2712
|
+
// =============================================================================
|
|
2713
|
+
|
|
2714
|
+
/** Metadata of an HTTP request sent by a service client. */
|
|
2715
|
+
export type RequestMeta = Omit<RequestInit, "body" | "method">;
|
|
2716
|
+
|
|
2717
|
+
/** Sends RPCs to a skir service. */
|
|
2718
|
+
export class ServiceClient {
|
|
2719
|
+
constructor(
|
|
2720
|
+
private readonly serviceUrl: string,
|
|
2721
|
+
private readonly getRequestMetadata: (
|
|
2722
|
+
m: Method<unknown, unknown>,
|
|
2723
|
+
) => Promise<RequestMeta> | RequestMeta = (): RequestMeta => ({}),
|
|
2724
|
+
) {
|
|
2725
|
+
const url = new URL(serviceUrl);
|
|
2726
|
+
if (url.search) {
|
|
2727
|
+
throw new Error("Service URL must not contain a query string");
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
/** Invokes the given method on the remote server through an RPC. */
|
|
2732
|
+
async invokeRemote<Request, Response>(
|
|
2733
|
+
method: Method<Request, Response>,
|
|
2734
|
+
request: Request,
|
|
2735
|
+
httpMethod: "GET" | "POST" = "POST",
|
|
2736
|
+
): Promise<Response> {
|
|
2737
|
+
this.lastRespHeaders = undefined;
|
|
2738
|
+
const requestJson = method.requestSerializer.toJsonCode(request);
|
|
2739
|
+
const requestBody = [method.name, method.number, "", requestJson].join(":");
|
|
2740
|
+
const requestInit: RequestInit = {
|
|
2741
|
+
...(await Promise.resolve(this.getRequestMetadata(method))),
|
|
2742
|
+
};
|
|
2743
|
+
const url = new URL(this.serviceUrl);
|
|
2744
|
+
requestInit.method = httpMethod;
|
|
2745
|
+
if (httpMethod === "POST") {
|
|
2746
|
+
requestInit.body = requestBody;
|
|
2747
|
+
} else {
|
|
2748
|
+
url.search = requestBody.replace(/%/g, "%25");
|
|
2749
|
+
}
|
|
2750
|
+
const httpResponse = await fetch(url, requestInit);
|
|
2751
|
+
this.lastRespHeaders = httpResponse.headers;
|
|
2752
|
+
const responseData = await httpResponse.blob();
|
|
2753
|
+
if (httpResponse.ok) {
|
|
2754
|
+
const jsonCode = await responseData.text();
|
|
2755
|
+
return method.responseSerializer.fromJsonCode(
|
|
2756
|
+
jsonCode,
|
|
2757
|
+
"keep-unrecognized-values",
|
|
2758
|
+
);
|
|
2759
|
+
} else {
|
|
2760
|
+
let message = "";
|
|
2761
|
+
if (/text\/plain\b/.test(responseData.type)) {
|
|
2762
|
+
message = `: ${await responseData.text()}`;
|
|
2763
|
+
}
|
|
2764
|
+
throw new Error(`HTTP status ${httpResponse.status}${message}`);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
get lastResponseHeaders(): Headers | undefined {
|
|
2769
|
+
return this.lastRespHeaders;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
private lastRespHeaders: Headers | undefined;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
/** Raw response returned by the server. */
|
|
2776
|
+
export class RawResponse {
|
|
2777
|
+
constructor(
|
|
2778
|
+
readonly data: string,
|
|
2779
|
+
readonly type: "ok-json" | "ok-html" | "bad-request" | "server-error",
|
|
2780
|
+
) {}
|
|
2781
|
+
|
|
2782
|
+
get statusCode(): number {
|
|
2783
|
+
switch (this.type) {
|
|
2784
|
+
case "ok-json":
|
|
2785
|
+
case "ok-html":
|
|
2786
|
+
return 200;
|
|
2787
|
+
case "bad-request":
|
|
2788
|
+
return 400;
|
|
2789
|
+
case "server-error":
|
|
2790
|
+
return 500;
|
|
2791
|
+
default: {
|
|
2792
|
+
const _: never = this.type;
|
|
2793
|
+
throw new Error(_);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
get contentType(): string {
|
|
2799
|
+
switch (this.type) {
|
|
2800
|
+
case "ok-json":
|
|
2801
|
+
return "application/json";
|
|
2802
|
+
case "ok-html":
|
|
2803
|
+
return "text/html; charset=utf-8";
|
|
2804
|
+
case "bad-request":
|
|
2805
|
+
case "server-error":
|
|
2806
|
+
return "text/plain; charset=utf-8";
|
|
2807
|
+
default: {
|
|
2808
|
+
const _: never = this.type;
|
|
2809
|
+
throw new Error(_);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// Copied from
|
|
2816
|
+
// https://github.com/gepheum/restudio/blob/main/index.jsdeliver.html
|
|
2817
|
+
const RESTUDIO_HTML = `<!DOCTYPE html>
|
|
2818
|
+
|
|
2819
|
+
<html>
|
|
2820
|
+
<head>
|
|
2821
|
+
<meta charset="utf-8" />
|
|
2822
|
+
<title>RESTudio</title>
|
|
2823
|
+
<script src="https://cdn.jsdelivr.net/npm/restudio/dist/restudio-standalone.js"></script>
|
|
2824
|
+
</head>
|
|
2825
|
+
<body style="margin: 0; padding: 0;">
|
|
2826
|
+
<restudio-app></restudio-app>
|
|
2827
|
+
</body>
|
|
2828
|
+
</html>
|
|
2829
|
+
`;
|
|
2830
|
+
|
|
2831
|
+
/**
|
|
2832
|
+
* Implementation of a skir service.
|
|
2833
|
+
*
|
|
2834
|
+
* Usage: call `.addMethod()` to register methods, then install the service on
|
|
2835
|
+
* an HTTP server either by:
|
|
2836
|
+
* - calling the `installServiceOnExpressApp()` top-level function if you are
|
|
2837
|
+
* using ExpressJS
|
|
2838
|
+
* - writing your own implementation of `installServiceOn*()` which calls
|
|
2839
|
+
* `.handleRequest()` if you are using another web application framework
|
|
2840
|
+
*/
|
|
2841
|
+
export class Service<
|
|
2842
|
+
RequestMeta = ExpressRequest,
|
|
2843
|
+
ResponseMeta = ExpressResponse,
|
|
2844
|
+
> {
|
|
2845
|
+
addMethod<Request, Response>(
|
|
2846
|
+
method: Method<Request, Response>,
|
|
2847
|
+
impl: (
|
|
2848
|
+
req: Request,
|
|
2849
|
+
reqMeta: RequestMeta,
|
|
2850
|
+
resMeta: ResponseMeta,
|
|
2851
|
+
) => Promise<Response>,
|
|
2852
|
+
): Service<RequestMeta, ResponseMeta> {
|
|
2853
|
+
const { number } = method;
|
|
2854
|
+
if (this.methodImpls[number]) {
|
|
2855
|
+
throw new Error(
|
|
2856
|
+
`Method with the same number already registered (${number})`,
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
this.methodImpls[number] = {
|
|
2860
|
+
method: method,
|
|
2861
|
+
impl: impl,
|
|
2862
|
+
} as MethodImpl<unknown, unknown, RequestMeta, ResponseMeta>;
|
|
2863
|
+
return this;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
/**
|
|
2867
|
+
* Parses the content of a user request and invokes the appropriate method.
|
|
2868
|
+
* If you are using ExpressJS as your web application framework, you don't
|
|
2869
|
+
* need to call this method, you can simply call the
|
|
2870
|
+
* `installServiceOnExpressApp()` top-level function.
|
|
2871
|
+
*
|
|
2872
|
+
* If the request is a GET request, pass in the decoded query string as the
|
|
2873
|
+
* request's body. The query string is the part of the URL after '?', and it
|
|
2874
|
+
* can be decoded with DecodeURIComponent.
|
|
2875
|
+
*
|
|
2876
|
+
* Pass in "keep-unrecognized-values" if the request cannot come from a
|
|
2877
|
+
* malicious user.
|
|
2878
|
+
*/
|
|
2879
|
+
async handleRequest(
|
|
2880
|
+
reqBody: string,
|
|
2881
|
+
reqMeta: RequestMeta,
|
|
2882
|
+
resMeta: ResponseMeta,
|
|
2883
|
+
keepUnrecognizedValues?: "keep-unrecognized-values",
|
|
2884
|
+
): Promise<RawResponse> {
|
|
2885
|
+
if (reqBody === "" || reqBody === "list") {
|
|
2886
|
+
const json = {
|
|
2887
|
+
methods: Object.values(this.methodImpls).map((methodImpl) => ({
|
|
2888
|
+
method: methodImpl.method.name,
|
|
2889
|
+
number: methodImpl.method.name,
|
|
2890
|
+
request: methodImpl.method.requestSerializer.typeDescriptor.asJson(),
|
|
2891
|
+
response:
|
|
2892
|
+
methodImpl.method.responseSerializer.typeDescriptor.asJson(),
|
|
2893
|
+
doc: methodImpl.method.doc,
|
|
2894
|
+
})),
|
|
2895
|
+
};
|
|
2896
|
+
const jsonCode = JSON.stringify(json, undefined, " ");
|
|
2897
|
+
return new RawResponse(jsonCode, "ok-json");
|
|
2898
|
+
} else if (reqBody === "debug" || reqBody === "restudio") {
|
|
2899
|
+
return new RawResponse(RESTUDIO_HTML, "ok-html");
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
// Parse request
|
|
2903
|
+
let methodName: string;
|
|
2904
|
+
let methodNumber: number | undefined;
|
|
2905
|
+
let format: string;
|
|
2906
|
+
let requestData: ["json", Json] | ["json-code", string];
|
|
2907
|
+
|
|
2908
|
+
const firstChar = reqBody.charAt(0);
|
|
2909
|
+
if (/\s/.test(firstChar) || firstChar === "{") {
|
|
2910
|
+
// A JSON object
|
|
2911
|
+
let reqBodyJson: Json;
|
|
2912
|
+
try {
|
|
2913
|
+
reqBodyJson = JSON.parse(reqBody);
|
|
2914
|
+
} catch (e) {
|
|
2915
|
+
return new RawResponse("bad request: invalid JSON", "bad-request");
|
|
2916
|
+
}
|
|
2917
|
+
const methodField = (reqBodyJson as AnyRecord)["method"];
|
|
2918
|
+
if (methodField === undefined) {
|
|
2919
|
+
return new RawResponse(
|
|
2920
|
+
"bad request: missing 'method' field in JSON",
|
|
2921
|
+
"bad-request",
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
if (typeof methodField === "string") {
|
|
2925
|
+
methodName = methodField;
|
|
2926
|
+
methodNumber = undefined;
|
|
2927
|
+
} else if (typeof methodField === "number") {
|
|
2928
|
+
methodName = "?";
|
|
2929
|
+
methodNumber = methodField;
|
|
2930
|
+
} else {
|
|
2931
|
+
return new RawResponse(
|
|
2932
|
+
"bad request: 'method' field must be a string or a number",
|
|
2933
|
+
"bad-request",
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
format = "readable";
|
|
2937
|
+
const requestField = (reqBodyJson as AnyRecord)["request"];
|
|
2938
|
+
if (requestField === undefined) {
|
|
2939
|
+
return new RawResponse(
|
|
2940
|
+
"bad request: missing 'request' field in JSON",
|
|
2941
|
+
"bad-request",
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
requestData = ["json", requestField as Json];
|
|
2945
|
+
} else {
|
|
2946
|
+
// A colon-separated string
|
|
2947
|
+
const match = reqBody.match(/^([^:]*):([^:]*):([^:]*):([\S\s]*)$/);
|
|
2948
|
+
if (!match) {
|
|
2949
|
+
return new RawResponse(
|
|
2950
|
+
"bad request: invalid request format",
|
|
2951
|
+
"bad-request",
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
methodName = match[1]!;
|
|
2955
|
+
const methodNumberStr = match[2]!;
|
|
2956
|
+
format = match[3]!;
|
|
2957
|
+
requestData = ["json-code", match[4]!];
|
|
2958
|
+
|
|
2959
|
+
if (methodNumberStr) {
|
|
2960
|
+
if (!/^-?[0-9]+$/.test(methodNumberStr)) {
|
|
2961
|
+
return new RawResponse(
|
|
2962
|
+
"bad request: can't parse method number",
|
|
2963
|
+
"bad-request",
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
methodNumber = parseInt(methodNumberStr);
|
|
2967
|
+
} else {
|
|
2968
|
+
methodNumber = undefined;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// Look up method by number or name
|
|
2973
|
+
if (methodNumber === undefined) {
|
|
2974
|
+
// Try to get the method number by name
|
|
2975
|
+
const allMethods = Object.values(this.methodImpls);
|
|
2976
|
+
const nameMatches = allMethods.filter(
|
|
2977
|
+
(m) => m.method.name === methodName,
|
|
2978
|
+
);
|
|
2979
|
+
if (nameMatches.length === 0) {
|
|
2980
|
+
return new RawResponse(
|
|
2981
|
+
`bad request: method not found: ${methodName}`,
|
|
2982
|
+
"bad-request",
|
|
2983
|
+
);
|
|
2984
|
+
} else if (nameMatches.length > 1) {
|
|
2985
|
+
return new RawResponse(
|
|
2986
|
+
`bad request: method name '${methodName}' is ambiguous; use method number instead`,
|
|
2987
|
+
"bad-request",
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
methodNumber = nameMatches[0]!.method.number;
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
const methodImpl = this.methodImpls[methodNumber];
|
|
2994
|
+
if (!methodImpl) {
|
|
2995
|
+
return new RawResponse(
|
|
2996
|
+
`bad request: method not found: ${methodName}; number: ${methodNumber}`,
|
|
2997
|
+
"bad-request",
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
let req: unknown;
|
|
3002
|
+
try {
|
|
3003
|
+
if (requestData[0] == "json") {
|
|
3004
|
+
req = methodImpl.method.requestSerializer.fromJson(
|
|
3005
|
+
requestData[1],
|
|
3006
|
+
keepUnrecognizedValues,
|
|
3007
|
+
);
|
|
3008
|
+
} else {
|
|
3009
|
+
req = methodImpl.method.requestSerializer.fromJsonCode(
|
|
3010
|
+
requestData[1],
|
|
3011
|
+
keepUnrecognizedValues,
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
} catch (e) {
|
|
3015
|
+
return new RawResponse(
|
|
3016
|
+
`bad request: can't parse JSON: ${e}`,
|
|
3017
|
+
"bad-request",
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
let res: unknown;
|
|
3022
|
+
try {
|
|
3023
|
+
res = await methodImpl.impl(req, reqMeta, resMeta);
|
|
3024
|
+
} catch (e) {
|
|
3025
|
+
return new RawResponse(`server error: ${e}`, "server-error");
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
let resJson: string;
|
|
3029
|
+
try {
|
|
3030
|
+
const flavor = format === "readable" ? "readable" : "dense";
|
|
3031
|
+
resJson = methodImpl.method.responseSerializer.toJsonCode(res, flavor);
|
|
3032
|
+
} catch (e) {
|
|
3033
|
+
return new RawResponse(
|
|
3034
|
+
`server error: can't serialize response to JSON: ${e}`,
|
|
3035
|
+
"server-error",
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
return new RawResponse(resJson, "ok-json");
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
private readonly methodImpls: {
|
|
3043
|
+
[number: number]: MethodImpl<unknown, unknown, RequestMeta, ResponseMeta>;
|
|
3044
|
+
} = {};
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
interface MethodImpl<Request, Response, RequestMeta, ResponseMeta> {
|
|
3048
|
+
method: Method<Request, Response>;
|
|
3049
|
+
impl: (
|
|
3050
|
+
req: Request,
|
|
3051
|
+
reqMeta: RequestMeta,
|
|
3052
|
+
resMeta: ResponseMeta,
|
|
3053
|
+
) => Promise<Response>;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
export function installServiceOnExpressApp(
|
|
3057
|
+
app: ExpressApp,
|
|
3058
|
+
queryPath: string,
|
|
3059
|
+
service: Service<ExpressRequest, ExpressResponse>,
|
|
3060
|
+
text: typeof ExpressText,
|
|
3061
|
+
json: typeof ExpressJson,
|
|
3062
|
+
keepUnrecognizedValues?: "keep-unrecognized-values",
|
|
3063
|
+
): void {
|
|
3064
|
+
const callback = async (
|
|
3065
|
+
req: ExpressRequest,
|
|
3066
|
+
res: ExpressResponse,
|
|
3067
|
+
): Promise<void> => {
|
|
3068
|
+
let body: string;
|
|
3069
|
+
const indexOfQuestionMark = req.originalUrl.indexOf("?");
|
|
3070
|
+
if (indexOfQuestionMark >= 0) {
|
|
3071
|
+
const queryString = req.originalUrl.substring(indexOfQuestionMark + 1);
|
|
3072
|
+
body = decodeURIComponent(queryString);
|
|
3073
|
+
} else {
|
|
3074
|
+
body =
|
|
3075
|
+
typeof req.body === "string"
|
|
3076
|
+
? req.body
|
|
3077
|
+
: typeof req.body === "object"
|
|
3078
|
+
? JSON.stringify(req.body)
|
|
3079
|
+
: "";
|
|
3080
|
+
}
|
|
3081
|
+
const rawResponse = await service.handleRequest(
|
|
3082
|
+
body,
|
|
3083
|
+
req,
|
|
3084
|
+
res,
|
|
3085
|
+
keepUnrecognizedValues,
|
|
3086
|
+
);
|
|
3087
|
+
res
|
|
3088
|
+
.status(rawResponse.statusCode)
|
|
3089
|
+
.contentType(rawResponse.contentType)
|
|
3090
|
+
.send(rawResponse.data);
|
|
3091
|
+
};
|
|
3092
|
+
app.get(queryPath, callback);
|
|
3093
|
+
app.post(queryPath, text(), json(), callback);
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// =============================================================================
|
|
3097
|
+
// Module classes initialization
|
|
3098
|
+
// =============================================================================
|
|
3099
|
+
|
|
3100
|
+
interface StructSpec {
|
|
3101
|
+
kind: "struct";
|
|
3102
|
+
ctor: { new (privateKey: symbol): unknown };
|
|
3103
|
+
initFn: (target: unknown, initializer: unknown) => void;
|
|
3104
|
+
name: string;
|
|
3105
|
+
parentCtor?: { new (): unknown };
|
|
3106
|
+
fields: readonly StructFieldSpec[];
|
|
3107
|
+
removedNumbers?: readonly number[];
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
interface StructFieldSpec {
|
|
3111
|
+
name: string;
|
|
3112
|
+
property: string;
|
|
3113
|
+
number: number;
|
|
3114
|
+
type: TypeSpec;
|
|
3115
|
+
mutableGetter?: string;
|
|
3116
|
+
indexable?: IndexableSpec;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
interface IndexableSpec {
|
|
3120
|
+
searchMethod: string;
|
|
3121
|
+
keyFn: (v: unknown) => unknown;
|
|
3122
|
+
keyToHashable?: (v: unknown) => unknown;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
interface EnumSpec<Enum = unknown> {
|
|
3126
|
+
kind: "enum";
|
|
3127
|
+
ctor: {
|
|
3128
|
+
new (
|
|
3129
|
+
privateKey: symbol,
|
|
3130
|
+
kind: string,
|
|
3131
|
+
value?: unknown,
|
|
3132
|
+
unrecognized?: UnrecognizedEnum,
|
|
3133
|
+
): Enum;
|
|
3134
|
+
};
|
|
3135
|
+
createValueFn?: (initializer: unknown) => unknown;
|
|
3136
|
+
name: string;
|
|
3137
|
+
parentCtor?: { new (): unknown };
|
|
3138
|
+
fields: EnumFieldSpec[];
|
|
3139
|
+
removedNumbers?: readonly number[];
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
interface EnumFieldSpec {
|
|
3143
|
+
name: string;
|
|
3144
|
+
number: number;
|
|
3145
|
+
type?: TypeSpec;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
type TypeSpec =
|
|
3149
|
+
| {
|
|
3150
|
+
kind: "optional";
|
|
3151
|
+
other: TypeSpec;
|
|
3152
|
+
}
|
|
3153
|
+
| {
|
|
3154
|
+
kind: "array";
|
|
3155
|
+
item: TypeSpec;
|
|
3156
|
+
keyChain?: string;
|
|
3157
|
+
}
|
|
3158
|
+
| {
|
|
3159
|
+
kind: "record";
|
|
3160
|
+
ctor: { new (): unknown };
|
|
3161
|
+
}
|
|
3162
|
+
| {
|
|
3163
|
+
kind: "primitive";
|
|
3164
|
+
primitive: keyof PrimitiveTypes;
|
|
3165
|
+
};
|
|
3166
|
+
|
|
3167
|
+
// The UNKNOWN variant is common to all enums.
|
|
3168
|
+
const UNKNOWN_FIELD_SPEC: EnumFieldSpec = {
|
|
3169
|
+
name: "?",
|
|
3170
|
+
number: 0,
|
|
3171
|
+
};
|
|
3172
|
+
|
|
3173
|
+
export function _initModuleClasses(
|
|
3174
|
+
modulePath: string,
|
|
3175
|
+
records: ReadonlyArray<StructSpec | EnumSpec>,
|
|
3176
|
+
): void {
|
|
3177
|
+
const privateKey = PRIVATE_KEY;
|
|
3178
|
+
|
|
3179
|
+
// First loop: add a serializer property to every record class.
|
|
3180
|
+
for (const record of records) {
|
|
3181
|
+
const clazz = record.ctor as unknown as AnyRecord;
|
|
3182
|
+
switch (record.kind) {
|
|
3183
|
+
case "struct": {
|
|
3184
|
+
const { ctor, initFn } = record;
|
|
3185
|
+
// Create the DEFAULT value. It will be initialized in a second loop.
|
|
3186
|
+
// To see why we can't initialize it in the first loop, consider this
|
|
3187
|
+
// example:
|
|
3188
|
+
// struct Foo { bar: Bar; }
|
|
3189
|
+
// struct Bar { foo: Foo; }
|
|
3190
|
+
// The default value for Foo must contain a reference to the default
|
|
3191
|
+
// value for Bar, and the default value for Bar also needs to contain
|
|
3192
|
+
// a reference to the default value for Foo.
|
|
3193
|
+
clazz.DEFAULT = new ctor(privateKey);
|
|
3194
|
+
// Expose the mutable class as a static property of the frozen class.
|
|
3195
|
+
const mutableCtor = makeMutableClassForRecord(record, clazz.DEFAULT);
|
|
3196
|
+
clazz.Mutable = mutableCtor;
|
|
3197
|
+
// Define the 'create' static factory function.
|
|
3198
|
+
const createFn = (initializer: unknown): unknown => {
|
|
3199
|
+
if (initializer instanceof ctor) {
|
|
3200
|
+
return initializer;
|
|
3201
|
+
}
|
|
3202
|
+
const ret = new ctor(privateKey);
|
|
3203
|
+
initFn(ret, initializer);
|
|
3204
|
+
if ((initializer as AnyRecord)["^"]) {
|
|
3205
|
+
(ret as AnyRecord)["^"] = (initializer as AnyRecord)["^"];
|
|
3206
|
+
}
|
|
3207
|
+
return Object.freeze(ret);
|
|
3208
|
+
};
|
|
3209
|
+
clazz.create = createFn;
|
|
3210
|
+
// Create the serializer. It will be initialized in a second loop.
|
|
3211
|
+
clazz.serializer = new StructSerializerImpl(
|
|
3212
|
+
clazz.DEFAULT,
|
|
3213
|
+
createFn as (initializer: AnyRecord) => unknown,
|
|
3214
|
+
() => new mutableCtor() as Freezable<unknown>,
|
|
3215
|
+
);
|
|
3216
|
+
break;
|
|
3217
|
+
}
|
|
3218
|
+
case "enum": {
|
|
3219
|
+
// Create the constants.
|
|
3220
|
+
// Prepend the UNKNOWN variant to the array of fields specified from the
|
|
3221
|
+
// generated code.
|
|
3222
|
+
record.fields = [UNKNOWN_FIELD_SPEC].concat(record.fields);
|
|
3223
|
+
for (const field of record.fields) {
|
|
3224
|
+
if (field.type) {
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
const property = enumConstantNameToProperty(field.name);
|
|
3228
|
+
clazz[property] = new record.ctor(PRIVATE_KEY, field.name);
|
|
3229
|
+
}
|
|
3230
|
+
// Define the 'create' static factory function.
|
|
3231
|
+
const createFn = makeCreateEnumFunction(record);
|
|
3232
|
+
clazz.create = createFn;
|
|
3233
|
+
// Create the serializer. It will be initialized in a second loop.
|
|
3234
|
+
clazz.serializer = new EnumSerializerImpl(createFn);
|
|
3235
|
+
break;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
// If the record is nested, expose the record class as a static property of
|
|
3239
|
+
// the parent class.
|
|
3240
|
+
if (record.parentCtor) {
|
|
3241
|
+
(record.parentCtor as unknown as AnyRecord)[record.name] = record.ctor;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// Second loop: initialize the serializer of every record, initialize the
|
|
3246
|
+
// default value of every struct, and freeze every class so new properties
|
|
3247
|
+
// can't be added to it.
|
|
3248
|
+
for (const record of records) {
|
|
3249
|
+
const clazz = record.ctor as unknown as AnyRecord;
|
|
3250
|
+
const parentTypeDescriptor = (record.parentCtor as unknown as AnyRecord)
|
|
3251
|
+
?.serializer as StructDescriptor | EnumDescriptor | undefined;
|
|
3252
|
+
switch (record.kind) {
|
|
3253
|
+
case "struct": {
|
|
3254
|
+
// Initializer serializer.
|
|
3255
|
+
const fields = record.fields.map(
|
|
3256
|
+
(f) =>
|
|
3257
|
+
new StructFieldImpl(
|
|
3258
|
+
f.name,
|
|
3259
|
+
f.property,
|
|
3260
|
+
f.number,
|
|
3261
|
+
getSerializerForType(f.type) as InternalSerializer,
|
|
3262
|
+
),
|
|
3263
|
+
);
|
|
3264
|
+
const serializer = clazz.serializer as StructSerializerImpl;
|
|
3265
|
+
serializer.init(
|
|
3266
|
+
record.name,
|
|
3267
|
+
modulePath,
|
|
3268
|
+
parentTypeDescriptor,
|
|
3269
|
+
fields,
|
|
3270
|
+
record.removedNumbers ?? [],
|
|
3271
|
+
);
|
|
3272
|
+
// Initialize DEFAULT.
|
|
3273
|
+
const { DEFAULT } = clazz;
|
|
3274
|
+
record.initFn(DEFAULT as AnyRecord, {});
|
|
3275
|
+
Object.freeze(DEFAULT);
|
|
3276
|
+
// Define the mutable getters in the Mutable class.
|
|
3277
|
+
const mutableCtor = clazz.Mutable as new (i: unknown) => unknown;
|
|
3278
|
+
for (const field of record.fields) {
|
|
3279
|
+
if (field.mutableGetter) {
|
|
3280
|
+
Object.defineProperty(mutableCtor.prototype, field.mutableGetter, {
|
|
3281
|
+
get: makeMutableGetterFn(field),
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
// Define the search methods in the frozen class.
|
|
3286
|
+
for (const field of record.fields) {
|
|
3287
|
+
if (field.indexable) {
|
|
3288
|
+
record.ctor.prototype[field.indexable.searchMethod] =
|
|
3289
|
+
makeSearchMethod(field);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
// Freeze the frozen class and the mutable class.
|
|
3293
|
+
Object.freeze(record.ctor);
|
|
3294
|
+
Object.freeze(record.ctor.prototype);
|
|
3295
|
+
Object.freeze(clazz.Mutable);
|
|
3296
|
+
Object.freeze((clazz.Mutable as AnyRecord).prototype);
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
case "enum": {
|
|
3300
|
+
const serializer = clazz.serializer as EnumSerializerImpl;
|
|
3301
|
+
const fields = record.fields.map((f) =>
|
|
3302
|
+
f.type
|
|
3303
|
+
? new EnumWrapperVariantImpl(
|
|
3304
|
+
f.name,
|
|
3305
|
+
f.number,
|
|
3306
|
+
getSerializerForType(f.type) as InternalSerializer,
|
|
3307
|
+
serializer.createFn,
|
|
3308
|
+
)
|
|
3309
|
+
: {
|
|
3310
|
+
name: f.name,
|
|
3311
|
+
number: f.number,
|
|
3312
|
+
constant: clazz[enumConstantNameToProperty(f.name)],
|
|
3313
|
+
},
|
|
3314
|
+
);
|
|
3315
|
+
serializer.init(
|
|
3316
|
+
record.name,
|
|
3317
|
+
modulePath,
|
|
3318
|
+
parentTypeDescriptor,
|
|
3319
|
+
fields,
|
|
3320
|
+
record.removedNumbers ?? [],
|
|
3321
|
+
);
|
|
3322
|
+
// Freeze the enum class.
|
|
3323
|
+
Object.freeze(record.ctor);
|
|
3324
|
+
Object.freeze(record.ctor.prototype);
|
|
3325
|
+
break;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function enumConstantNameToProperty(name: string): string {
|
|
3332
|
+
return name === "?" ? "UNKNOWN" : name;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
function makeCreateEnumFunction(
|
|
3336
|
+
enumSpec: EnumSpec,
|
|
3337
|
+
): (initializer: unknown) => unknown {
|
|
3338
|
+
const { ctor, createValueFn } = enumSpec;
|
|
3339
|
+
const createValue = createValueFn || ((): undefined => undefined);
|
|
3340
|
+
const privateKey = PRIVATE_KEY;
|
|
3341
|
+
return (initializer: unknown) => {
|
|
3342
|
+
if (initializer instanceof ctor) {
|
|
3343
|
+
return initializer;
|
|
3344
|
+
}
|
|
3345
|
+
if (typeof initializer === "string") {
|
|
3346
|
+
const maybeResult = (ctor as unknown as AnyRecord)[
|
|
3347
|
+
enumConstantNameToProperty(initializer)
|
|
3348
|
+
];
|
|
3349
|
+
if (maybeResult instanceof ctor) {
|
|
3350
|
+
return maybeResult;
|
|
3351
|
+
}
|
|
3352
|
+
throw new Error(`Constant not found: ${initializer}`);
|
|
3353
|
+
}
|
|
3354
|
+
if (initializer instanceof UnrecognizedEnum) {
|
|
3355
|
+
return new ctor(privateKey, "?", undefined, initializer);
|
|
3356
|
+
}
|
|
3357
|
+
const kind = (initializer as { kind: string }).kind;
|
|
3358
|
+
if (kind === undefined) {
|
|
3359
|
+
throw new Error("Missing entry: kind");
|
|
3360
|
+
}
|
|
3361
|
+
const value = createValue(initializer);
|
|
3362
|
+
if (value === undefined) {
|
|
3363
|
+
throw new Error(`Wrapper field not found: ${kind}`);
|
|
3364
|
+
}
|
|
3365
|
+
return new ctor(privateKey, kind, value);
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function makeMutableClassForRecord(
|
|
3370
|
+
structSpec: StructSpec,
|
|
3371
|
+
defaultFrozen: unknown,
|
|
3372
|
+
): new (initializer?: unknown) => unknown {
|
|
3373
|
+
const { ctor: frozenCtor, initFn } = structSpec;
|
|
3374
|
+
const frozenClass = frozenCtor as unknown as AnyRecord;
|
|
3375
|
+
class Mutable {
|
|
3376
|
+
constructor(initializer: unknown = defaultFrozen) {
|
|
3377
|
+
initFn(this, initializer);
|
|
3378
|
+
if ((initializer as AnyRecord)["^"]) {
|
|
3379
|
+
(this as AnyRecord)["^"] = (initializer as AnyRecord)["^"];
|
|
3380
|
+
}
|
|
3381
|
+
Object.seal(this);
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
toFrozen(): unknown {
|
|
3385
|
+
return (frozenClass.create as (i: unknown) => unknown)(this);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
toString(): string {
|
|
3389
|
+
const serializer = frozenClass.serializer as Serializer<unknown>;
|
|
3390
|
+
return serializer.toJsonCode(this, "readable");
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
return Mutable;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function getSerializerForType(type: TypeSpec): Serializer<unknown> {
|
|
3397
|
+
switch (type.kind) {
|
|
3398
|
+
case "array":
|
|
3399
|
+
return arraySerializer(getSerializerForType(type.item), type.keyChain);
|
|
3400
|
+
case "optional":
|
|
3401
|
+
return optionalSerializer(getSerializerForType(type.other));
|
|
3402
|
+
case "primitive":
|
|
3403
|
+
return primitiveSerializer(type.primitive);
|
|
3404
|
+
case "record":
|
|
3405
|
+
return (type.ctor as unknown as AnyRecord)
|
|
3406
|
+
.serializer as Serializer<unknown>;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
// The `mutableArray()` getter of the Mutable class returns `this.array` if and
|
|
3411
|
+
// only if `mutableArray()` was never called before or if `this.array` is the
|
|
3412
|
+
// last value returned by `mutableArray()`.
|
|
3413
|
+
// Otherwise, it makes a mutable copy of `this.array`, assigns it to
|
|
3414
|
+
// `this.array` and returns it.
|
|
3415
|
+
const arraysReturnedByMutableGetters = new WeakMap<
|
|
3416
|
+
ReadonlyArray<unknown>,
|
|
3417
|
+
unknown
|
|
3418
|
+
>();
|
|
3419
|
+
|
|
3420
|
+
function makeMutableGetterFn(field: StructFieldSpec): () => unknown {
|
|
3421
|
+
const { property, type } = field;
|
|
3422
|
+
switch (type.kind) {
|
|
3423
|
+
case "array": {
|
|
3424
|
+
class Class {
|
|
3425
|
+
static ret(): unknown {
|
|
3426
|
+
const value = this[property] as readonly unknown[];
|
|
3427
|
+
if (arraysReturnedByMutableGetters.get(value) === this) {
|
|
3428
|
+
return value;
|
|
3429
|
+
}
|
|
3430
|
+
const copy = [...value];
|
|
3431
|
+
arraysReturnedByMutableGetters.set(copy, this);
|
|
3432
|
+
return (this[property] = copy);
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
static [_: string]: unknown;
|
|
3436
|
+
}
|
|
3437
|
+
return Class.ret;
|
|
3438
|
+
}
|
|
3439
|
+
case "record": {
|
|
3440
|
+
const mutableCtor = (type.ctor as unknown as AnyRecord).Mutable as new (
|
|
3441
|
+
i: unknown,
|
|
3442
|
+
) => unknown;
|
|
3443
|
+
class Class {
|
|
3444
|
+
static ret(): unknown {
|
|
3445
|
+
const value = this[property];
|
|
3446
|
+
if (value instanceof mutableCtor) {
|
|
3447
|
+
return value;
|
|
3448
|
+
}
|
|
3449
|
+
return (this[property] = new mutableCtor(value));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
static [_: string]: unknown;
|
|
3453
|
+
}
|
|
3454
|
+
return Class.ret;
|
|
3455
|
+
}
|
|
3456
|
+
default: {
|
|
3457
|
+
throw new Error();
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
function makeSearchMethod(field: StructFieldSpec): (key: unknown) => unknown {
|
|
3463
|
+
const { property } = field;
|
|
3464
|
+
const indexable = field.indexable!;
|
|
3465
|
+
const { keyFn } = indexable;
|
|
3466
|
+
const keyToHashable = indexable.keyToHashable ?? ((e: unknown): unknown => e);
|
|
3467
|
+
class Class {
|
|
3468
|
+
ret(key: unknown): unknown {
|
|
3469
|
+
const array = this[property] as ReadonlyArray<unknown>;
|
|
3470
|
+
const frozenArrayInfo = frozenArrayRegistry.get(array)!;
|
|
3471
|
+
let { keyFnToIndexing } = frozenArrayInfo;
|
|
3472
|
+
if (!keyFnToIndexing) {
|
|
3473
|
+
frozenArrayInfo.keyFnToIndexing = keyFnToIndexing = //
|
|
3474
|
+
new Map<unknown, Map<unknown, unknown>>();
|
|
3475
|
+
}
|
|
3476
|
+
let hashableToValue = keyFnToIndexing.get(keyFn);
|
|
3477
|
+
if (!hashableToValue) {
|
|
3478
|
+
// The array has not been indexed yet. Index it.
|
|
3479
|
+
hashableToValue = new Map<unknown, unknown>();
|
|
3480
|
+
for (const v of array) {
|
|
3481
|
+
const hashable = keyToHashable(keyFn(v));
|
|
3482
|
+
hashableToValue.set(hashable, v);
|
|
3483
|
+
}
|
|
3484
|
+
keyFnToIndexing.set(keyFn, hashableToValue);
|
|
3485
|
+
}
|
|
3486
|
+
return hashableToValue.get(keyToHashable(key));
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
[_: string]: unknown;
|
|
3490
|
+
}
|
|
3491
|
+
return Class.prototype.ret;
|
|
3492
|
+
}
|