refactor(struct): type plugin system rewrok v2

This commit is contained in:
Simon Chan 2021-01-06 18:16:26 +08:00
parent f9502f0e6f
commit c37e3dd953
32 changed files with 865 additions and 729 deletions

View file

@ -22,8 +22,9 @@ const AdbReverseStringResponse =
.string('content', { lengthField: 'length' }); .string('content', { lengthField: 'length' });
const AdbReverseErrorResponse = const AdbReverseErrorResponse =
AdbReverseStringResponse new Struct({ littleEndian: true })
.afterParsed((value) => { .fields(AdbReverseStringResponse)
.postDeserialize((value) => {
throw new Error(value.content); throw new Error(value.content);
}); });

View file

@ -1,12 +1,12 @@
import { StructValueType } from '@yume-chan/struct'; import { Struct, StructValueType } from '@yume-chan/struct';
import { AdbBufferedStream } from '../../stream'; import { AdbBufferedStream } from '../../stream';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request'; import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response'; import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response';
import { AdbSyncLstatResponse } from './stat'; import { AdbSyncLstatResponse } from './stat';
export const AdbSyncEntryResponse = export const AdbSyncEntryResponse =
AdbSyncLstatResponse new Struct({ littleEndian: true })
.afterParsed() .fields(AdbSyncLstatResponse)
.uint32('nameLength') .uint32('nameLength')
.string('name', { lengthField: 'nameLength' }) .string('name', { lengthField: 'nameLength' })
.extra({ id: AdbSyncResponseId.Entry as const }); .extra({ id: AdbSyncResponseId.Entry as const });

View file

