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