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.
@@ -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
+ }