@ -18,7 +18,8 @@ export const AdbSyncNumberRequest =
.uint32('arg'); .uint32('arg');
export const AdbSyncDataRequest = export const AdbSyncDataRequest =
AdbSyncNumberRequest new Struct({ littleEndian: true })
.fields(AdbSyncNumberRequest)
.arrayBuffer('data', { lengthField: 'arg' }); .arrayBuffer('data', { lengthField: 'arg' });
export async function adbSyncWriteRequest( export async function adbSyncWriteRequest(

View file

@ -35,7 +35,7 @@ export const AdbSyncFailResponse =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('messageLength') .uint32('messageLength')
.string('message', { lengthField: 'messageLength' }) .string('message', { lengthField: 'messageLength' })
.afterParsed(object => { .postDeserialize(object => {
throw new Error(object.message); throw new Error(object.message);
}); });

View file

@ -20,7 +20,7 @@ export const AdbSyncLstatResponse =
get type() { return this.mode >> 12 as LinuxFileType; }, get type() { return this.mode >> 12 as LinuxFileType; },
get permission() { return this.mode & 0b00001111_11111111; }, get permission() { return this.mode & 0b00001111_11111111; },
}) })
.afterParsed((object) => { .postDeserialize((object) => {
if (object.mode === 0 && if (object.mode === 0 &&
object.size === 0 && object.size === 0 &&
object.mtime === 0 object.mtime === 0
@ -56,7 +56,7 @@ export enum AdbSyncStatErrorCode {
export const AdbSyncStatResponse = export const AdbSyncStatResponse =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('error', undefined, placeholder<AdbSyncStatErrorCode>()) .uint32('error', placeholder<AdbSyncStatErrorCode>())
.uint64('dev') .uint64('dev')
.uint64('ino') .uint64('ino')
.uint32('mode') .uint32('mode')
@ -72,7 +72,7 @@ export const AdbSyncStatResponse =
get type() { return this.mode >> 12 as LinuxFileType; }, get type() { return this.mode >> 12 as LinuxFileType; },
get permission() { return this.mode & 0b00001111_11111111; }, get permission() { return this.mode & 0b00001111_11111111; },
}) })
.afterParsed((object) => { .postDeserialize((object) => {
if (object.error) { if (object.error) {
throw new Error(AdbSyncStatErrorCode[object.error]); throw new Error(AdbSyncStatErrorCode[object.error]);
} }

View file

@ -13,7 +13,7 @@ export enum AdbCommand {
const AdbPacketHeader = const AdbPacketHeader =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('command', undefined) .uint32('command')
.uint32('arg0') .uint32('arg0')
.uint32('arg1') .uint32('arg1')
.uint32('payloadLength') .uint32('payloadLength')
@ -21,7 +21,8 @@ const AdbPacketHeader =
.int32('magic'); .int32('magic');
const AdbPacketStruct = const AdbPacketStruct =
AdbPacketHeader new Struct({ littleEndian: true })
.fields(AdbPacketHeader)
.arrayBuffer('payload', { lengthField: 'payloadLength' }); .arrayBuffer('payload', { lengthField: 'payloadLength' });
export type AdbPacket = StructValueType<typeof AdbPacketStruct>; export type AdbPacket = StructValueType<typeof AdbPacketStruct>;

View file

@ -27,6 +27,7 @@ interface Progress {
export const Install = withDisplayName('Install')(({ export const Install = withDisplayName('Install')(({
device, device,
}: RouteProps): JSX.Element => { }: RouteProps): JSX.Element => {
const [installing, setInstalling] = useState(false);
const [progress, setProgress] = useState<Progress>(); const [progress, setProgress] = useState<Progress>();
const handleOpen = useCallback(async () => { const handleOpen = useCallback(async () => {
@ -35,6 +36,7 @@ export const Install = withDisplayName('Install')(({
return; return;
} }
setInstalling(true);
setProgress({ setProgress({
filename: file.name, filename: file.name,
stage: Stage.Uploading, stage: Stage.Uploading,
@ -70,13 +72,14 @@ export const Install = withDisplayName('Install')(({
totalSize: file.size, totalSize: file.size,
value: 1, value: 1,
}); });
setInstalling(false);
}, [device]); }, [device]);
return ( return (
<> <>
<Stack horizontal> <Stack horizontal>
<DefaultButton <DefaultButton
disabled={!device || !!progress} disabled={!device || installing}
text="Open" text="Open"
onClick={handleOpen} onClick={handleOpen}
/> />

View file

@ -16,7 +16,7 @@ export enum ScrcpyControlMessageType {
export const ScrcpySimpleControlMessage = export const ScrcpySimpleControlMessage =
new Struct() new Struct()
.uint8('type', undefined, placeholder<ScrcpyControlMessageType.BackOrScreenOn>()); .uint8('type', placeholder<ScrcpyControlMessageType.BackOrScreenOn>());
export type ScrcpySimpleControlMessage = StructInitType<typeof ScrcpySimpleControlMessage>; export type ScrcpySimpleControlMessage = StructInitType<typeof ScrcpySimpleControlMessage>;
@ -38,8 +38,8 @@ export enum AndroidMotionEventAction {
export const ScrcpyInjectTouchControlMessage = export const ScrcpyInjectTouchControlMessage =
new Struct() new Struct()
.uint8('type', undefined, ScrcpyControlMessageType.InjectTouch as const) .uint8('type', ScrcpyControlMessageType.InjectTouch as const)
.uint8('action', undefined, placeholder<AndroidMotionEventAction>()) .uint8('action', placeholder<AndroidMotionEventAction>())
.uint64('pointerId') .uint64('pointerId')
.uint32('pointerX') .uint32('pointerX')
.uint32('pointerY') .uint32('pointerY')
@ -52,7 +52,7 @@ export type ScrcpyInjectTouchControlMessage = StructInitType<typeof ScrcpyInject
export const ScrcpyInjectTextControlMessage = export const ScrcpyInjectTextControlMessage =
new Struct() new Struct()
.uint8('type', undefined, ScrcpyControlMessageType.InjectText as const) .uint8('type', ScrcpyControlMessageType.InjectText as const)
.uint32('length') .uint32('length')
.string('text', { lengthField: 'length' }); .string('text', { lengthField: 'length' });
@ -99,8 +99,8 @@ export enum AndroidKeyCode {
export const ScrcpyInjectKeyCodeControlMessage = export const ScrcpyInjectKeyCodeControlMessage =
new Struct() new Struct()
.uint8('type', undefined, ScrcpyControlMessageType.InjectKeycode as const) .uint8('type', ScrcpyControlMessageType.InjectKeycode as const)
.uint8('action', undefined, placeholder<AndroidKeyEventAction>()) .uint8('action', placeholder<AndroidKeyEventAction>())
.uint32('keyCode') .uint32('keyCode')
.uint32('repeat') .uint32('repeat')
.uint32('metaState'); .uint32('metaState');

View file

@ -1,13 +1,13 @@
"use strict"; "use strict";
const tslib_1 = require("tslib"); var tslib_1 = require("tslib");
const clean_webpack_plugin_1 = require("clean-webpack-plugin"); var clean_webpack_plugin_1 = require("clean-webpack-plugin");
const copy_webpack_plugin_1 = tslib_1.__importDefault(require("copy-webpack-plugin")); var copy_webpack_plugin_1 = tslib_1.__importDefault(require("copy-webpack-plugin"));
const html_webpack_plugin_1 = tslib_1.__importDefault(require("html-webpack-plugin")); var html_webpack_plugin_1 = tslib_1.__importDefault(require("html-webpack-plugin"));
const mini_css_extract_plugin_1 = tslib_1.__importDefault(require("mini-css-extract-plugin")); var mini_css_extract_plugin_1 = tslib_1.__importDefault(require("mini-css-extract-plugin"));
const path_1 = tslib_1.__importDefault(require("path")); var path_1 = tslib_1.__importDefault(require("path"));
const webpack_bundle_analyzer_1 = require("webpack-bundle-analyzer"); var webpack_bundle_analyzer_1 = require("webpack-bundle-analyzer");
const context = path_1.default.resolve(process.cwd()); var context = path_1.default.resolve(process.cwd());
const plugins = [ var plugins = [
new clean_webpack_plugin_1.CleanWebpackPlugin(), new clean_webpack_plugin_1.CleanWebpackPlugin(),
new mini_css_extract_plugin_1.default({ new mini_css_extract_plugin_1.default({
filename: '[name].[contenthash].css', filename: '[name].[contenthash].css',
@ -29,10 +29,10 @@ const plugins = [
if (process.env.ANALYZE) { if (process.env.ANALYZE) {
plugins.push(new webpack_bundle_analyzer_1.BundleAnalyzerPlugin()); plugins.push(new webpack_bundle_analyzer_1.BundleAnalyzerPlugin());
} }
const config = (env, argv) => ({ var config = function (env, argv) { return ({
mode: 'development', mode: 'development',
devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map', devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map',
context, context: context,
target: 'web', target: 'web',
entry: { entry: {
index: './src/index.tsx', index: './src/index.tsx',
@ -43,10 +43,9 @@ const config = (env, argv) => ({
}, },
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js'], extensions: ['.ts', '.tsx', '.js'],
// @ts-expect-error typing is not up to date
fallback: { "path": require.resolve("path-browserify") }, fallback: { "path": require.resolve("path-browserify") },
}, },
plugins, plugins: plugins,
module: { module: {
rules: [ rules: [
{ test: /\.js$/, enforce: 'pre', use: ['source-map-loader'], }, { test: /\.js$/, enforce: 'pre', use: ['source-map-loader'], },
@ -59,5 +58,5 @@ const config = (env, argv) => ({
contentBase: path_1.default.resolve(context, 'lib'), contentBase: path_1.default.resolve(context, 'lib'),
port: 9000, port: 9000,
}, },
}); }); };
module.exports = config; module.exports = config;

View file

@ -1,14 +1,9 @@
import { StructDefaultOptions } from './context'; import { StructDefaultOptions } from './context';
import { GlobalStructFieldRuntimeTypeRegistry } from './registry';
describe('Runtime', () => { describe('Runtime', () => {
describe('StructDefaultOptions', () => { describe('StructDefaultOptions', () => {
it('should have `littleEndian` equals to `false`', () => { it('should have `littleEndian` that equals to `false`', () => {
expect(StructDefaultOptions.littleEndian).toBe(false); expect(StructDefaultOptions).toHaveProperty('littleEndian', false);
});
it('should have `fieldRuntimeTypeRegistry` equals to `GlobalStructFieldRuntimeTypeRegistry`', () => {
expect(StructDefaultOptions.fieldRuntimeTypeRegistry).toBe(GlobalStructFieldRuntimeTypeRegistry);
}); });
}); });
}); });

View file

@ -1,6 +1,3 @@
import type { StructFieldRuntimeTypeRegistry } from './registry';
import { GlobalStructFieldRuntimeTypeRegistry } from './registry';
/** /**
* Context with enough methods to serialize a struct * Context with enough methods to serialize a struct
*/ */
@ -21,9 +18,10 @@ export interface StructDeserializationContext extends StructSerializationContext
decodeUtf8(buffer: ArrayBuffer): string; decodeUtf8(buffer: ArrayBuffer): string;
/** /**
* Read exactly `length` bytes of data from underlying storage. * Read data from the underlying data source.
* *
* Errors can be thrown to indicates end of file or other errors. * Context should return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it should throw an error.
*/ */
read(length: number): ArrayBuffer | Promise<ArrayBuffer>; read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
} }
@ -35,11 +33,8 @@ export interface StructOptions {
* Default to `false` * Default to `false`
*/ */
littleEndian: boolean; littleEndian: boolean;
fieldRuntimeTypeRegistry: StructFieldRuntimeTypeRegistry;
} }
export const StructDefaultOptions: Readonly<StructOptions> = { export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false, littleEndian: false,
fieldRuntimeTypeRegistry: GlobalStructFieldRuntimeTypeRegistry,
}; };

View file

@ -0,0 +1,22 @@
import { FieldDefinition } from './definition';
import { FieldRuntimeValue } from './runtime-value';
describe('FieldDefinition', () => {
describe('new', () => {
it('should save the `options` parameter', () => {
class MockFieldDefinition extends FieldDefinition<number>{
public constructor(options: number) {
super(options);
}
public getSize(): number {
throw new Error('Method not implemented.');
}
public createValue(): FieldRuntimeValue<FieldDefinition<any, any, any>> {
throw new Error('Method not implemented.');
}
}
expect(new MockFieldDefinition(42)).toHaveProperty('options', 42);
});
});
});

View file

@ -0,0 +1,57 @@
import type { StructOptions, StructSerializationContext } from './context';
import type { FieldRuntimeValue } from './runtime-value';
/**
* A field definition is a bridge between its type and its runtime value.
*
* `Struct` record fields in a list of `FieldDefinition`s.
*
* When `Struct#create` or `Struct#deserialize` are called, each field's definition
* crates its own type of `FieldRuntimeValue` to manage the field value in that `Struct` instance.
*
* One `FieldDefinition` can represents multiple similar types, just returns the corresponding
* `FieldRuntimeValue` when `createValue` was called.
*
* @template TOptions TypeScript type of this definition's `options`.
* @template TValueType TypeScript type of this field.
* @template TRemoveFields Optional remove keys from current `Struct`. Should be a union of string literal types.
*/
export abstract class FieldDefinition<
TOptions = void,
TValueType = unknown,
TRemoveFields = never,
> {
public readonly options: TOptions;
/**
* When `T` is a type initiated `FieldDefinition`,
* use `T['valueType']` to retrieve its `TValueType` type parameter
*/
public readonly valueType!: TValueType;
/**
* When `T` is a type initiated `FieldDefinition`,
* use `T['removeFields']` to retrieve its `TRemoveFields` type parameter .
*/
public readonly removeFields!: TRemoveFields;
public constructor(options: TOptions) {
this.options = options;
}
/**
* When implemented in derived classes, returns the static size (or smallest size) of this field.
*
* Actual size can be retrieved from `FieldRuntimeValue#getSize`
*/
public abstract getSize(): number;
/**
* When implemented in derived classes, creates a `FieldRuntimeValue` for the current field definition.
*/
public abstract createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): FieldRuntimeValue;
}

View file

@ -1,26 +0,0 @@
export enum BuiltInFieldType {
Number,
FixedLengthArrayBufferLike,
VariableLengthArrayBufferLike,
}
export interface FieldDescriptorBaseOptions {
}
export interface FieldDescriptorBase<
TName extends string = string,
TResultObject = {},
TInitObject = {},
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> {
type: BuiltInFieldType | string;
name: TName;
options: TOptions;
resultObject?: TResultObject;
initObject?: TInitObject;
}

View file

@ -1,5 +1,4 @@
export * from './context'; export * from './context';
export * from './descriptor'; export * from './definition';
export * from './registry';
export * from './runtime-type';
export * from './runtime-value'; export * from './runtime-value';
export * from './runtime-object';

View file

@ -1,61 +0,0 @@
import { StructDeserializationContext, StructSerializationContext } from './context';
import { GlobalStructFieldRuntimeTypeRegistry, StructFieldRuntimeTypeRegistry } from './registry';
import { FieldRuntimeValue } from './runtime-type';
describe('Runtime', () => {
describe('StructFieldRuntimeTypeRegistry', () => {
it('should be able to get registered type', () => {
const registry = new StructFieldRuntimeTypeRegistry();
const type = 'mock';
const MockFieldRuntimeValue = class extends FieldRuntimeValue {
static getSize() { return 0; }
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
};
registry.register(type, MockFieldRuntimeValue);
expect(registry.get(type)).toBe(MockFieldRuntimeValue);
});
it('should throw an error if same type been registered twice', () => {
const registry = new StructFieldRuntimeTypeRegistry();
const type = 'mock';
const MockFieldRuntimeValue = class extends FieldRuntimeValue {
static getSize() { return 0; }
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
};
registry.register(type, MockFieldRuntimeValue);
expect(() => registry.register(type, MockFieldRuntimeValue)).toThrowError();
});
});
describe('GlobalStructFieldRuntimeTypeRegistry', () => {
it('should be defined', () => {
expect(GlobalStructFieldRuntimeTypeRegistry).toBeInstanceOf(StructFieldRuntimeTypeRegistry);
});
});
});

View file

@ -1,26 +0,0 @@
import type { BuiltInFieldType } from './descriptor';
import type { FieldRuntimeType } from './runtime-type';
export class StructFieldRuntimeTypeRegistry {
private store: Record<number | string, FieldRuntimeType> = {};
public get(
type: BuiltInFieldType | string
): FieldRuntimeType {
return this.store[type];
}
public register<
TConstructor extends FieldRuntimeType<any>
>(
type: BuiltInFieldType | string,
Constructor: TConstructor
): void {
if (type in this.store) {
throw new Error(`Struct field runtime type '${type}' has already been registered`);
}
this.store[type] = Constructor;
}
}
export const GlobalStructFieldRuntimeTypeRegistry = new StructFieldRuntimeTypeRegistry();

View file

@ -0,0 +1,38 @@
import { createRuntimeObject, getRuntimeValue, setRuntimeValue } from './runtime-object';
describe('RuntimeObject', () => {
describe('createRuntimeObject', () => {
it('should create a special object', () => {
const object = createRuntimeObject();
expect(Object.getOwnPropertySymbols(object)).toHaveLength(1);
});
});
describe('getRuntimeValue', () => {
it('should return previously set value', () => {
const object = createRuntimeObject();
const field = 'foo';
const value = {} as any;
setRuntimeValue(object, field, value);
expect(getRuntimeValue(object, field)).toBe(value);
});
});
describe('setRuntimeValue', () => {
it('should define a proxy property to underlying `RuntimeValue`', () => {
const object = createRuntimeObject();
const field = 'foo';
const getter = jest.fn(() => 42);
const setter = jest.fn((value: number) => { });
const value = { get: getter, set: setter } as any;
setRuntimeValue(object, field, value);
expect((object as any)[field]).toBe(42);
expect(getter).toBeCalledTimes(1);
(object as any)[field] = 100;
expect(setter).toBeCalledTimes(1);
expect(setter).lastCalledWith(100);
});
});
});

View file

@ -0,0 +1,36 @@
import { FieldRuntimeValue } from './runtime-value';
const RuntimeValues = Symbol('RuntimeValues');
export interface RuntimeObject {
[RuntimeValues]: Record<PropertyKey, FieldRuntimeValue>;
}
/** Creates a new runtime object that can be used with `getRuntimeValue` and `setRuntimeValue` */
export function createRuntimeObject(): RuntimeObject {
return {
[RuntimeValues]: {},
};
}
/** Gets the previously set `RuntimeValue` for specified `key` on `object` */
export function getRuntimeValue(object: RuntimeObject, key: PropertyKey): FieldRuntimeValue {
return object[RuntimeValues][key as any] as FieldRuntimeValue;
}
/**
* Sets the `RuntimeValue` for specified `key` on `object`,
* also sets up property accessors so reads/writes to `object`'s `key` will be forwarded to
* the underlying `RuntimeValue`
*/
export function setRuntimeValue(object: RuntimeObject, key: PropertyKey, runtimeValue: FieldRuntimeValue): void {
delete (object as any)[key];
object[RuntimeValues][key as any] = runtimeValue;
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() { return runtimeValue.get(); },
set(value) { runtimeValue.set(value); },
});
}

View file

@ -1,27 +0,0 @@
import { StructDeserializationContext, StructSerializationContext } from './context';
import { FieldRuntimeValue } from './runtime-type';
describe('Runtime', () => {
describe('FieldRuntimeValue', () => {
it('`getSize` should return same value as static `getSize`', () => {
class MockFieldRuntimeValue extends FieldRuntimeValue {
static getSize() { return 42; }
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
const instance = new MockFieldRuntimeValue(undefined as any, undefined as any, undefined as any, undefined as any);
expect(instance.getSize()).toBe(42);
});
});
});

View file

@ -1,45 +0,0 @@
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { FieldDescriptorBase } from './descriptor';
export abstract class FieldRuntimeValue<TDescriptor extends FieldDescriptorBase = FieldDescriptorBase> {
public readonly descriptor: TDescriptor;
public readonly options: Readonly<StructOptions>;
public readonly context: StructSerializationContext;
public constructor(
descriptor: TDescriptor,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
) {
this.descriptor = descriptor;
this.options = options;
this.context = context;
}
public abstract deserialize(context: StructDeserializationContext, object: any): void | Promise<void>;
public getSize(): number {
const Constructor = Object.getPrototypeOf(this).constructor as FieldRuntimeType<TDescriptor>;
return Constructor.getSize(this.descriptor, this.options);
}
public abstract get(): unknown;
public abstract set(value: unknown): void;
public abstract serialize(dataView: DataView, offset: number, context: StructSerializationContext): void;
}
export interface FieldRuntimeType<TDescriptor extends FieldDescriptorBase = FieldDescriptorBase> {
new(
descriptor: TDescriptor,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
): FieldRuntimeValue<TDescriptor>;
getSize(descriptor: TDescriptor, options: Readonly<StructOptions>): number;
}

View file

@ -1,34 +1,67 @@
import { createObjectWithRuntimeValues, getRuntimeValue, setRuntimeValue } from './runtime-value'; import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import { FieldDefinition } from './definition';
import { FieldRuntimeValue } from './runtime-value';
describe('Runtime', () => { describe('FieldRuntimeValue', () => {
describe('RuntimeValue', () => { describe('.constructor', () => {
it('`createObjectWithRuntimeValues` should create an object with symbol', () => { it('should save parameters', () => {
const object = createObjectWithRuntimeValues(); class MockFieldRuntimeValue extends FieldRuntimeValue {
expect(Object.getOwnPropertySymbols(object)).toHaveLength(1); public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
const definition = 1 as any;
const options = 2 as any;
const context = 3 as any;
const object = 4 as any;
const fieldRuntimeValue = new MockFieldRuntimeValue(definition, options, context, object);
expect(fieldRuntimeValue).toHaveProperty('definition', definition);
expect(fieldRuntimeValue).toHaveProperty('options', options);
expect(fieldRuntimeValue).toHaveProperty('context', context);
expect(fieldRuntimeValue).toHaveProperty('object', object);
});
}); });
it('`getRuntimeValue` should return previously set value', () => { describe('#getSize', () => {
const object = createObjectWithRuntimeValues(); it('should return same value as definition\'s', () => {
const field = 'foo'; class MockFieldDefinition extends FieldDefinition {
const value = {} as any; public getSize(): number {
setRuntimeValue(object, field, value); return 42;
expect(getRuntimeValue(object, field)).toBe(value); }
}); public createValue(options: Readonly<StructOptions>, context: StructSerializationContext, object: any): FieldRuntimeValue<FieldDefinition<any, any, any>> {
throw new Error('Method not implemented.');
}
}
it('`setRuntimeValue` should define a proxy to underlying `RuntimeValue`', () => { class MockFieldRuntimeValue extends FieldRuntimeValue {
const object = createObjectWithRuntimeValues(); public deserialize(context: StructDeserializationContext): void | Promise<void> {
const field = 'foo'; throw new Error('Method not implemented.');
const getter = jest.fn(() => 42); }
const setter = jest.fn((value: number) => { }); public get(): unknown {
const value = { get: getter, set: setter } as any; throw new Error('Method not implemented.');
setRuntimeValue(object, field, value); }
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
expect((object as any)[field]).toBe(42); const fieldDefinition = new MockFieldDefinition();
expect(getter).toBeCalledTimes(1); const fieldRuntimeValue = new MockFieldRuntimeValue(fieldDefinition, undefined as any, undefined as any, undefined as any);
expect(fieldRuntimeValue.getSize()).toBe(42);
(object as any)[field] = 100;
expect(setter).toBeCalledTimes(1);
expect(setter).lastCalledWith(100);
}); });
}); });
}); });

View file

@ -1,28 +1,69 @@
import { FieldRuntimeValue } from './runtime-type'; import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { FieldDefinition } from './definition';
const RuntimeValues = Symbol('RuntimeValues'); /**
* Field runtime value manages one field of one `Struct` instance.
*
* If one `FieldDefinition` needs to change other field's semantics
* It can override other fields' `FieldRuntimeValue` in its own `FieldRuntimeValue`'s constructor
*/
export abstract class FieldRuntimeValue<
TDefinition extends FieldDefinition<any, any, any> = FieldDefinition<any, any, any>
> {
/** Gets the definition associated with this runtime value */
public readonly definition: TDefinition;
export interface WithRuntimeValues { /** Gets the options of the associated `Struct` */
[RuntimeValues]: Record<string, FieldRuntimeValue>; public readonly options: Readonly<StructOptions>;
/** Gets the serialization context of the associated `Struct` instance */
public readonly context: StructSerializationContext;
/** Gets the associated `Struct` instance */
public readonly object: any;
public constructor(
definition: TDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
) {
this.definition = definition;
this.options = options;
this.context = context;
this.object = object;
} }
export function createObjectWithRuntimeValues(): WithRuntimeValues { /** When implemented in derived classes, deserialize this field from the specified `context` */
const object = {} as any; public abstract deserialize(
object[RuntimeValues] = {}; context: StructDeserializationContext
return object; ): void | Promise<void>;
/**
* Gets the actual size of this field. By default, the return value of its `definition.getSize()`
*
* When overridden in derived classes, can have custom logic to calculate the actual size.
*/
public getSize(): number {
return this.definition.getSize();
} }
export function getRuntimeValue(object: WithRuntimeValues, field: string): FieldRuntimeValue { /**
return (object as any)[RuntimeValues][field] as FieldRuntimeValue; * When implemented in derived classes, returns the current value of this field
} */
public abstract get(): unknown;
export function setRuntimeValue(object: WithRuntimeValues, field: string, runtimeValue: FieldRuntimeValue): void { /**
(object as any)[RuntimeValues][field] = runtimeValue; * When implemented in derived classes, update the current value of this field
delete (object as any)[field]; */
Object.defineProperty(object, field, { public abstract set(value: unknown): void;
configurable: true,
enumerable: true, /**
get() { return runtimeValue.get(); }, * When implemented in derived classes, serializes this field into `dataView` at `offset`
set(value) { runtimeValue.set(value); }, */
}); public abstract serialize(
dataView: DataView,
offset: number,
context: StructSerializationContext
): void;
} }

View file

@ -1,4 +1,4 @@
export * from './runtime'; export * from './basic';
export * from './struct'; export * from './struct';
export { default as Struct } from './struct'; export { default as Struct } from './struct';
export * from './types'; export * from './types';

View file

@ -1,5 +1,5 @@
import { BuiltInFieldType, createObjectWithRuntimeValues, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './runtime'; import { createRuntimeObject, FieldDefinition, FieldRuntimeValue, getRuntimeValue, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './basic';
import { ArrayBufferLikeFieldDescriptor, Evaluate, FixedLengthArrayBufferFieldDescriptor, Identity, KeysOfType, NumberFieldDescriptor, NumberFieldSubType, OmitNever, Overwrite, VariableLengthArrayBufferFieldDescriptor } from './types'; import { ArrayBufferFieldType, ArrayBufferLikeFieldType, Evaluate, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, Identity, KeysOfType, NumberFieldDefinition, NumberFieldType, Overwrite, StringFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
/** /**
* Extract the value type of the specified `Struct` * Extract the value type of the specified `Struct`
@ -12,7 +12,7 @@ export type StructValueType<T> =
/** /**
* Extract the init type of the specified `Struct` * Extract the init type of the specified `Struct`
*/ */
export type StructInitType<T extends Struct<object, object, object, unknown>> = export type StructInitType<T extends Struct<any, object, object, any>> =
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never; T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
/** /**
@ -22,35 +22,28 @@ type AddFieldDescriptor<
TValue extends object, TValue extends object,
TInit extends object, TInit extends object,
TExtra extends object, TExtra extends object,
TAfterParsed, TPostDeserialized,
TDescriptor extends FieldDescriptorBase> = TFieldName extends PropertyKey,
TDefinition extends FieldDefinition<any, any, any>> =
Identity<Struct< Identity<Struct<
// Merge two types // Merge two types
Evaluate< // Evaluate immediately to optimize editor hover tooltip
TValue & Evaluate<TValue & Record<TFieldName, TDefinition['valueType']>>,
// `TDescriptor.resultObject` is optional, so remove `undefined` from its type // There is no `Evaluate` here, because otherwise the type of a `Struct` with many fields
Exclude<TDescriptor['resultObject'], undefined> // can become too complex for TypeScript to compute
>, Evaluate<Omit<TInit, TDefinition['removeFields']> & Record<TFieldName, TDefinition['valueType']>>,
// `TDescriptor.initObject` signals removal of fields by setting its type to `never`
// I don't `Evaluate` here, because if I do, the result type will become too complex,
// and TypeScript will refuse to evaluate it.
OmitNever<
TInit &
// `TDescriptor.initObject` is optional, so remove `undefined` from its type
Exclude<TDescriptor['initObject'], undefined>
>,
TExtra, TExtra,
TAfterParsed TPostDeserialized
>>; >>;
/** /**
* Overload methods to add an array typed field * Overload methods to add an array buffer like field
*/ */
interface AddArrayBufferFieldDescriptor< interface ArrayBufferLikeFieldCreator<
TValue extends object, TValue extends object,
TInit extends object, TInit extends object,
TExtra extends object, TExtra extends object,
TAfterParsed TPostDeserialized
> { > {
/** /**
* Append a fixed-length array to the `Struct` * Append a fixed-length array to the `Struct`
@ -62,23 +55,23 @@ interface AddArrayBufferFieldDescriptor<
* For example, if this field is a string, you can declare it as a string enum or literal union. * For example, if this field is a string, you can declare it as a string enum or literal union.
*/ */
< <
TName extends string, TName extends PropertyKey,
TType extends ArrayBufferLikeFieldDescriptor.SubType, TType extends ArrayBufferLikeFieldType,
TTypeScriptType extends ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> TTypeScriptType = TType['valueType'],
>( >(
name: TName, name: TName,
type: TType, type: TType,
options: FixedLengthArrayBufferFieldDescriptor.Options, options: FixedLengthArrayBufferLikeFieldOptions,
typescriptType?: TTypeScriptType, typescriptType?: TTypeScriptType,
): AddFieldDescriptor< ): AddFieldDescriptor<
TValue, TValue,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TPostDeserialized,
FixedLengthArrayBufferFieldDescriptor<
TName, TName,
FixedLengthArrayBufferLikeFieldDefinition<
TType, TType,
TTypeScriptType FixedLengthArrayBufferLikeFieldOptions
> >
>; >;
@ -86,342 +79,350 @@ interface AddArrayBufferFieldDescriptor<
* Append a variable-length array to the `Struct` * Append a variable-length array to the `Struct`
*/ */
< <
TName extends string, TName extends PropertyKey,
TType extends ArrayBufferLikeFieldDescriptor.SubType, TType extends ArrayBufferLikeFieldType,
TLengthField extends KeysOfType<TInit, number | string>, TOptions extends VariableLengthArrayBufferLikeFieldOptions<TInit>,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> TTypeScriptType = TType['valueType'],
>( >(
name: TName, name: TName,
type: TType, type: TType,
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>, options: TOptions,
typescriptType?: TTypeScriptType, typescriptType?: TTypeScriptType,
): AddFieldDescriptor< ): AddFieldDescriptor<
TValue, TValue,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TPostDeserialized,
VariableLengthArrayBufferFieldDescriptor<
TName, TName,
VariableLengthArrayBufferLikeFieldDefinition<
TType, TType,
TInit, TOptions
TLengthField,
TTypeScriptType
> >
>; >;
} }
interface AddArrayBufferSubTypeFieldDescriptor< /**
TResult extends object, * Similar to `ArrayBufferLikeFieldCreator`, but bind to a `ArrayBufferLikeFieldType`
*/
interface ArrayBufferTypeFieldDefinitionCreator<
TValue extends object,
TInit extends object, TInit extends object,
TExtra extends object, TExtra extends object,
TAfterParsed, TPostDeserialized,
TType extends ArrayBufferLikeFieldDescriptor.SubType TType extends ArrayBufferLikeFieldType
> { > {
< <
TName extends string, TName extends PropertyKey,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> TTypeScriptType = TType['valueType'],
>( >(
name: TName, name: TName,
options: FixedLengthArrayBufferFieldDescriptor.Options, options: FixedLengthArrayBufferLikeFieldOptions,
typescriptType?: TTypeScriptType, typescriptType?: TTypeScriptType,
): AddFieldDescriptor< ): AddFieldDescriptor<
TResult, TValue,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TPostDeserialized,
FixedLengthArrayBufferFieldDescriptor<
TName, TName,
FixedLengthArrayBufferLikeFieldDefinition<
TType, TType,
TTypeScriptType FixedLengthArrayBufferLikeFieldOptions
> >
>; >;
< <
TName extends string, TName extends PropertyKey,
TLengthField extends KeysOfType<TInit, number | string>, TLengthField extends KeysOfType<TInit, number | string>,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> TOptions extends VariableLengthArrayBufferLikeFieldOptions<TInit, TLengthField>,
TTypeScriptType = TType['valueType'],
>( >(
name: TName, name: TName,
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>, options: TOptions,
_typescriptType?: TTypeScriptType, typescriptType?: TTypeScriptType,
): AddFieldDescriptor< ): AddFieldDescriptor<
TResult, TValue,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TPostDeserialized,
VariableLengthArrayBufferFieldDescriptor<
TName, TName,
VariableLengthArrayBufferLikeFieldDefinition<
TType, TType,
TInit, TOptions
TLengthField,
TTypeScriptType
> >
>; >;
} }
export type StructAfterParsed<TResult, TAfterParsed> = export type StructPostDeserialized<TValue, TPostDeserialized> =
(this: TResult, object: TResult) => TAfterParsed; (this: TValue, object: TValue) => TPostDeserialized;
export default class Struct< export default class Struct<
TResult extends object = {}, TValue extends object = {},
TInit extends object = {}, TInit extends object = {},
TExtra extends object = {}, TExtra extends object = {},
TAfterParsed = undefined, TPostDeserialized = undefined,
> { > {
public readonly valueType!: TValue;
public readonly initType!: TInit;
public readonly extraType!: TExtra;
public readonly options: Readonly<StructOptions>; public readonly options: Readonly<StructOptions>;
private _size = 0; private _size = 0;
/**
* Get the static size (exclude fields that can change size at runtime)
*/
public get size() { return this._size; } public get size() { return this._size; }
private fieldDescriptors: FieldDescriptorBase[] = []; private _fields: [name: PropertyKey, definition: FieldDefinition<any, any, any>][] = [];
private _extra: PropertyDescriptorMap = {}; private _extra: PropertyDescriptorMap = {};
private _afterParsed?: StructAfterParsed<any, any>; private _postDeserialized?: StructPostDeserialized<TValue, any>;
public constructor(options?: Partial<StructOptions>) { public constructor(options?: Partial<StructOptions>) {
this.options = { ...StructDefaultOptions, ...options }; this.options = { ...StructDefaultOptions, ...options };
} }
private clone(): Struct<any, any, any, any> { public field<
const result = new Struct<any, any, any, any>(this.options); TName extends PropertyKey,
result.fieldDescriptors = this.fieldDescriptors.slice(); TDefinition extends FieldDefinition<any, any, any>
result._size = this._size; >(
result._extra = this._extra; name: TName,
result._afterParsed = this._afterParsed; definition: TDefinition,
return result; ): AddFieldDescriptor<
TValue,
TInit,
TExtra,
TPostDeserialized,
TName,
TDefinition
> {
this._fields.push([name, definition]);
const size = definition.getSize();
this._size += size;
// Force cast `this` to another type
return this as any;
} }
public field<TDescriptor extends FieldDescriptorBase>( public fields<TOther extends Struct<any, any, any, any>>(
descriptor: TDescriptor, struct: TOther
): AddFieldDescriptor<TResult, TInit, TExtra, TAfterParsed, TDescriptor> { ): Struct<
const result = this.clone(); TValue & TOther['valueType'],
result.fieldDescriptors.push(descriptor); TInit & TOther['initType'],
TExtra & TOther['extraType'],
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type); TPostDeserialized
const size = Constructor.getSize(descriptor, this.options); > {
result._size += size; for (const field of struct._fields) {
this._fields.push(field);
return result; }
this._size += struct._size;
Object.assign(this._extra, struct._extra);
return this as any;
} }
private number< private number<
TName extends string, TName extends PropertyKey,
TType extends NumberFieldSubType = NumberFieldSubType, TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = TType['value'] TTypeScriptType = TType['valueType']
>( >(
name: TName, name: TName,
type: TType, type: TType,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.field<NumberFieldDescriptor<TName, TType, TTypeScriptType>>({ return this.field(
type: BuiltInFieldType.Number,
name, name,
subType: type, new NumberFieldDefinition(type, _typescriptType),
options, );
});
} }
public uint8< public uint8<
TName extends string, TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldSubType)['Uint8']['value'] TTypeScriptType = (typeof NumberFieldType)['Uint8']['valueType']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
NumberFieldSubType.Uint8, NumberFieldType.Uint8,
options,
_typescriptType _typescriptType
); );
} }
public uint16< public uint16<
TName extends string, TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldSubType)['Uint16']['value'] TTypeScriptType = (typeof NumberFieldType)['Uint16']['valueType']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
NumberFieldSubType.Uint16, NumberFieldType.Uint16,
options,
_typescriptType _typescriptType
); );
} }
public int32< public int32<
TName extends string, TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldSubType)['Int32']['value'] TTypeScriptType = (typeof NumberFieldType)['Int32']['valueType']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
NumberFieldSubType.Int32, NumberFieldType.Int32,
options,
_typescriptType _typescriptType
); );
} }
public uint32< public uint32<
TName extends string, TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldSubType)['Uint32']['value'] TTypeScriptType = (typeof NumberFieldType)['Uint32']['valueType']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {},
typescriptType?: TTypeScriptType, typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
NumberFieldSubType.Uint32, NumberFieldType.Uint32,
options,
typescriptType typescriptType
); );
} }
public int64< public int64<
TName extends string, TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldSubType)['Int64']['value'] TTypeScriptType = (typeof NumberFieldType)['Int64']['valueType']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
NumberFieldSubType.Int64, NumberFieldType.Int64,
options,
_typescriptType _typescriptType
); );
} }
public uint64< public uint64<
TName extends string, TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldSubType)['Uint64']['value'] TTypeScriptType = (typeof NumberFieldType)['Uint64']['valueType']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
NumberFieldSubType.Uint64, NumberFieldType.Uint64,
options,
_typescriptType _typescriptType
); );
} }
private arrayBufferLike: AddArrayBufferFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = ( private arrayBufferLike: ArrayBufferLikeFieldCreator<TValue, TInit, TExtra, TPostDeserialized> = (
name: string, name: PropertyKey,
type: ArrayBufferLikeFieldDescriptor.SubType, type: ArrayBufferLikeFieldType,
options: FixedLengthArrayBufferFieldDescriptor.Options | VariableLengthArrayBufferFieldDescriptor.Options options: FixedLengthArrayBufferLikeFieldOptions | VariableLengthArrayBufferLikeFieldOptions
): Struct<any, any, any, any> => { ): any => {
if ('length' in options) { if ('length' in options) {
return this.field<FixedLengthArrayBufferFieldDescriptor>({ return this.field(
type: BuiltInFieldType.FixedLengthArrayBufferLike,
name, name,
subType: type, new FixedLengthArrayBufferLikeFieldDefinition(type, options),
options: options, );
});
} else { } else {
return this.field<VariableLengthArrayBufferFieldDescriptor>({ return this.field(
type: BuiltInFieldType.VariableLengthArrayBufferLike,
name, name,
subType: type, new VariableLengthArrayBufferLikeFieldDefinition(type, options),
options: options, );
});
} }
}; };
public arrayBuffer: AddArrayBufferSubTypeFieldDescriptor< public arrayBuffer: ArrayBufferTypeFieldDefinitionCreator<
TResult, TValue,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TPostDeserialized,
ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer ArrayBufferFieldType
> = <TName extends string>( > = (
name: TName, name: PropertyKey,
options: any options: any
) => { ): any => {
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer, options); return this.arrayBufferLike(name, ArrayBufferFieldType.instance, options);
}; };
public string: AddArrayBufferSubTypeFieldDescriptor< public string: ArrayBufferTypeFieldDefinitionCreator<
TResult, TValue,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TPostDeserialized,
ArrayBufferLikeFieldDescriptor.SubType.String StringFieldType
> = <TName extends string>( > = (
name: TName, name: PropertyKey,
options: any options: any
) => { ): any => {
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.String, options); return this.arrayBufferLike(name, StringFieldType.instance, options);
}; };
public extra<TValue extends Record< public extra<T extends Record<
// This trick disallows any keys that are already in `TValue`
Exclude< Exclude<
keyof TValue, keyof T,
Exclude<keyof TValue, keyof TResult>>, Exclude<keyof T, keyof TValue>
>,
never never
>>( >>(
value: TValue & ThisType<Overwrite<Overwrite<TExtra, TValue>, TResult>> value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TValue>>
): Struct< ): Struct<
TResult, TValue,
TInit, TInit,
Overwrite<TExtra, TValue>, Overwrite<TExtra, T>,
TAfterParsed TPostDeserialized
> { > {
const result = this.clone(); Object.assign(this._extra, Object.getOwnPropertyDescriptors(value));
result._extra = { ...result._extra, ...Object.getOwnPropertyDescriptors(value) }; return this as any;
return result;
} }
public afterParsed( /**
callback: StructAfterParsed<TResult, never> *
): Struct<TResult, TInit, TExtra, never>; */
public afterParsed( public postDeserialize(
callback?: StructAfterParsed<TResult, void> callback: StructPostDeserialized<TValue, never>
): Struct<TResult, TInit, TExtra, undefined>; ): Struct<TValue, TInit, TExtra, never>;
public afterParsed<TAfterParsed>( public postDeserialize(
callback?: StructAfterParsed<TResult, TAfterParsed> callback?: StructPostDeserialized<TValue, void>
): Struct<TResult, TInit, TExtra, TAfterParsed>; ): Struct<TValue, TInit, TExtra, undefined>;
public afterParsed( public postDeserialize<TPostSerialize>(
callback?: StructAfterParsed<TResult, any> callback?: StructPostDeserialized<TValue, TPostSerialize>
): Struct<TValue, TInit, TExtra, TPostSerialize>;
public postDeserialize(
callback?: StructPostDeserialized<TValue, any>
) { ) {
const result = this.clone(); this._postDeserialized = callback;
result._afterParsed = callback; return this as any;
return result;
} }
private initializeObject(context: StructSerializationContext) { private initializeObject(context: StructSerializationContext) {
const object = createObjectWithRuntimeValues(); const object = createRuntimeObject();
Object.defineProperties(object, this._extra); Object.defineProperties(object, this._extra);
for (const descriptor of this.fieldDescriptors) { for (const [name, definition] of this._fields) {
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type); const runtimeValue = definition.createValue(this.options, context, object);
setRuntimeValue(object, name, runtimeValue);
const runtimeValue = new Constructor(descriptor, this.options, context, object);
setRuntimeValue(object, descriptor.name, runtimeValue);
} }
return object; return object;
} }
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> { public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TValue> {
const object = this.initializeObject(context); const object = this.initializeObject(context);
for (const { name: fieldName } of this.fieldDescriptors) { for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, fieldName); const runtimeValue = getRuntimeValue(object, name);
runtimeValue.set((init as any)[fieldName]); runtimeValue.set((init as any)[name]);
} }
return object as any; return object as any;
@ -429,16 +430,16 @@ export default class Struct<
public async deserialize( public async deserialize(
context: StructDeserializationContext context: StructDeserializationContext
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> { ): Promise<TPostDeserialized extends undefined ? Overwrite<TExtra, TValue> : TPostDeserialized> {
const object = this.initializeObject(context); const object = this.initializeObject(context);
for (const { name: fieldName } of this.fieldDescriptors) { for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, fieldName); const runtimeValue = getRuntimeValue(object, name);
await runtimeValue.deserialize(context, object); await runtimeValue.deserialize(context);
} }
if (this._afterParsed) { if (this._postDeserialized) {
const result = this._afterParsed.call(object, object); const result = this._postDeserialized.call(object as TValue, object as TValue);
if (result) { if (result) {
return result; return result;
} }
@ -453,8 +454,8 @@ export default class Struct<
let structSize = 0; let structSize = 0;
const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = []; const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = [];
for (const { name: fieldName } of this.fieldDescriptors) { for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, fieldName); const runtimeValue = getRuntimeValue(object, name);
const size = runtimeValue.getSize(); const size = runtimeValue.getSize();
fieldsInfo.push({ runtimeValue, size }); fieldsInfo.push({ runtimeValue, size });
structSize += size; structSize += size;

View file

@ -0,0 +1,79 @@
import { StructDeserializationContext, StructSerializationContext } from '../basic';
import { ArrayBufferFieldType, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
describe('Types', () => {
describe('ArrayBufferFieldType', () => {
it('should have a static instance', () => {
expect(ArrayBufferFieldType.instance).toBeInstanceOf(ArrayBufferFieldType);
});
it('`toArrayBuffer` should return the same `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.toArrayBuffer(arrayBuffer)).toBe(arrayBuffer);
});
it('`fromArrayBuffer` should return the same `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.fromArrayBuffer(arrayBuffer)).toBe(arrayBuffer);
});
it('`getSize` should return the `byteLength` of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.getSize(arrayBuffer)).toBe(10);
});
});
describe('Uint8ClampedArrayFieldType', () => {
it('should have a static instance', () => {
expect(Uint8ClampedArrayFieldType.instance).toBeInstanceOf(Uint8ClampedArrayFieldType);
});
it('`toArrayBuffer` should return its `buffer`', () => {
const array = new Uint8ClampedArray(10);
const buffer = array.buffer;
expect(Uint8ClampedArrayFieldType.instance.toArrayBuffer(array)).toBe(buffer);
});
it('`fromArrayBuffer` should return a view of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
const array = Uint8ClampedArrayFieldType.instance.fromArrayBuffer(arrayBuffer);
expect(array).toHaveProperty('buffer', arrayBuffer);
expect(array).toHaveProperty('byteOffset', 0);
expect(array).toHaveProperty('byteLength', 10);
});
});
describe('StringFieldType', () => {
it('should have a static instance', () => {
expect(StringFieldType.instance).toBeInstanceOf(StringFieldType);
});
it('`toArrayBuffer` should return the decoded string', () => {
const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructSerializationContext = {
encodeUtf8(input) {
return Buffer.from(input, 'utf-8');
},
};
expect(StringFieldType.instance.toArrayBuffer(text, context)).toEqual(arrayBuffer);
});
it('`fromArrayBuffer` should return the encoded ArrayBuffer', () => {
const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructDeserializationContext = {
decodeUtf8(arrayBuffer: ArrayBuffer): string {
return Buffer.from(arrayBuffer).toString('utf-8');
},
encodeUtf8(input) {
throw new Error('Method not implemented.');
},
read(length) {
throw new Error('Method not implemented.');
},
};
expect(StringFieldType.instance.fromArrayBuffer(arrayBuffer, context)).toBe(text);
});
});
});

View file

@ -1,135 +1,159 @@
import { FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, StructDeserializationContext, StructSerializationContext } from '../runtime'; import { FieldDefinition, FieldRuntimeValue, StructDeserializationContext, StructSerializationContext } from '../basic';
export namespace ArrayBufferLikeFieldDescriptor { /**
export enum SubType { * Base class for all types that
ArrayBuffer, * can be converted from an ArrayBuffer when deserialized,
Uint8ClampedArray, * and need to be converted to an ArrayBuffer when serializing
String, *
* @template TType The actual TypeScript type of this type
* @template TTypeScriptType Optional another type (should be compatible with `TType`)
* specified by user when creating field definitions.
*/
export abstract class ArrayBufferLikeFieldType<TType = unknown, TTypeScriptType = TType> {
public readonly valueType!: TTypeScriptType;
/**
* When implemented in derived classes, converts the type-specific `value` to an `ArrayBuffer`
*
* This function should be "pure", i.e.,
* same `value` should always be converted to `ArrayBuffer`s that have same content.
*/
public abstract toArrayBuffer(value: TType, context: StructSerializationContext): ArrayBuffer;
/** When implemented in derived classes, converts the `ArrayBuffer` to a type-specific value */
public abstract fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): TType;
/**
* When implemented in derived classes, gets the size in byte of the type-specific `value`.
*
* If the size can't be calculated without first converting the `value` back to an `ArrayBuffer`,
* implementer should returns `-1` so the caller will get its size by first converting it to
* an `ArrayBuffer` (and cache the result).
*/
public abstract getSize(value: TType): number;
} }
export type TypeScriptType<TType extends SubType = SubType> = /** An ArrayBufferLike type that's actually an `ArrayBuffer` */
TType extends SubType.ArrayBuffer ? ArrayBuffer : export class ArrayBufferFieldType extends ArrayBufferLikeFieldType<ArrayBuffer> {
TType extends SubType.Uint8ClampedArray ? Uint8ClampedArray : public static readonly instance = new ArrayBufferFieldType();
TType extends SubType.String ? string :
never; protected constructor() {
super();
} }
export interface ArrayBufferLikeFieldDescriptor< public toArrayBuffer(value: ArrayBuffer): ArrayBuffer {
TName extends string = string, return value;
TType extends ArrayBufferLikeFieldDescriptor.SubType = ArrayBufferLikeFieldDescriptor.SubType, }
TResultObject = {},
TInitObject = {}, public fromArrayBuffer(arrayBuffer: ArrayBuffer): ArrayBuffer {
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions return arrayBuffer;
> extends FieldDescriptorBase< }
TName,
TResultObject, public getSize(value: ArrayBuffer): number {
TInitObject, return value.byteLength;
TOptions }
}
/** Am ArrayBufferLike type that converts to/from the `ArrayBuffer` from/to a `Uint8ClampedArray` */
export class Uint8ClampedArrayFieldType
extends ArrayBufferLikeFieldType<Uint8ClampedArray, Uint8ClampedArray> {
public static readonly instance = new Uint8ClampedArrayFieldType();
protected constructor() {
super();
}
public toArrayBuffer(value: Uint8ClampedArray): ArrayBuffer {
return value.buffer;
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer): Uint8ClampedArray {
return new Uint8ClampedArray(arrayBuffer);
}
public getSize(value: Uint8ClampedArray): number {
return value.byteLength;
}
}
/** Am ArrayBufferLike type that converts to/from the `ArrayBuffer` from/to a `string` */
export class StringFieldType<TTypeScriptType = string>
extends ArrayBufferLikeFieldType<string, TTypeScriptType> {
public static readonly instance = new StringFieldType();
public toArrayBuffer(value: string, context: StructSerializationContext): ArrayBuffer {
return context.encodeUtf8(value);
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): string {
return context.decodeUtf8(arrayBuffer);
}
public getSize(): number {
// Return `-1`, so `ArrayBufferLikeFieldDefinition` will
// convert this `value` into an `ArrayBuffer` (and cache the result),
// Then get the size from that `ArrayBuffer`
return -1;
}
}
export abstract class ArrayBufferLikeFieldDefinition<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TOptions = void,
TRemoveFields = never,
> extends FieldDefinition<
TOptions,
TType['valueType'],
TRemoveFields
>{ >{
subType: TType; public readonly type: TType;
public constructor(type: TType, options: TOptions) {
super(options);
this.type = type;
}
} }
const EmptyArrayBuffer = new ArrayBuffer(0); const EmptyArrayBuffer = new ArrayBuffer(0);
const EmptyUint8ClampedArray = new Uint8ClampedArray(EmptyArrayBuffer);
const EmptyString = '';
export abstract class ArrayBufferLikeFieldRuntimeValue<TDescriptor extends ArrayBufferLikeFieldDescriptor> export abstract class ArrayBufferLikeFieldRuntimeValue<
extends FieldRuntimeValue<TDescriptor> { TDefinition extends ArrayBufferLikeFieldDefinition<any, any, any>,
> extends FieldRuntimeValue<TDefinition> {
protected arrayBuffer: ArrayBuffer | undefined; protected arrayBuffer: ArrayBuffer | undefined;
protected typedArray: ArrayBufferView | undefined; protected typedValue: unknown;
protected string: string | undefined; protected getDeserializeSize(): number {
protected getDeserializeSize(object: any): number {
return this.getSize(); return this.getSize();
} }
public async deserialize(context: StructDeserializationContext, object: any): Promise<void> { public async deserialize(context: StructDeserializationContext): Promise<void> {
const size = this.getDeserializeSize(object); const size = this.getDeserializeSize();
this.arrayBuffer = undefined; this.arrayBuffer = undefined;
this.typedArray = undefined; this.typedValue = undefined;
this.string = undefined;
if (size === 0) { if (size === 0) {
this.arrayBuffer = EmptyArrayBuffer; this.arrayBuffer = EmptyArrayBuffer;
switch (this.descriptor.subType) { } else {
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray: this.arrayBuffer = await context.read(size);
this.typedArray = EmptyUint8ClampedArray;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.string = EmptyString;
break;
}
return;
} }
const buffer = await context.read(size); this.typedValue = this.definition.type.fromArrayBuffer(this.arrayBuffer, context);
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
this.arrayBuffer = buffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.arrayBuffer = buffer;
this.typedArray = new Uint8ClampedArray(buffer);
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.arrayBuffer = buffer;
this.string = context.decodeUtf8(buffer);
break;
default:
throw new Error('Unknown type');
}
} }
public get(): unknown { public get(): unknown {
switch (this.descriptor.subType) { return this.typedValue;
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
return this.arrayBuffer;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
return this.typedArray;
case ArrayBufferLikeFieldDescriptor.SubType.String:
return this.string;
default:
throw new Error('Unknown type');
}
} }
public set(value: unknown): void { public set(value: unknown): void {
this.typedValue = value;
this.arrayBuffer = undefined; this.arrayBuffer = undefined;
this.typedArray = undefined;
this.string = undefined;
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
this.arrayBuffer = value as ArrayBuffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.typedArray = value as Uint8ClampedArray;
this.arrayBuffer = this.typedArray.buffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.string = value as string;
break;
default:
throw new Error('Unknown type');
}
} }
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void { public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
if (this.descriptor.subType !== ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer && if (!this.arrayBuffer) {
!this.arrayBuffer) { this.arrayBuffer = this.definition.type.toArrayBuffer(this.typedValue, context);
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.arrayBuffer = this.typedArray!.buffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.arrayBuffer = context.encodeUtf8(this.string!);
break;
default:
throw new Error('Unknown type');
}
} }
new Uint8Array(dataView.buffer) new Uint8Array(dataView.buffer)

