diff --git a/src/lib/object.spec.ts b/src/lib/object.spec.ts index 66699d4..c446c59 100644 --- a/src/lib/object.spec.ts +++ b/src/lib/object.spec.ts @@ -1,4 +1,4 @@ -import { isObject, convertNullToUndefined } from "./object"; +import { isObject, convertNullToUndefined, pathOf } from "./object"; describe("object tests", () => { test.each([ @@ -40,4 +40,73 @@ describe("object tests", () => { convertNullToUndefined(obj); expect(obj).toStrictEqual(expected); }); + + describe("pathOf", () => { + interface TestObject { + prop1: string; + nested: { + prop2: string; + deepNested: { + prop3: string; + }; + }; + Recursion: { + Recursion: { + Recursion: { + Recursion: string; + }; + }; + }; + } + + test("should return correct path for simple property access", () => { + expect(pathOf((x: TestObject) => x.prop1)).toBe("prop1"); + }); + + test("should return correct path for nested property access", () => { + expect(pathOf((x: TestObject) => x.nested.prop2)).toBe("nested.prop2"); + }); + + test("should return correct path for deep nested property access", () => { + expect(pathOf((x: TestObject) => x.nested.deepNested.prop3)).toBe("nested.deepNested.prop3"); + }); + + test("should return correct path for recursive property access (matching C# example)", () => { + expect(pathOf((x: TestObject) => x.Recursion.Recursion.Recursion.Recursion)).toBe("Recursion.Recursion.Recursion.Recursion"); + }); + + test("should handle different property types", () => { + expect(pathOf((x: { id: number }) => x.id)).toBe("id"); + expect(pathOf((x: { name: string }) => x.name)).toBe("name"); + }); + + test("should return empty string for expression that doesn't access properties", () => { + expect(pathOf(() => "constant")).toBe(""); + }); + + test("should handle expressions that access properties on returned values", () => { + interface ComplexObject { + getData: { + value: string; + }; + } + + // This should work as it's accessing a property, not calling a function + const path = pathOf((x: ComplexObject) => x.getData.value); + expect(path).toBe("getData.value"); + }); + + test("should handle expressions that may throw errors", () => { + interface TestObject { + prop: string; + } + + // This expression might throw an error during execution, but the path should still be captured + const path = pathOf((x: TestObject) => { + // This would throw in normal execution, but we want to capture the path + return x.prop.toString(); + }); + expect(path).toBe("prop.toString"); + }); + }); }); diff --git a/src/lib/object.ts b/src/lib/object.ts index ac2f707..4c3a55a 100644 --- a/src/lib/object.ts +++ b/src/lib/object.ts @@ -24,3 +24,44 @@ export const convertNullToUndefined = (obj: object): void => { } } }; + +/** + * Get the full path of a nested object property from a lambda expression + * @param expression A function that accesses properties on an object + * @returns The property path as a string (e.g., "prop1.prop2.prop3") + */ +export const pathOf = (expression: (obj: T) => unknown): string => { + const path: string[] = []; + + /** + * Create a proxy that tracks property access + * @param currentPath The current path being tracked + * @returns A proxy object that captures property access + */ + const createProxy = (currentPath: string[] = []): T => { + return new Proxy({} as T, { + /** + * Intercept property access and build the path + * @param _target The proxy target (unused) + * @param prop The property being accessed + * @returns A new proxy for continued property access + */ + get(_target, prop) { + if (typeof prop === "string") { + const newPath = [...currentPath, prop]; + path.splice(0, path.length, ...newPath); + return createProxy(newPath); + } + return; + }, + }); + }; + + try { + expression(createProxy()); + } catch { + // Ignore errors that might occur during property access + } + + return path.join("."); +};