diff --git a/src/decorators/__tests__/condition.test.ts b/src/decorators/__tests__/condition.test.ts index 323a99a..4e0f350 100644 --- a/src/decorators/__tests__/condition.test.ts +++ b/src/decorators/__tests__/condition.test.ts @@ -101,7 +101,7 @@ describe('@Condition: basic testing', () => { foo: number = 1 bar: number = 2 @Choice('foo', { - 1: [TestArg, 'foo, bar'], + 1: [TestArg, 'foo,bar'], }) field: TestArg } diff --git a/src/decorators/__tests__/controller.test.ts b/src/decorators/__tests__/controller.test.ts index e858658..87dd4f5 100644 --- a/src/decorators/__tests__/controller.test.ts +++ b/src/decorators/__tests__/controller.test.ts @@ -347,18 +347,6 @@ describe('@Controller: errors', () => { testControllerCursor(TestClass, 'field', () => cur.read(PrimitiveSymbol.u8), [], cur) }).toThrow(EOFError) }) - it('@Count: recursiveGet should throw an error for non existing property', () => { - expect(() => { - class TestClass { - child = { count: 2 } - - @Count('child.x', { primitiveCheck: false }) - field: number - } - - testController(TestClass, 'field', () => 1, [1, 1]) - }).toThrow(ReferenceError) - }) it('@Count: recursiveGet should throw an error when referencing a string in an arithmetic expression', () => { expect(() => { class TestClass { diff --git a/src/decorators/common.ts b/src/decorators/common.ts index 9e5093e..41da122 100644 --- a/src/decorators/common.ts +++ b/src/decorators/common.ts @@ -67,6 +67,42 @@ export function createClassMetaDescriptor (type: symbol, name: string, metadata: } } +type RecursiveKeyOf = { + [TKey in keyof TObj & (string | number)]: + TObj[TKey] extends any[] + ? `${TKey}` + : TObj[TKey] extends object + ? `${TKey}.${RecursiveKeyOf}` + : `${TKey}` +}[keyof TObj & (string | number)]; + +type NumericalString = `${number}` + +type RecursiveKeyWithOperations | NumericalString> = + T extends U + ? T + : T extends `${U} + ${infer R}` + ? T extends `${infer F} + ${R}` + ? `${F} + ${RecursiveKeyWithOperations>}` + : never + : T extends `${U} - ${infer R}` + ? T extends `${infer F} - ${R}` + ? `${F} - ${RecursiveKeyWithOperations>}` + : never + : U + +type CommaSeparetedRecursiveKey> = + T extends U + ? T + : T extends `${U},${infer R}` + ? T extends `${infer F},${R}` + ? `${F},${CommaSeparetedRecursiveKey>}` + : never + : U; + +export type StringFormattedRecursiveKeyOf = Args extends RecursiveKeyWithOperations ? Args : RecursiveKeyWithOperations +export type StringFormattedCommaSepRecursiveKeyOf = Args extends CommaSeparetedRecursiveKey ? Args : CommaSeparetedRecursiveKey + /** * Chained accessor to get the value of `expr` for `obj`. * @@ -77,7 +113,7 @@ export function createClassMetaDescriptor (type: symbol, name: string, metadata: * @throws {ReferenceError} if you attempt to access a non existing property. * */ -export function recursiveGet (obj: any, expr: string): any { +export function recursiveGet (obj: T, expr: RecursiveKeyWithOperations): any { const _isOperation = (x: string): boolean => ['+', '-'].includes(x) const _get = (path: string): any => path.split('.').reduce((acc: any, key: string) => { @@ -109,7 +145,7 @@ export function recursiveGet (obj: any, expr: string): any { } } -export function commaSeparetedRecursiveGet (obj: any, args: string): any[] { +export function commaSeparetedRecursiveGet (obj: T, args: CommaSeparetedRecursiveKey): any[] { const keys = args.split(',') - return keys.map(key => recursiveGet(obj, key.trim())) + return keys.map(key => recursiveGet(obj, key.trim() as RecursiveKeyWithOperations)) } diff --git a/src/decorators/condition.ts b/src/decorators/condition.ts index 5b0bee8..77a8155 100644 --- a/src/decorators/condition.ts +++ b/src/decorators/condition.ts @@ -40,7 +40,7 @@ * * @module Condition */ -import { recursiveGet, type PropertyMetaDescriptor, createPropertyMetaDescriptor } from './common' +import { recursiveGet, type PropertyMetaDescriptor, createPropertyMetaDescriptor, StringFormattedRecursiveKeyOf } from './common' import { type PrimitiveTypeProperty, type RelationTypeProperty, type RelationParameters, Relation, createPrimitiveTypeProperty, createRelationTypeProperty } from './primitive' import { isPrimitiveSymbol, type DecoratorType, type Primitive, type Context } from '../types' import { NoConditionMatched } from '../error' @@ -87,7 +87,7 @@ export interface Condition extends PropertyMetaDescriptor { * * @category Advanced Use */ -export function conditionDecoratorFactory (name: string, cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { +export function conditionDecoratorFactory (name: string, cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { return function (_: undefined, context: Context) { const propertyName = context.name as keyof This function createRelation (relationOrPrimitive: Primitive): PrimitiveTypeProperty | RelationTypeProperty { @@ -125,7 +125,7 @@ export function conditionDecoratorFactory (name: string, co * * @category Advanced Use */ -export function dynamicConditionDecoratorFactory (name: string, func: DynamicGetterFunction, args?: RelationParameters): DecoratorType { +export function dynamicConditionDecoratorFactory (name: string, func: DynamicGetterFunction, args?: RelationParameters): DecoratorType { return function (_: undefined, context: Context) { const propertyName = context.name as keyof This function createRelation (relationOrPrimitive: Primitive): PrimitiveTypeProperty | RelationTypeProperty { @@ -204,7 +204,7 @@ export function dynamicConditionDecoratorFactory (name: str * * @category Decorators */ -export function IfThen (cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { +export function IfThen (cond: ConditionFunction, then?: Primitive, args?: RelationParameters): DecoratorType { return conditionDecoratorFactory('ifthen', cond, then, args) } @@ -251,7 +251,7 @@ export function IfThen (cond: ConditionFunction, then * * @category Decorators */ -export function Else (then?: Primitive, args?: RelationParameters): DecoratorType { +export function Else (then?: Primitive, args?: RelationParameters): DecoratorType { return conditionDecoratorFactory('else', () => true, then, args) } @@ -430,7 +430,7 @@ export function Else (then?: Primitive, args?: Rela * * @category Decorators */ -export function Choice (cmp: string | ((targetInstance: This) => any), match: Record | [Primitive, RelationParameters] | undefined>, args?: RelationParameters): DecoratorType { +export function Choice (cmp: StringFormattedRecursiveKeyOf | ((targetInstance: This) => any), match: Record | [Primitive, RelationParameters] | undefined>, args?: RelationParameters): DecoratorType { const valueToCompare = typeof cmp === 'string' ? (targetInstance: This) => recursiveGet(targetInstance, cmp) : cmp // Mandatory to cast to String because the key is always a string even though you declare it as a number const decorators = Object.keys(match).map((key: keyof typeof match) => { @@ -500,7 +500,7 @@ export function Choice (cmp: string | ((targetInstance: This) => an * @returns {DecoratorType} The property decorator function. * @category Decorators */ -export function Select (getter: ((targetInstance: This) => Primitive), args?: RelationParameters): DecoratorType { +export function Select (getter: ((targetInstance: This) => Primitive), args?: RelationParameters): DecoratorType { return dynamicConditionDecoratorFactory('select', getter, args) } diff --git a/src/decorators/controller.ts b/src/decorators/controller.ts index c54fc2e..e04bac7 100644 --- a/src/decorators/controller.ts +++ b/src/decorators/controller.ts @@ -50,7 +50,7 @@ * * @module Controller */ -import { createPropertyMetaDescriptor, type PropertyMetaDescriptor, recursiveGet } from './common' +import { createPropertyMetaDescriptor, type PropertyMetaDescriptor, recursiveGet, StringFormattedRecursiveKeyOf } from './common' import { type Cursor } from '../cursor' import { EOF, type DecoratorType, type InstantiableObject, type Context } from '../types' import { relationExistsOrThrow, EOFError } from '../error' @@ -98,6 +98,8 @@ export const ControllerOptionsDefault = { export type ControllerFunction = (targetInstance: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => any export type OptionlessControllerFunction = (targetInstance: any, cursor: Cursor, read: ControllerReader) => any +type NumberOrRecursiveKey = number | StringFormattedRecursiveKeyOf + /** * Controller type interface structure definition. * @@ -500,7 +502,7 @@ export function NullTerminatedString (opt?: Partial (arg: number | string, opt?: Partial): DecoratorType { +export function Count (arg: NumberOrRecursiveKey, opt?: Partial): DecoratorType { function countController ( currStateObject: This, cursor: Cursor, @@ -542,14 +544,14 @@ export function Count (arg: number | string, opt?: Partial (width: number | string, height: number | string, opt?: Partial): DecoratorType { +export function Matrix (width: NumberOrRecursiveKey, height: NumberOrRecursiveKey, opt?: Partial): DecoratorType { function matrixController ( currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions, ): any { - const getArg = (x: number | string): number => typeof x === 'string' + const getArg = (x: NumberOrRecursiveKey): number => typeof x === 'string' ? recursiveGet(currStateObject, x) : x const finalWidth = getArg(width) @@ -614,7 +616,7 @@ export function Matrix (width: number | string, height: number | st * * @category Decorators */ -export function Size (size: number | string, opt?: Partial): DecoratorType { +export function Size (size: NumberOrRecursiveKey, opt?: Partial): DecoratorType { return controllerDecoratorFactory( 'size', (currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => { @@ -669,7 +671,7 @@ export function Size (size: number | string, opt?: Partial (arr: string | any[] | ((_: This) => any[]), opt?: Partial): DecoratorType { +export function MapTo (arr: StringFormattedRecursiveKeyOf | any[] | ((_: This) => any[]), opt?: Partial): DecoratorType { return controllerDecoratorFactory( 'map', (currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => { diff --git a/src/decorators/prepost.ts b/src/decorators/prepost.ts index 09060a4..ec1f8c3 100644 --- a/src/decorators/prepost.ts +++ b/src/decorators/prepost.ts @@ -47,7 +47,7 @@ * * @module PrePost */ -import { ClassMetaDescriptor, type PropertyMetaDescriptor, createClassMetaDescriptor, createPropertyMetaDescriptor, recursiveGet } from './common' +import { ClassMetaDescriptor, type PropertyMetaDescriptor, StringFormattedRecursiveKeyOf, createClassMetaDescriptor, createPropertyMetaDescriptor, recursiveGet } from './common' import { relationExistsOrThrow } from '../error' import { ExecutionScope, type ClassAndPropertyDecoratorType, type ClassAndPropertyDecoratorContext, type DecoratorType, type Context } from '../types' import { type Cursor, type BinaryCursorEndianness, BinaryCursor } from '../cursor' @@ -400,7 +400,7 @@ export function Post (func: PrePostFunction, opt?: Partial (offset: number | string | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { +export function Offset (offset: number | StringFormattedRecursiveKeyOf | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { return preFunctionDecoratorFactory('offset', (targetInstance, cursor) => { const offCompute = typeof offset === 'string' ? recursiveGet(targetInstance, offset) as number @@ -483,7 +483,7 @@ export function Offset (offset: number | string | ((instance: This, cursor * * @category Decorators */ -export function Peek (offset?: number | string | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { +export function Peek (offset?: number | StringFormattedRecursiveKeyOf | ((instance: This, cursor: Cursor) => number), opt?: Partial): ClassAndPropertyDecoratorType { return function (_: undefined, context: ClassAndPropertyDecoratorContext) { preFunctionDecoratorFactory('pre-peek', (targetInstance, cursor) => { const preOff = cursor.offset() @@ -667,7 +667,7 @@ type ValueSetFunction = (instance: This) => Value * * @throws {@link Primitive.RelationAlreadyDefinedError} if a relation metadata is found. */ -export function ValueSet (setter: ValueSetFunction, opt?: Partial): DecoratorType { +export function ValueSet (setter: ValueSetFunction, opt?: Partial): DecoratorType { return function (_: any, context: Context) { const propertyName = context.name as keyof This if (!Meta.isFieldDecorated(context.metadata, propertyName)) { diff --git a/src/decorators/primitive.ts b/src/decorators/primitive.ts index 3980628..18b1a81 100644 --- a/src/decorators/primitive.ts +++ b/src/decorators/primitive.ts @@ -33,7 +33,7 @@ * * @module Primitive */ -import { type PropertyMetaDescriptor, createPropertyMetaDescriptor, commaSeparetedRecursiveGet } from './common' +import { type PropertyMetaDescriptor, createPropertyMetaDescriptor, commaSeparetedRecursiveGet, StringFormattedCommaSepRecursiveKeyOf } from './common' import { RelationAlreadyDefinedError, WrongBitfieldClassImplementation } from '../error' import Meta from '../metadatas' import { type PrimitiveSymbol, isPrimitiveSymbol, type InstantiableObject, type DecoratorType, type Context, type DecoratorMetadataObject } from '../types' @@ -111,8 +111,7 @@ export interface PrimitiveTypeProperty extends PropertyMetaDescriptor = ((targetInstance: This) => any[]) | string - +export type RelationParameters = ((targetInstance: This) => any[]) | StringFormattedCommaSepRecursiveKeyOf /** * `RelationTypeProperty` are primitive type that holds information about * another binary type definition. @@ -225,7 +224,7 @@ export function createPrimitiveTypeProperty (metadata: DecoratorMetadataOb * * @category Advanced Use */ -export function createRelationTypeProperty (metadata: DecoratorMetadataObject, propertyKey: keyof This, relation: InstantiableObject, args?: RelationParameters): RelationTypeProperty { +export function createRelationTypeProperty (metadata: DecoratorMetadataObject, propertyKey: keyof This, relation: InstantiableObject, args?: RelationParameters): RelationTypeProperty { const argsFunc = typeof args === 'string' ? (targetInstance: This) => commaSeparetedRecursiveGet(targetInstance, args) : args as ((targetInstance: This) => any[]) | undefined @@ -344,7 +343,7 @@ export function createRelationTypeProperty (metadata: DecoratorMet * * @category Decorators */ -export function Relation (relation?: InstantiableObject | PrimitiveSymbol, args?: RelationParameters): DecoratorType { +export function Relation (relation?: InstantiableObject | PrimitiveSymbol, args?: RelationParameters): DecoratorType { return function (_: undefined, context: Context): void { if (Meta.getBitFields(context.metadata).length > 0) { throw new WrongBitfieldClassImplementation(String(context.name)) @@ -360,7 +359,7 @@ export function Relation (relation?: InstantiableObject(context.metadata, propertyName, relation)) } else { - Meta.setField(context.metadata, createRelationTypeProperty(context.metadata, propertyName, relation, args)) + Meta.setField(context.metadata, createRelationTypeProperty(context.metadata, propertyName, relation, args)) } } }