From 2d21d7816f65bb626a9c0a5b343eeb0c93034e0d Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 Jan 2025 18:14:47 +0000 Subject: [PATCH 1/5] feat: add reflective serialization of objects with getters --- src/common/__tests__/json.test.ts | 96 +++++++++++++++++++++++++++++++ src/common/json.ts | 22 +++++++ 2 files changed, 118 insertions(+) create mode 100644 src/common/__tests__/json.test.ts diff --git a/src/common/__tests__/json.test.ts b/src/common/__tests__/json.test.ts new file mode 100644 index 00000000..b38f6f6e --- /dev/null +++ b/src/common/__tests__/json.test.ts @@ -0,0 +1,96 @@ +import { toSerializable } from "../json"; + +describe("toSerializable", () => { + /** + * Asserts converting objects. + */ + it("object", () => { + // Arrange. + const obj = { + name: "Elgato", + }; + + // Act. + const result = toSerializable(obj); + + // Assert. + expect(result).not.toBe(obj); + expect(result).toStrictEqual(obj); + }); + + /** + * Asserts converting objects with getters. + */ + it("object (with getters)", () => { + // Arrange. + const obj = new MockClass(); + + // Act. + const result = toSerializable(obj); + + // Assert. + expect(result).not.toBe(obj); + expect(result).toStrictEqual({ + getter: "Elgato", + member: "Elgato", + }); + }); + + /** + * Asserts converting booleans. + */ + it("boolean", () => { + expect(toSerializable(true)).toBe(true); + expect(toSerializable(false)).toBe(false); + }); + + /** + * Asserts converting numbers. + */ + it("number", () => { + expect(toSerializable(1)).toBe(1); + }); + + /** + * Asserts converting strings. + */ + it("string", () => { + expect(toSerializable("Hello world")).toBe("Hello world"); + }); + + /** + * Asserts converting functions. + */ + it("function", () => { + // Arrange. + const noop = () => ({ + /* no-op */ + }); + + // Act, assert. + expect(toSerializable(noop)).toBe(noop); + }); +}); + +class MockClass { + #name: string; + + constructor() { + this.#name = "Elgato"; + this.member = this.#name; + } + + get getter(): string { + return this.#name; + } + + set setter(value: string) { + // Should not be serialized. + } + + public readonly member: string; + + public fn(): void { + // Should not be serialized. + } +} diff --git a/src/common/json.ts b/src/common/json.ts index cd56564b..ada66e06 100644 --- a/src/common/json.ts +++ b/src/common/json.ts @@ -14,3 +14,25 @@ export type JsonPrimitive = boolean | number | string | null | undefined; * JSON value. */ export type JsonValue = JsonObject | JsonPrimitive | JsonValue[]; + +/** + * Converts the source object to a serializable instance that includes members, and properties with getters. + * @param source Source object to convert to a serializable. + * @returns Serializable instance. + */ +export function toSerializable(source: unknown): unknown { + if (source === undefined || source === null || typeof source !== "object") { + return source; + } + + const result: Record = { ...source }; + const proto = Object.getPrototypeOf(source); + + for (const [name, desc] of Object.entries(Object.getOwnPropertyDescriptors(proto))) { + if (desc.get) { + result[name] = source[name as keyof typeof source]; + } + } + + return result; +} From 99ca52abaa651a9e592cb4a8221d3fb3c9d6bec3 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 Jan 2025 18:39:16 +0000 Subject: [PATCH 2/5] feat: enable serialization of enumerable --- src/common/__tests__/enumerable.test.ts | 20 ++++++++++++++++++++ src/common/enumerable.ts | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/common/__tests__/enumerable.test.ts b/src/common/__tests__/enumerable.test.ts index 63de1a96..f0e005f1 100644 --- a/src/common/__tests__/enumerable.test.ts +++ b/src/common/__tests__/enumerable.test.ts @@ -813,4 +813,24 @@ describe("Enumerable", () => { expect(res).toHaveLength(0); }); }); + + /** + * Provides assertions for {@link Enumerable.toJSON}. + */ + test("toJSON", () => { + const arr = ["One", "Two"]; + const enumerable = new Enumerable(arr); + + expect(JSON.stringify(arr)).toEqual(JSON.stringify(enumerable)); + }); + + /** + * Provides assertions for {@link Enumerable.toString}. + */ + test("toString", () => { + const arr = ["One", "Two"]; + const enumerable = new Enumerable(arr); + + expect(arr.toString()).toEqual(enumerable.toString()); + }); }); diff --git a/src/common/enumerable.ts b/src/common/enumerable.ts index 00b80adf..87f17332 100644 --- a/src/common/enumerable.ts +++ b/src/common/enumerable.ts @@ -339,4 +339,20 @@ export class Enumerable implements IterableIterator { public toArray(): T[] { return Array.from(this); } + + /** + * Converts this iterator to serializable collection. + * @returns The serializable collection of items. + */ + public toJSON(): T[] { + return this.toArray(); + } + + /** + * Converts this iterator to a string. + * @returns The string. + */ + public toString(): string { + return `${this.toArray()}`; + } } From a6ec07beb5ebb1b68c3ac95e47f8c53ade7b68e5 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 Jan 2025 18:40:50 +0000 Subject: [PATCH 3/5] feat: enable serialization of action instances --- src/common/__tests__/json.test.ts | 96 ------------------------------- src/common/json.ts | 22 ------- src/plugin/actions/context.ts | 13 +++++ src/plugin/actions/dial.ts | 10 ++++ src/plugin/actions/key.ts | 11 ++++ 5 files changed, 34 insertions(+), 118 deletions(-) delete mode 100644 src/common/__tests__/json.test.ts diff --git a/src/common/__tests__/json.test.ts b/src/common/__tests__/json.test.ts deleted file mode 100644 index b38f6f6e..00000000 --- a/src/common/__tests__/json.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { toSerializable } from "../json"; - -describe("toSerializable", () => { - /** - * Asserts converting objects. - */ - it("object", () => { - // Arrange. - const obj = { - name: "Elgato", - }; - - // Act. - const result = toSerializable(obj); - - // Assert. - expect(result).not.toBe(obj); - expect(result).toStrictEqual(obj); - }); - - /** - * Asserts converting objects with getters. - */ - it("object (with getters)", () => { - // Arrange. - const obj = new MockClass(); - - // Act. - const result = toSerializable(obj); - - // Assert. - expect(result).not.toBe(obj); - expect(result).toStrictEqual({ - getter: "Elgato", - member: "Elgato", - }); - }); - - /** - * Asserts converting booleans. - */ - it("boolean", () => { - expect(toSerializable(true)).toBe(true); - expect(toSerializable(false)).toBe(false); - }); - - /** - * Asserts converting numbers. - */ - it("number", () => { - expect(toSerializable(1)).toBe(1); - }); - - /** - * Asserts converting strings. - */ - it("string", () => { - expect(toSerializable("Hello world")).toBe("Hello world"); - }); - - /** - * Asserts converting functions. - */ - it("function", () => { - // Arrange. - const noop = () => ({ - /* no-op */ - }); - - // Act, assert. - expect(toSerializable(noop)).toBe(noop); - }); -}); - -class MockClass { - #name: string; - - constructor() { - this.#name = "Elgato"; - this.member = this.#name; - } - - get getter(): string { - return this.#name; - } - - set setter(value: string) { - // Should not be serialized. - } - - public readonly member: string; - - public fn(): void { - // Should not be serialized. - } -} diff --git a/src/common/json.ts b/src/common/json.ts index ada66e06..cd56564b 100644 --- a/src/common/json.ts +++ b/src/common/json.ts @@ -14,25 +14,3 @@ export type JsonPrimitive = boolean | number | string | null | undefined; * JSON value. */ export type JsonValue = JsonObject | JsonPrimitive | JsonValue[]; - -/** - * Converts the source object to a serializable instance that includes members, and properties with getters. - * @param source Source object to convert to a serializable. - * @returns Serializable instance. - */ -export function toSerializable(source: unknown): unknown { - if (source === undefined || source === null || typeof source !== "object") { - return source; - } - - const result: Record = { ...source }; - const proto = Object.getPrototypeOf(source); - - for (const [name, desc] of Object.entries(Object.getOwnPropertyDescriptors(proto))) { - if (desc.get) { - result[name] = source[name as keyof typeof source]; - } - } - - return result; -} diff --git a/src/plugin/actions/context.ts b/src/plugin/actions/context.ts index 8b2d00bc..39b4d2d9 100644 --- a/src/plugin/actions/context.ts +++ b/src/plugin/actions/context.ts @@ -65,4 +65,17 @@ export class ActionContext { public get manifestId(): string { return this.#source.action; } + + /** + * Converts this instance to a serializable object. + * @returns The serializable object. + */ + public toJSON(): Object { + return { + controllerType: this.controllerType, + device: this.device, + id: this.id, + manifestId: this.manifestId, + }; + } } diff --git a/src/plugin/actions/dial.ts b/src/plugin/actions/dial.ts index 749d4d68..46e6ea8e 100644 --- a/src/plugin/actions/dial.ts +++ b/src/plugin/actions/dial.ts @@ -114,6 +114,16 @@ export class DialAction extends Action { payload: descriptions || {}, }); } + + /** + * @inheritdoc + */ + public override toJSON(): Object { + return { + ...super.toJSON(), + coordinates: this.coordinates, + }; + } } /** diff --git a/src/plugin/actions/key.ts b/src/plugin/actions/key.ts index 95c89230..20fc9b87 100644 --- a/src/plugin/actions/key.ts +++ b/src/plugin/actions/key.ts @@ -115,6 +115,17 @@ export class KeyAction extends Action { context: this.id, }); } + + /** + * @inheritdoc + */ + public override toJSON(): Object { + return { + ...super.toJSON(), + coordinates: this.coordinates, + isInMultiAction: this.isInMultiAction(), + }; + } } /** From 3c42d43785826ac9759325f39a9b1cc7cfef0efd Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 Jan 2025 18:42:54 +0000 Subject: [PATCH 4/5] fix: linting --- src/plugin/actions/context.ts | 2 +- src/plugin/actions/dial.ts | 2 +- src/plugin/actions/key.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugin/actions/context.ts b/src/plugin/actions/context.ts index 39b4d2d9..fc8cc598 100644 --- a/src/plugin/actions/context.ts +++ b/src/plugin/actions/context.ts @@ -70,7 +70,7 @@ export class ActionContext { * Converts this instance to a serializable object. * @returns The serializable object. */ - public toJSON(): Object { + public toJSON(): object { return { controllerType: this.controllerType, device: this.device, diff --git a/src/plugin/actions/dial.ts b/src/plugin/actions/dial.ts index 46e6ea8e..1abb797d 100644 --- a/src/plugin/actions/dial.ts +++ b/src/plugin/actions/dial.ts @@ -118,7 +118,7 @@ export class DialAction extends Action { /** * @inheritdoc */ - public override toJSON(): Object { + public override toJSON(): object { return { ...super.toJSON(), coordinates: this.coordinates, diff --git a/src/plugin/actions/key.ts b/src/plugin/actions/key.ts index 20fc9b87..3c12267e 100644 --- a/src/plugin/actions/key.ts +++ b/src/plugin/actions/key.ts @@ -119,7 +119,7 @@ export class KeyAction extends Action { /** * @inheritdoc */ - public override toJSON(): Object { + public override toJSON(): object { return { ...super.toJSON(), coordinates: this.coordinates, From 799a5e6fe837f6f15025b3f74c4071a5560157d4 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 Jan 2025 18:50:50 +0000 Subject: [PATCH 5/5] test: add tests for KeyAction and DialAction serialization --- src/plugin/actions/__tests__/dial.test.ts | 33 ++++++++++++++++++++++ src/plugin/actions/__tests__/key.test.ts | 34 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/plugin/actions/__tests__/dial.test.ts b/src/plugin/actions/__tests__/dial.test.ts index 78e9446f..a71361e8 100644 --- a/src/plugin/actions/__tests__/dial.test.ts +++ b/src/plugin/actions/__tests__/dial.test.ts @@ -88,6 +88,39 @@ describe("DialAction", () => { expect(() => new DialAction(keypadSource)).toThrow(); }); + /** + * Asserts {@link DialAction.toJSON} includes properties. + */ + it("JSON has properties", () => { + // Array. + const action = new DialAction({ + action: "action1", + context: "com.test.action.one", + device: "dev1", + event: "willAppear", + payload: { + controller: "Encoder", + settings: {}, + isInMultiAction: false, + coordinates: { + column: 1, + row: 2, + }, + }, + }); + + // Act. + const jsonStr = JSON.stringify(action); + const jsonObj: DialAction = JSON.parse(jsonStr); + + // Assert. + expect(jsonObj.controllerType).toBe(action.controllerType); + expect(jsonObj.coordinates).toStrictEqual(action.coordinates); + expect(jsonObj.device).toStrictEqual({ id: action.device.id }); + expect(jsonObj.id).toBe(action.id); + expect(jsonObj.manifestId).toBe(action.manifestId); + }); + describe("sending", () => { let action!: DialAction; beforeAll(() => (action = new DialAction(source))); diff --git a/src/plugin/actions/__tests__/key.test.ts b/src/plugin/actions/__tests__/key.test.ts index 2b718e22..dc4482db 100644 --- a/src/plugin/actions/__tests__/key.test.ts +++ b/src/plugin/actions/__tests__/key.test.ts @@ -118,6 +118,40 @@ describe("KeyAction", () => { expect(action.coordinates).toBeUndefined(); }); + /** + * Asserts {@link KeyAction.toJSON} includes properties. + */ + it("JSON has properties", () => { + // Array. + const action = new KeyAction({ + action: "action1", + context: "com.test.action.one", + device: "dev1", + event: "willAppear", + payload: { + controller: "Keypad", + settings: {}, + isInMultiAction: false, + coordinates: { + column: 1, + row: 2, + }, + }, + }); + + // Act. + const jsonStr = JSON.stringify(action); + const jsonObj: KeyAction = JSON.parse(jsonStr); + + // Assert. + expect(jsonObj.controllerType).toBe(action.controllerType); + expect(jsonObj.coordinates).toStrictEqual(action.coordinates); + expect(jsonObj.device).toStrictEqual({ id: action.device.id }); + expect(jsonObj.id).toBe(action.id); + expect(jsonObj.isInMultiAction).toBe(action.isInMultiAction()); + expect(jsonObj.manifestId).toBe(action.manifestId); + }); + describe("sending", () => { let action!: KeyAction; beforeAll(() => (action = new KeyAction(source)));