View file

@ -1,37 +1,33 @@
import { BuiltInFieldType, FieldDescriptorBaseOptions, GlobalStructFieldRuntimeTypeRegistry } from '../runtime'; import { StructOptions, StructSerializationContext } from '../basic';
import { ArrayBufferLikeFieldDescriptor, ArrayBufferLikeFieldRuntimeValue } from './array-buffer'; import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldRuntimeValue, ArrayBufferLikeFieldType } from './array-buffer';
export namespace FixedLengthArrayBufferFieldDescriptor { export interface FixedLengthArrayBufferLikeFieldOptions {
export interface Options extends FieldDescriptorBaseOptions {
length: number; length: number;
} }
}
export interface FixedLengthArrayBufferFieldDescriptor< export class FixedLengthArrayBufferLikeFieldDefinition<
TName extends string = string, TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TType extends ArrayBufferLikeFieldDescriptor.SubType = ArrayBufferLikeFieldDescriptor.SubType, TOptions extends FixedLengthArrayBufferLikeFieldOptions = FixedLengthArrayBufferLikeFieldOptions,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>, > extends ArrayBufferLikeFieldDefinition<
TOptions extends FixedLengthArrayBufferFieldDescriptor.Options = FixedLengthArrayBufferFieldDescriptor.Options
> extends ArrayBufferLikeFieldDescriptor<
TName,
TType, TType,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions TOptions
> { > {
type: BuiltInFieldType.FixedLengthArrayBufferLike; public getSize(): number {
return this.options.length;
}
options: TOptions; public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): FixedLengthArrayBufferFieldRuntimeValue {
return new FixedLengthArrayBufferFieldRuntimeValue(this, options, context, object);
}
}; };
class FixedLengthArrayBufferFieldRuntimeValue class FixedLengthArrayBufferFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<FixedLengthArrayBufferFieldDescriptor>{ extends ArrayBufferLikeFieldRuntimeValue<FixedLengthArrayBufferLikeFieldDefinition>{
public static getSize(descriptor: FixedLengthArrayBufferFieldDescriptor) { public static getSize(descriptor: FixedLengthArrayBufferLikeFieldDefinition) {
return descriptor.options.length; return descriptor.options.length;
} }
} }
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.FixedLengthArrayBufferLike,
FixedLengthArrayBufferFieldRuntimeValue,
);

