feat(struct): reuse initialize in deserialize

This commit is contained in:
Simon Chan 2020-09-26 04:13:15 +08:00
parent 13526e9020
commit b1cc2ce9a2
13 changed files with 522 additions and 317 deletions

View file

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

View file

@ -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"

View 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; };

View file

@ -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 });

View file

@ -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;

View file

@ -1,5 +1,3 @@
export const BackingField = Symbol('BackingField');
export const enum FieldType {
Number,
FixedLengthArray,

View file

@ -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
);
}
});
}
);

View file

@ -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
);
},
});
}
);

View file

@ -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
);
},
});
}
);

View file

@ -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';

View file

@ -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) {

View 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,
};

View file

@ -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;
}