diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 01b1aac..5b0ff41 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -4,9 +4,10 @@ import { EOF, PrimitiveSymbol, type InstantiableObject } from '../types' import { binread } from '../reader' import { withBinspectorContext } from '../context' import { BinaryReader, BinaryCursorEndianness } from '../cursor' +import { CtxGet, CtxSet } from '../decorators/context' -function expectReadTest (buffer: Array, ObjectDefinition: InstantiableObject, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ...args: any[]) { - return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ...args)) +function expectReadTest (buffer: Array, ObjectDefinition: InstantiableObject, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ctx = {}, ...args: any[]) { + return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ctx, ...args)) } function expectReadTestToThrow (buffer: Array, ObjectDefinition: InstantiableObject) { @@ -536,7 +537,7 @@ describe('Reading binary definition with PrePost decorators', () => { } } - expectReadTest([0x01, 0x02, 0x03, 0x04], Protocol, BinaryCursorEndianness.BigEndian, 2).toMatchObject({ + expectReadTest([0x01, 0x02, 0x03, 0x04], Protocol, BinaryCursorEndianness.BigEndian, {}, 2).toMatchObject({ value: 0x03, }) }) @@ -652,6 +653,29 @@ describe('Reading binary definition with PrePost decorators', () => { }) }) +describe('Reading binary definition with Ctx decorators', () => { + it('should ', () => { + class Protocol { + @CtxGet('Settings.Count') + data_type: number + + @CtxSet('Settings.Value') + @Count('data_type') + @Relation(PrimitiveSymbol.u8) + foo: number + } + + const ctx = { Settings: { Count: 3 } } + + expectReadTest([0x01, 0x02, 0x03], Protocol, BinaryCursorEndianness.LittleEndian, ctx).toMatchObject({ + data_type: 3, + foo: [1, 2, 3], + }) + + expect(ctx).toMatchObject({ Settings: { Count: 3, Value: [1, 2, 3] } }) + }) +}) + describe('Reading a relation to an empty definition', () => { it('should throw an error', () => { class Header { diff --git a/src/decorators/__tests__/context.test.ts b/src/decorators/__tests__/context.test.ts new file mode 100644 index 0000000..a9dbe1a --- /dev/null +++ b/src/decorators/__tests__/context.test.ts @@ -0,0 +1,73 @@ +import { describe, expect } from '@jest/globals' +import { CtxSet, CtxGet, useContextGet, useContextSet } from '../context' +import Meta from '../../metadatas' + +function testContextGet (TargetClass: new () => any, field: string, ctx: object, expected: any) { + const instance = new TargetClass() + + const contexts = Meta.getContext(TargetClass[Symbol.metadata] as DecoratorMetadataObject, field) + expect(useContextGet(contexts, instance, ctx)).toEqual(expected) +} + +function testContextSet (TargetClass: new () => any, field: string, value: any, ctx: object) { + const instance = new TargetClass() + + const contexts = Meta.getContext(TargetClass[Symbol.metadata] as DecoratorMetadataObject, field) + useContextSet(contexts, value, instance, ctx) +} + +describe('@Ctx: functions', () => { + it('should modify the ctx object', () => { + class TestClass { + @CtxSet('test') + data: number + } + const ctx: any = {} + const VALUE = 1 + testContextSet(TestClass, 'data', VALUE, ctx) + expect(ctx.test).toEqual(VALUE) + }) + it('should modify the ctx object with recursive accessors', () => { + class TestClass { + @CtxSet('foo.bar.1') + data: number + + @CtxSet('foo.bar.2') + data_2: number + } + const ctx: any = {} + const VALUE = 1 + testContextSet(TestClass, 'data', VALUE, ctx) + testContextSet(TestClass, 'data_2', VALUE, ctx) + // @ts-ignore + expect(ctx.foo.bar[1]).toEqual(VALUE) + expect(ctx.foo.bar[2]).toEqual(VALUE) + }) + it('should get value from the ctx object', () => { + class TestClass { + @CtxGet('test') + data: number + } + const VALUE = 1 + const ctx = { test: VALUE } + testContextGet(TestClass, 'data', ctx, VALUE) + }) + it('should get value from the ctx object with recursive accessors', () => { + class TestClass { + @CtxGet('foo.bar') + data: number + } + const VALUE = 1 + const ctx = { foo: { bar: VALUE } } + testContextGet(TestClass, 'data', ctx, VALUE) + }) + it('should throw when accessing non existing properties', () => { + class TestClass { + @CtxGet('test.foo') + data: number + } + const VALUE = 1 + const ctx = { test: VALUE } + expect(() => testContextGet(TestClass, 'data', ctx, VALUE)).toThrow() + }) +}) diff --git a/src/decorators/context.ts b/src/decorators/context.ts new file mode 100644 index 0000000..423e308 --- /dev/null +++ b/src/decorators/context.ts @@ -0,0 +1,264 @@ +/** + * Module definition of {@link Context} property decorators. + * + * The {@link Context} decorators provides functions to read/write the context + * shared among the binary objects during the reading phase. + * + * @module Context + */ +import { Context, DecoratorType } from '../types' +import { createPropertyMetaDescriptor, PropertyMetaDescriptor, recursiveGet, StringFormattedRecursiveKeyOf } from './common' +import { Relation } from './primitive' +import Meta from '../metadatas' + +export const CtxSymbol = Symbol('context') + +export type GlobalCtx = object + +export type CtxKeyFunction = (targetInstance: This) => string + +export enum CtxType { + CtxGetter, + CtxSetter, +} + +export interface CtxOptions { + /** + * Ensures that a relation exists before defining the Transformer decorator. + */ + base_type: 'array' | undefined +} + +export const CtxOptionsDefault = { + base_type: undefined, +} + +/** + * Ctx metadata type definition. + * + * @extends {PropertyMetaDescriptor} + */ +export interface Ctx extends PropertyMetaDescriptor { + options: CtxOptions + /** + * Function that retrieve the key to access the context + */ + keyGetter: CtxKeyFunction + /** + * Context type: retrieve a value or set a value + */ + func_type: CtxType +} + +function ctxPropertyFunctionDecoratorFactory (name: string, func_type: CtxType, keyGetter: string | CtxKeyFunction, opt: Partial = CtxOptionsDefault): DecoratorType { + const options = { ...CtxOptionsDefault, ...opt } + + return function (_: any, context: Context) { + const propertyKey = context.name as keyof This + if (!Meta.isFieldDecorated(context.metadata, propertyKey)) { + // Create an empty relation that wont be read. + Relation()(_, context) + } + const ctx: Ctx = { + ...createPropertyMetaDescriptor(CtxSymbol, name, context.metadata, propertyKey), + func_type, + options, + keyGetter: typeof keyGetter === 'string' ? () => keyGetter : keyGetter, + } + + Meta.setContext(context.metadata, propertyKey, ctx) + } +} + +/** + * `@CtxGet` decorator retrieve a value based on the key passed as argument + * from a 'context' shared during the reading phase. + * + * @example + * + * In the following example, a streaming protocol that receives records + * of arbitrary length is defined. + * Records have two different type a 'definition' or a 'data' both use an + * 'id' to identify themself. The definition defines the size of the data + * message they define by using `CtxSet` to store that size into the + * context. The data message uses `CtxGet` to fetch its size defined + * previously by the definition. + * + * ```typescript + * class RecordDefinition { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxSet(_ => `Definition.${_.id}`) + * @Relation(PrimitiveSymbol.u8) + * size: number + * } + * + * class RecordMessage { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxGet(_ => `Definition.${_.id}`) + * _size: number + * + * @Count('_size') + * @Relation(PrimitiveSymbol.u8) + * data + * } + * + * class Record { + * @Relation(PrimitiveSymbol.u8) + * type: number + * + * @Choice('type', { + * 0x00: RecordDefinition, + * 0x01: RecordMessage, + * }) + * message: RecordDefinition | RecordMessage + * } + * + * class Protocol { + * @Until(EOF) + * records: Record[] + * } + * ``` + * + * @param {CtxKeyFunction} keyGetter Either a string formatted as + * recursive key or a function that returns that string based on the + * instance value. + * @param {Partial} [opt] Optional configuration. + * @returns {DecoratorType} The property decorator function. + * + * @category Decorators + */ +export function CtxGet (keyGetter: CtxKeyFunction | string, opt?: Partial): DecoratorType { + return ctxPropertyFunctionDecoratorFactory ('ctx-get', CtxType.CtxGetter, keyGetter, opt) +} + +/** + * `@CtxSet` decorator set the value of the decorated property into a + * shared 'context' during the reading phase. + * + * @example + * + * In the following example, a streaming protocol that receives records + * of arbitrary length is defined. + * Records have two different type a 'definition' or a 'data' both use an + * 'id' to identify themself. The definition defines the size of the data + * message they define by using `CtxSet` to store that size into the + * context. The data message uses `CtxGet` to fetch its size defined + * previously by the definition. + * + * ```typescript + * class RecordDefinition { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxSet(_ => `Definition.${_.id}`) + * @Relation(PrimitiveSymbol.u8) + * size: number + * } + * + * class RecordMessage { + * @Relation(PrimitiveSymbol.u8) + * id: number + * + * @CtxGet(_ => `Definition.${_.id}`) + * _size: number + * + * @Count('_size') + * @Relation(PrimitiveSymbol.u8) + * data + * } + * + * class Record { + * @Relation(PrimitiveSymbol.u8) + * type: number + * + * @Choice('type', { + * 0x00: RecordDefinition, + * 0x01: RecordMessage, + * }) + * message: RecordDefinition | RecordMessage + * } + * + * class Protocol { + * @Until(EOF) + * records: Record[] + * } + * ``` + * + * @param {CtxKeyFunction} keyGetter Either a string formatted as + * recursive key or a function that returns that string based on the + * instance value. + * @param {Partial} [opt] Optional configuration. + * @returns {DecoratorType} The property decorator function. + * + * @category Decorators + */ +export function CtxSet (keyGetter: CtxKeyFunction | string, opt?: Partial): DecoratorType { + return ctxPropertyFunctionDecoratorFactory ('ctx-set', CtxType.CtxSetter, keyGetter, opt) +} + +/** + * useContextGet execute an array of `Ctx` decorator metadata. + * + * @typeParam This The type of the class the decorator is applied to. + * + * @param {Array>} metaCtx An array of context function to apply. + * @param {This} targetInstance The target class instance containing the property. + * @param {GlobalCtx} ctx The shared context reference. + * @returns {any} The context retrieved. + * + * @category Advanced Use + */ +export function useContextGet (metaCtx: Array>, targetInstance: This, ctx: GlobalCtx): any { + const values = metaCtx.filter(x => x.func_type === CtxType.CtxGetter).map((x) => { + const key = x.keyGetter(targetInstance) + + // TODO future version should pass some typing to the context + return key.split('.').reduce((acc: any, key: string) => { + if (Object.prototype.hasOwnProperty.call(acc, key) === false) { + throw new ReferenceError(`Can't retrieve key: '${key}' from ctx: ${JSON.stringify(ctx)}.`) + } + return acc[key] + }, ctx) + }) + + return values.length === 1 ? values[0] : values +} + +/** + * useContextSet execute an array of `Ctx` decorator metadata. + * + * @typeParam This The type of the class the decorator is applied to. + * + * @param {Array>} metaCtx An array of context function to apply. + * @param {This} targetInstance The target class instance containing the property. + * @param {GlobalCtx} ctx The shared context reference. + * @returns {any} The context retrieved. + * + * @category Advanced Use + */ +export function useContextSet (metaCtx: Array>, propertyValue: any, targetInstance: This, ctx: GlobalCtx): void { + metaCtx.filter(x => x.func_type === CtxType.CtxSetter).forEach((x) => { + const key = x.keyGetter(targetInstance) + const accessors = key.split('.') + const lastKey = accessors[accessors.length - 1] + const ref = accessors.slice(0, -1).reduce((acc: any, key: string) => { + if (Object.prototype.hasOwnProperty.call(acc, key) === false) { + if (x.options.base_type == 'array') { + Object.defineProperty(acc, key, { + value: [] + }) + } else { + Object.defineProperty(acc, key, { + value: {} + }) + } + } + return acc[key] + }, ctx) + ref[lastKey] = propertyValue + }) +} diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 7648078..0f77271 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -10,3 +10,4 @@ export * from './primitive' export * from './prepost' export * from './bitfield' export * from './transformer' +export * from './context' diff --git a/src/metadatas.ts b/src/metadatas.ts index 1151ac0..db1c288 100644 --- a/src/metadatas.ts +++ b/src/metadatas.ts @@ -4,6 +4,7 @@ import { type Controller, ControllerSymbol } from './decorators/controller' import { type Transformer, TransformerSymbol } from './decorators/transformer' import { type Validator, ValidatorSymbol } from './decorators/validator' import { type BitField, BitFieldSymbol } from './decorators/bitfield' +import { type Ctx, CtxSymbol } from './decorators/context' import { type PrePost, PreFunctionSymbol, PostFunctionSymbol, PreClassFunctionSymbol, PostClassFunctionSymbol, PrePostSymbols, PrePostClass } from './decorators/prepost' import { type DecoratorMetadataObject } from './types' @@ -252,6 +253,22 @@ function setBitField ( return bitfields } +function getContext (metadata: DecoratorMetadataObject, propertyKey: keyof This): Array> { + return getMetadata( + metadata, + propertyKey, + CtxSymbol, + ) +} + +function setContext ( + metadata: DecoratorMetadataObject, + propertyKey: keyof This, + ctx: Ctx, +): Array> { + return setMetadata(metadata, propertyKey, ctx.type, ctx) +} + export default { getMetadata, setMetadata, @@ -276,4 +293,6 @@ export default { getBitField, getBitFields, setBitField, + getContext, + setContext, } diff --git a/src/reader.ts b/src/reader.ts index 913b4ab..d5260dc 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -21,6 +21,7 @@ import { useValidators } from './decorators/validator' import { useConditions } from './decorators/condition' import { usePrePost } from './decorators/prepost' import { useBitField } from './decorators/bitfield' +import { useContextGet, useContextSet, CtxType } from './decorators/context' /** * binread. @@ -44,7 +45,7 @@ import { useBitField } from './decorators/bitfield' * `ObjectDefinition` passed in param. * You can create self refering field by using conditionnal decorator. */ -export function binread (content: Cursor, ObjectDefinition: InstantiableObject, ...args: any[]): Target { +export function binread (content: Cursor, ObjectDefinition: InstantiableObject, ctx = {}, ...args: any[]): Target { const ObjectDefinitionName = ObjectDefinition.name function getBinReader (field: PropertyType, instance: Target): ControllerReader { if (isPrimitiveRelation(field)) { @@ -63,7 +64,7 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable } try { - return binread(content, field.relation, ...[...finalArgs, instance]) + return binread(content, field.relation, ctx, ...[...finalArgs, instance]) } catch (error) { // We need to catch the EOF error because the binread function // can't return it so it just throw it EOFError. @@ -105,6 +106,14 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable Meta.getFields(metadata).forEach((field) => { usePrePost(Meta.getPre(metadata, field.propertyName), instance, content, ExecutionScope.OnRead) + const metaCtx = Meta.getContext(field.metadata, field.propertyName) + + const ctxGetter = metaCtx.filter(x => x.func_type === CtxType.CtxGetter) + if (ctxGetter.length) { + instance[field.propertyName] = useContextGet(metaCtx, instance, ctx) + return + } + // TODO [Cursor] Pass the field name information to add to the namespace const finalRelationField = isUnknownProperty(field) ? useConditions(Meta.getConditions(field.metadata, field.propertyName), instance) : field if (finalRelationField !== undefined) { @@ -127,6 +136,7 @@ export function binread (content: Cursor, ObjectDefinition: Instantiable useValidators(Meta.getValidators(metadata, field.propertyName), transformedValue, instance, content) instance[field.propertyName] = transformedValue + useContextSet(metaCtx, transformedValue, instance, ctx) } usePrePost(Meta.getPost(metadata, field.propertyName), instance, content, ExecutionScope.OnRead) })