Skip to content

Commit

Permalink
fix: better type checking for strings arguments of decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
tperale committed Dec 11, 2024
1 parent fb639c0 commit bc38ff3
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/decorators/__tests__/condition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 0 additions & 12 deletions src/decorators/__tests__/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 39 additions & 3 deletions src/decorators/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,42 @@ export function createClassMetaDescriptor (type: symbol, name: string, metadata:
}
}

type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
TObj[TKey] extends any[]

Check failure on line 72 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 2 spaces but found 4

Check failure on line 72 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
? `${TKey}`

Check failure on line 73 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 4 spaces but found 6
: TObj[TKey] extends object

Check failure on line 74 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 4 spaces but found 6
? `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`

Check failure on line 75 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 6 spaces but found 8
: `${TKey}`

Check failure on line 76 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Expected indentation of 6 spaces but found 8
}[keyof TObj & (string | number)];

Check failure on line 77 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon

type NumericalString = `${number}`

type RecursiveKeyWithOperations<TObj extends object, T extends string, U extends string = RecursiveKeyOf<TObj> | NumericalString> =

Check failure on line 81 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
T extends U

Check failure on line 82 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
? T

Check failure on line 83 in src/decorators/common.ts

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
: T extends `${U} + ${infer R}`
? T extends `${infer F} + ${R}`
? `${F} + ${RecursiveKeyWithOperations<TObj, R, Exclude<U, F>>}`
: never
: T extends `${U} - ${infer R}`
? T extends `${infer F} - ${R}`
? `${F} - ${RecursiveKeyWithOperations<TObj, R, Exclude<U, F>>}`
: never
: U

type CommaSeparetedRecursiveKey<TObj extends object, T extends string, U extends string = RecursiveKeyOf<TObj>> =
T extends U
? T
: T extends `${U},${infer R}`
? T extends `${infer F},${R}`
? `${F},${CommaSeparetedRecursiveKey<TObj, R, Exclude<U, F>>}`
: never
: U;

export type StringFormattedRecursiveKeyOf<T extends object, Args extends string> = Args extends RecursiveKeyWithOperations<T, Args> ? Args : RecursiveKeyWithOperations<T, Args>
export type StringFormattedCommaSepRecursiveKeyOf<T extends object, Args extends string> = Args extends CommaSeparetedRecursiveKey<T, Args> ? Args : CommaSeparetedRecursiveKey<T, Args>