View file

@ -1,49 +1,49 @@
import { StructDefaultOptions, StructDeserializationContext } from '../runtime'; import { StructDefaultOptions, StructDeserializationContext } from '../basic';
import { NumberFieldSubType, NumberFieldRuntimeValue } from './number'; import { NumberFieldDefinition, NumberFieldRuntimeValue, NumberFieldType } from './number';
describe('Types', () => { describe('Types', () => {
describe('Number', () => { describe('Number', () => {
describe('NumberFieldSubType', () => { describe('NumberFieldType', () => {
it('Uint8 validation', () => { it('Uint8 validation', () => {
const key = 'Uint8'; const key = 'Uint8';
expect(NumberFieldSubType[key]).toHaveProperty('size', 1); expect(NumberFieldType[key]).toHaveProperty('size', 1);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
}); });
it('Uint16 validation', () => { it('Uint16 validation', () => {
const key = 'Uint16'; const key = 'Uint16';
expect(NumberFieldSubType[key]).toHaveProperty('size', 2); expect(NumberFieldType[key]).toHaveProperty('size', 2);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
}); });
it('Int32 validation', () => { it('Int32 validation', () => {
const key = 'Int32'; const key = 'Int32';
expect(NumberFieldSubType[key]).toHaveProperty('size', 4); expect(NumberFieldType[key]).toHaveProperty('size', 4);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
}); });
it('Uint32 validation', () => { it('Uint32 validation', () => {
const key = 'Uint32'; const key = 'Uint32';
expect(NumberFieldSubType[key]).toHaveProperty('size', 4); expect(NumberFieldType[key]).toHaveProperty('size', 4);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
}); });
it('Int64 validation', () => { it('Int64 validation', () => {
const key = 'Int64'; const key = 'Int64';
expect(NumberFieldSubType[key]).toHaveProperty('size', 8); expect(NumberFieldType[key]).toHaveProperty('size', 8);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'getBig' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'getBig' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'setBig' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'setBig' + key);
}); });
it('Uint64 validation', () => { it('Uint64 validation', () => {
const key = 'Uint64'; const key = 'Uint64';
expect(NumberFieldSubType[key]).toHaveProperty('size', 8); expect(NumberFieldType[key]).toHaveProperty('size', 8);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'getBig' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'getBig' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'setBig' + key); expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'setBig' + key);
}); });
}); });
@ -57,7 +57,7 @@ describe('Types', () => {
}; };
const value = new NumberFieldRuntimeValue( const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint8 } as any, new NumberFieldDefinition(NumberFieldType.Uint8),
StructDefaultOptions, StructDefaultOptions,
undefined as any, undefined as any,
undefined as any, undefined as any,
@ -66,7 +66,7 @@ describe('Types', () => {
expect(value.get()).toBe(1); expect(value.get()).toBe(1);
expect(read).toBeCalledTimes(1); expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint8.size); expect(read).lastCalledWith(NumberFieldType.Uint8.size);
}); });
it('should deserialize Uint16', async () => { it('should deserialize Uint16', async () => {
@ -78,7 +78,7 @@ describe('Types', () => {
}; };
const value = new NumberFieldRuntimeValue( const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint16 } as any, new NumberFieldDefinition(NumberFieldType.Uint16),
StructDefaultOptions, StructDefaultOptions,
undefined as any, undefined as any,
undefined as any, undefined as any,
@ -87,7 +87,7 @@ describe('Types', () => {
expect(value.get()).toBe((1 << 8) | 2); expect(value.get()).toBe((1 << 8) | 2);
expect(read).toBeCalledTimes(1); expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint16.size); expect(read).lastCalledWith(NumberFieldType.Uint16.size);
}); });
it('should deserialize Uint16LE', async () => { it('should deserialize Uint16LE', async () => {
@ -99,7 +99,7 @@ describe('Types', () => {
}; };
const value = new NumberFieldRuntimeValue( const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint16 } as any, new NumberFieldDefinition(NumberFieldType.Uint16),
{ ...StructDefaultOptions, littleEndian: true }, { ...StructDefaultOptions, littleEndian: true },
undefined as any, undefined as any,
undefined as any, undefined as any,
@ -108,7 +108,7 @@ describe('Types', () => {
expect(value.get()).toBe((2 << 8) | 1); expect(value.get()).toBe((2 << 8) | 1);
expect(read).toBeCalledTimes(1); expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint16.size); expect(read).lastCalledWith(NumberFieldType.Uint16.size);
}); });
}); });
}); });

View file

@ -1,4 +1,4 @@
import { BuiltInFieldType, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, StructDeserializationContext } from '../runtime'; import { FieldDefinition, FieldRuntimeValue, StructDeserializationContext, StructOptions, StructSerializationContext } from '../basic';
export type DataViewGetters = export type DataViewGetters =
{ [TKey in keyof DataView]: TKey extends `get${string}` ? TKey : never }[keyof DataView]; { [TKey in keyof DataView]: TKey extends `get${string}` ? TKey : never }[keyof DataView];
@ -6,20 +6,20 @@ export type DataViewGetters =
export type DataViewSetters = export type DataViewSetters =
{ [TKey in keyof DataView]: TKey extends `set${string}` ? TKey : never }[keyof DataView]; { [TKey in keyof DataView]: TKey extends `set${string}` ? TKey : never }[keyof DataView];
export class NumberFieldSubType<TTypeScriptType extends number | bigint = number | bigint> { export class NumberFieldType<TTypeScriptType extends number | bigint = number | bigint> {
public static readonly Uint8 = new NumberFieldSubType<number>(1, 'getUint8', 'setUint8'); public static readonly Uint8 = new NumberFieldType<number>(1, 'getUint8', 'setUint8');
public static readonly Uint16 = new NumberFieldSubType<number>(2, 'getUint16', 'setUint16'); public static readonly Uint16 = new NumberFieldType<number>(2, 'getUint16', 'setUint16');
public static readonly Int32 = new NumberFieldSubType<number>(4, 'getInt32', 'setInt32'); public static readonly Int32 = new NumberFieldType<number>(4, 'getInt32', 'setInt32');
public static readonly Uint32 = new NumberFieldSubType<number>(4, 'getUint32', 'setUint32'); public static readonly Uint32 = new NumberFieldType<number>(4, 'getUint32', 'setUint32');
public static readonly Int64 = new NumberFieldSubType<bigint>(8, 'getBigInt64', 'setBigInt64'); public static readonly Int64 = new NumberFieldType<bigint>(8, 'getBigInt64', 'setBigInt64');
public static readonly Uint64 = new NumberFieldSubType<bigint>(8, 'getBigUint64', 'setBigUint64'); public static readonly Uint64 = new NumberFieldType<bigint>(8, 'getBigUint64', 'setBigUint64');
public readonly value!: TTypeScriptType; public readonly valueType!: TTypeScriptType;
public readonly size: number; public readonly size: number;
@ -38,33 +38,44 @@ export class NumberFieldSubType<TTypeScriptType extends number | bigint = number
} }
} }
export interface NumberFieldDescriptor< export class NumberFieldDefinition<
TName extends string = string, TType extends NumberFieldType = NumberFieldType,
TSubType extends NumberFieldSubType<any> = NumberFieldSubType<any>, TTypeScriptType = TType['valueType'],
TTypeScriptType = TSubType['value'], > extends FieldDefinition<
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions void,
> extends FieldDescriptorBase< TTypeScriptType,
TName, never
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions
> { > {
type: BuiltInFieldType.Number; public readonly type: TType;
subType: TSubType; public constructor(type: TType, _typescriptType?: TTypeScriptType) {
super();
this.type = type;
} }
export class NumberFieldRuntimeValue extends FieldRuntimeValue<NumberFieldDescriptor> { public getSize(): number {
public static getSize(descriptor: NumberFieldDescriptor): number { return this.type.size;
return descriptor.subType.size;
} }
public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): NumberFieldRuntimeValue<TType, TTypeScriptType> {
return new NumberFieldRuntimeValue(this, options, context, object);
}
}
export class NumberFieldRuntimeValue<
TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = TType['valueType'],
> extends FieldRuntimeValue<NumberFieldDefinition<TType, TTypeScriptType>> {
protected value: number | bigint | undefined; protected value: number | bigint | undefined;
public async deserialize(context: StructDeserializationContext): Promise<void> { public async deserialize(context: StructDeserializationContext): Promise<void> {
const buffer = await context.read(this.getSize()); const buffer = await context.read(this.getSize());
const view = new DataView(buffer); const view = new DataView(buffer);
this.value = view[this.descriptor.subType.dataViewGetter]( this.value = view[this.definition.type.dataViewGetter](
0, 0,
this.options.littleEndian this.options.littleEndian
); );
@ -82,15 +93,10 @@ export class NumberFieldRuntimeValue extends FieldRuntimeValue<NumberFieldDescri
// `setBigInt64` requires a `bigint` while others require `number` // `setBigInt64` requires a `bigint` while others require `number`
// So `dataView[DataViewSetters]` requires `bigint & number` // So `dataView[DataViewSetters]` requires `bigint & number`
// and that is, `never` // and that is, `never`
(dataView[this.descriptor.subType.dataViewSetter] as any)( (dataView[this.definition.type.dataViewSetter] as any)(
offset, offset,
this.value!, this.value!,
this.options.littleEndian this.options.littleEndian
); );
} }
} }
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.Number,
NumberFieldRuntimeValue,
);

