test(struct): 100% code coverage

This commit is contained in:
Simon Chan 2021-01-17 19:57:34 +08:00
parent b71f9714b2
commit 83910f1a35
12 changed files with 985 additions and 466 deletions

View file

@ -1,8 +1,8 @@
import { ValueOrPromise } from '../utils';
import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { ValueOrPromise } from '../utils';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import { StructFieldDefinition } from './definition';
import { StructFieldValue } from './field-value';
import { StructValue } from './struct-value';
import type { StructFieldValue } from './field-value';
import type { StructValue } from './struct-value';
describe('StructFieldDefinition', () => {
describe('.constructor', () => {

View file

@ -1,8 +1,8 @@
import { ValueOrPromise } from '../utils';
import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { ValueOrPromise } from '../utils';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import { StructFieldDefinition } from './definition';
import { StructFieldValue } from './field-value';
import { StructValue } from './struct-value';
import type { StructValue } from './struct-value';
describe('StructFieldValue', () => {
describe('.constructor', () => {

View file

@ -7,8 +7,11 @@ describe('StructValue', () => {
const bar = new StructValue();
expect(foo).toHaveProperty('fieldValues', {});
expect(foo).toHaveProperty('value', {});
expect(bar).toHaveProperty('fieldValues', {});
expect(bar).toHaveProperty('value', {});
expect(foo.fieldValues).not.toBe(bar.fieldValues);
expect(foo.value).not.toBe(bar.fieldValues);
});
});

View file

@ -0,0 +1,407 @@
import { StructDefaultOptions, StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from './basic';
import { Struct } from './struct';
import { ArrayBufferFieldType, FixedLengthArrayBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition } from './types';
import { ValueOrPromise } from './utils';
class MockDeserializationContext implements StructDeserializationContext {
public buffer = new ArrayBuffer(0);
public read = jest.fn((length: number) => this.buffer);
public encodeUtf8 = jest.fn((input: string) => Buffer.from(input, 'utf-8'));
public decodeUtf8 = jest.fn((buffer: ArrayBuffer) => Buffer.from(buffer).toString('utf-8'));
}
describe('Struct', () => {
describe('.constructor', () => {
it('should initialize fields', () => {
const struct = new Struct();
expect(struct).toHaveProperty('options', StructDefaultOptions);
expect(struct).toHaveProperty('size', 0);
});
});
describe('#field', () => {
class MockFieldDefinition extends StructFieldDefinition<number>{
public constructor(size: number) {
super(size);
}
public getSize = jest.fn(() => {
return this.options;
});
public create(options: Readonly<StructOptions>, context: StructSerializationContext, object: StructValue, struct: unknown): StructFieldValue<this> {
throw new Error('Method not implemented.');
}
public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, struct: StructValue): ValueOrPromise<StructFieldValue<this>> {
throw new Error('Method not implemented.');
}
}
it('should push a field and update size', () => {
const struct = new Struct();
const field1 = 'foo';
const fieldDefinition1 = new MockFieldDefinition(4);
struct.field(field1, fieldDefinition1);
expect(struct).toHaveProperty('size', 4);
expect(fieldDefinition1.getSize).toBeCalledTimes(1);
expect(struct['_fields']).toEqual([[field1, fieldDefinition1]]);
const field2 = 'bar';
const fieldDefinition2 = new MockFieldDefinition(8);
struct.field(field2, fieldDefinition2);
expect(struct).toHaveProperty('size', 12);
expect(fieldDefinition2.getSize).toBeCalledTimes(1);
expect(struct['_fields']).toEqual([
[field1, fieldDefinition1],
[field2, fieldDefinition2],
]);
});
it('should throw an error if field name already exists', () => {
const struct = new Struct();
const fieldName = 'foo';
struct.field(fieldName, new MockFieldDefinition(4));
expect(() => struct.field(fieldName, new MockFieldDefinition(4))).toThrowError();
});
});
describe('#number', () => {
it('`int8` should append an `int8` field', () => {
const struct = new Struct();
struct.int8('foo');
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int8);
});
it('`uint8` should append an `uint8` field', () => {
const struct = new Struct();
struct.uint8('foo');
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint8);
});
it('`int16` should append an `int16` field', () => {
const struct = new Struct();
struct.int16('foo');
expect(struct).toHaveProperty('size', 2);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int16);
});
it('`uint16` should append an `uint16` field', () => {
const struct = new Struct();
struct.uint16('foo');
expect(struct).toHaveProperty('size', 2);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint16);
});
it('`int32` should append an `int32` field', () => {
const struct = new Struct();
struct.int32('foo');
expect(struct).toHaveProperty('size', 4);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int32);
});
it('`uint32` should append an `uint32` field', () => {
const struct = new Struct();
struct.uint32('foo');
expect(struct).toHaveProperty('size', 4);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint32);
});
it('`int64` should append an `int64` field', () => {
const struct = new Struct();
struct.int64('foo');
expect(struct).toHaveProperty('size', 8);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int64);
});
it('`uint64` should append an `uint64` field', () => {
const struct = new Struct();
struct.uint64('foo');
expect(struct).toHaveProperty('size', 8);
const definition = struct['_fields'][0][1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint64);
});
describe('#arrayBufferLike', () => {
describe('FixedLengthArrayBufferLikeFieldDefinition', () => {
it('`#arrayBuffer` with fixed length', () => {
let struct = new Struct();
struct.arrayBuffer('foo', { length: 10 });
expect(struct).toHaveProperty('size', 10);
const definition = struct['_fields'][0][1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(ArrayBufferFieldType);
expect(definition.options.length).toBe(10);
});
it('`#uint8ClampedArray` with fixed length', () => {
let struct = new Struct();
struct.uint8ClampedArray('foo', { length: 10 });
expect(struct).toHaveProperty('size', 10);
const definition = struct['_fields'][0][1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(Uint8ClampedArrayFieldType);
expect(definition.options.length).toBe(10);
});
it('`#string` with fixed length', () => {
let struct = new Struct();
struct.string('foo', { length: 10 });
expect(struct).toHaveProperty('size', 10);
const definition = struct['_fields'][0][1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(StringFieldType);
expect(definition.options.length).toBe(10);
});
});
describe('VariableLengthArrayBufferLikeFieldDefinition', () => {
it('`#arrayBuffer` with variable length', () => {
const struct = new Struct().int8('barLength');
expect(struct).toHaveProperty('size', 1);
struct.arrayBuffer('bar', { lengthField: 'barLength' });
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][1][1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(ArrayBufferFieldType);
expect(definition.options.lengthField).toBe('barLength');
});
it('`#uint8ClampedArray` with variable length', () => {
const struct = new Struct().int8('barLength');
expect(struct).toHaveProperty('size', 1);
struct.uint8ClampedArray('bar', { lengthField: 'barLength' });
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][1][1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(Uint8ClampedArrayFieldType);
expect(definition.options.lengthField).toBe('barLength');
});
it('`#string` with variable length', () => {
const struct = new Struct().int8('barLength');
expect(struct).toHaveProperty('size', 1);
struct.string('bar', { lengthField: 'barLength' });
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][1][1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(StringFieldType);
expect(definition.options.lengthField).toBe('barLength');
});
});
});
describe('#fields', () => {
it('should append all fields from other struct', async () => {
const sub = new Struct()
.int16('int16')
.int32('int32');
const struct = new Struct()
.int8('int8')
.fields(sub)
.int64('int64');
const field0 = struct['_fields'][0];
expect(field0).toHaveProperty('0', 'int8');
expect(field0[1]).toHaveProperty('type', NumberFieldType.Int8);
const field1 = struct['_fields'][1];
expect(field1).toHaveProperty('0', 'int16');
expect(field1[1]).toHaveProperty('type', NumberFieldType.Int16);
const field2 = struct['_fields'][2];
expect(field2).toHaveProperty('0', 'int32');
expect(field2[1]).toHaveProperty('type', NumberFieldType.Int32);
const field3 = struct['_fields'][3];
expect(field3).toHaveProperty('0', 'int64');
expect(field3[1]).toHaveProperty('type', NumberFieldType.Int64);
});
});
describe('deserialize', () => {
it('should deserialize without dynamic size fields', async () => {
const struct = new Struct()
.int8('foo')
.int16('bar');
const context = new MockDeserializationContext();
context.read
.mockReturnValueOnce(new Uint8Array([2]).buffer)
.mockReturnValueOnce(new Uint8Array([0, 16]).buffer);
const result = await struct.deserialize(context);
expect(result).toEqual({ foo: 2, bar: 16 });
expect(context.read).toBeCalledTimes(2);
expect(context.read).nthCalledWith(1, 1);
expect(context.read).nthCalledWith(2, 2);
});
it('should deserialize with dynamic size fields', async () => {
const struct = new Struct()
.int8('fooLength')
.uint8ClampedArray('foo', { lengthField: 'fooLength' });
const context = new MockDeserializationContext();
context.read
.mockReturnValueOnce(new Uint8Array([2]).buffer)
.mockReturnValueOnce(new Uint8Array([3, 4]).buffer);
const result = await struct.deserialize(context);
expect(result).toEqual({ fooLength: 2, foo: new Uint8ClampedArray([3, 4]) });
expect(context.read).toBeCalledTimes(2);
expect(context.read).nthCalledWith(1, 1);
expect(context.read).nthCalledWith(2, 2);
});
});
describe('#extra', () => {
it('should accept plain field', async () => {
const struct = new Struct()
.extra({ foo: 42, bar: true });
const context = new MockDeserializationContext();
const result = await struct.deserialize(context);
expect(Object.entries(Object.getOwnPropertyDescriptors(result))).toEqual([
['foo', { configurable: true, enumerable: true, writable: true, value: 42 }],
['bar', { configurable: true, enumerable: true, writable: true, value: true }],
]);
});
it('should accept accessors', async () => {
const struct = new Struct()
.extra({
get foo() { return 42; },
get bar() { return true; },
set bar(value) { },
});
const context = new MockDeserializationContext();
const result = await struct.deserialize(context);
expect(Object.entries(Object.getOwnPropertyDescriptors(result))).toEqual([
['foo', { configurable: true, enumerable: true, get: expect.any(Function) }],
['bar', { configurable: true, enumerable: true, get: expect.any(Function), set: expect.any(Function) }],
]);
});
});
describe('#postDeserialize', () => {
it('can throw errors', async () => {
const struct = new Struct();
const callback = jest.fn(() => { throw new Error('mock'); });
struct.postDeserialize(callback);
const context = new MockDeserializationContext();
expect(struct.deserialize(context)).rejects.toThrowError('mock');
expect(callback).toBeCalledTimes(1);
});
it('can replace return value', async () => {
const struct = new Struct();
const callback = jest.fn(() => 'mock');
struct.postDeserialize(callback);
const context = new MockDeserializationContext();
expect(struct.deserialize(context)).resolves.toBe('mock');
expect(callback).toBeCalledTimes(1);
expect(callback).toBeCalledWith({});
});
it('can return nothing', async () => {
const struct = new Struct();
const callback = jest.fn();
struct.postDeserialize(callback);
const context = new MockDeserializationContext();
const result = await struct.deserialize(context);
expect(callback).toBeCalledTimes(1);
expect(callback).toBeCalledWith(result);
});
it('should overwrite callback', async () => {
const struct = new Struct();
const callback1 = jest.fn();
struct.postDeserialize(callback1);
const callback2 = jest.fn();
struct.postDeserialize(callback2);
const context = new MockDeserializationContext();
await struct.deserialize(context);
expect(callback1).toBeCalledTimes(0);
expect(callback2).toBeCalledTimes(1);
expect(callback2).toBeCalledWith({});
});
});
describe('#serialize', () => {
it('should serialize without dynamic size fields', () => {
const struct = new Struct()
.int8('foo')
.int16('bar');
const context = new MockDeserializationContext();
const result = new Uint8Array(struct.serialize({ foo: 0x42, bar: 0x1024 }, context));
expect(result).toEqual(new Uint8Array([0x42, 0x10, 0x24]));
});
it('should serialize with dynamic size fields', () => {
const struct = new Struct()
.int8('fooLength')
.arrayBuffer('foo', { lengthField: 'fooLength' });
const context = new MockDeserializationContext();
const result = new Uint8Array(struct.serialize({ foo: new Uint8Array([0x03, 0x04, 0x05]).buffer }, context));
expect(result).toEqual(new Uint8Array([0x03, 0x03, 0x04, 0x05]));
});
});
});
});

View file

@ -1,13 +1,11 @@
import { StructDefaultOptions, StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from './basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldType, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
import { Evaluate, Identity, KeysOfType, Overwrite } from './utils';
import { Awaited, Evaluate, Identity, KeysOfType, Overwrite } from './utils';
export interface StructLike<TValue> {
deserialize(context: StructDeserializationContext): Promise<TValue>;
}
export type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
/**
* Extract the value type of the specified `Struct`
*
@ -160,17 +158,17 @@ export type StructDeserializedType<TFields extends object, TExtra extends object
export class Struct<
TFields extends object = {},
TOmitInit extends string = never,
TOmitInitKey extends string = never,
TExtra extends object = {},
TPostDeserialized = undefined,
> implements StructLike<StructDeserializedType<TFields, TExtra, TPostDeserialized>>{
public readonly fieldsType!: TFields;
public readonly omitInitType!: TOmitInit;
public readonly omitInitType!: TOmitInitKey;
public readonly extraType!: TExtra;
public readonly initType!: Evaluate<Omit<TFields, TOmitInit>>;
public readonly initType!: Evaluate<Omit<TFields, TOmitInitKey>>;
public readonly deserializedType!: StructDeserializedType<TFields, TExtra, TPostDeserialized>;
@ -188,7 +186,7 @@ export class Struct<
private _postDeserialized?: StructPostDeserialized<any, any>;
public constructor(options?: Partial<StructOptions>) {
public constructor(options?: Partial<Readonly<StructOptions>>) {
this.options = { ...StructDefaultOptions, ...options };
}
@ -203,12 +201,18 @@ export class Struct<
definition: TDefinition,
): AddFieldDescriptor<
TFields,
TOmitInit,
TOmitInitKey,
TExtra,
TPostDeserialized,
TName,
TDefinition
> {
for (const field of this._fields) {
if (field[0] === name) {
throw new Error(`This struct already have a field with name '${name}'`);
}
}
this._fields.push([name, definition]);
const size = definition.getSize();
@ -225,7 +229,7 @@ export class Struct<
other: TOther
): Struct<
TFields & TOther['fieldsType'],
TOmitInit | TOther['omitInitType'],
TOmitInitKey | TOther['omitInitType'],
TExtra & TOther['extraType'],
TPostDeserialized
> {
@ -394,7 +398,7 @@ export class Struct<
private arrayBufferLike: ArrayBufferLikeFieldCreator<
TFields,
TOmitInit,
TOmitInitKey,
TExtra,
TPostDeserialized
> = (
@ -417,7 +421,7 @@ export class Struct<
public arrayBuffer: ArrayBufferTypeFieldDefinitionCreator<
TFields,
TOmitInit,
TOmitInitKey,
TExtra,
TPostDeserialized,
ArrayBufferFieldType
@ -430,7 +434,7 @@ export class Struct<
public uint8ClampedArray: ArrayBufferTypeFieldDefinitionCreator<
TFields,
TOmitInit,
TOmitInitKey,
TExtra,
TPostDeserialized,
Uint8ClampedArrayFieldType
@ -443,7 +447,7 @@ export class Struct<
public string: ArrayBufferTypeFieldDefinitionCreator<
TFields,
TOmitInit,
TOmitInitKey,
TExtra,
TPostDeserialized,
StringFieldType
@ -475,7 +479,7 @@ export class Struct<
value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>
): Struct<
TFields,
TOmitInit,
TOmitInitKey,
Overwrite<TExtra, T>,
TPostDeserialized
> {
@ -491,7 +495,7 @@ export class Struct<
*/
public postDeserialize(
callback: StructPostDeserialized<TFields, never>
): Struct<TFields, TOmitInit, TExtra, never>;
): Struct<TFields, TOmitInitKey, TExtra, never>;
/**
* Registers (or replaces) a custom callback to be run after deserialized.
*
@ -500,7 +504,7 @@ export class Struct<
*/
public postDeserialize(
callback?: StructPostDeserialized<TFields, void>
): Struct<TFields, TOmitInit, TExtra, undefined>;
): Struct<TFields, TOmitInitKey, TExtra, undefined>;
/**
* Registers (or replaces) a custom callback to be run after deserialized.
*
@ -509,7 +513,7 @@ export class Struct<
*/
public postDeserialize<TPostSerialize>(
callback?: StructPostDeserialized<TFields, TPostSerialize>
): Struct<TFields, TOmitInit, TExtra, TPostSerialize>;
): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
public postDeserialize(
callback?: StructPostDeserialized<TFields, any>
) {
@ -517,16 +521,11 @@ export class Struct<
return this as any;
}
private initializeStructValue() {
const value = new StructValue();
Object.defineProperties(value.value, this._extra);
return value;
}
public async deserialize(
context: StructDeserializationContext
): Promise<StructDeserializedType<TFields, TExtra, TPostDeserialized>> {
const value = this.initializeStructValue();
const value = new StructValue();
Object.defineProperties(value.value, this._extra);
for (const [name, definition] of this._fields) {
const fieldValue = await definition.deserialize(this.options, context, value);
@ -534,7 +533,8 @@ export class Struct<
}
if (this._postDeserialized) {
const result = this._postDeserialized.call(value.value as TFields, value as TFields);
const object = value.value as TFields;
const result = this._postDeserialized.call(object, object);
if (result) {
return result;
}
@ -543,8 +543,8 @@ export class Struct<
return value.value as any;
}
public serialize(init: Evaluate<Omit<TFields, TOmitInit>>, context: StructSerializationContext): ArrayBuffer {
const value = this.initializeStructValue();
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, context: StructSerializationContext): ArrayBuffer {
const value = new StructValue();
for (const [name, definition] of this._fields) {
const fieldValue = definition.create(this.options, context, value, (init as any)[name]);

View file

@ -1,5 +1,15 @@
import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, ArrayBufferLikeFieldValue, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
import { StructDefaultOptions, StructDeserializationContext, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
class MockDeserializationContext implements StructDeserializationContext {
public buffer = new ArrayBuffer(0);
public read = jest.fn((length: number) => this.buffer);
public encodeUtf8 = jest.fn((input: string) => Buffer.from(input, 'utf-8'));
public decodeUtf8 = jest.fn((buffer: ArrayBuffer) => Buffer.from(buffer).toString('utf-8'));
}
describe('Types', () => {
describe('ArrayBufferLike', () => {
@ -57,12 +67,10 @@ describe('Types', () => {
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');
},
};
const context = new MockDeserializationContext();
expect(StringFieldType.instance.toArrayBuffer(text, context)).toEqual(arrayBuffer);
expect(context.encodeUtf8).toBeCalledTimes(1);
expect(context.encodeUtf8).toBeCalledWith(text);
});
it('`#fromArrayBuffer` should return the encoded ArrayBuffer', () => {
@ -96,41 +104,34 @@ describe('Types', () => {
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 context = new MockDeserializationContext();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(size);
expect(context.read).toBeCalledTimes(1);
expect(context.read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
const value = fieldValue.get();
expect(value).toBe(buffer);
expect(fieldValue.get()).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 context = new MockDeserializationContext();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(10);
expect(context.read).toBeCalledTimes(1);
expect(context.read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
const value = fieldValue.get();
@ -139,21 +140,18 @@ describe('Types', () => {
});
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 context = new MockDeserializationContext();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(0);
expect(fieldValue.arrayBuffer).toBeInstanceOf(ArrayBuffer);
expect(fieldValue.arrayBuffer).toHaveProperty('byteLength', 0);
expect(context.read).toBeCalledTimes(0);
expect(fieldValue['arrayBuffer']).toBeInstanceOf(ArrayBuffer);
expect(fieldValue['arrayBuffer']).toHaveProperty('byteLength', 0);
const value = fieldValue.get();
expect(value).toBeInstanceOf(ArrayBuffer);
@ -164,36 +162,34 @@ describe('Types', () => {
describe('ArrayBufferLikeFieldValue', () => {
describe('#set', () => {
it('should clear `arrayBuffer` field', async () => {
const size = 10;
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
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(''); },
};
context.buffer = buffer;
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
fieldValue.set(new ArrayBuffer(20));
const newValue = new ArrayBuffer(20);
fieldValue.set(newValue);
expect(fieldValue.get()).toBe(newValue);
expect(fieldValue).toHaveProperty('arrayBuffer', undefined);
});
});
describe('#serialize', () => {
it('should be able to serialize a deserialized value', async () => {
const size = 10;
it('should be able to serialize with cached `arrayBuffer`', async () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
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 buffer = sourceArray.buffer;
context.buffer = buffer;
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
const targetArray = new Uint8Array(size);
@ -204,18 +200,17 @@ describe('Types', () => {
});
it('should be able to serialize a modified value', async () => {
const size = 10;
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
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 buffer = sourceArray.buffer;
context.buffer = buffer;
const struct = new StructValue();
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
fieldValue.set(sourceArray.buffer);
const targetArray = new Uint8Array(size);

View file

@ -127,8 +127,9 @@ export abstract class ArrayBufferLikeFieldDefinition<
context: StructSerializationContext,
struct: StructValue,
value: TType['valueType'],
arrayBuffer?: ArrayBuffer,
): ArrayBufferLikeFieldValue<this> {
return new ArrayBufferLikeFieldValue(this, options, context, struct, value);
return new ArrayBufferLikeFieldValue(this, options, context, struct, value, arrayBuffer);
}
public async deserialize(
@ -146,16 +147,26 @@ export abstract class ArrayBufferLikeFieldDefinition<
}
const value = this.type.fromArrayBuffer(arrayBuffer, context);
const fieldValue = this.create(options, context, struct, value);
fieldValue.arrayBuffer = arrayBuffer;
return fieldValue;
return this.create(options, context, struct, value, arrayBuffer);
}
}
export class ArrayBufferLikeFieldValue<
TDefinition extends ArrayBufferLikeFieldDefinition<ArrayBufferLikeFieldType, any, any>,
> extends StructFieldValue<TDefinition> {
public arrayBuffer: ArrayBuffer | undefined;
protected arrayBuffer: ArrayBuffer | undefined;
public constructor(
definition: TDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TDefinition['valueType'],
arrayBuffer?: ArrayBuffer,
) {
super(definition, options, context, struct, value);
this.arrayBuffer = arrayBuffer;
}
public set(value: TDefinition['valueType']): void {
super.set(value);

View file

@ -5,7 +5,10 @@ describe('Types', () => {
describe('FixedLengthArrayBufferLikeFieldDefinition', () => {
describe('#getSize', () => {
it('should return size in its options', () => {
const definition = new FixedLengthArrayBufferLikeFieldDefinition(ArrayBufferFieldType.instance, { length: 10 });
const definition = new FixedLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ length: 10 },
);
expect(definition.getSize()).toBe(10);
});
});

View file

@ -1,467 +1,556 @@
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';
import { StructDefaultOptions, StructDeserializationContext, StructFieldValue, StructSerializationContext, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldType } from './array-buffer';
import { VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldLengthValue, VariableLengthArrayBufferLikeStructFieldValue } from './variable-length-array-buffer';
class MockDeserializationContext implements StructDeserializationContext {
public buffer = new ArrayBuffer(0);
public read = jest.fn((length: number) => this.buffer);
public encodeUtf8 = jest.fn((input: string) => Buffer.from(input, 'utf-8'));
public decodeUtf8 = jest.fn((buffer: ArrayBuffer) => Buffer.from(buffer).toString('utf-8'));
}
class MockOriginalFieldValue extends StructFieldValue {
public constructor() {
super({} as any, {} as any, {} as any, {} as any, {});
}
public value: string | number = 0;
public get = jest.fn((): string | number => this.value);
public size = 0;
public getSize = jest.fn((): number => this.size);
public set = jest.fn((value: string | number) => { });
public serialize = jest.fn((dataView: DataView, offset: number, context: StructSerializationContext): void => { });
}
describe('Types', () => {
describe('VariableLengthArrayBufferLikeFieldDefinition', () => {
describe('VariableLengthArrayBufferLikeFieldLengthValue', () => {
class MockArrayBufferFieldValue extends StructFieldValue {
public constructor() {
super({} as any, {} as any, {} as any, {} as any, {});
}
public size = 0;
public getSize = jest.fn(() => this.size);
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
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,
),
it('should return size of its original field value', () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const lengthFieldValue = new VariableLengthArrayBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
mockOriginalFieldValue.size = 0;
expect(lengthFieldValue.getSize()).toBe(0);
expect(mockOriginalFieldValue.getSize).toBeCalledTimes(1);
const value = fieldValue.get();
expect(value).toBe(buffer);
mockOriginalFieldValue.getSize.mockClear();
mockOriginalFieldValue.size = 100;
expect(lengthFieldValue.getSize()).toBe(100);
expect(mockOriginalFieldValue.getSize).toBeCalledTimes(1);
});
});
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()
),
describe('#get', () => {
it('should return size of its `arrayBufferField`', async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const lengthFieldValue = new VariableLengthArrayBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(read).toBeCalledTimes(1);
expect(read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', buffer);
mockOriginalFieldValue.value = 0;
mockArrayBufferFieldValue.size = 0;
expect(lengthFieldValue.get()).toBe(0);
expect(mockArrayBufferFieldValue.getSize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
const value = fieldValue.get();
expect(value).toBe(buffer);
mockArrayBufferFieldValue.getSize.mockClear();
mockOriginalFieldValue.get.mockClear();
mockArrayBufferFieldValue.size = 100;
expect(lengthFieldValue.get()).toBe(100);
expect(mockArrayBufferFieldValue.getSize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
});
it('should return size of its `arrayBufferField` as string', async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const lengthFieldValue = new VariableLengthArrayBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
);
mockOriginalFieldValue.value = '0';
mockArrayBufferFieldValue.size = 0;
expect(lengthFieldValue.get()).toBe('0');
expect(mockArrayBufferFieldValue.getSize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
mockArrayBufferFieldValue.getSize.mockClear();
mockOriginalFieldValue.get.mockClear();
mockArrayBufferFieldValue.size = 100;
expect(lengthFieldValue.get()).toBe('100');
expect(mockArrayBufferFieldValue.getSize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
});
});
describe('#set', () => {
it('should does nothing', async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const lengthFieldValue = new VariableLengthArrayBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
);
mockOriginalFieldValue.value = 0;
mockArrayBufferFieldValue.size = 0;
expect(lengthFieldValue.get()).toBe(0);
(lengthFieldValue as StructFieldValue).set(100);
expect(lengthFieldValue.get()).toBe(0);
});
});
describe('#serialize', () => {
it('should call `serialize` of its `originalField`', async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const lengthFieldValue = new VariableLengthArrayBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
);
let dataView = 0 as any;
let offset = 1 as any;
let context = 2 as any;
mockArrayBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, offset, context);
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith(0);
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockArrayBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, offset, context);
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith(100);
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
});
});
});
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(''); },
};
it('should forward parameters', () => {
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
struct.set(lengthField, originalLengthFieldValue,);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
definition,
const context = new MockDeserializationContext();
const value = new ArrayBuffer(0);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
buffer,
value,
);
expect(fieldValue).toHaveProperty('definition', definition);
expect(arrayBufferFieldValue).toHaveProperty('definition', arrayBufferFieldDefinition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', undefined);
expect(arrayBufferFieldValue).toHaveProperty('length', undefined);
});
expect(struct.fieldValues[lengthField]).not.toBe(originalLengthFieldValue);
it('should forward parameters with `arrayBuffer`', () => {
const struct = new StructValue();
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
value,
);
expect(arrayBufferFieldValue).toHaveProperty('definition', arrayBufferFieldDefinition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', value);
expect(arrayBufferFieldValue).toHaveProperty('length', 100);
});
it('should replace `lengthField` on `struct`', () => {
const struct = new StructValue();
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(0);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
);
expect(arrayBufferFieldValue['lengthFieldValue']).toBeInstanceOf(StructFieldValue);
expect(struct.fieldValues[lengthField]).toBe(arrayBufferFieldValue['lengthFieldValue']);
});
});
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(''); },
};
class MockArrayBufferFieldType extends ArrayBufferLikeFieldType<ArrayBuffer> {
public toArrayBuffer = jest.fn((value: ArrayBuffer, context: StructSerializationContext): ArrayBuffer => {
return value;
});
public fromArrayBuffer = jest.fn((arrayBuffer: ArrayBuffer, context: StructDeserializationContext): ArrayBuffer => {
return arrayBuffer;
});
public size = 0;
public getSize = jest.fn((value: ArrayBuffer): number => {
return this.size;
});
}
it('should return cached size if exist', async () => {
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
const arrayBufferFieldType = new MockArrayBufferFieldType();
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
arrayBufferFieldType,
{ lengthField },
);
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(fieldValue.getSize()).toBe(size);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
value,
);
expect(arrayBufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.fromArrayBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.toArrayBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.getSize).toBeCalledTimes(0);
});
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 lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
const arrayBufferFieldType = new MockArrayBufferFieldType();
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
arrayBufferFieldType,
{ lengthField },
),
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
buffer,
value,
);
expect(fieldValue.getSize()).toBe(size);
arrayBufferFieldType.size = 100;
expect(arrayBufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.fromArrayBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.toArrayBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.getSize).toBeCalledTimes(1);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', undefined);
expect(arrayBufferFieldValue).toHaveProperty('length', 100);
});
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 lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
const arrayBufferFieldType = new MockArrayBufferFieldType();
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
arrayBufferFieldType,
{ lengthField },
),
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
'test',
value,
);
expect(fieldValue.getSize()).toBe(4);
arrayBufferFieldType.size = -1;
expect(arrayBufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.fromArrayBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.toArrayBuffer).toBeCalledTimes(1);
expect(arrayBufferFieldType.getSize).toBeCalledTimes(1);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', value);
expect(arrayBufferFieldValue).toHaveProperty('length', 100);
});
});
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(''); },
};
it('should call `ArrayBufferLikeFieldValue#set`', () => {
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
struct.set(lengthField, originalLengthFieldValue,);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
Uint8ClampedArrayFieldType.instance,
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
),
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
new Uint8ClampedArray(buffer),
value,
value,
);
const newArray = new Uint8ClampedArray(size);
fieldValue.set(newArray);
expect(fieldValue.get()).toBe(newArray);
const newValue = new ArrayBuffer(100);
arrayBufferFieldValue.set(newValue);
expect(arrayBufferFieldValue.get()).toBe(newValue);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', undefined);
});
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()
);
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
struct.set(lengthField, originalLengthFieldValue,);
const fieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
new VariableLengthArrayBufferLikeFieldDefinition(
Uint8ClampedArrayFieldType.instance,
const arrayBufferFieldDefinition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
),
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
new Uint8ClampedArray(buffer),
value,
value,
);
const newArray = new Uint8ClampedArray(size);
fieldValue.set(newArray);
expect(fieldValue['length']).toBeUndefined();
const newValue = new ArrayBuffer(100);
arrayBufferFieldValue.set(newValue);
expect(arrayBufferFieldValue).toHaveProperty('length', undefined);
});
});
});
describe('VariableLengthArrayBufferLikeLengthStructFieldValue', () => {
describe('VariableLengthArrayBufferLikeFieldDefinition', () => {
describe('#getSize', () => {
it('should return size of its original field value', () => {
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 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 originalLengthFieldValue = new MockOriginalFieldValue();
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);
originalLengthFieldValue.value = 0;
expect(definition['getDeserializeSize'](struct)).toBe(0);
expect(originalLengthFieldValue.get).toBeCalledTimes(1);
originalLengthFieldValue.get.mockClear();
originalLengthFieldValue.value = 100;
expect(definition['getDeserializeSize'](struct)).toBe(100);
expect(originalLengthFieldValue.get).toBeCalledTimes(1);
});
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(''); },
};
it('should return value of its `lengthField` as number', async () => {
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
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());
originalLengthFieldValue.value = '0';
expect(definition['getDeserializeSize'](struct)).toBe(0);
expect(originalLengthFieldValue.get).toBeCalledTimes(1);
originalLengthFieldValue.get.mockClear();
originalLengthFieldValue.value = '100';
expect(definition['getDeserializeSize'](struct)).toBe(100);
expect(originalLengthFieldValue.get).toBeCalledTimes(1);
});
});
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(''); },
};
describe('#create', () => {
it('should create a `VariableLengthArrayBufferLikeFieldValue`', () => {
const struct = new StructValue();
const originalLengthFieldValue = new ArrayBufferLikeFieldValue(
new FixedLengthArrayBufferLikeFieldDefinition(
StringFieldType.instance,
{ length: 2 },
),
StructDefaultOptions,
context,
struct,
size.toString()
);
struct.set(lengthField, originalLengthFieldValue,);
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const fieldValue = (await definition.deserialize(StructDefaultOptions, context, struct)) as any as VariableLengthArrayBufferLikeStructFieldValue;
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = definition.create(
StructDefaultOptions,
context,
struct,
value,
);
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))));
expect(arrayBufferFieldValue).toHaveProperty('definition', definition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', undefined);
expect(arrayBufferFieldValue).toHaveProperty('length', undefined);
});
it('should create a `VariableLengthArrayBufferLikeFieldValue` with `arrayBuffer`', () => {
const struct = new StructValue();
const lengthField = 'foo';
const originalLengthFieldValue = new MockOriginalFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthArrayBufferLikeFieldDefinition(
ArrayBufferFieldType.instance,
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = definition.create(
StructDefaultOptions,
context,
struct,
value,
value,
);
expect(arrayBufferFieldValue).toHaveProperty('definition', definition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', value);
expect(arrayBufferFieldValue).toHaveProperty('length', 100);
});
});
});