/**
* Chained accessor to get the value of `expr` for `obj`.
*
Expand All @@ -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<T extends object, Args extends string> (obj: T, expr: RecursiveKeyWithOperations<T, Args>): any {
const _isOperation = (x: string): boolean => ['+', '-'].includes(x)

const _get = (path: string): any => path.split('.').reduce((acc: any, key: string) => {
Expand Down Expand Up @@ -109,7 +145,7 @@ export function recursiveGet (obj: any, expr: string): any {
}
}

export function commaSeparetedRecursiveGet (obj: any, args: string): any[] {
export function commaSeparetedRecursiveGet<T extends object, Args extends string> (obj: T, args: CommaSeparetedRecursiveKey<T, Args>): any[] {
const keys = args.split(',')
return keys.map(key => recursiveGet(obj, key.trim()))
return keys.map(key => recursiveGet(obj, key.trim() as RecursiveKeyWithOperations<T, typeof key>))
}
14 changes: 7 additions & 7 deletions src/decorators/condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -87,7 +87,7 @@ export interface Condition<This> extends PropertyMetaDescriptor<This> {
*
* @category Advanced Use
*/
export function conditionDecoratorFactory<This, Target, Value> (name: string, cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function conditionDecoratorFactory<This extends object, Target, Value, Args extends string> (name: string, cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return function (_: undefined, context: Context<This, Value>) {
const propertyName = context.name as keyof This
function createRelation (relationOrPrimitive: Primitive<Target>): PrimitiveTypeProperty<This> | RelationTypeProperty<This, Target> {
Expand Down Expand Up @@ -125,7 +125,7 @@ export function conditionDecoratorFactory<This, Target, Value> (name: string, co
*
* @category Advanced Use
*/
export function dynamicConditionDecoratorFactory<This, Target, Value> (name: string, func: DynamicGetterFunction<This, Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function dynamicConditionDecoratorFactory<This extends object, Target, Value, Args extends string> (name: string, func: DynamicGetterFunction<This, Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return function (_: undefined, context: Context<This, Value>) {
const propertyName = context.name as keyof This
function createRelation (relationOrPrimitive: Primitive<Target>): PrimitiveTypeProperty<This> | RelationTypeProperty<This, Target> {
Expand Down Expand Up @@ -204,7 +204,7 @@ export function dynamicConditionDecoratorFactory<This, Target, Value> (name: str
*
* @category Decorators
*/
export function IfThen<This, Target, Value> (cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function IfThen<This extends object, Target, Value, Args extends string> (cond: ConditionFunction<This>, then?: Primitive<Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return conditionDecoratorFactory('ifthen', cond, then, args)
}

Expand Down Expand Up @@ -251,7 +251,7 @@ export function IfThen<This, Target, Value> (cond: ConditionFunction<This>, then
*
* @category Decorators
*/
export function Else<This, Target, Value> (then?: Primitive<Target>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Else<This extends object, Target, Value, Args extends string> (then?: Primitive<Target>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return conditionDecoratorFactory('else', () => true, then, args)
}

Expand Down Expand Up @@ -430,7 +430,7 @@ export function Else<This, Target, Value> (then?: Primitive<Target>, args?: Rela
*
* @category Decorators
*/
export function Choice<This, Value> (cmp: string | ((targetInstance: This) => any), match: Record<any, Primitive<any> | [Primitive<any>, RelationParameters<This>] | undefined>, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Choice<This extends object, Value, Args extends string> (cmp: StringFormattedRecursiveKeyOf<This, Args> | ((targetInstance: This) => any), match: Record<any, Primitive<any> | [Primitive<any>, RelationParameters<This, Args>] | undefined>, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
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) => {
Expand Down Expand Up @@ -500,7 +500,7 @@ export function Choice<This, Value> (cmp: string | ((targetInstance: This) => an
* @returns {DecoratorType<This, Value>} The property decorator function.
* @category Decorators
*/
export function Select<This, Value> (getter: ((targetInstance: This) => Primitive<any>), args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Select<This extends object, Value, Args extends string> (getter: ((targetInstance: This) => Primitive<any>), args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return dynamicConditionDecoratorFactory('select', getter, args)
}

Expand Down
14 changes: 8 additions & 6 deletions src/decorators/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -98,6 +98,8 @@ export const ControllerOptionsDefault = {
export type ControllerFunction<This> = (targetInstance: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => any
export type OptionlessControllerFunction = (targetInstance: any, cursor: Cursor, read: ControllerReader) => any

type NumberOrRecursiveKey<This extends object, Args extends string> = number | StringFormattedRecursiveKeyOf<This, Args>

/**
* Controller type interface structure definition.
*
Expand Down Expand Up @@ -500,7 +502,7 @@ export function NullTerminatedString<This, Value> (opt?: Partial<ControllerOptio
*
* @category Decorators
*/
export function Count<This, Value> (arg: number | string, opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
export function Count<This extends object, Value, Args extends string> (arg: NumberOrRecursiveKey<This, Args>, opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
function countController (
currStateObject: This,
cursor: Cursor,
Expand Down Expand Up @@ -542,14 +544,14 @@ export function Count<This, Value> (arg: number | string, opt?: Partial<Controll
*
* @category Decorators
*/
export function Matrix<This, Value> (width: number | string, height: number | string, opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
export function Matrix<This extends object, Value, Args extends string> (width: NumberOrRecursiveKey<This, Args>, height: NumberOrRecursiveKey<This, Args>, opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
function matrixController (
currStateObject: This,
cursor: Cursor,
read: ControllerReader,
opt: ControllerOptions,
): any {
const getArg = (x: number | string): number => typeof x === 'string'
const getArg = (x: NumberOrRecursiveKey<This, Args>): number => typeof x === 'string'
? recursiveGet(currStateObject, x)
: x
const finalWidth = getArg(width)
Expand Down Expand Up @@ -614,7 +616,7 @@ export function Matrix<This, Value> (width: number | string, height: number | st
*
* @category Decorators
*/
export function Size<This, Value> (size: number | string, opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
export function Size<This extends object, Value, Args extends string> (size: NumberOrRecursiveKey<This, Args>, opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
return controllerDecoratorFactory(
'size',
(currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => {
Expand Down Expand Up @@ -669,7 +671,7 @@ export function Size<This, Value> (size: number | string, opt?: Partial<Controll
*
* @category Decorators
*/
export function MapTo<This, Value> (arr: string | any[] | ((_: This) => any[]), opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
export function MapTo<This extends object, Value, Args extends string> (arr: StringFormattedRecursiveKeyOf<This, Args> | any[] | ((_: This) => any[]), opt?: Partial<ControllerOptions>): DecoratorType<This, Value> {
return controllerDecoratorFactory(
'map',
(currStateObject: This, cursor: Cursor, read: ControllerReader, opt: ControllerOptions) => {
Expand Down
8 changes: 4 additions & 4 deletions src/decorators/prepost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -400,7 +400,7 @@ export function Post<This> (func: PrePostFunction<This>, opt?: Partial<PrePostOp
*
* @category Decorators
*/
export function Offset<This> (offset: number | string | ((instance: This, cursor: Cursor) => number), opt?: Partial<PrePostOptions>): ClassAndPropertyDecoratorType<This> {
export function Offset<This extends object, Args extends string> (offset: number | StringFormattedRecursiveKeyOf<This, Args> | ((instance: This, cursor: Cursor) => number), opt?: Partial<PrePostOptions>): ClassAndPropertyDecoratorType<This> {
return preFunctionDecoratorFactory('offset', (targetInstance, cursor) => {
const offCompute = typeof offset === 'string'
? recursiveGet(targetInstance, offset) as number
Expand Down Expand Up @@ -483,7 +483,7 @@ export function Offset<This> (offset: number | string | ((instance: This, cursor
*
* @category Decorators
*/
export function Peek<This> (offset?: number | string | ((instance: This, cursor: Cursor) => number), opt?: Partial<PrePostOptions>): ClassAndPropertyDecoratorType<This> {
export function Peek<This extends object, Args extends string> (offset?: number | StringFormattedRecursiveKeyOf<This, Args> | ((instance: This, cursor: Cursor) => number), opt?: Partial<PrePostOptions>): ClassAndPropertyDecoratorType<This> {
return function (_: undefined, context: ClassAndPropertyDecoratorContext<This>) {
preFunctionDecoratorFactory<This>('pre-peek', (targetInstance, cursor) => {
const preOff = cursor.offset()
Expand Down Expand Up @@ -667,7 +667,7 @@ type ValueSetFunction<This, Value> = (instance: This) => Value
*
* @throws {@link Primitive.RelationAlreadyDefinedError} if a relation metadata is found.
*/
export function ValueSet<This, Value extends This[keyof This]> (setter: ValueSetFunction<This, Value>, opt?: Partial<PrePostOptions>): DecoratorType<This, Value> {
export function ValueSet<This extends object, Value extends This[keyof This]> (setter: ValueSetFunction<This, Value>, opt?: Partial<PrePostOptions>): DecoratorType<This, Value> {
return function (_: any, context: Context<This, Value>) {
const propertyName = context.name as keyof This
if (!Meta.isFieldDecorated(context.metadata, propertyName)) {
Expand Down
11 changes: 5 additions & 6 deletions src/decorators/primitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -111,8 +111,7 @@ export interface PrimitiveTypeProperty<This> extends PropertyMetaDescriptor<This
*
* @typeParam This The type of the class the decorator is applied to.
*/
export type RelationParameters<This> = ((targetInstance: This) => any[]) | string

export type RelationParameters<This extends object, Args extends string> = ((targetInstance: This) => any[]) | StringFormattedCommaSepRecursiveKeyOf<This, Args>
/**
* `RelationTypeProperty` are primitive type that holds information about
* another binary type definition.
Expand Down Expand Up @@ -225,7 +224,7 @@ export function createPrimitiveTypeProperty<This> (metadata: DecoratorMetadataOb
*
* @category Advanced Use
*/
export function createRelationTypeProperty<This, Target> (metadata: DecoratorMetadataObject, propertyKey: keyof This, relation: InstantiableObject<Target>, args?: RelationParameters<This>): RelationTypeProperty<This, Target> {
export function createRelationTypeProperty<This extends object, Target, Args extends string> (metadata: DecoratorMetadataObject, propertyKey: keyof This, relation: InstantiableObject<Target>, args?: RelationParameters<This, Args>): RelationTypeProperty<This, Target> {
const argsFunc = typeof args === 'string'
? (targetInstance: This) => commaSeparetedRecursiveGet(targetInstance, args)
: args as ((targetInstance: This) => any[]) | undefined
Expand Down Expand Up @@ -344,7 +343,7 @@ export function createRelationTypeProperty<This, Target> (metadata: DecoratorMet
*
* @category Decorators
*/
export function Relation<This, Target, Value> (relation?: InstantiableObject<Target> | PrimitiveSymbol, args?: RelationParameters<This>): DecoratorType<This, Value> {
export function Relation<This extends object, Target, Value, Args extends string> (relation?: InstantiableObject<Target> | PrimitiveSymbol, args?: RelationParameters<This, Args>): DecoratorType<This, Value> {
return function (_: undefined, context: Context<This, Value>): void {
if (Meta.getBitFields(context.metadata).length > 0) {
throw new WrongBitfieldClassImplementation(String(context.name))
Expand All @@ -360,7 +359,7 @@ export function Relation<This, Target, Value> (relation?: InstantiableObject<Tar
} else if (isPrimitiveSymbol(relation)) {
Meta.setField(context.metadata, createPrimitiveTypeProperty<This>(context.metadata, propertyName, relation))
} else {
Meta.setField(context.metadata, createRelationTypeProperty<This, Target>(context.metadata, propertyName, relation, args))
Meta.setField(context.metadata, createRelationTypeProperty<This, Target, Args>(context.metadata, propertyName, relation, args))
}
}
}

0 comments on commit bc38ff3

Please sign in to comment.