Skip to content

Commit

Permalink
feat: basic support for Ctx{Get,Set} decorators
Browse files Browse the repository at this point in the history
`Ctx` decorators are a way to share informations across
objects during the reading of the binary file definition.

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[]
}
```
  • Loading branch information
tperale committed Dec 12, 2024
1 parent 3061928 commit 9ac59df
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 5 deletions.
30 changes: 27 additions & 3 deletions src/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Target> (buffer: Array<number>, ObjectDefinition: InstantiableObject<Target>, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ...args: any[]) {
return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ...args))
function expectReadTest<Target> (buffer: Array<number>, ObjectDefinition: InstantiableObject<Target>, endian: BinaryCursorEndianness = BinaryCursorEndianness.BigEndian, ctx = {}, ...args: any[]) {
return expect(binread(new BinaryReader(new Uint8Array(buffer).buffer, endian), ObjectDefinition, ctx, ...args))
}

function expectReadTestToThrow<Target> (buffer: Array<number>, ObjectDefinition: InstantiableObject<Target>) {
Expand Down Expand Up @@ -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,
})
})
Expand Down Expand Up @@ -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 {
Expand Down
85 changes: 85 additions & 0 deletions src/decorators/__tests__/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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)
// expect(useContext(ctx, value, instance)).toStrictEqual(equal)
useContextSet(contexts, value, instance, ctx)
}

describe('@Ctx: functions', () => {
it('should modify the ctx object', () => {
class TestClass {
@CtxSet('test')
data: number
}
const ctx = {}
const VALUE = 1
testContextSet(TestClass, 'data', VALUE, ctx)
// @ts-ignore

Check failure on line 29 in src/decorators/__tests__/context.test.ts

View workflow job for this annotation

GitHub Actions / lint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
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 = {}
const VALUE = 1
testContextSet(TestClass, 'data', VALUE, ctx)
testContextSet(TestClass, 'data_2', VALUE, ctx)
// @ts-ignore

Check failure on line 44 in src/decorators/__tests__/context.test.ts

View workflow job for this annotation

GitHub Actions / lint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
expect(ctx.foo.bar[1]).toEqual(VALUE)
// @ts-ignore

Check failure on line 46 in src/decorators/__tests__/context.test.ts

View workflow job for this annotation

GitHub Actions / lint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
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('test')
data: number
}
const VALUE = 1
const ctx = { test: 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 }
// testContextGet(TestClass, 'data', ctx, VALUE)
// })
// it('should throw when overriding an existing object', () => {
// class TestClass {
// @CtxGet('test.foo')
// data: number
// }
// const VALUE = 1
// const ctx = { test: VALUE }
// testContextGet(TestClass, 'data', ctx, VALUE)
// })
})
Loading

0 comments on commit 9ac59df

Please sign in to comment.