View file

@ -1,5 +1,5 @@
import { StructFieldValue, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { KeysOfType } from '../utils';
import type { KeysOfType } from '../utils';
import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, ArrayBufferLikeFieldValue } from './array-buffer';
export interface VariableLengthArrayBufferLikeFieldOptions<
@ -34,8 +34,16 @@ export class VariableLengthArrayBufferLikeFieldDefinition<
context: StructSerializationContext,
struct: StructValue,
value: TType['valueType'],
arrayBuffer?: ArrayBuffer
): VariableLengthArrayBufferLikeStructFieldValue<this> {
return new VariableLengthArrayBufferLikeStructFieldValue(this, options, context, struct, value);
return new VariableLengthArrayBufferLikeStructFieldValue(
this,
options,
context,
struct,
value,
arrayBuffer,
);
}
}
@ -44,7 +52,7 @@ export class VariableLengthArrayBufferLikeStructFieldValue<
> extends ArrayBufferLikeFieldValue<TDefinition> {
protected length: number | undefined;
protected lengthFieldValue: VariableLengthArrayBufferLikeLengthStructFieldValue;
protected lengthFieldValue: VariableLengthArrayBufferLikeFieldLengthValue;
public constructor(
definition: TDefinition,
@ -52,14 +60,19 @@ export class VariableLengthArrayBufferLikeStructFieldValue<
context: StructSerializationContext,
struct: StructValue,
value: TDefinition['valueType'],
arrayBuffer?: ArrayBuffer,
) {
super(definition, options, context, struct, value);
super(definition, options, context, struct, value, arrayBuffer);
if (arrayBuffer) {
this.length = arrayBuffer.byteLength;
}
// Patch the associated length field.
const lengthField = this.definition.options.lengthField;
const originalValue = struct.get(lengthField);
this.lengthFieldValue = new VariableLengthArrayBufferLikeLengthStructFieldValue(
this.lengthFieldValue = new VariableLengthArrayBufferLikeFieldLengthValue(
originalValue,
this,
);
@ -68,35 +81,32 @@ export class VariableLengthArrayBufferLikeStructFieldValue<
public getSize() {
if (this.length === undefined) {
if (this.arrayBuffer !== undefined) {
this.length = this.arrayBuffer.byteLength;
} else {
this.length = this.definition.type.getSize(this.value);
if (this.length === -1) {
this.arrayBuffer = this.definition.type.toArrayBuffer(this.value, this.context);
this.length = this.arrayBuffer.byteLength;
}
}
}
return this.length;
}
public set(value: unknown) {
super.set(value);
this.arrayBuffer = undefined;
this.length = undefined;
}
}
export class VariableLengthArrayBufferLikeLengthStructFieldValue
export class VariableLengthArrayBufferLikeFieldLengthValue
extends StructFieldValue {
protected originalField: StructFieldValue;
protected arrayBufferField: VariableLengthArrayBufferLikeStructFieldValue;
protected arrayBufferField: StructFieldValue;
public constructor(
originalField: StructFieldValue,
arrayBufferField: VariableLengthArrayBufferLikeStructFieldValue,
arrayBufferField: StructFieldValue,
) {
super(originalField.definition, originalField.options, originalField.context, originalField.struct, 0);
this.originalField = originalField;

View file

@ -41,6 +41,7 @@ export type KeysOfType<T, TValue> =
export type ValueOrPromise<T> = T | Promise<T>;
export type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
/**
* Returns a (fake) value of the given type.

View file

@ -2,7 +2,7 @@
"compilerOptions": {
"composite": true,
/* Basic Options */
"target": "ES5", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"target": "ES2015", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */
"ESNext"