Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion src/lib/object.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isObject, convertNullToUndefined } from "./object";
import { isObject, convertNullToUndefined, pathOf } from "./object";

describe("object tests", () => {
test.each([
Expand Down Expand Up @@ -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");
});
});
});
41 changes: 41 additions & 0 deletions src/lib/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T extends object>(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(".");
};