View file

@ -1,47 +1,47 @@
import { BuiltInFieldType, FieldDescriptorBaseOptions, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructOptions, StructSerializationContext } from '../runtime'; import { getRuntimeValue, setRuntimeValue, StructOptions, StructSerializationContext } from '../basic';
import { ArrayBufferLikeFieldDescriptor, ArrayBufferLikeFieldRuntimeValue } from './array-buffer'; import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldRuntimeValue, ArrayBufferLikeFieldType } from './array-buffer';
import { NumberFieldDescriptor, NumberFieldRuntimeValue } from './number'; import { NumberFieldDefinition, NumberFieldRuntimeValue } from './number';
import { KeysOfType } from './utils'; import { KeysOfType } from './utils';
export namespace VariableLengthArrayBufferFieldDescriptor { export interface VariableLengthArrayBufferLikeFieldOptions<
export interface Options<
TInit = object, TInit = object,
TLengthField extends KeysOfType<TInit, number | string> = any, TLengthField extends KeysOfType<TInit, number | string> = any,
> extends FieldDescriptorBaseOptions { > {
lengthField: TLengthField; lengthField: TLengthField;
} }
}
export interface VariableLengthArrayBufferFieldDescriptor< export class VariableLengthArrayBufferLikeFieldDefinition<
TName extends string = string, TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TType extends ArrayBufferLikeFieldDescriptor.SubType = ArrayBufferLikeFieldDescriptor.SubType, TOptions extends VariableLengthArrayBufferLikeFieldOptions = VariableLengthArrayBufferLikeFieldOptions
TInit = object, > extends ArrayBufferLikeFieldDefinition<
TLengthField extends KeysOfType<TInit, number | string> = any,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>,
TOptions extends VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField> = VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>
> extends ArrayBufferLikeFieldDescriptor<
TName,
TType, TType,
Record<TName, TTypeScriptType>, TOptions,
Record<TName, TTypeScriptType> & Record<TLengthField, never>, TOptions['lengthField']
TOptions
> { > {
type: BuiltInFieldType.VariableLengthArrayBufferLike; public getSize(): number {
return 0;
options: TOptions;
} }
class VariableLengthArrayBufferLengthFieldRuntimeValue extends NumberFieldRuntimeValue { public createValue(
protected arrayBufferValue: VariableLengthArrayBufferFieldRuntimeValue; options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): VariableLengthArrayBufferLikeFieldRuntimeValue {
return new VariableLengthArrayBufferLikeFieldRuntimeValue(this, options, context, object);
}
}
class VariableLengthArrayBufferLikeLengthFieldRuntimeValue extends NumberFieldRuntimeValue {
protected arrayBufferValue: VariableLengthArrayBufferLikeFieldRuntimeValue;
public constructor( public constructor(
descriptor: NumberFieldDescriptor, definition: NumberFieldDefinition,
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructSerializationContext, context: StructSerializationContext,
object: any, object: any,
arrayBufferValue: VariableLengthArrayBufferFieldRuntimeValue, arrayBufferValue: VariableLengthArrayBufferLikeFieldRuntimeValue,
) { ) {
super(descriptor, options, context, object); super(definition, options, context, object);
this.arrayBufferValue = arrayBufferValue; this.arrayBufferValue = arrayBufferValue;
} }
@ -61,28 +61,28 @@ class VariableLengthArrayBufferLengthFieldRuntimeValue extends NumberFieldRuntim
} }
} }
class VariableLengthArrayBufferFieldRuntimeValue class VariableLengthArrayBufferLikeFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<VariableLengthArrayBufferFieldDescriptor> { extends ArrayBufferLikeFieldRuntimeValue<VariableLengthArrayBufferLikeFieldDefinition> {
public static getSize() { public static getSize() {
return 0; return 0;
} }
protected length: number | undefined; protected length: number | undefined;
protected lengthFieldValue: VariableLengthArrayBufferLengthFieldRuntimeValue; protected lengthFieldValue: VariableLengthArrayBufferLikeLengthFieldRuntimeValue;
public constructor( public constructor(
descriptor: VariableLengthArrayBufferFieldDescriptor, descriptor: VariableLengthArrayBufferLikeFieldDefinition,
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructSerializationContext, context: StructSerializationContext,
object: any object: any
) { ) {
super(descriptor, options, context, object); super(descriptor, options, context, object);
const lengthField = this.descriptor.options.lengthField; const lengthField = this.definition.options.lengthField;
const oldValue = getRuntimeValue(object, lengthField) as NumberFieldRuntimeValue; const oldValue = getRuntimeValue(object, lengthField) as NumberFieldRuntimeValue;
this.lengthFieldValue = new VariableLengthArrayBufferLengthFieldRuntimeValue( this.lengthFieldValue = new VariableLengthArrayBufferLikeLengthFieldRuntimeValue(
oldValue.descriptor, oldValue.definition,
this.options, this.options,
this.context, this.context,
object, object,
@ -98,19 +98,17 @@ class VariableLengthArrayBufferFieldRuntimeValue
public getSize() { public getSize() {
if (this.length === undefined) { if (this.length === undefined) {
switch (this.descriptor.subType) { if (this.arrayBuffer !== undefined) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer: this.length = this.arrayBuffer.byteLength;
this.length = this.arrayBuffer!.byteLength; } else {
break; this.length = this.definition.type.getSize(this.typedValue);
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray: if (this.length === -1) {
this.length = this.typedArray!.byteLength; this.arrayBuffer = this.definition.type.toArrayBuffer(this.typedValue, this.context);
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.arrayBuffer = this.context.encodeUtf8(this.string!);
this.length = this.arrayBuffer.byteLength; this.length = this.arrayBuffer.byteLength;
break;
} }
} }
}
return this.length; return this.length;
} }
@ -119,8 +117,3 @@ class VariableLengthArrayBufferFieldRuntimeValue
this.length = undefined; this.length = undefined;
} }
} }
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.VariableLengthArrayBufferLike,
VariableLengthArrayBufferFieldRuntimeValue,
);

View file

@ -3,7 +3,8 @@
"compilerOptions": { "compilerOptions": {
"noEmit": true, "noEmit": true,
"types": [ "types": [
"jest" "@types/node",
"jest",
] ]
}, },
"exclude": [] "exclude": []