mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
feat(struct): reuse initialize in deserialize
This commit is contained in:
parent
13526e9020
commit
b1cc2ce9a2
13 changed files with 522 additions and 317 deletions
|
@ -1,13 +1,16 @@
|
|||
# @yume-chan/struct
|
||||
|
||||
Serialize and deserialize C-style structures.
|
||||
C-style structure serializer and deserializer.
|
||||
|
||||
Fully compatible with TypeScript.
|
||||
|
||||
- [Compatibility](#compatibility)
|
||||
- [Quick Start](#quick-start)
|
||||
- [API](#api)
|
||||
- [`placeholder` method](#placeholder-method)
|
||||
- [`Struct` constructor](#struct-constructor)
|
||||
- [`int32`/`uint32` methods](#int32uint32-methods)
|
||||
- [`Struct#int32`/`Struct#uint32` methods](#structint32structuint32-methods)
|
||||
- [`Struct#uint64` method](#structuint64-method)
|
||||
- [`extra` function](#extra-function)
|
||||
- [`afterParsed` method](#afterparsed-method)
|
||||
- [`deserialize` method](#deserialize-method)
|
||||
|
@ -26,6 +29,39 @@ Fully compatible with TypeScript.
|
|||
- [`registerFieldTypeDefinition` method](#registerfieldtypedefinition-method)
|
||||
- [Data flow](#data-flow)
|
||||
|
||||
## Compatibility
|
||||
|
||||
Basic usage requires [`Promise`][MDN_Promise], [`ArrayBuffer`][MDN_ArrayBuffer], [`Uint8Array`][MDN_Uint8Array] and [`DataView`][MDN_DataView]. All can be globally polyfilled to support older runtime.
|
||||
|
||||
[MDN_Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
[MDN_ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
|
||||
[MDN_Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
|
||||
[MDN_DataView]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
|
||||
|
||||
| Runtime | Minimal Supported Version | Note |
|
||||
| --------------------- | ------------------------- | ------------------------------- |
|
||||
| **Chrome** | 32 | |
|
||||
| **Edge** | 12 | |
|
||||
| **Firefox** | 29 | |
|
||||
| **Internet Explorer** | 10 | Requires polyfill for `Promise` |
|
||||
| **Safari** | 8 | |
|
||||
| **Node.js** | 0.12 | |
|
||||
|
||||
Usage of `uint64` requires [`BigInt`][MDN_BigInt] (**can't** be polyfilled), [`DataView#getBigUint64`][MDN_DataView_getBigUint64] and [`DataView#setBigUint64`][MDN_DataView_setBigUint64] (can be polyfilled).
|
||||
|
||||
[MDN_BigInt]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
|
||||
[MDN_DataView_getBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64
|
||||
[MDN_DataView_setBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64
|
||||
|
||||
| Runtime | Minimal Supported Version | Note |
|
||||
| --------------------- | ------------------------- | ---------------------------------------------------------------------- |
|
||||
| **Chrome** | 67 | |
|
||||
| **Edge** | 79 | |
|
||||
| **Firefox** | 68 | |
|
||||
| **Internet Explorer** | *N/A* | Doesn't support `BigInt`, can't be polyfilled. |
|
||||
| **Safari** | 14 | Requires polyfills for `DataView#getBigUint64`/`DataView#setBigUint64` |
|
||||
| **Node.js** | 10.4.0 | |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
|
@ -37,39 +73,62 @@ const MyStruct =
|
|||
.int32('bar');
|
||||
|
||||
const value = MyStruct.deserialize(someStream);
|
||||
// TypeScript can infer type of the result object.
|
||||
const { foo, bar } = value;
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
**While all APIs heavily rely on generic, DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
|
||||
### `placeholder` method
|
||||
|
||||
```ts
|
||||
export function placeholder<T>(): T {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
```
|
||||
|
||||
Return a (fake) value of the given type.
|
||||
|
||||
Because TypeScript only supports supply all or none type arguments, this library allows all type parameters to be inferred from arguments.
|
||||
|
||||
This method can be used where an argument is only used to infer a type parameter.
|
||||
|
||||
**While all following APIs heavily rely on generic, DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
|
||||
|
||||
### `Struct` constructor
|
||||
|
||||
```ts
|
||||
declare class Struct<TObject = {}, TAfterParsed = undefined, TInit = {}> {
|
||||
export default class Struct<
|
||||
TResult extends object = {},
|
||||
TInit extends object = {},
|
||||
TExtra extends object = {},
|
||||
TAfterParsed = undefined,
|
||||
> {
|
||||
public constructor(options: Partial<StructOptions> = StructDefaultOptions);
|
||||
}
|
||||
```
|
||||
|
||||
Create a new structure definition.
|
||||
Creates a new structure definition.
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
1. `TObject`: Type of the result object.
|
||||
1. `TAfterParsed`: Special case for the `afterParsed` function.
|
||||
1. `TInit`: Type requirement to create such a structure. (Because some fields may implies other fields, and there is the `extra` function)
|
||||
1. `TResult`: Type of the result object.
|
||||
2. `TInit`: Type requirement to create such a structure. (Because some fields may implies other fields)
|
||||
3. `TExtra`: Type of extra fields.
|
||||
4. `TAfterParsed`: State of the `afterParsed` function.
|
||||
|
||||
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
|
||||
|
||||
These are considered "internal state" of the `Struct` and it will take care of them by itself.
|
||||
These are considered "internal state" of the `Struct` and will be taken care of by methods below.
|
||||
|
||||
**Parameters**
|
||||
|
||||
1. `options`:
|
||||
* `littleEndian:boolean = false`: Whether all multi-byte fields are little-endian encoded.
|
||||
* `littleEndian:boolean = false`: Whether all multi-byte fields are [little-endian encoded][Wikipeida_Endianess].
|
||||
|
||||
### `int32`/`uint32` methods
|
||||
[Wikipeida_Endianess]: https://en.wikipedia.org/wiki/Endianness
|
||||
|
||||
### `Struct#int32`/`Struct#uint32` methods
|
||||
|
||||
```ts
|
||||
public int32<
|
||||
|
@ -77,12 +136,13 @@ public int32<
|
|||
TTypeScriptType = number
|
||||
>(
|
||||
name: TName,
|
||||
options: {} = {},
|
||||
_typescriptType?: () => TTypeScriptType,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TObject & Record<TName, TTypeScriptType>,
|
||||
TResult & Record<TName, TTypeScriptType>,
|
||||
TInit & Record<TName, TTypeScriptType>,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
TInit & Record<TName, TTypeScriptType>
|
||||
>;
|
||||
|
||||
public uint32<
|
||||
|
@ -91,20 +151,25 @@ public uint32<
|
|||
>(
|
||||
name: TName,
|
||||
options: {} = {},
|
||||
_typescriptType?: () => TTypeScriptType,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TObject & Record<TName, TTypeScriptType>,
|
||||
TResult & Record<TName, TTypeScriptType>,
|
||||
TInit & Record<TName, TTypeScriptType>,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
TInit & Record<TName, TTypeScriptType>
|
||||
>;
|
||||
```
|
||||
|
||||
Append an int32/uint32 field into the `Struct`.
|
||||
Return a new `Struct` instance with an `int32`/`uint32` field appended to the end.
|
||||
|
||||
The original `Struct` instance will not be changed.
|
||||
|
||||
TypeScript will also append a `name: TTypeScriptType` field into the result object and the init object.
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
1. `TName`: Literal type of the field's name.
|
||||
1. `TTypeScriptType = number`: Type of the field in the result object.
|
||||
2. `TTypeScriptType = number`: Type of the field in the result object.
|
||||
|
||||
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
|
||||
|
||||
|
@ -114,15 +179,19 @@ TypeScript will infer them from arguments. See examples below.
|
|||
|
||||
1. `name`: (Required) Field name. Should be a string literal to make types work.
|
||||
1. `options`: currently unused.
|
||||
1. `_typescriptType`: Supply a function to change the field's type. See examples below.
|
||||
2. `_typescriptType`: Set field's type. See examples below.
|
||||
|
||||
**Returns**
|
||||
**Note**
|
||||
|
||||
A new instance of `Struct`, with `{ [name]: TTypeScriptType }` added to its result object type.
|
||||
There is no generic constraints on the `TTypeScriptType` type because TypeScript doesn't allow casting enum types to `number`.
|
||||
|
||||
So it's technically possible to pass in an incompatible type (e.g. `string`).
|
||||
|
||||
But obviously, it's a bad idea.
|
||||
|
||||
**Examples**
|
||||
|
||||
1. Add an int32 field named `foo`
|
||||
1. Append an int32 field named `foo`
|
||||
|
||||
```ts
|
||||
const struct = new Struct()
|
||||
|
@ -136,7 +205,7 @@ A new instance of `Struct`, with `{ [name]: TTypeScriptType }` added to its resu
|
|||
struct.create({ foo: 42 }) // ok
|
||||
```
|
||||
|
||||
2. Set `foo`'s type
|
||||
2. Set `foo`'s type (can be used with the [`placeholder` method](#placeholder-method))
|
||||
|
||||
```ts
|
||||
enum MyEnum {
|
||||
|
@ -145,50 +214,79 @@ A new instance of `Struct`, with `{ [name]: TTypeScriptType }` added to its resu
|
|||
}
|
||||
|
||||
const struct = new Struct()
|
||||
.int32('foo', () => MyEnum.a); // The inferred return type is `MyEnum`
|
||||
.int32('foo', placeholder<MyEnum>())
|
||||
.int32('bar', MyEnum.a as const);
|
||||
|
||||
const value = await struct.deserialize(stream);
|
||||
value.foo; // MyEnum
|
||||
value.bar; // MyEnum.a
|
||||
|
||||
struct.create({ foo: 42 }); // error: 'foo' must be of type `MyEnum`
|
||||
struct.create({ foo: MyEnum.b }); // ok
|
||||
|
||||
// Although there is no generic constraints on the `TTypeScriptType`
|
||||
// So it's possible to set it to an incompatible type, like `string`
|
||||
// But obviously, it's a bad idea
|
||||
struct.create({ foo: 42, bar: MyEnum.a }); // error: 'foo' must be of type `MyEnum`
|
||||
struct.create({ foo: MyEnum.a, bar: MyEnum.b }); // error: 'bar' must be of type `MyEnum.a`
|
||||
struct.create({ foo: MyEnum.a, bar: MyEnum.b }); // ok
|
||||
```
|
||||
|
||||
3. Create a new struct by extending exist one
|
||||
3. Create a new struct by extending existing one
|
||||
|
||||
```ts
|
||||
const struct1 = new Struct()
|
||||
.int32('foo');
|
||||
|
||||
const struct2 = struct1
|
||||
.int32('bar'); // `struct1` was not changed
|
||||
.int32('bar');
|
||||
|
||||
assert(struct2 !== struct1);
|
||||
// `struct1` will not be changed
|
||||
```
|
||||
|
||||
### `Struct#uint64` method
|
||||
|
||||
```ts
|
||||
public uint64<
|
||||
TName extends string,
|
||||
TTypeScriptType = bigint
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TResult & Record<TName, TTypeScriptType>,
|
||||
TInit & Record<TName, TTypeScriptType>,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
>;
|
||||
```
|
||||
|
||||
Return a new `Struct` instance with an `uint64` field appended to the end.
|
||||
|
||||
The original `Struct` instance will not be changed.
|
||||
|
||||
TypeScript will also append a `name: TTypeScriptType` field into the result object and the init object.
|
||||
|
||||
Require native `BigInt` support in runtime. See [compatibility](#compatibility).
|
||||
|
||||
### `extra` function
|
||||
|
||||
```ts
|
||||
public extra<TExtra extends object>(
|
||||
value: TExtra & ThisType<Omit<TObject, keyof TExtra> & TExtra>
|
||||
public extra<TValue extends object>(
|
||||
value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>>
|
||||
): Struct<
|
||||
StructExtraResult<TObject, TExtra>,
|
||||
TAfterParsed,
|
||||
Omit<TInit, keyof TExtra>
|
||||
TResult,
|
||||
TInit,
|
||||
Overwrite<TExtra, TValue>,
|
||||
TAfterParsed
|
||||
>;
|
||||
```
|
||||
|
||||
Add custom extra fields to the structure.
|
||||
Return a new `Struct` instance adding some extra fields.
|
||||
|
||||
Extra fields will be added onto the result object after deserializing all fields, overwriting exist fields with same name.
|
||||
The original `Struct` instance will not be changed.
|
||||
|
||||
If an extra field doesn't overlap the existing fields, it will not be serialized. However if it does, the value of extra field will be serialized instead of the parsed value.
|
||||
TypeScript will also append all extra fields into the result object (if not already exited).
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
1. `TExtra`: Type of your extra fields.
|
||||
1. `TValue`: Type of the extra fields.
|
||||
|
||||
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
|
||||
|
||||
|
@ -196,11 +294,13 @@ TypeScript will infer them from arguments. See examples below.
|
|||
|
||||
**Parameters**
|
||||
|
||||
1. `extra`: An object containing anything you want to add to the result object. Getters/setters and methods are also allowed.
|
||||
1. `value`: An object containing anything you want to add to the result object. Accessors and methods are also allowed.
|
||||
|
||||
**Returns**
|
||||
**Note**
|
||||
|
||||
A new instance of `Struct`, with `extra` merged with existing ones.
|
||||
1. If the current `Struct` already has some extra fields, it will be merged with `value`, with `value` taking precedence.
|
||||
2. Extra fields will not be serialized.
|
||||
3. Extra fields will be ignored if it has the same name with some defined fields.
|
||||
|
||||
**Examples**
|
||||
|
||||
|
@ -247,30 +347,27 @@ A new instance of `Struct`, with `extra` merged with existing ones.
|
|||
|
||||
```ts
|
||||
public afterParsed(
|
||||
callback: StructAfterParsed<TObject, never>
|
||||
): Struct<TObject, never, TInit>
|
||||
public afterParsed(
|
||||
callback?: StructAfterParsed<TObject, void>
|
||||
): Struct<TObject, undefined, TInit>;
|
||||
callback?: StructAfterParsed<TResult, void>
|
||||
): Struct<TResult, TInit, TExtra, undefined>;
|
||||
```
|
||||
|
||||
Run custom logic after deserializing.
|
||||
Return a new `Struct` instance, registering (or replacing) a custom callback to be run after deserialized.
|
||||
|
||||
Call `afterParsed` again will replace existing callback.
|
||||
The original `Struct` instance will not be changed.
|
||||
|
||||
```ts
|
||||
public afterParsed<TResult>(
|
||||
callback?: StructAfterParsed<TObject, TResult>
|
||||
): Struct<TObject, TResult, TInit>;
|
||||
public afterParsed<TAfterParsed>(
|
||||
callback?: StructAfterParsed<TResult, TAfterParsed>
|
||||
): Struct<TResult, TInit, TExtra, TAfterParsed>;
|
||||
```
|
||||
|
||||
Run custom logic after deserializing and replace the result object with the returned value.
|
||||
Return a new `Struct` instance, registering (or replacing) a custom callback to be run after deserialized, and replacing the result object with the returned value.
|
||||
|
||||
Call `afterParsed` again will replace existing callback.
|
||||
The original `Struct` instance will not be changed.
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
1. `TResult`: Type of the result object
|
||||
1. `TAfterParsed`: Type of the new result object.
|
||||
|
||||
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
|
||||
|
||||
|
@ -280,10 +377,6 @@ TypeScript will infer them from arguments. See examples below.
|
|||
|
||||
1. `callback`: An function contains the custom logic to be run, optionally returns a new result object. Or `undefined`, to clear the previously set `afterParsed` callback.
|
||||
|
||||
**Returns**
|
||||
|
||||
A new instance of `Struct`, with `afterParsed` callback replaced.
|
||||
|
||||
**Examples**
|
||||
|
||||
1. Handle an "error" packet
|
||||
|
@ -343,7 +436,7 @@ A new instance of `Struct`, with `afterParsed` callback replaced.
|
|||
```ts
|
||||
public async deserialize(
|
||||
context: StructDeserializationContext
|
||||
): Promise<TAfterParsed extends undefined ? TObject : TAfterParsed>
|
||||
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed>;
|
||||
```
|
||||
|
||||
Deserialize one structure from the `context`.
|
||||
|
@ -366,28 +459,25 @@ There are two concepts around the type plugin system.
|
|||
|
||||
### Backing Field
|
||||
|
||||
This library defines a common method to hide implementation details of each field types on the result object.
|
||||
|
||||
It can be accessed with the exported `BackingField` symbol.
|
||||
The result object has a hidden backing field, containing implementation details of each field.
|
||||
|
||||
```ts
|
||||
import { BackingField } from '@yume-chan/struct';
|
||||
import { getBackingField, setBackingField } from '@yume-chan/struct';
|
||||
|
||||
const backingField = resultObject[BackingField];
|
||||
const value = getBackingField<number>(resultObject, 'foo');
|
||||
setBackingField(resultObject, 'foo', value);
|
||||
```
|
||||
|
||||
The backing field is a map between field key and arbitrary data each type definition want to store. Normally type definitions will store info on the backing field and then define getter/setter on the result object to access them.
|
||||
|
||||
It's also possible to access other fields' data if you know the data type. But it's not recommended to modify them.
|
||||
It's possible to access other fields' data if you know the type. But it's not recommended to modify them.
|
||||
|
||||
### `FieldDescriptorBase` interface
|
||||
|
||||
This interface describe one field in the struct, and will be stored in `Struct` class.
|
||||
This interface describes one field, and will be stored in `Struct` class.
|
||||
|
||||
**Generic Parameters**
|
||||
|
||||
* `TName extends string = string`: Name of the field. Although `FieldDescriptorBase` doesn't need it to be generic, derived types will need it. So marking this way helps TypeScript infer the type.
|
||||
* `TResultObject = {}`: Type that will be merged into the result object (`TObject`). Any key that has `never` type will be removed.
|
||||
* `TResultObject = {}`: Type that will be merged into the result object (`TResult`). Any key that has `never` type will be removed.
|
||||
* `TInitObject = {}`: Type that will be merged into the init object (`TInit`). Any key that has `never` type will be removed. Normally you only need to add the current field into `TInit`, but sometimes one field will imply other fields, so you may want to also remove those implied fields from `TInit`.
|
||||
* `TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions`: Type of the `options`. currently `FieldDescriptorBaseOptions` is empty but maybe something will happen later.
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@yume-chan/struct",
|
||||
"version": "0.0.0",
|
||||
"description": "Easy to use C structure serializer and deserializer",
|
||||
"description": "C-style structure serializer and deserializer.",
|
||||
"keywords": [
|
||||
"structure",
|
||||
"typescript"
|
||||
|
|
20
packages/struct/src/backing-field.ts
Normal file
20
packages/struct/src/backing-field.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export const BackingField = Symbol('BackingField');
|
||||
|
||||
export function getBackingField<T = unknown>(object: unknown, field: string): T {
|
||||
return (object as any)[BackingField][field] as T;
|
||||
}
|
||||
|
||||
export function setBackingField(object: unknown, field: string, value: any): void {
|
||||
(object as any)[BackingField][field] = value;
|
||||
}
|
||||
|
||||
export function defineSimpleAccessors(object: unknown, field: string): void {
|
||||
Object.defineProperty(object, field, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() { return getBackingField(object, field); },
|
||||
set(value) { setBackingField(object, field, value); },
|
||||
});
|
||||
}
|
||||
|
||||
export type WithBackingField<T> = T & { [BackingField]: any; };
|
|
@ -1,4 +1,5 @@
|
|||
import { BackingField, FieldDescriptorBase, FieldDescriptorBaseOptions } from './descriptor';
|
||||
import { getBackingField, setBackingField } from '../backing-field';
|
||||
import { FieldDescriptorBase, FieldDescriptorBaseOptions } from './descriptor';
|
||||
|
||||
export namespace Array {
|
||||
export const enum SubType {
|
||||
|
@ -17,14 +18,6 @@ export namespace Array {
|
|||
string?: string;
|
||||
}
|
||||
|
||||
export function getBackingField(object: any, name: string): BackingField {
|
||||
return object[BackingField][name];
|
||||
}
|
||||
|
||||
export function setBackingField(object: any, name: string, value: BackingField): void {
|
||||
object[BackingField][name] = value;
|
||||
}
|
||||
|
||||
export function initialize(object: any, field: Array, value: BackingField): void {
|
||||
switch (field.subType) {
|
||||
case SubType.ArrayBuffer:
|
||||
|
@ -32,7 +25,7 @@ export namespace Array {
|
|||
configurable: true,
|
||||
enumerable: true,
|
||||
get(): ArrayBuffer {
|
||||
return getBackingField(object, field.name).buffer!;
|
||||
return getBackingField<BackingField>(object, field.name).buffer!;
|
||||
},
|
||||
set(buffer: ArrayBuffer) {
|
||||
setBackingField(object, field.name, { buffer });
|
||||
|
@ -44,7 +37,7 @@ export namespace Array {
|
|||
configurable: true,
|
||||
enumerable: true,
|
||||
get(): string {
|
||||
return getBackingField(object, field.name).string!;
|
||||
return getBackingField<BackingField>(object, field.name).string!;
|
||||
},
|
||||
set(string: string) {
|
||||
setBackingField(object, field.name, { string });
|
||||
|
|
|
@ -1,21 +1,9 @@
|
|||
import { StructDeserializationContext, StructOptions, StructSerializationContext } from '../types';
|
||||
import { FieldDescriptorBase, FieldType } from './descriptor';
|
||||
|
||||
export interface StructSerializationContext {
|
||||
encodeUtf8(input: string): ArrayBuffer;
|
||||
}
|
||||
|
||||
export interface StructDeserializationContext extends StructSerializationContext {
|
||||
decodeUtf8(buffer: ArrayBuffer): string;
|
||||
|
||||
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
export interface StructOptions {
|
||||
littleEndian: boolean;
|
||||
}
|
||||
|
||||
export interface FieldTypeDefinition<
|
||||
TDescriptor extends FieldDescriptorBase = FieldDescriptorBase,
|
||||
TInitExtra = undefined,
|
||||
> {
|
||||
type: FieldType | string;
|
||||
|
||||
|
@ -24,7 +12,7 @@ export interface FieldTypeDefinition<
|
|||
field: TDescriptor;
|
||||
object: any;
|
||||
options: StructOptions;
|
||||
}): Promise<void>;
|
||||
}): Promise<{ value: any; extra?: TInitExtra; }>;
|
||||
|
||||
getSize(options: {
|
||||
field: TDescriptor;
|
||||
|
@ -32,16 +20,17 @@ export interface FieldTypeDefinition<
|
|||
}): number;
|
||||
|
||||
getDynamicSize?(options: {
|
||||
context: StructSerializationContext,
|
||||
field: TDescriptor,
|
||||
object: any,
|
||||
options: StructOptions,
|
||||
context: StructSerializationContext;
|
||||
field: TDescriptor;
|
||||
object: any;
|
||||
options: StructOptions;
|
||||
}): number;
|
||||
|
||||
initialize?(options: {
|
||||
context: StructSerializationContext;
|
||||
field: TDescriptor;
|
||||
init: any;
|
||||
value: any;
|
||||
extra?: TInitExtra;
|
||||
object: any;
|
||||
options: StructOptions;
|
||||
}): void;
|
||||
|
@ -56,17 +45,19 @@ export interface FieldTypeDefinition<
|
|||
}): void;
|
||||
}
|
||||
|
||||
const registry: Record<number | string, FieldTypeDefinition> = {};
|
||||
const registry: Record<number | string, FieldTypeDefinition<any, any>> = {};
|
||||
|
||||
export function getFieldTypeDefinition(type: FieldType | string): FieldTypeDefinition {
|
||||
export function getFieldTypeDefinition(type: FieldType | string): FieldTypeDefinition<any, any> {
|
||||
return registry[type];
|
||||
}
|
||||
|
||||
export function registerFieldTypeDefinition<
|
||||
TDescriptor extends FieldDescriptorBase,
|
||||
TDefinition extends FieldTypeDefinition<TDescriptor>
|
||||
TInitExtra,
|
||||
TDefinition extends FieldTypeDefinition<TDescriptor, TInitExtra>
|
||||
>(
|
||||
_field: TDescriptor,
|
||||
_initExtra: TInitExtra,
|
||||
methods: TDefinition
|
||||
): void {
|
||||
registry[methods.type] = methods;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export const BackingField = Symbol('BackingField');
|
||||
|
||||
export const enum FieldType {
|
||||
Number,
|
||||
FixedLengthArray,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { getBackingField } from '../backing-field';
|
||||
import { placeholder } from '../utils';
|
||||
import { Array } from './array';
|
||||
import { registerFieldTypeDefinition } from './definition';
|
||||
import { FieldDescriptorBaseOptions, FieldType } from './descriptor';
|
||||
|
@ -25,38 +27,49 @@ export interface FixedLengthArray<
|
|||
options: TOptions;
|
||||
};
|
||||
|
||||
registerFieldTypeDefinition(undefined as unknown as FixedLengthArray, {
|
||||
registerFieldTypeDefinition(
|
||||
placeholder<FixedLengthArray>(),
|
||||
placeholder<ArrayBuffer>(),
|
||||
{
|
||||
type: FieldType.FixedLengthArray,
|
||||
|
||||
async deserialize({ context, field, object, }) {
|
||||
const value: Array.BackingField = {
|
||||
buffer: await context.read(field.options.length),
|
||||
};
|
||||
async deserialize(
|
||||
{ context, field }
|
||||
): Promise<{ value: string | ArrayBuffer, extra?: ArrayBuffer; }> {
|
||||
const buffer = await context.read(field.options.length);
|
||||
|
||||
switch (field.subType) {
|
||||
case Array.SubType.ArrayBuffer:
|
||||
break;
|
||||
return { value: buffer };
|
||||
case Array.SubType.String:
|
||||
value.string = context.decodeUtf8(value.buffer!);
|
||||
break;
|
||||
return {
|
||||
value: context.decodeUtf8(buffer),
|
||||
extra: buffer
|
||||
};
|
||||
default:
|
||||
throw new Error('Unknown type');
|
||||
}
|
||||
|
||||
Array.initialize(object, field, value);
|
||||
},
|
||||
|
||||
getSize({ field }) {
|
||||
return field.options.length;
|
||||
},
|
||||
|
||||
initialize({ field, init, object }) {
|
||||
Array.initialize(object, field, {});
|
||||
object[field.name] = init[field.name];
|
||||
initialize({ extra, field, object, value }) {
|
||||
const backingField: Array.BackingField = {};
|
||||
if (typeof value === 'string') {
|
||||
backingField.string = value;
|
||||
if (extra) {
|
||||
backingField.buffer = extra;
|
||||
}
|
||||
} else {
|
||||
backingField.buffer = value;
|
||||
}
|
||||
Array.initialize(object, field, backingField);
|
||||
},
|
||||
|
||||
serialize({ context, dataView, field, object, offset }) {
|
||||
const backingField = Array.getBackingField(object, field.name);
|
||||
const backingField = getBackingField<Array.BackingField>(object, field.name);
|
||||
backingField.buffer ??=
|
||||
context.encodeUtf8(backingField.string!);
|
||||
|
||||
|
@ -65,4 +78,5 @@ registerFieldTypeDefinition(undefined as unknown as FixedLengthArray, {
|
|||
offset
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,33 +1,40 @@
|
|||
import { placeholder } from '../utils';
|
||||
import { registerFieldTypeDefinition } from './definition';
|
||||
import { BackingField, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType } from './descriptor';
|
||||
import { FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType } from './descriptor';
|
||||
|
||||
export namespace Number {
|
||||
export type TypeScriptType = number;
|
||||
export type TypeScriptType<T extends SubType> =
|
||||
T extends SubType.Uint64 ? bigint : number;
|
||||
|
||||
export const enum SubType {
|
||||
Int32,
|
||||
Uint32,
|
||||
Uint64,
|
||||
}
|
||||
|
||||
export const SizeMap: Record<SubType, number> = {
|
||||
[SubType.Int32]: 4,
|
||||
[SubType.Uint32]: 4,
|
||||
[SubType.Uint64]: 8,
|
||||
};
|
||||
|
||||
export const DataViewGetterMap = {
|
||||
[SubType.Int32]: 'getInt32',
|
||||
[SubType.Uint32]: 'getUint32',
|
||||
[SubType.Uint64]: 'getBigUint64',
|
||||
} as const;
|
||||
|
||||
export const DataViewSetterMap = {
|
||||
[SubType.Int32]: 'setInt32',
|
||||
[SubType.Uint32]: 'setUint32',
|
||||
[SubType.Uint64]: 'setBigUint64',
|
||||
} as const;
|
||||
}
|
||||
|
||||
export interface Number<
|
||||
TName extends string = string,
|
||||
TTypeScriptType = Number.TypeScriptType,
|
||||
TSubType extends Number.SubType = Number.SubType,
|
||||
TTypeScriptType = Number.TypeScriptType<TSubType>,
|
||||
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
|
||||
> extends FieldDescriptorBase<
|
||||
TName,
|
||||
|
@ -37,37 +44,35 @@ export interface Number<
|
|||
> {
|
||||
type: FieldType.Number;
|
||||
|
||||
subType: Number.SubType;
|
||||
subType: TSubType;
|
||||
}
|
||||
|
||||
registerFieldTypeDefinition(undefined as unknown as Number, {
|
||||
registerFieldTypeDefinition(
|
||||
placeholder<Number>(),
|
||||
undefined,
|
||||
{
|
||||
type: FieldType.Number,
|
||||
|
||||
getSize({ field }) {
|
||||
return Number.SizeMap[field.subType];
|
||||
},
|
||||
|
||||
async deserialize({ context, field, object, options }) {
|
||||
async deserialize({ context, field, options }) {
|
||||
const buffer = await context.read(Number.SizeMap[field.subType]);
|
||||
const view = new DataView(buffer);
|
||||
const value = view[Number.DataViewGetterMap[field.subType]](
|
||||
0,
|
||||
options.littleEndian
|
||||
);
|
||||
object[BackingField][field.name] = value;
|
||||
Object.defineProperty(object, field.name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() { return object[BackingField][field.name]; },
|
||||
set(value) { object[BackingField][field.name] = value; },
|
||||
});
|
||||
return { value };
|
||||
},
|
||||
|
||||
serialize({ dataView, field, object, offset, options }) {
|
||||
dataView[Number.DataViewSetterMap[field.subType]](
|
||||
(dataView[Number.DataViewSetterMap[field.subType]] as any)(
|
||||
offset,
|
||||
object[field.name],
|
||||
options.littleEndian
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Identity } from '../utils';
|
||||
import { getBackingField, setBackingField } from '../backing-field';
|
||||
import { StructSerializationContext } from '../types';
|
||||
import { Identity, placeholder } from '../utils';
|
||||
import { Array } from './array';
|
||||
import { registerFieldTypeDefinition, StructSerializationContext } from './definition';
|
||||
import { BackingField, FieldDescriptorBaseOptions, FieldType } from './descriptor';
|
||||
import { registerFieldTypeDefinition } from './definition';
|
||||
import { FieldDescriptorBaseOptions, FieldType } from './descriptor';
|
||||
|
||||
export namespace VariableLengthArray {
|
||||
export type TypeScriptTypeCanBeUndefined<
|
||||
|
@ -40,8 +42,11 @@ export namespace VariableLengthArray {
|
|||
emptyBehavior?: TEmptyBehavior;
|
||||
}
|
||||
|
||||
export function getLengthBackingField(object: any, field: VariableLengthArray): number | undefined {
|
||||
return object[BackingField][field.options.lengthField];
|
||||
export function getLengthBackingField(
|
||||
object: any,
|
||||
field: VariableLengthArray
|
||||
): number | undefined {
|
||||
return getBackingField<number>(object, field.options.lengthField);
|
||||
}
|
||||
|
||||
export function setLengthBackingField(
|
||||
|
@ -49,7 +54,7 @@ export namespace VariableLengthArray {
|
|||
field: VariableLengthArray,
|
||||
value: number | undefined
|
||||
) {
|
||||
object[BackingField][field.options.lengthField] = value;
|
||||
setBackingField(object, field.options.lengthField, value);
|
||||
}
|
||||
|
||||
export function initialize(
|
||||
|
@ -101,7 +106,7 @@ export namespace VariableLengthArray {
|
|||
get() {
|
||||
let value = getLengthBackingField(object, field);
|
||||
if (value === undefined) {
|
||||
const backingField = Array.getBackingField(object, field.name);
|
||||
const backingField = getBackingField<Array.BackingField>(object, field.name);
|
||||
const buffer = context.encodeUtf8(backingField.string!);
|
||||
backingField.buffer = buffer;
|
||||
|
||||
|
@ -115,7 +120,7 @@ export namespace VariableLengthArray {
|
|||
default:
|
||||
throw new Error('Unknown type');
|
||||
}
|
||||
Array.setBackingField(object, field.name, value);
|
||||
setBackingField(object, field.name, value);
|
||||
if (value.buffer) {
|
||||
setLengthBackingField(object, field, value.buffer.byteLength);
|
||||
}
|
||||
|
@ -142,33 +147,43 @@ export interface VariableLengthArray<
|
|||
options: TOptions;
|
||||
}
|
||||
|
||||
registerFieldTypeDefinition(undefined as unknown as VariableLengthArray, {
|
||||
registerFieldTypeDefinition(
|
||||
placeholder<VariableLengthArray>(),
|
||||
placeholder<ArrayBuffer>(),
|
||||
{
|
||||
type: FieldType.VariableLengthArray,
|
||||
|
||||
async deserialize({ context, field, object }) {
|
||||
const value: Array.BackingField = {};
|
||||
async deserialize(
|
||||
{ context, field, object }
|
||||
): Promise<{ value: string | ArrayBuffer | undefined, extra?: ArrayBuffer; }> {
|
||||
const length = object[field.options.lengthField];
|
||||
if (length === 0) {
|
||||
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
|
||||
value.buffer = new ArrayBuffer(0);
|
||||
value.string = '';
|
||||
}
|
||||
|
||||
VariableLengthArray.initialize(object, field, value, context);
|
||||
return;
|
||||
}
|
||||
|
||||
value.buffer = await context.read(length);
|
||||
switch (field.subType) {
|
||||
case Array.SubType.ArrayBuffer:
|
||||
break;
|
||||
return { value: new ArrayBuffer(0) };
|
||||
case Array.SubType.String:
|
||||
value.string = context.decodeUtf8(value.buffer);
|
||||
break;
|
||||
return { value: '', extra: new ArrayBuffer(0) };
|
||||
default:
|
||||
throw new Error('Unknown type');
|
||||
}
|
||||
} else {
|
||||
return { value: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = await context.read(length);
|
||||
switch (field.subType) {
|
||||
case Array.SubType.ArrayBuffer:
|
||||
return { value: buffer };
|
||||
case Array.SubType.String:
|
||||
return {
|
||||
value: context.decodeUtf8(buffer),
|
||||
extra: buffer
|
||||
};
|
||||
default:
|
||||
throw new Error('Unknown type');
|
||||
}
|
||||
VariableLengthArray.initialize(object, field, value, context);
|
||||
},
|
||||
|
||||
getSize() { return 0; },
|
||||
|
@ -177,16 +192,26 @@ registerFieldTypeDefinition(undefined as unknown as VariableLengthArray, {
|
|||
return object[field.options.lengthField];
|
||||
},
|
||||
|
||||
initialize({ context, field, init, object }) {
|
||||
VariableLengthArray.initialize(object, field, {}, context);
|
||||
object[field.name] = init[field.name];
|
||||
initialize({ context, extra, field, object, value }) {
|
||||
const backingField: Array.BackingField = {};
|
||||
if (typeof value === 'string') {
|
||||
backingField.string = value;
|
||||
if (extra) {
|
||||
backingField.buffer = extra;
|
||||
}
|
||||
} else {
|
||||
backingField.buffer = value;
|
||||
}
|
||||
Array.initialize(object, field, backingField);
|
||||
VariableLengthArray.initialize(object, field, backingField, context);
|
||||
},
|
||||
|
||||
serialize({ dataView, field, object, offset }) {
|
||||
const backingField = Array.getBackingField(object, field.name);
|
||||
const backingField = getBackingField<Array.BackingField>(object, field.name);
|
||||
new Uint8Array(dataView.buffer).set(
|
||||
new Uint8Array(backingField.buffer!),
|
||||
offset
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export * from './backing-field';
|
||||
export * from './field';
|
||||
export * from './struct';
|
||||
export { default as Struct } from './struct';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import { Array, BackingField, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FixedLengthArray, getFieldTypeDefinition, Number, StructDeserializationContext, StructOptions, StructSerializationContext, VariableLengthArray } from './field';
|
||||
import { Evaluate, Identity } from './utils';
|
||||
import { BackingField, defineSimpleAccessors, setBackingField, WithBackingField } from './backing-field';
|
||||
import { Array, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FieldTypeDefinition, FixedLengthArray, getFieldTypeDefinition, Number, VariableLengthArray } from './field';
|
||||
import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './types';
|
||||
import { Evaluate, Identity, OmitNever, Overwrite } from './utils';
|
||||
|
||||
export type StructValueType<T extends Struct<object, object, unknown>> =
|
||||
export type StructValueType<T extends Struct<object, object, object, unknown>> =
|
||||
T extends { deserialize(reader: StructDeserializationContext): Promise<infer R>; } ? R : never;
|
||||
|
||||
export type StructInitType<T extends Struct<object, object, unknown>> =
|
||||
export type StructInitType<T extends Struct<object, object, object, unknown>> =
|
||||
T extends { create(value: infer R, ...args: any): any; } ? R : never;
|
||||
|
||||
export const StructDefaultOptions: Readonly<StructOptions> = {
|
||||
littleEndian: false,
|
||||
};
|
||||
|
||||
interface AddArrayFieldDescriptor<
|
||||
TResult extends object,
|
||||
TInit extends object,
|
||||
TExtra extends object,
|
||||
TAfterParsed
|
||||
> {
|
||||
<
|
||||
|
@ -28,6 +27,7 @@ interface AddArrayFieldDescriptor<
|
|||
): MergeStruct<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
FixedLengthArray<
|
||||
TName,
|
||||
|
@ -50,6 +50,7 @@ interface AddArrayFieldDescriptor<
|
|||
): MergeStruct<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
VariableLengthArray<
|
||||
TName,
|
||||
|
@ -65,6 +66,7 @@ interface AddArrayFieldDescriptor<
|
|||
interface AddArraySubTypeFieldDescriptor<
|
||||
TResult extends object,
|
||||
TInit extends object,
|
||||
TExtra extends object,
|
||||
TAfterParsed,
|
||||
TType extends Array.SubType
|
||||
> {
|
||||
|
@ -78,6 +80,7 @@ interface AddArraySubTypeFieldDescriptor<
|
|||
): MergeStruct<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
FixedLengthArray<
|
||||
TName,
|
||||
|
@ -94,10 +97,11 @@ interface AddArraySubTypeFieldDescriptor<
|
|||
>(
|
||||
name: TName,
|
||||
options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
|
||||
_typescriptType?: () => TTypeScriptType,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): MergeStruct<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
VariableLengthArray<
|
||||
TName,
|
||||
|
@ -110,31 +114,27 @@ interface AddArraySubTypeFieldDescriptor<
|
|||
>;
|
||||
}
|
||||
|
||||
export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
|
||||
|
||||
type MergeStruct<
|
||||
TResult extends object,
|
||||
TInit extends object,
|
||||
TExtra extends object,
|
||||
TAfterParsed,
|
||||
TDescriptor extends FieldDescriptorBase
|
||||
> =
|
||||
Identity<Struct<
|
||||
Evaluate<TResult & Exclude<TDescriptor['resultObject'], undefined>>,
|
||||
Evaluate<OmitNever<TInit & Exclude<TDescriptor['initObject'], undefined>>>,
|
||||
TExtra,
|
||||
TAfterParsed
|
||||
>>;
|
||||
|
||||
type WithBackingField<T> = T & { [BackingField]: any; };
|
||||
|
||||
export type StructExtraResult<TResult, TExtra> =
|
||||
Evaluate<Omit<TResult, keyof TExtra> & TExtra>;
|
||||
|
||||
export type StructAfterParsed<TResult, TAfterParsed> =
|
||||
(this: WithBackingField<TResult>, object: WithBackingField<TResult>) => TAfterParsed;
|
||||
|
||||
export default class Struct<
|
||||
TResult extends object = {},
|
||||
TInit extends object = {},
|
||||
TExtra extends object = {},
|
||||
TAfterParsed = undefined,
|
||||
> {
|
||||
public readonly options: Readonly<StructOptions>;
|
||||
|
@ -152,8 +152,8 @@ export default class Struct<
|
|||
this.options = { ...StructDefaultOptions, ...options };
|
||||
}
|
||||
|
||||
private clone(): Struct<any, any, any> {
|
||||
const result = new Struct<any, any, any>(this.options);
|
||||
private clone(): Struct<any, any, any, any> {
|
||||
const result = new Struct<any, any, any, any>(this.options);
|
||||
result.fields = this.fields.slice();
|
||||
result._size = this._size;
|
||||
result._extra = this._extra;
|
||||
|
@ -163,7 +163,7 @@ export default class Struct<
|
|||
|
||||
public field<TDescriptor extends FieldDescriptorBase>(
|
||||
field: TDescriptor,
|
||||
): MergeStruct<TResult, TInit, TAfterParsed, TDescriptor> {
|
||||
): MergeStruct<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
|
||||
const result = this.clone();
|
||||
result.fields.push(field);
|
||||
|
||||
|
@ -176,14 +176,15 @@ export default class Struct<
|
|||
|
||||
private number<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType
|
||||
TSubType extends Number.SubType = Number.SubType,
|
||||
TTypeScriptType = Number.TypeScriptType<TSubType>
|
||||
>(
|
||||
name: TName,
|
||||
type: Number.SubType,
|
||||
type: TSubType,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: () => TTypeScriptType,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.field<Number<TName, TTypeScriptType>>({
|
||||
return this.field<Number<TName, TSubType, TTypeScriptType>>({
|
||||
type: FieldType.Number,
|
||||
name,
|
||||
subType: type,
|
||||
|
@ -193,11 +194,11 @@ export default class Struct<
|
|||
|
||||
public int32<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Int32>
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: () => TTypeScriptType,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.number(
|
||||
name,
|
||||
|
@ -209,11 +210,11 @@ export default class Struct<
|
|||
|
||||
public uint32<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint32>
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: () => TTypeScriptType,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.number(
|
||||
name,
|
||||
|
@ -223,11 +224,27 @@ export default class Struct<
|
|||
);
|
||||
}
|
||||
|
||||
private array: AddArrayFieldDescriptor<TResult, TInit, TAfterParsed> = (
|
||||
public uint64<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint64>
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Uint64,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
}
|
||||
|
||||
private array: AddArrayFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
|
||||
name: string,
|
||||
type: Array.SubType,
|
||||
options: FixedLengthArray.Options | VariableLengthArray.Options
|
||||
): Struct<any, any, any> => {
|
||||
): Struct<any, any, any, any> => {
|
||||
if ('length' in options) {
|
||||
return this.field<FixedLengthArray>({
|
||||
type: FieldType.FixedLengthArray,
|
||||
|
@ -248,6 +265,7 @@ export default class Struct<
|
|||
public arrayBuffer: AddArraySubTypeFieldDescriptor<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
Array.SubType.ArrayBuffer
|
||||
> = <TName extends string>(
|
||||
|
@ -260,6 +278,7 @@ export default class Struct<
|
|||
public string: AddArraySubTypeFieldDescriptor<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
Array.SubType.String
|
||||
> = <TName extends string>(
|
||||
|
@ -269,11 +288,12 @@ export default class Struct<
|
|||
return this.array(name, Array.SubType.String, options);
|
||||
};
|
||||
|
||||
public extra<TExtra extends object>(
|
||||
value: TExtra & ThisType<WithBackingField<StructExtraResult<TResult, TExtra>>>
|
||||
public extra<TValue extends object>(
|
||||
value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>>
|
||||
): Struct<
|
||||
StructExtraResult<TResult, TExtra>,
|
||||
Evaluate<Omit<TInit, keyof TExtra>>,
|
||||
TResult,
|
||||
TInit,
|
||||
Overwrite<TExtra, TValue>,
|
||||
TAfterParsed
|
||||
> {
|
||||
const result = this.clone();
|
||||
|
@ -283,69 +303,90 @@ export default class Struct<
|
|||
|
||||
public afterParsed(
|
||||
callback: StructAfterParsed<TResult, never>
|
||||
): Struct<TResult, TInit, never>;
|
||||
): Struct<TResult, TInit, TExtra, never>;
|
||||
public afterParsed(
|
||||
callback?: StructAfterParsed<TResult, void>
|
||||
): Struct<TResult, TInit, undefined>;
|
||||
): Struct<TResult, TInit, TExtra, undefined>;
|
||||
public afterParsed<TAfterParsed>(
|
||||
callback?: StructAfterParsed<TResult, TAfterParsed>
|
||||
): Struct<TResult, TInit, TAfterParsed>;
|
||||
): Struct<TResult, TInit, TExtra, TAfterParsed>;
|
||||
public afterParsed(
|
||||
callback?: StructAfterParsed<TResult, any>
|
||||
): Struct<any, any, any> {
|
||||
) {
|
||||
const result = this.clone();
|
||||
result._afterParsed = callback;
|
||||
return result;
|
||||
}
|
||||
|
||||
public create(init: TInit, context: StructSerializationContext): TResult {
|
||||
private initializeField(
|
||||
context: StructSerializationContext,
|
||||
field: FieldDescriptorBase,
|
||||
fieldTypeDefinition: FieldTypeDefinition<any, any>,
|
||||
object: any,
|
||||
value: any,
|
||||
extra?: any
|
||||
) {
|
||||
if (fieldTypeDefinition.initialize) {
|
||||
fieldTypeDefinition.initialize({
|
||||
context,
|
||||
extra,
|
||||
field,
|
||||
object,
|
||||
options: this.options,
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
setBackingField(object, field.name, value);
|
||||
defineSimpleAccessors(object, field.name);
|
||||
}
|
||||
}
|
||||
|
||||
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> {
|
||||
const object: any = {
|
||||
[BackingField]: {},
|
||||
};
|
||||
Object.defineProperties(object, this._extra);
|
||||
|
||||
for (const field of this.fields) {
|
||||
const type = getFieldTypeDefinition(field.type);
|
||||
if (type.initialize) {
|
||||
type.initialize({
|
||||
const fieldTypeDefinition = getFieldTypeDefinition(field.type);
|
||||
this.initializeField(
|
||||
context,
|
||||
field,
|
||||
init,
|
||||
fieldTypeDefinition,
|
||||
object,
|
||||
options: this.options,
|
||||
});
|
||||
} else {
|
||||
object[BackingField][field.name] = (init as any)[field.name];
|
||||
Object.defineProperty(object, field.name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() { return object[BackingField][field.name]; },
|
||||
set(value) { object[BackingField][field.name] = value; },
|
||||
});
|
||||
}
|
||||
(init as any)[field.name]
|
||||
);
|
||||
}
|
||||
|
||||
Object.defineProperties(object, this._extra);
|
||||
return object;
|
||||
}
|
||||
|
||||
public async deserialize(
|
||||
context: StructDeserializationContext
|
||||
): Promise<TAfterParsed extends undefined ? TResult : TAfterParsed> {
|
||||
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
|
||||
const object: any = {
|
||||
[BackingField]: {},
|
||||
};
|
||||
Object.defineProperties(object, this._extra);
|
||||
|
||||
for (const field of this.fields) {
|
||||
await getFieldTypeDefinition(field.type).deserialize({
|
||||
const fieldTypeDefinition = getFieldTypeDefinition(field.type);
|
||||
const { value, extra } = await fieldTypeDefinition.deserialize({
|
||||
context,
|
||||
field,
|
||||
object,
|
||||
options: this.options,
|
||||
});
|
||||
this.initializeField(
|
||||
context,
|
||||
field,
|
||||
fieldTypeDefinition,
|
||||
object,
|
||||
value,
|
||||
extra
|
||||
);
|
||||
}
|
||||
|
||||
Object.defineProperties(object, this._extra);
|
||||
|
||||
if (this._afterParsed) {
|
||||
const result = this._afterParsed.call(object, object);
|
||||
if (result) {
|
||||
|
|
17
packages/struct/src/types.ts
Normal file
17
packages/struct/src/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export interface StructSerializationContext {
|
||||
encodeUtf8(input: string): ArrayBuffer;
|
||||
}
|
||||
|
||||
export interface StructDeserializationContext extends StructSerializationContext {
|
||||
decodeUtf8(buffer: ArrayBuffer): string;
|
||||
|
||||
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
export interface StructOptions {
|
||||
littleEndian: boolean;
|
||||
}
|
||||
|
||||
export const StructDefaultOptions: Readonly<StructOptions> = {
|
||||
littleEndian: false,
|
||||
};
|
|
@ -1,3 +1,12 @@
|
|||
export type Identity<T> = T;
|
||||
|
||||
export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
|
||||
|
||||
export type Overwrite<TBase extends object, TNew extends object> =
|
||||
Evaluate<Omit<TBase, keyof TNew> & TNew>;
|
||||
|
||||
export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
|
||||
|
||||
export function placeholder<T>(): T {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue