test(struct): add more unit tests

This commit is contained in:
Simon Chan 2021-01-15 19:35:42 +08:00
parent 547e9781e7
commit b71f9714b2
26 changed files with 1151 additions and 473 deletions

View file

@ -59,5 +59,7 @@
"yume", "yume",
"zstd" "zstd"
], ],
"editor.tabSize": 4 "editor.tabSize": 4,
"jest.rootPath": "packages/struct",
"jest.showCoverageOnLoad": true
} }

View file

@ -5,4 +5,5 @@ export * from './device-view';
export * from './error-dialog'; export * from './error-dialog';
export * from './external-link'; export * from './external-link';
export * from './logger'; export * from './logger';
export * from './number-picker';
export * from './router'; export * from './router';

View file

@ -55,11 +55,11 @@ const buffer = MyStruct.serialize({
- [`postDeserialize`](#postdeserialize) - [`postDeserialize`](#postdeserialize)
- [Custom field type](#custom-field-type) - [Custom field type](#custom-field-type)
- [`Struct#field` method](#structfield-method) - [`Struct#field` method](#structfield-method)
- [`FieldDefinition`](#fielddefinition) - [`StructFieldDefinition`](#fielddefinition)
- [`getSize`](#getsize) - [`getSize`](#getsize)
- [`deserialize`](#deserialize-1) - [`deserialize`](#deserialize-1)
- [`createValue`](#createvalue) - [`createValue`](#createvalue)
- [`FieldRuntimeValue`](#fieldruntimevalue) - [`StructFieldValue`](#StructFieldValue)
## Compatibility ## Compatibility
@ -562,7 +562,7 @@ This library has a plugin system to support adding fields with custom types.
```ts ```ts
field< field<
TName extends PropertyKey, TName extends PropertyKey,
TDefinition extends FieldDefinition<any, any, any> TDefinition extends StructFieldDefinition<any, any, any>
>( >(
name: TName, name: TName,
definition: TDefinition definition: TDefinition
@ -574,21 +574,21 @@ field<
>; >;
``` ```
Appends a `FieldDefinition` to the `Struct`. Appends a `StructFieldDefinition` to the `Struct`.
All above built-in methods are alias of `field`. To add a field of a custom type, let users call `field` with your custom `FieldDefinition` implementation. All above built-in methods are alias of `field`. To add a field of a custom type, let users call `field` with your custom `StructFieldDefinition` implementation.
### `FieldDefinition` ### `StructFieldDefinition`
```ts ```ts
abstract class FieldDefinition<TOptions = void, TValueType = unknown, TOmitInit = never> { abstract class StructFieldDefinition<TOptions = void, TValueType = unknown, TOmitInit = never> {
readonly options: TOptions; readonly options: TOptions;
constructor(options: TOptions); constructor(options: TOptions);
} }
``` ```
A `FieldDefinition` describes type, size and runtime semantics of a field. A `StructFieldDefinition` describes type, size and runtime semantics of a field.
It's an `abstract` class, means it lacks some method implementations, so it shouldn't be constructed. It's an `abstract` class, means it lacks some method implementations, so it shouldn't be constructed.
@ -600,7 +600,7 @@ abstract getSize(): number;
Returns the size (or minimal size if it's dynamic) of this field. Returns the size (or minimal size if it's dynamic) of this field.
Actual size should been returned from `FieldRuntimeValue#getSize` Actual size should been returned from `StructFieldValue#getSize`
#### `deserialize` #### `deserialize`
@ -609,7 +609,7 @@ abstract deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructDeserializationContext, context: StructDeserializationContext,
object: any, object: any,
): ValueOrPromise<FieldRuntimeValue<FieldDefinition<TOptions, TValueType, TRemoveInitFields>>>; ): ValueOrPromise<StructFieldValue<StructFieldDefinition<TOptions, TValueType, TRemoveInitFields>>>;
``` ```
Defines how to deserialize a value from `context`. Can also return a `Promise`. Defines how to deserialize a value from `context`. Can also return a `Promise`.
@ -617,9 +617,9 @@ Defines how to deserialize a value from `context`. Can also return a `Promise`.
Usually implementations should be: Usually implementations should be:
1. Somehow parse the value from `context` 1. Somehow parse the value from `context`
2. Pass the value into `FieldDefinition#createValue` 2. Pass the value into `StructFieldDefinition#createValue`
Sometimes, some metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `FieldRuntimeValue` instance for later use. Sometimes, some metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
#### `createValue` #### `createValue`
@ -629,15 +629,15 @@ abstract createValue(
context: StructSerializationContext, context: StructSerializationContext,
object: any, object: any,
value: TValueType, value: TValueType,
): FieldRuntimeValue<FieldDefinition<TOptions, TValueType, TRemoveInitFields>>; ): StructFieldValue<StructFieldDefinition<TOptions, TValueType, TRemoveInitFields>>;
``` ```
Similar to `deserialize`, creates a `FieldRuntimeValue` for this instance. Similar to `deserialize`, creates a `StructFieldValue` for this instance.
The difference is `createValue` will be called when a init value was provided to create a Struct value. The difference is `createValue` will be called when a init value was provided to create a Struct value.
### `FieldRuntimeValue` ### `StructFieldValue`
One `FieldDefinition` instance represents one field declaration, and one `FieldRuntimeValue` instance represents one value. One `StructFieldDefinition` instance represents one field declaration, and one `StructFieldValue` instance represents one value.
It defines how to get, set, and serialize a value. It defines how to get, set, and serialize a value.

View file

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

View file

@ -1,3 +1,5 @@
import type { ValueOrPromise } from '../utils';
/** /**
* Context with enough methods to serialize a struct * Context with enough methods to serialize a struct
*/ */
@ -23,7 +25,7 @@ export interface StructDeserializationContext extends StructSerializationContext
* Context should return exactly `length` bytes or data. If that's not possible * 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. * (due to end of file or other error condition), it should throw an error.
*/ */
read(length: number): ArrayBuffer | Promise<ArrayBuffer>; read(length: number): ValueOrPromise<ArrayBuffer>;
} }
export interface StructOptions { export interface StructOptions {

View file

@ -1,21 +1,23 @@
import { StructOptions, StructDeserializationContext, StructSerializationContext } from './context'; import { ValueOrPromise } from '../utils';
import { FieldDefinition } from './definition'; import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import { FieldRuntimeValue } from './runtime-value'; import { StructFieldDefinition } from './definition';
import { StructFieldValue } from './field-value';
import { StructValue } from './struct-value';
describe('FieldDefinition', () => { describe('StructFieldDefinition', () => {
describe('new', () => { describe('.constructor', () => {
it('should save the `options` parameter', () => { it('should save the `options` parameter', () => {
class MockFieldDefinition extends FieldDefinition<number>{ class MockFieldDefinition extends StructFieldDefinition<number>{
public constructor(options: number) { public constructor(options: number) {
super(options); super(options);
} }
public getSize(): number { public getSize(): number {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, object: any): FieldRuntimeValue<FieldDefinition<number, unknown, never>> | Promise<FieldRuntimeValue<FieldDefinition<number, unknown, never>>> { public create(options: Readonly<StructOptions>, context: StructSerializationContext, object: StructValue, struct: unknown): StructFieldValue<this> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
public createValue(options: Readonly<StructOptions>, context: StructSerializationContext, object: any, value: unknown): FieldRuntimeValue<FieldDefinition<number, unknown, never>> { public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, struct: StructValue): ValueOrPromise<StructFieldValue<this>> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
} }

View file

@ -1,41 +1,41 @@
import type { ValueOrPromise } from '../utils';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context'; import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { FieldRuntimeValue } from './runtime-value'; import type { StructFieldValue } from './field-value';
import type { StructValue } from './struct-value';
type ValueOrPromise<T> = T | Promise<T>;
/** /**
* A field definition is a bridge between its type and its runtime value. * A field definition is a bridge between its type and its runtime value.
* *
* `Struct` record fields in a list of `FieldDefinition`s. * `Struct` record fields in a list of `StructFieldDefinition`s.
* *
* When `Struct#create` or `Struct#deserialize` are called, each field's definition * 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. * crates its own type of `StructFieldValue` to manage the field value in that `Struct` instance.
* *
* One `FieldDefinition` can represents multiple similar types, just returns the corresponding * One `StructFieldDefinition` can represents multiple similar types, just returns the corresponding
* `FieldRuntimeValue` when `createValue` was called. * `StructFieldValue` when `createValue` was called.
* *
* @template TOptions TypeScript type of this definition's `options`. * @template TOptions TypeScript type of this definition's `options`.
* @template TValueType TypeScript type of this field. * @template TValue TypeScript type of this field.
* @template TOmitInit Optionally remove some fields from the init type. Should be a union of string literal types. * @template TOmitInitKey Optionally remove some fields from the init type. Should be a union of string literal types.
*/ */
export abstract class FieldDefinition< export abstract class StructFieldDefinition<
TOptions = void, TOptions = void,
TValueType = unknown, TValue = unknown,
TOmitInit = never, TOmitInitKey = never,
> { > {
public readonly options: TOptions; public readonly options: TOptions;
/** /**
* When `T` is a type initiated `FieldDefinition`, * When `T` is a type initiated `StructFieldDefinition`,
* use `T['valueType']` to retrieve its `TValueType` type parameter. * use `T['valueType']` to retrieve its `TValue` type parameter.
*/ */
public readonly valueType!: TValueType; public readonly valueType!: TValue;
/** /**
* When `T` is a type initiated `FieldDefinition`, * When `T` is a type initiated `StructFieldDefinition`,
* use `T['omitInitType']` to retrieve its `TOmitInit` type parameter. * use `T['omitInitKeyType']` to retrieve its `TOmitInitKey` type parameter.
*/ */
public readonly omitInitType!: TOmitInit; public readonly omitInitKeyType!: TOmitInitKey;
public constructor(options: TOptions) { public constructor(options: TOptions) {
this.options = options; this.options = options;
@ -44,26 +44,26 @@ export abstract class FieldDefinition<
/** /**
* When implemented in derived classes, returns the size (or minimal size if it's dynamic) of this field. * When implemented in derived classes, returns the size (or minimal size if it's dynamic) of this field.
* *
* Actual size can be retrieved from `FieldRuntimeValue#getSize` * Actual size can be retrieved from `StructFieldValue#getSize`
*/ */
public abstract getSize(): number; public abstract getSize(): number;
/** /**
* When implemented in derived classes, creates a `FieldRuntimeValue` by parsing the `context`. * When implemented in derived classes, creates a `StructFieldValue` from a given `value`.
*/
public abstract create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: StructValue,
struct: TValue,
): StructFieldValue<this>;
/**
* When implemented in derived classes, creates a `StructFieldValue` by parsing `context`.
*/ */
public abstract deserialize( public abstract deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructDeserializationContext, context: StructDeserializationContext,
object: any, struct: StructValue,
): ValueOrPromise<FieldRuntimeValue<FieldDefinition<TOptions, TValueType, TOmitInit>>>; ): ValueOrPromise<StructFieldValue<this>>;
/**
* When implemented in derived classes, creates a `FieldRuntimeValue` from a given `value`.
*/
public abstract createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
value: TValueType,
): FieldRuntimeValue<FieldDefinition<TOptions, TValueType, TOmitInit>>;
} }

View file

@ -1,11 +1,13 @@
import { ValueOrPromise } from '../utils';
import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context'; import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import { FieldDefinition } from './definition'; import { StructFieldDefinition } from './definition';
import { FieldRuntimeValue } from './runtime-value'; import { StructFieldValue } from './field-value';
import { StructValue } from './struct-value';
describe('FieldRuntimeValue', () => { describe('StructFieldValue', () => {
describe('.constructor', () => { describe('.constructor', () => {
it('should save parameters', () => { it('should save parameters', () => {
class MockFieldRuntimeValue extends FieldRuntimeValue { class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void { public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@ -14,58 +16,58 @@ describe('FieldRuntimeValue', () => {
const definition = 1 as any; const definition = 1 as any;
const options = 2 as any; const options = 2 as any;
const context = 3 as any; const context = 3 as any;
const object = 4 as any; const struct = 4 as any;
const value = 5 as any; const value = 5 as any;
const fieldRuntimeValue = new MockFieldRuntimeValue(definition, options, context, object, value); const fieldValue = new MockStructFieldValue(definition, options, context, struct, value);
expect(fieldRuntimeValue).toHaveProperty('definition', definition); expect(fieldValue).toHaveProperty('definition', definition);
expect(fieldRuntimeValue).toHaveProperty('options', options); expect(fieldValue).toHaveProperty('options', options);
expect(fieldRuntimeValue).toHaveProperty('context', context); expect(fieldValue).toHaveProperty('context', context);
expect(fieldRuntimeValue).toHaveProperty('object', object); expect(fieldValue).toHaveProperty('struct', struct);
expect(fieldRuntimeValue.get()).toBe(value); expect(fieldValue.get()).toBe(value);
}); });
}); });
describe('#getSize', () => { describe('#getSize', () => {
it('should return same value as definition\'s', () => { it('should return same value as definition\'s', () => {
class MockFieldDefinition extends FieldDefinition { class MockFieldDefinition extends StructFieldDefinition {
public getSize(): number { public getSize(): number {
return 42; return 42;
} }
public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, object: any): FieldRuntimeValue<FieldDefinition<void, unknown, never>> | Promise<FieldRuntimeValue<FieldDefinition<void, unknown, never>>> { public create(options: Readonly<StructOptions>, context: StructSerializationContext, object: StructValue, struct: unknown): StructFieldValue<this> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
public createValue(options: Readonly<StructOptions>, context: StructSerializationContext, object: any, value: unknown): FieldRuntimeValue<FieldDefinition<void, unknown, never>> { public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, struct: StructValue): ValueOrPromise<StructFieldValue<this>> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
} }
class MockFieldRuntimeValue extends FieldRuntimeValue { class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void { public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
} }
const fieldDefinition = new MockFieldDefinition(); const fieldDefinition = new MockFieldDefinition();
const fieldRuntimeValue = new MockFieldRuntimeValue(fieldDefinition, undefined as any, undefined as any, undefined as any, undefined as any); const fieldValue = new MockStructFieldValue(fieldDefinition, undefined as any, undefined as any, undefined as any, undefined as any);
expect(fieldRuntimeValue.getSize()).toBe(42); expect(fieldValue.getSize()).toBe(42);
}); });
}); });
describe('#set', () => { describe('#set', () => {
it('should update interval value', () => { it('should update its internal value', () => {
class MockFieldRuntimeValue extends FieldRuntimeValue { class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void { public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
} }
const fieldRuntimeValue = new MockFieldRuntimeValue(undefined as any, undefined as any, undefined as any, undefined as any, undefined as any); const fieldValue = new MockStructFieldValue(undefined as any, undefined as any, undefined as any, undefined as any, undefined as any);
fieldRuntimeValue.set(1); fieldValue.set(1);
expect(fieldRuntimeValue.get()).toBe(1); expect(fieldValue.get()).toBe(1);
fieldRuntimeValue.set(2); fieldValue.set(2);
expect(fieldRuntimeValue.get()).toBe(2); expect(fieldValue.get()).toBe(2);
}); });
}); });
}); });

View file

@ -1,14 +1,15 @@
import type { StructOptions, StructSerializationContext } from './context'; import type { StructOptions, StructSerializationContext } from './context';
import type { FieldDefinition } from './definition'; import type { StructFieldDefinition } from './definition';
import type { StructValue } from './struct-value';
/** /**
* Field runtime value manages one field of one `Struct` instance. * Field runtime value manages one field of one `Struct` instance.
* *
* If one `FieldDefinition` needs to change other field's semantics * If one `StructFieldDefinition` needs to change other field's semantics
* It can override other fields' `FieldRuntimeValue` in its own `FieldRuntimeValue`'s constructor * It can override other fields' `StructFieldValue` in its own `StructFieldValue`'s constructor
*/ */
export abstract class FieldRuntimeValue< export abstract class StructFieldValue<
TDefinition extends FieldDefinition<any, any, any> = FieldDefinition<any, any, any> TDefinition extends StructFieldDefinition<any, any, any> = StructFieldDefinition<any, any, any>
> { > {
/** Gets the definition associated with this runtime value */ /** Gets the definition associated with this runtime value */
public readonly definition: TDefinition; public readonly definition: TDefinition;
@ -20,7 +21,7 @@ export abstract class FieldRuntimeValue<
public readonly context: StructSerializationContext; public readonly context: StructSerializationContext;
/** Gets the associated `Struct` instance */ /** Gets the associated `Struct` instance */
public readonly object: any; public readonly struct: StructValue;
protected value: TDefinition['valueType']; protected value: TDefinition['valueType'];
@ -28,13 +29,13 @@ export abstract class FieldRuntimeValue<
definition: TDefinition, definition: TDefinition,
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructSerializationContext, context: StructSerializationContext,
object: any, struct: StructValue,
value: TDefinition['valueType'], value: TDefinition['valueType'],
) { ) {
this.definition = definition; this.definition = definition;
this.options = options; this.options = options;
this.context = context; this.context = context;
this.object = object; this.struct = struct;
this.value = value; this.value = value;
} }

View file

@ -1,4 +1,4 @@
export * from './context'; export * from './context';
export * from './definition'; export * from './definition';
export * from './runtime-value'; export * from './field-value';
export * from './runtime-object'; export * from './struct-value';

View file

@ -1,38 +1,68 @@
import { createRuntimeObject, getRuntimeValue, setRuntimeValue } from './runtime-object'; import { StructValue } from './struct-value';
describe('RuntimeObject', () => { describe('StructValue', () => {
describe('createRuntimeObject', () => { describe('.constructor', () => {
it('should create a special object', () => { it('should create `fieldValues` and `value`', () => {
const object = createRuntimeObject(); const foo = new StructValue();
expect(Object.getOwnPropertySymbols(object)).toHaveLength(1); const bar = new StructValue();
expect(foo).toHaveProperty('fieldValues', {});
expect(bar).toHaveProperty('fieldValues', {});
expect(foo.fieldValues).not.toBe(bar.fieldValues);
}); });
}); });
describe('getRuntimeValue', () => { describe('#set', () => {
it('should return previously set value', () => { it('should save the `StructFieldValue`', () => {
const object = createRuntimeObject(); const object = new StructValue();
const field = 'foo';
const value = {} as any; const foo = 'foo';
setRuntimeValue(object, field, value); const fooValue = {} as any;
expect(getRuntimeValue(object, field)).toBe(value); object.set(foo, fooValue);
const bar = 'bar';
const barValue = {} as any;
object.set(bar, barValue);
expect(object.fieldValues[foo]).toBe(fooValue);
expect(object.fieldValues[bar]).toBe(barValue);
});
it('should define a property for `key`', () => {
const object = new StructValue();
const foo = 'foo';
const fooGetter = jest.fn(() => 42);
const fooSetter = jest.fn((value: number) => { });
const fooValue = { get: fooGetter, set: fooSetter } as any;
object.set(foo, fooValue);
const bar = 'bar';
const barGetter = jest.fn(() => true);
const barSetter = jest.fn((value: number) => { });
const barValue = { get: barGetter, set: barSetter } as any;
object.set(bar, barValue);
expect(object.value).toHaveProperty(foo, 42);
expect(fooGetter).toBeCalledTimes(1);
expect(barGetter).toBeCalledTimes(0);
object.value[foo] = 100;
expect(fooSetter).toBeCalledTimes(1);
expect(fooSetter).lastCalledWith(100);
expect(barSetter).toBeCalledTimes(0);
}); });
}); });
describe('setRuntimeValue', () => { describe('#get', () => {
it('should define a proxy property to underlying `RuntimeValue`', () => { it('should return previously set `StructFieldValue`', () => {
const object = createRuntimeObject(); const object = new StructValue();
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); const foo = 'foo';
expect(getter).toBeCalledTimes(1); const fooValue = {} as any;
object.set(foo, fooValue);
(object as any)[field] = 100; expect(object.get(foo)).toBe(fooValue);
expect(setter).toBeCalledTimes(1);
expect(setter).lastCalledWith(100);
}); });
}); });
}); });

View file

@ -1,36 +1,45 @@
import { FieldRuntimeValue } from './runtime-value'; import type { StructFieldValue } from './field-value';
const RuntimeValues = Symbol('RuntimeValues'); /**
* Manages the initialization process of a struct value
*/
export class StructValue {
/** @internal */ readonly fieldValues: Record<PropertyKey, StructFieldValue> = {};
export interface RuntimeObject { /**
[RuntimeValues]: Record<PropertyKey, FieldRuntimeValue>; * Gets the result struct value object
} */
public readonly value: Record<PropertyKey, unknown> = {};
/** Creates a new runtime object that can be used with `getRuntimeValue` and `setRuntimeValue` */ /**
export function createRuntimeObject(): RuntimeObject { * Sets a `StructFieldValue` for `key`
return { *
[RuntimeValues]: {}, * @param key The field name
}; * @param value The associated `StructFieldValue`
} */
public set(key: PropertyKey, value: StructFieldValue): void {
// TODO: TypeScript 4.2 will allow this behavior
// https://github.com/microsoft/TypeScript/pull/26797
// @ts-expect-error Type 'symbol' cannot be used as an index type. ts(2538)
this.fieldValues[key] = value;
/** Gets the previously set `RuntimeValue` for specified `key` on `object` */ Object.defineProperty(this.value, key, {
export function getRuntimeValue(object: RuntimeObject, key: PropertyKey): FieldRuntimeValue { configurable: true,
return object[RuntimeValues][key as any] as FieldRuntimeValue; enumerable: true,
get() { return value.get(); },
set(v) { value.set(v); },
});
} }
/** /**
* Sets the `RuntimeValue` for specified `key` on `object`, * Gets a previously `StructFieldValue` for `key`
* also sets up property accessors so reads/writes to `object`'s `key` will be forwarded to *
* the underlying `RuntimeValue` * @param key The field name
*/ */
export function setRuntimeValue(object: RuntimeObject, key: PropertyKey, runtimeValue: FieldRuntimeValue): void { public get(key: PropertyKey): StructFieldValue {
delete (object as any)[key]; // TODO: TypeScript 4.2 will allow this behavior
// https://github.com/microsoft/TypeScript/pull/26797
object[RuntimeValues][key as any] = runtimeValue; // @ts-expect-error Type 'symbol' cannot be used as an index type. ts(2538)
Object.defineProperty(object, key, { return this.fieldValues[key];
configurable: true, }
enumerable: true,
get() { return runtimeValue.get(); },
set(value) { runtimeValue.set(value); },
});
} }

View file

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

View file

@ -1,5 +1,6 @@
import { createRuntimeObject, FieldDefinition, FieldRuntimeValue, getRuntimeValue, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './basic'; import { StructDefaultOptions, StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from './basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldType, Evaluate, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, Identity, KeysOfType, NumberFieldDefinition, NumberFieldType, Overwrite, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types'; import { ArrayBufferFieldType, ArrayBufferLikeFieldType, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
import { Evaluate, Identity, KeysOfType, Overwrite } from './utils';
export interface StructLike<TValue> { export interface StructLike<TValue> {
deserialize(context: StructDeserializationContext): Promise<TValue>; deserialize(context: StructDeserializationContext): Promise<TValue>;
@ -24,13 +25,13 @@ type AddFieldDescriptor<
TExtra extends object, TExtra extends object,
TPostDeserialized, TPostDeserialized,
TFieldName extends PropertyKey, TFieldName extends PropertyKey,
TDefinition extends FieldDefinition<any, any, any>> = TDefinition extends StructFieldDefinition<any, any, any>> =
Identity<Struct< Identity<Struct<
// Merge two types // Merge two types
// Evaluate immediately to optimize editor hover tooltip // Evaluate immediately to optimize editor hover tooltip
Evaluate<TFields & Record<TFieldName, TDefinition['valueType']>>, Evaluate<TFields & Record<TFieldName, TDefinition['valueType']>>,
// Merge two `TOmitInit // Merge two `TOmitInit
TOmitInit | TDefinition['omitInitType'], TOmitInit | TDefinition['omitInitKeyType'],
TExtra, TExtra,
TPostDeserialized TPostDeserialized
>>; >>;
@ -181,7 +182,7 @@ export class Struct<
*/ */
public get size() { return this._size; } public get size() { return this._size; }
private _fields: [name: PropertyKey, definition: FieldDefinition<any, any, any>][] = []; private _fields: [name: PropertyKey, definition: StructFieldDefinition<any, any, any>][] = [];
private _extra: PropertyDescriptorMap = {}; private _extra: PropertyDescriptorMap = {};
@ -192,11 +193,11 @@ export class Struct<
} }
/** /**
* Appends a `FieldDefinition` to the `Struct * Appends a `StructFieldDefinition` to the `Struct
*/ */
public field< public field<
TName extends PropertyKey, TName extends PropertyKey,
TDefinition extends FieldDefinition<any, any, any> TDefinition extends StructFieldDefinition<any, any, any>
>( >(
name: TName, name: TName,
definition: TDefinition, definition: TDefinition,
@ -516,61 +517,55 @@ export class Struct<
return this as any; return this as any;
} }
private initializeObject() { private initializeStructValue() {
const object = createRuntimeObject(); const value = new StructValue();
Object.defineProperties(object, this._extra); Object.defineProperties(value.value, this._extra);
return object; return value;
}
public create(init: Evaluate<Omit<TFields, TOmitInit>>, context: StructSerializationContext): Overwrite<TExtra, TFields> {
const object = this.initializeObject();
for (const [name, definition] of this._fields) {
const runtimeValue = definition.createValue(this.options, context, object, (init as any)[name]);
setRuntimeValue(object, name, runtimeValue);
}
return object as any;
} }
public async deserialize( public async deserialize(
context: StructDeserializationContext context: StructDeserializationContext
): Promise<StructDeserializedType<TFields, TExtra, TPostDeserialized>> { ): Promise<StructDeserializedType<TFields, TExtra, TPostDeserialized>> {
const object = this.initializeObject(); const value = this.initializeStructValue();
for (const [name, definition] of this._fields) { for (const [name, definition] of this._fields) {
const runtimeValue = await definition.deserialize(this.options, context, object); const fieldValue = await definition.deserialize(this.options, context, value);
setRuntimeValue(object, name, runtimeValue); value.set(name, fieldValue);
} }
if (this._postDeserialized) { if (this._postDeserialized) {
const result = this._postDeserialized.call(object as TFields, object as TFields); const result = this._postDeserialized.call(value.value as TFields, value as TFields);
if (result) { if (result) {
return result; return result;
} }
} }
return object as any; return value.value as any;
} }
public serialize(init: Evaluate<Omit<TFields, TOmitInit>>, context: StructSerializationContext): ArrayBuffer { public serialize(init: Evaluate<Omit<TFields, TOmitInit>>, context: StructSerializationContext): ArrayBuffer {
const object = this.create(init, context) as any; const value = this.initializeStructValue();
for (const [name, definition] of this._fields) {
const fieldValue = definition.create(this.options, context, value, (init as any)[name]);
value.set(name, fieldValue);
}
let structSize = 0; let structSize = 0;
const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = []; const fieldsInfo: { fieldValue: StructFieldValue, size: number; }[] = [];
for (const [name] of this._fields) { for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, name); const fieldValue = value.get(name);
const size = runtimeValue.getSize(); const size = fieldValue.getSize();
fieldsInfo.push({ runtimeValue, size }); fieldsInfo.push({ fieldValue, size });
structSize += size; structSize += size;
} }
const buffer = new ArrayBuffer(structSize); const buffer = new ArrayBuffer(structSize);
const dataView = new DataView(buffer); const dataView = new DataView(buffer);
let offset = 0; let offset = 0;
for (const { runtimeValue, size } of fieldsInfo) { for (const { fieldValue, size } of fieldsInfo) {
runtimeValue.serialize(dataView, offset, context); fieldValue.serialize(dataView, offset, context);
offset += size; offset += size;
} }

View file

@ -1,23 +1,24 @@
import { StructDeserializationContext, StructSerializationContext } from '../basic'; import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { ArrayBufferFieldType, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer'; import { ArrayBufferFieldType, ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, ArrayBufferLikeFieldValue, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
describe('Types', () => { describe('Types', () => {
describe('ArrayBufferLike', () => {
describe('ArrayBufferFieldType', () => { describe('ArrayBufferFieldType', () => {
it('should have a static instance', () => { it('should have a static instance', () => {
expect(ArrayBufferFieldType.instance).toBeInstanceOf(ArrayBufferFieldType); expect(ArrayBufferFieldType.instance).toBeInstanceOf(ArrayBufferFieldType);
}); });
it('`toArrayBuffer` should return the same `ArrayBuffer`', () => { it('`#toArrayBuffer` should return the same `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10); const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.toArrayBuffer(arrayBuffer)).toBe(arrayBuffer); expect(ArrayBufferFieldType.instance.toArrayBuffer(arrayBuffer)).toBe(arrayBuffer);
}); });
it('`fromArrayBuffer` should return the same `ArrayBuffer`', () => { it('`#fromArrayBuffer` should return the same `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10); const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.fromArrayBuffer(arrayBuffer)).toBe(arrayBuffer); expect(ArrayBufferFieldType.instance.fromArrayBuffer(arrayBuffer)).toBe(arrayBuffer);
}); });
it('`getSize` should return the `byteLength` of the `ArrayBuffer`', () => { it('`#getSize` should return the `byteLength` of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10); const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.getSize(arrayBuffer)).toBe(10); expect(ArrayBufferFieldType.instance.getSize(arrayBuffer)).toBe(10);
}); });
@ -28,13 +29,13 @@ describe('Types', () => {
expect(Uint8ClampedArrayFieldType.instance).toBeInstanceOf(Uint8ClampedArrayFieldType); expect(Uint8ClampedArrayFieldType.instance).toBeInstanceOf(Uint8ClampedArrayFieldType);
}); });
it('`toArrayBuffer` should return its `buffer`', () => { it('`#toArrayBuffer` should return its `buffer`', () => {
const array = new Uint8ClampedArray(10); const array = new Uint8ClampedArray(10);
const buffer = array.buffer; const buffer = array.buffer;
expect(Uint8ClampedArrayFieldType.instance.toArrayBuffer(array)).toBe(buffer); expect(Uint8ClampedArrayFieldType.instance.toArrayBuffer(array)).toBe(buffer);
}); });
it('`fromArrayBuffer` should return a view of the `ArrayBuffer`', () => { it('`#fromArrayBuffer` should return a view of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10); const arrayBuffer = new ArrayBuffer(10);
const array = Uint8ClampedArrayFieldType.instance.fromArrayBuffer(arrayBuffer); const array = Uint8ClampedArrayFieldType.instance.fromArrayBuffer(arrayBuffer);
expect(array).toHaveProperty('buffer', arrayBuffer); expect(array).toHaveProperty('buffer', arrayBuffer);
@ -42,7 +43,7 @@ describe('Types', () => {
expect(array).toHaveProperty('byteLength', 10); expect(array).toHaveProperty('byteLength', 10);
}); });
it('`getSize` should return the `byteLength` of the `ArrayBuffer`', () => { it('`#getSize` should return the `byteLength` of the `Uint8ClampedArray`', () => {
const array = new Uint8ClampedArray(10); const array = new Uint8ClampedArray(10);
expect(Uint8ClampedArrayFieldType.instance.getSize(array)).toBe(10); expect(Uint8ClampedArrayFieldType.instance.getSize(array)).toBe(10);
}); });
@ -53,7 +54,7 @@ describe('Types', () => {
expect(StringFieldType.instance).toBeInstanceOf(StringFieldType); expect(StringFieldType.instance).toBeInstanceOf(StringFieldType);
}); });
it('`toArrayBuffer` should return the decoded string', () => { it('`#toArrayBuffer` should return the decoded string', () => {
const text = 'foo'; const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8'); const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructSerializationContext = { const context: StructSerializationContext = {
@ -64,7 +65,7 @@ describe('Types', () => {
expect(StringFieldType.instance.toArrayBuffer(text, context)).toEqual(arrayBuffer); expect(StringFieldType.instance.toArrayBuffer(text, context)).toEqual(arrayBuffer);
}); });
it('`fromArrayBuffer` should return the encoded ArrayBuffer', () => { it('`#fromArrayBuffer` should return the encoded ArrayBuffer', () => {
const text = 'foo'; const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8'); const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructDeserializationContext = { const context: StructDeserializationContext = {
@ -81,8 +82,149 @@ describe('Types', () => {
expect(StringFieldType.instance.fromArrayBuffer(arrayBuffer, context)).toBe(text); expect(StringFieldType.instance.fromArrayBuffer(arrayBuffer, context)).toBe(text);
}); });
it('`getSize` should return -1', () => { it('`#getSize` should return -1', () => {
expect(StringFieldType.instance.getSize()).toBe(-1); expect(StringFieldType.instance.getSize()).toBe(-1);
}); });
}); });
class MockArrayBufferFieldDefinition<TType extends ArrayBufferLikeFieldType>
extends ArrayBufferLikeFieldDefinition<TType, number> {
public getSize(): number {
return this.options;
}
}
describe('ArrayBufferLikeFieldDefinition', () => {
it('should work with `ArrayBufferFieldType`', async () => {
const buffer = new ArrayBuffer(10);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const size = 10;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
const value = fieldValue.get();
expect(value).toBe(buffer);
});
it('should work with `Uint8ClampedArrayFieldType`', async () => {
const buffer = new ArrayBuffer(10);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const size = 10;
const definition = new MockArrayBufferFieldDefinition(Uint8ClampedArrayFieldType.instance, size);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(10);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
const value = fieldValue.get();
expect(value).toBeInstanceOf(Uint8ClampedArray);
expect(value).toHaveProperty('buffer', buffer);
});
it('should work when `#getSize` returns `0`', async () => {
const buffer = new ArrayBuffer(10);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(0);
expect(fieldValue.arrayBuffer).toBeInstanceOf(ArrayBuffer);
expect(fieldValue.arrayBuffer).toHaveProperty('byteLength', 0);
const value = fieldValue.get();
expect(value).toBeInstanceOf(ArrayBuffer);
expect(value).toHaveProperty('byteLength', 0);
});
});
describe('ArrayBufferLikeFieldValue', () => {
describe('#set', () => {
it('should clear `arrayBuffer` field', async () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
fieldValue.set(new ArrayBuffer(20));
expect(fieldValue).toHaveProperty('arrayBuffer', undefined);
});
});
describe('#serialize', () => {
it('should be able to serialize a deserialized value', async () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const sourceArray = new Uint8Array(Array.from({ length: size }, (_, i) => i));
const sourceBuffer = sourceArray.buffer;
const read = jest.fn((length: number) => sourceBuffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
const targetArray = new Uint8Array(size);
const targetView = new DataView(targetArray.buffer);
fieldValue.serialize(targetView, 0, context);
expect(targetArray).toEqual(sourceArray);
});
it('should be able to serialize a modified value', async () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const sourceArray = new Uint8Array(Array.from({ length: size }, (_, i) => i));
const sourceBuffer = sourceArray.buffer;
const read = jest.fn((length: number) => sourceBuffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
fieldValue.set(sourceArray.buffer);
const targetArray = new Uint8Array(size);
const targetView = new DataView(targetArray.buffer);
fieldValue.serialize(targetView, 0, context);
expect(targetArray).toEqual(sourceArray);
});
});
});
});
}); });

View file

@ -1,15 +1,15 @@
import { FieldDefinition, FieldRuntimeValue, StructDeserializationContext, StructOptions, StructSerializationContext } from '../basic'; import { StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from '../basic';
/** /**
* Base class for all types that * Base class for all types that
* can be converted from an ArrayBuffer when deserialized, * can be converted from an ArrayBuffer when deserialized,
* and need to be converted to an ArrayBuffer when serializing * and need to be converted to an ArrayBuffer when serializing
* *
* @template TType The actual TypeScript type of this type * @template TValue The actual TypeScript type of this type
* @template TTypeScriptType Optional another type (should be compatible with `TType`) * @template TTypeScriptType Optional another type (should be compatible with `TType`)
* specified by user when creating field definitions. * specified by user when creating field definitions.
*/ */
export abstract class ArrayBufferLikeFieldType<TType = unknown, TTypeScriptType = TType> { export abstract class ArrayBufferLikeFieldType<TValue = unknown, TTypeScriptType = TValue> {
public readonly valueType!: TTypeScriptType; public readonly valueType!: TTypeScriptType;
/** /**
@ -18,10 +18,10 @@ export abstract class ArrayBufferLikeFieldType<TType = unknown, TTypeScriptType
* This function should be "pure", i.e., * This function should be "pure", i.e.,
* same `value` should always be converted to `ArrayBuffer`s that have same content. * same `value` should always be converted to `ArrayBuffer`s that have same content.
*/ */
public abstract toArrayBuffer(value: TType, context: StructSerializationContext): ArrayBuffer; public abstract toArrayBuffer(value: TValue, context: StructSerializationContext): ArrayBuffer;
/** When implemented in derived classes, converts the `ArrayBuffer` to a type-specific value */ /** When implemented in derived classes, converts the `ArrayBuffer` to a type-specific value */
public abstract fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): TType; public abstract fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): TValue;
/** /**
* When implemented in derived classes, gets the size in byte of the type-specific `value`. * When implemented in derived classes, gets the size in byte of the type-specific `value`.
@ -30,7 +30,7 @@ export abstract class ArrayBufferLikeFieldType<TType = unknown, TTypeScriptType
* implementer should returns `-1` so the caller will get its size by first converting it to * implementer should returns `-1` so the caller will get its size by first converting it to
* an `ArrayBuffer` (and cache the result). * an `ArrayBuffer` (and cache the result).
*/ */
public abstract getSize(value: TType): number; public abstract getSize(value: TValue): number;
} }
/** An ArrayBufferLike type that's actually an `ArrayBuffer` */ /** An ArrayBufferLike type that's actually an `ArrayBuffer` */
@ -54,7 +54,7 @@ export class ArrayBufferFieldType extends ArrayBufferLikeFieldType<ArrayBuffer>
} }
} }
/** Am ArrayBufferLike type that converts to/from the `ArrayBuffer` from/to a `Uint8ClampedArray` */ /** Am ArrayBufferLike type that converts between `ArrayBuffer` and `Uint8ClampedArray` */
export class Uint8ClampedArrayFieldType export class Uint8ClampedArrayFieldType
extends ArrayBufferLikeFieldType<Uint8ClampedArray, Uint8ClampedArray> { extends ArrayBufferLikeFieldType<Uint8ClampedArray, Uint8ClampedArray> {
public static readonly instance = new Uint8ClampedArrayFieldType(); public static readonly instance = new Uint8ClampedArrayFieldType();
@ -76,7 +76,7 @@ export class Uint8ClampedArrayFieldType
} }
} }
/** Am ArrayBufferLike type that converts to/from the `ArrayBuffer` from/to a `string` */ /** Am ArrayBufferLike type that converts between `ArrayBuffer` and `string` */
export class StringFieldType<TTypeScriptType = string> export class StringFieldType<TTypeScriptType = string>
extends ArrayBufferLikeFieldType<string, TTypeScriptType> { extends ArrayBufferLikeFieldType<string, TTypeScriptType> {
public static readonly instance = new StringFieldType(); public static readonly instance = new StringFieldType();
@ -102,11 +102,11 @@ const EmptyArrayBuffer = new ArrayBuffer(0);
export abstract class ArrayBufferLikeFieldDefinition< export abstract class ArrayBufferLikeFieldDefinition<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType, TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TOptions = void, TOptions = void,
TOmitInit = never, TOmitInitKey = never,
> extends FieldDefinition< > extends StructFieldDefinition<
TOptions, TOptions,
TType['valueType'], TType['valueType'],
TOmitInit TOmitInitKey
>{ >{
public readonly type: TType; public readonly type: TType;
@ -115,16 +115,28 @@ export abstract class ArrayBufferLikeFieldDefinition<
this.type = type; this.type = type;
} }
protected getDeserializeSize(object: any): number { protected getDeserializeSize(struct: StructValue): number {
return this.getSize(); return this.getSize();
} }
/**
* When implemented in derived classes, creates a `StructFieldValue` for the current field definition.
*/
public create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TType['valueType'],
): ArrayBufferLikeFieldValue<this> {
return new ArrayBufferLikeFieldValue(this, options, context, struct, value);
}
public async deserialize( public async deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructDeserializationContext, context: StructDeserializationContext,
object: any, struct: StructValue,
): Promise<ArrayBufferLikeFieldRuntimeValue<ArrayBufferLikeFieldDefinition<TType, TOptions, TOmitInit>>> { ): Promise<ArrayBufferLikeFieldValue<this>> {
const size = this.getDeserializeSize(object); const size = this.getDeserializeSize(struct);
let arrayBuffer: ArrayBuffer; let arrayBuffer: ArrayBuffer;
if (size === 0) { if (size === 0) {
@ -134,25 +146,15 @@ export abstract class ArrayBufferLikeFieldDefinition<
} }
const value = this.type.fromArrayBuffer(arrayBuffer, context); const value = this.type.fromArrayBuffer(arrayBuffer, context);
const runtimeValue = this.createValue(options, context, object, value); const fieldValue = this.create(options, context, struct, value);
runtimeValue.arrayBuffer = arrayBuffer; fieldValue.arrayBuffer = arrayBuffer;
return runtimeValue; return fieldValue;
}
} }
/** export class ArrayBufferLikeFieldValue<
* When implemented in derived classes, creates a `FieldRuntimeValue` for the current field definition. TDefinition extends ArrayBufferLikeFieldDefinition<ArrayBufferLikeFieldType, any, any>,
*/ > extends StructFieldValue<TDefinition> {
public abstract createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
value: TType['valueType'],
): ArrayBufferLikeFieldRuntimeValue<ArrayBufferLikeFieldDefinition<TType, TOptions, TOmitInit>>;
}
export class ArrayBufferLikeFieldRuntimeValue<
TDefinition extends ArrayBufferLikeFieldDefinition<any, any, any>,
> extends FieldRuntimeValue<TDefinition> {
public arrayBuffer: ArrayBuffer | undefined; public arrayBuffer: ArrayBuffer | undefined;
public set(value: TDefinition['valueType']): void { public set(value: TDefinition['valueType']): void {
@ -166,6 +168,6 @@ export class ArrayBufferLikeFieldRuntimeValue<
} }
new Uint8Array(dataView.buffer) new Uint8Array(dataView.buffer)
.set(new Uint8Array(this.arrayBuffer!), offset); .set(new Uint8Array(this.arrayBuffer), offset);
} }
} }

View file

@ -0,0 +1,13 @@
import { ArrayBufferFieldType } from './array-buffer';
import { FixedLengthArrayBufferLikeFieldDefinition } from './fixed-length-array-buffer';
describe('Types', () => {
describe('FixedLengthArrayBufferLikeFieldDefinition', () => {
describe('#getSize', () => {
it('should return size in its options', () => {
const definition = new FixedLengthArrayBufferLikeFieldDefinition(ArrayBufferFieldType.instance, { length: 10 });
expect(definition.getSize()).toBe(10);
});
});
});
});

View file

@ -1,5 +1,4 @@
import { StructOptions, StructSerializationContext } from '../basic'; import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType } from './array-buffer';
import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldRuntimeValue, ArrayBufferLikeFieldType } from './array-buffer';
export interface FixedLengthArrayBufferLikeFieldOptions { export interface FixedLengthArrayBufferLikeFieldOptions {
length: number; length: number;
@ -15,13 +14,4 @@ export class FixedLengthArrayBufferLikeFieldDefinition<
public getSize(): number { public getSize(): number {
return this.options.length; return this.options.length;
} }
public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
value: TType['valueType']
): ArrayBufferLikeFieldRuntimeValue<FixedLengthArrayBufferLikeFieldDefinition<TType, TOptions>> {
return new ArrayBufferLikeFieldRuntimeValue(this, options, context, object, value);
}
}; };

View file

@ -1,5 +1,4 @@
export * from './array-buffer'; export * from './array-buffer';
export * from './fixed-length-array-buffer'; export * from './fixed-length-array-buffer';
export * from './number'; export * from './number';
export * from './utils';
export * from './variable-length-array-buffer'; export * from './variable-length-array-buffer';

View file

@ -1,5 +1,5 @@
import { StructDefaultOptions, StructDeserializationContext, StructSerializationContext } from '../basic'; import { StructDefaultOptions, StructDeserializationContext, StructSerializationContext, StructValue } from '../basic';
import { NumberFieldDefinition, NumberFieldRuntimeValue, NumberFieldType } from './number'; import { NumberFieldDefinition, NumberFieldType } from './number';
describe('Types', () => { describe('Types', () => {
describe('Number', () => { describe('Number', () => {
@ -62,41 +62,20 @@ describe('Types', () => {
}); });
describe('NumberFieldDefinition', () => { describe('NumberFieldDefinition', () => {
describe('getSize', () => { describe('#getSize', () => {
it('should return 1 for int8', () => { it('should return size of its type', () => {
expect(new NumberFieldDefinition(NumberFieldType.Int8).getSize()).toBe(1); expect(new NumberFieldDefinition(NumberFieldType.Int8).getSize()).toBe(1);
});
it('should return 1 for uint8', () => {
expect(new NumberFieldDefinition(NumberFieldType.Uint8).getSize()).toBe(1); expect(new NumberFieldDefinition(NumberFieldType.Uint8).getSize()).toBe(1);
});
it('should return 2 for int16', () => {
expect(new NumberFieldDefinition(NumberFieldType.Int16).getSize()).toBe(2); expect(new NumberFieldDefinition(NumberFieldType.Int16).getSize()).toBe(2);
});
it('should return 2 for uint16', () => {
expect(new NumberFieldDefinition(NumberFieldType.Uint16).getSize()).toBe(2); expect(new NumberFieldDefinition(NumberFieldType.Uint16).getSize()).toBe(2);
});
it('should return 4 for int32', () => {
expect(new NumberFieldDefinition(NumberFieldType.Int32).getSize()).toBe(4); expect(new NumberFieldDefinition(NumberFieldType.Int32).getSize()).toBe(4);
});
it('should return 4 for uint32', () => {
expect(new NumberFieldDefinition(NumberFieldType.Uint32).getSize()).toBe(4); expect(new NumberFieldDefinition(NumberFieldType.Uint32).getSize()).toBe(4);
});
it('should return 8 for int64', () => {
expect(new NumberFieldDefinition(NumberFieldType.Int64).getSize()).toBe(8); expect(new NumberFieldDefinition(NumberFieldType.Int64).getSize()).toBe(8);
});
it('should return 8 for uint64', () => {
expect(new NumberFieldDefinition(NumberFieldType.Uint64).getSize()).toBe(8); expect(new NumberFieldDefinition(NumberFieldType.Uint64).getSize()).toBe(8);
}); });
}); });
describe('deserialize', () => { describe('#deserialize', () => {
it('should deserialize Uint8', async () => { it('should deserialize Uint8', async () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]).buffer); const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]).buffer);
const context: StructDeserializationContext = { const context: StructDeserializationContext = {
@ -106,10 +85,11 @@ describe('Types', () => {
}; };
const definition = new NumberFieldDefinition(NumberFieldType.Uint8); const definition = new NumberFieldDefinition(NumberFieldType.Uint8);
const struct = new StructValue();
const value = await definition.deserialize( const value = await definition.deserialize(
StructDefaultOptions, StructDefaultOptions,
context, context,
{} struct,
); );
expect(value.get()).toBe(1); expect(value.get()).toBe(1);
@ -126,10 +106,11 @@ describe('Types', () => {
}; };
const definition = new NumberFieldDefinition(NumberFieldType.Uint16); const definition = new NumberFieldDefinition(NumberFieldType.Uint16);
const struct = new StructValue();
const value = await definition.deserialize( const value = await definition.deserialize(
StructDefaultOptions, StructDefaultOptions,
context, context,
{} struct,
); );
expect(value.get()).toBe((1 << 8) | 2); expect(value.get()).toBe((1 << 8) | 2);
@ -146,10 +127,11 @@ describe('Types', () => {
}; };
const definition = new NumberFieldDefinition(NumberFieldType.Uint16); const definition = new NumberFieldDefinition(NumberFieldType.Uint16);
const struct = new StructValue();
const value = await definition.deserialize( const value = await definition.deserialize(
{ ...StructDefaultOptions, littleEndian: true }, { ...StructDefaultOptions, littleEndian: true },
context, context,
{} struct,
); );
expect(value.get()).toBe((2 << 8) | 1); expect(value.get()).toBe((2 << 8) | 1);
@ -159,100 +141,121 @@ describe('Types', () => {
}); });
}); });
describe('NumberRuntimeValue', () => { describe('NumberFieldValue', () => {
describe('getSize', () => { describe('#getSize', () => {
it('should return 1 for int8', () => { it('should return size of its definition', () => {
const context: StructSerializationContext = { const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); }, encodeUtf8(input) { throw new Error(''); },
}; };
const definition = new NumberFieldDefinition(NumberFieldType.Int8); const struct = new StructValue();
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42);
expect(runtimeValue.getSize()).toBe(1); expect(
}); new NumberFieldDefinition(NumberFieldType.Int8)
.create(
StructDefaultOptions,
context,
struct,
42,
)
.getSize()
).toBe(1);
it('should return 1 for uint8', () => { expect(
const context: StructSerializationContext = { new NumberFieldDefinition(NumberFieldType.Uint8)
encodeUtf8(input) { throw new Error(''); }, .create(
}; StructDefaultOptions,
const definition = new NumberFieldDefinition(NumberFieldType.Uint8); context,
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42); struct,
42,
)
.getSize()
).toBe(1);
expect(runtimeValue.getSize()).toBe(1); expect(
}); new NumberFieldDefinition(NumberFieldType.Int16)
.create(
StructDefaultOptions,
context,
struct,
42,
)
.getSize()
).toBe(2);
it('should return 2 for int16', () => { expect(
const context: StructSerializationContext = { new NumberFieldDefinition(NumberFieldType.Uint16)
encodeUtf8(input) { throw new Error(''); }, .create(
}; StructDefaultOptions,
const definition = new NumberFieldDefinition(NumberFieldType.Int16); context,
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42); struct,
42,
)
.getSize()
).toBe(2);
expect(runtimeValue.getSize()).toBe(2); expect(
}); new NumberFieldDefinition(NumberFieldType.Int32)
.create(
StructDefaultOptions,
context,
struct,
42,
)
.getSize()
).toBe(4);
it('should return 2 for uint16', () => { expect(
const context: StructSerializationContext = { new NumberFieldDefinition(NumberFieldType.Uint32)
encodeUtf8(input) { throw new Error(''); }, .create(
}; StructDefaultOptions,
const definition = new NumberFieldDefinition(NumberFieldType.Uint16); context,
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42); struct,
42,
)
.getSize()
).toBe(4);
expect(runtimeValue.getSize()).toBe(2); expect(
}); new NumberFieldDefinition(NumberFieldType.Int64)
.create(
StructDefaultOptions,
context,
struct,
BigInt(100),
)
.getSize()
).toBe(8);
it('should return 4 for int32', () => { expect(
const context: StructSerializationContext = { new NumberFieldDefinition(NumberFieldType.Uint64)
encodeUtf8(input) { throw new Error(''); }, .create(
}; StructDefaultOptions,
const definition = new NumberFieldDefinition(NumberFieldType.Int32); context,
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42); struct,
BigInt(100),
expect(runtimeValue.getSize()).toBe(4); )
}); .getSize()
).toBe(8);
it('should return 4 for uint32', () => {
const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); },
};
const definition = new NumberFieldDefinition(NumberFieldType.Uint32);
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42);
expect(runtimeValue.getSize()).toBe(4);
});
it('should return 8 for int64', () => {
const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); },
};
const definition = new NumberFieldDefinition(NumberFieldType.Int64);
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, BigInt(42));
expect(runtimeValue.getSize()).toBe(8);
});
it('should return 8 for uint64', () => {
const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); },
};
const definition = new NumberFieldDefinition(NumberFieldType.Uint64);
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, BigInt(42));
expect(runtimeValue.getSize()).toBe(8);
}); });
}); });
describe('serialize', () => { describe('#serialize', () => {
it('should serialize uint8', () => { it('should serialize uint8', () => {
const context: StructSerializationContext = { const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); }, encodeUtf8(input) { throw new Error(''); },
}; };
const definition = new NumberFieldDefinition(NumberFieldType.Int8); const definition = new NumberFieldDefinition(NumberFieldType.Int8);
const runtimeValue = definition.createValue(StructDefaultOptions, context, {}, 42); const struct = new StructValue();
const value = definition.create(
StructDefaultOptions,
context,
struct,
42,
);
const array = new Uint8Array(10); const array = new Uint8Array(10);
const dataView = new DataView(array.buffer); const dataView = new DataView(array.buffer);
runtimeValue.serialize(dataView, 2); value.serialize(dataView, 2);
expect(Array.from(array)).toEqual([0, 0, 42, 0, 0, 0, 0, 0, 0, 0]); expect(Array.from(array)).toEqual([0, 0, 42, 0, 0, 0, 0, 0, 0, 0]);
}); });

View file

@ -1,4 +1,4 @@
import { FieldDefinition, FieldRuntimeValue, StructDeserializationContext, StructOptions, StructSerializationContext } from '../basic'; import { StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } 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];
@ -45,7 +45,7 @@ export class NumberFieldType<TTypeScriptType extends number | bigint = number |
export class NumberFieldDefinition< export class NumberFieldDefinition<
TType extends NumberFieldType = NumberFieldType, TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = TType['valueType'], TTypeScriptType = TType['valueType'],
> extends FieldDefinition< > extends StructFieldDefinition<
void, void,
TTypeScriptType TTypeScriptType
> { > {
@ -60,34 +60,33 @@ export class NumberFieldDefinition<
return this.type.size; return this.type.size;
} }
public create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TTypeScriptType,
): NumberFieldValue<this> {
return new NumberFieldValue(this, options, context, struct, value);
}
public async deserialize( public async deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructDeserializationContext, context: StructDeserializationContext,
object: any, struct: StructValue,
): Promise<NumberFieldRuntimeValue<TType, TTypeScriptType>> { ): Promise<NumberFieldValue<this>> {
const buffer = await context.read(this.getSize()); const buffer = await context.read(this.getSize());
const view = new DataView(buffer); const view = new DataView(buffer);
const value = view[this.type.dataViewGetter]( const value = view[this.type.dataViewGetter](
0, 0,
options.littleEndian options.littleEndian
) as any; ) as any;
return this.createValue(options, context, object, value); return this.create(options, context, struct, value);
}
public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
value: TTypeScriptType,
): NumberFieldRuntimeValue<TType, TTypeScriptType> {
return new NumberFieldRuntimeValue(this, options, context, object, value);
} }
} }
export class NumberFieldRuntimeValue< export class NumberFieldValue<
TType extends NumberFieldType = NumberFieldType, TDefinition extends NumberFieldDefinition<NumberFieldType, any>,
TTypeScriptType = TType['valueType'], > extends StructFieldValue<TDefinition> {
> extends FieldRuntimeValue<NumberFieldDefinition<TType, TTypeScriptType>> {
public serialize(dataView: DataView, offset: number): void { public serialize(dataView: DataView, offset: number): void {
// `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`

View file

@ -0,0 +1,468 @@
import { StructDefaultOptions, StructDeserializationContext, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldValue, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
import { FixedLengthArrayBufferLikeFieldDefinition } from './fixed-length-array-buffer';
import { NumberFieldDefinition, NumberFieldType, NumberFieldValue } from './number';
import { VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeLengthStructFieldValue, VariableLengthArrayBufferLikeStructFieldValue } from './variable-length-array-buffer';
describe('Types', () => {
describe('VariableLengthArrayBufferLikeFieldDefinition', () => {
describe('#getSize', () => {
it('should always return `0`', () => {
const definition = new VariableLengthArrayBufferLikeFieldDefinition(ArrayBufferFieldType.instance, { lengthField: 'foo' });
expect(definition.getSize()).toBe(0);
});
});
describe('#getDeserializeSize', () => {
it('should return value of its `lengthField`', async () => {
const lengthField = 'foo';
const size = 10;
const definition = new VariableLengthArrayBufferLikeFieldDefinition(ArrayBufferFieldType.instance, { lengthField });
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
struct.set(
lengthField,
new NumberFieldValue(
new NumberFieldDefinition(
NumberFieldType.Int8
),
StructDefaultOptions,
context,
struct,
size,
),
);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
const value = fieldValue.get();
expect(value).toBe(buffer);
});
it('should return value of its `lengthField` as number', async () => {
const lengthField = 'foo';
const size = 10;
const definition = new VariableLengthArrayBufferLikeFieldDefinition(ArrayBufferFieldType.instance, { lengthField });
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
struct.set(
lengthField,
new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
),
);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
const value = fieldValue.get();
expect(value).toBe(buffer);
});
});
});
describe('VariableLengthArrayBufferLikeStructFieldValue', () => {
describe('.constructor', () => {
it('should replace `lengthField` on `struct`', () => {
const lengthField = 'foo';
const size = 10;
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
definition,
StructDefaultOptions,
context,
struct,
buffer,
);
expect(fieldValue).toHaveProperty('definition', definition);
expect(struct.fieldValues[lengthField]).not.toBe(originalLengthFieldValue);
});
});
describe('#getSize', () => {
it('should return size of `arrayBuffer` if exist', async () => {
const lengthField = 'foo';
const size = 10;
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(fieldValue.getSize()).toBe(size);
});
it('should call `getSize` of its `type`', () => {
const lengthField = 'foo';
const size = 10;
const buffer = new ArrayBuffer(size);
const context: StructDeserializationContext = {
read(length) { throw new Error(''); },
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
),
StructDefaultOptions,
context,
struct,
buffer,
);
expect(fieldValue.getSize()).toBe(size);
});
it('should call `toArrayBuffer` of its `type` if it does not support `getSize`', () => {
const lengthField = 'foo';
const size = 10;
const context: StructDeserializationContext = {
read(length) { throw new Error(''); },
encodeUtf8(input) {
return Buffer.from(input, 'utf-8');
},
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ lengthField },
),
StructDefaultOptions,
context,
struct,
'test',
);
expect(fieldValue.getSize()).toBe(4);
});
});
describe('#set', () => {
it('should store value', () => {
const lengthField = 'foo';
const size = 10;
const array = new Uint8ClampedArray(size);
const buffer = array.buffer;
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
Uint8ClampedArrayFieldType.instance,
{ lengthField },
),
StructDefaultOptions,
context,
struct,
new Uint8ClampedArray(buffer),
);
const newArray = new Uint8ClampedArray(size);
fieldValue.set(newArray);
expect(fieldValue.get()).toBe(newArray);
});
it('should clear length', () => {
const lengthField = 'foo';
const size = 10;
const array = new Uint8ClampedArray(size);
const buffer = array.buffer;
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
Uint8ClampedArrayFieldType.instance,
{ lengthField },
),
StructDefaultOptions,
context,
struct,
new Uint8ClampedArray(buffer),
);
const newArray = new Uint8ClampedArray(size);
fieldValue.set(newArray);
expect(fieldValue['length']).toBeUndefined();
});
});
});
describe('VariableLengthArrayBufferLikeLengthStructFieldValue', () => {
describe('#getSize', () => {
it('should return size of its original field value', () => {
const struct = new StructValue();
const originalFieldValue = new NumberFieldValue(
new NumberFieldDefinition(
NumberFieldType.Int8
),
StructDefaultOptions,
{} as any,
struct,
42,
);
const fieldValue = new VariableLengthArrayBufferLikeLengthStructFieldValue(
originalFieldValue,
{} as any,
);
expect(fieldValue.getSize()).toBe(originalFieldValue.getSize());
});
});
describe('#get', () => {
it('should return size of its `arrayBufferField`', async () => {
const lengthField = 'foo';
const size = 10;
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new NumberFieldValue(
new NumberFieldDefinition(
NumberFieldType.Int32
),
StructDefaultOptions,
context,
struct,
size
);
struct.set(lengthField, originalLengthFieldValue,);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = (await definition.deserialize(StructDefaultOptions, context, struct)) as any as VariableLengthArrayBufferLikeStructFieldValue;
expect(fieldValue['lengthFieldValue'].get()).toBe(size);
});
it('should return size of its `arrayBufferField` as string', async () => {
const lengthField = 'foo';
const size = 10;
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) { throw new Error(''); },
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = (await definition.deserialize(StructDefaultOptions, context, struct)) as any as VariableLengthArrayBufferLikeStructFieldValue;
expect(fieldValue['lengthFieldValue'].get()).toBe(size.toString());
});
});
describe('#serialize', () => {
it('should call `serialize` of its `originalField`', async () => {
const lengthField = 'foo';
const size = 10;
const buffer = new ArrayBuffer(size);
const read = jest.fn((length: number) => buffer);
const context: StructDeserializationContext = {
read,
encodeUtf8(input) {
return Buffer.from(input, 'utf-8');
},
decodeUtf8(buffer) { throw new Error(''); },
};
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = (await definition.deserialize(StructDefaultOptions, context, struct)) as any as VariableLengthArrayBufferLikeStructFieldValue;
const targetArray = new Uint8Array(2);
const targetView = new DataView(targetArray.buffer);
fieldValue['lengthFieldValue'].serialize(targetView, 0, context);
expect(targetArray).toEqual(new Uint8Array('10'.split('').map(c => c.charCodeAt(0))));
});
});
});
});

View file

@ -1,6 +1,6 @@
import { FieldRuntimeValue, getRuntimeValue, setRuntimeValue, StructOptions, StructSerializationContext } from '../basic'; import { StructFieldValue, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldRuntimeValue, ArrayBufferLikeFieldType } from './array-buffer'; import { KeysOfType } from '../utils';
import { KeysOfType } from './utils'; import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, ArrayBufferLikeFieldValue } from './array-buffer';
export interface VariableLengthArrayBufferLikeFieldOptions< export interface VariableLengthArrayBufferLikeFieldOptions<
TFields = object, TFields = object,
@ -21,81 +21,49 @@ export class VariableLengthArrayBufferLikeFieldDefinition<
return 0; return 0;
} }
protected getDeserializeSize(object: any) { protected getDeserializeSize(struct: StructValue) {
let value = object[this.options.lengthField] as number | string; let value = struct.value[this.options.lengthField] as number | string;
if (typeof value === 'string') { if (typeof value === 'string') {
value = Number.parseInt(value, 10); value = Number.parseInt(value, 10);
} }
return value; return value;
} }
public createValue( public create(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructSerializationContext, context: StructSerializationContext,
object: any, struct: StructValue,
value: TType['valueType'], value: TType['valueType'],
): VariableLengthArrayBufferLikeFieldRuntimeValue<TType, TOptions> { ): VariableLengthArrayBufferLikeStructFieldValue<this> {
return new VariableLengthArrayBufferLikeFieldRuntimeValue(this, options, context, object, value); return new VariableLengthArrayBufferLikeStructFieldValue(this, options, context, struct, value);
} }
} }
class VariableLengthArrayBufferLikeLengthFieldRuntimeValue extends FieldRuntimeValue { export class VariableLengthArrayBufferLikeStructFieldValue<
protected originalValue: FieldRuntimeValue; TDefinition extends VariableLengthArrayBufferLikeFieldDefinition = VariableLengthArrayBufferLikeFieldDefinition,
> extends ArrayBufferLikeFieldValue<TDefinition> {
protected arrayBufferValue: VariableLengthArrayBufferLikeFieldRuntimeValue;
public constructor(
originalValue: FieldRuntimeValue,
arrayBufferValue: VariableLengthArrayBufferLikeFieldRuntimeValue,
) {
super(originalValue.definition, originalValue.options, originalValue.context, originalValue.object, 0);
this.originalValue = originalValue;
this.arrayBufferValue = arrayBufferValue;
}
public getSize() {
return this.originalValue.getSize();
}
get() {
// TODO: originalValue might be a `string` type, now it always returns `number`.
return this.arrayBufferValue.getSize();
}
set() { }
serialize(dataView: DataView, offset: number, context: StructSerializationContext) {
this.originalValue.set(this.get());
this.originalValue.serialize(dataView, offset, context);
}
}
class VariableLengthArrayBufferLikeFieldRuntimeValue<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TOptions extends VariableLengthArrayBufferLikeFieldOptions = VariableLengthArrayBufferLikeFieldOptions
> extends ArrayBufferLikeFieldRuntimeValue<VariableLengthArrayBufferLikeFieldDefinition<TType, TOptions>> {
public static getSize() {
return 0;
}
protected length: number | undefined; protected length: number | undefined;
protected lengthFieldValue: VariableLengthArrayBufferLikeLengthFieldRuntimeValue; protected lengthFieldValue: VariableLengthArrayBufferLikeLengthStructFieldValue;
public constructor( public constructor(
definition: VariableLengthArrayBufferLikeFieldDefinition<TType, TOptions>, definition: TDefinition,
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
context: StructSerializationContext, context: StructSerializationContext,
object: any, struct: StructValue,
value: TType['valueType'], value: TDefinition['valueType'],
) { ) {
super(definition, options, context, object, value); super(definition, options, context, struct, value);
// Patch the associated length field. // Patch the associated length field.
const lengthField = this.definition.options.lengthField; const lengthField = this.definition.options.lengthField;
const originalValue = getRuntimeValue(object, lengthField);
this.lengthFieldValue = new VariableLengthArrayBufferLikeLengthFieldRuntimeValue(originalValue, this); const originalValue = struct.get(lengthField);
setRuntimeValue(object, lengthField, this.lengthFieldValue); this.lengthFieldValue = new VariableLengthArrayBufferLikeLengthStructFieldValue(
originalValue,
this,
);
struct.set(lengthField, this.lengthFieldValue);
} }
public getSize() { public getSize() {
@ -119,3 +87,41 @@ class VariableLengthArrayBufferLikeFieldRuntimeValue<
this.length = undefined; this.length = undefined;
} }
} }
export class VariableLengthArrayBufferLikeLengthStructFieldValue
extends StructFieldValue {
protected originalField: StructFieldValue;
protected arrayBufferField: VariableLengthArrayBufferLikeStructFieldValue;
public constructor(
originalField: StructFieldValue,
arrayBufferField: VariableLengthArrayBufferLikeStructFieldValue,
) {
super(originalField.definition, originalField.options, originalField.context, originalField.struct, 0);
this.originalField = originalField;
this.arrayBufferField = arrayBufferField;
}
public getSize() {
return this.originalField.getSize();
}
get() {
let value: string | number = this.arrayBufferField.getSize();
const originalValue = this.originalField.get();
if (typeof originalValue === 'string') {
value = value.toString();
}
return value;
}
set() { }
serialize(dataView: DataView, offset: number, context: StructSerializationContext) {
this.originalField.set(this.get());
this.originalField.serialize(dataView, offset, context);
}
}

View file

@ -0,0 +1,7 @@
import { placeholder } from './utils';
describe('placeholder', () => {
it('should return `undefined`', () => {
expect(placeholder()).toBe(undefined);
});
});

View file

@ -39,6 +39,9 @@ export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? ne
export type KeysOfType<T, TValue> = export type KeysOfType<T, TValue> =
{ [TKey in keyof T]: T[TKey] extends TValue ? TKey : never }[keyof T]; { [TKey in keyof T]: T[TKey] extends TValue ? TKey : never }[keyof T];
export type ValueOrPromise<T> = T | Promise<T>;
/** /**
* Returns a (fake) value of the given type. * Returns a (fake) value of the given type.
*/ */

View file

@ -13,6 +13,7 @@
"declaration": true, "declaration": true,
"declarationDir": "dts", "declarationDir": "dts",
"declarationMap": true, "declarationMap": true,
"stripInternal": true,
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "noEmit": true, /* Do not emit outputs. */ // "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, // /* Import emit helpers from 'tslib'. */ "importHelpers": true, // /* Import emit helpers from 'tslib'. */