From 0ffcdf4342d3adc4a6c35ed04d2b386a633f075d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 20:44:16 +0100 Subject: [PATCH] Add get parent(s) of target class util functions --- .../src/reflections/getAllParentsOfClass.ts | 36 +++++++++ .../src/reflections/getParentOfClass.ts | 29 +++++++ packages/support/src/reflections/index.ts | 2 + .../reflections/getAllParentsOfClass.test.js | 78 +++++++++++++++++++ .../reflections/getParentOfClass.test.js | 43 ++++++++++ 5 files changed, 188 insertions(+) create mode 100644 packages/support/src/reflections/getAllParentsOfClass.ts create mode 100644 packages/support/src/reflections/getParentOfClass.ts create mode 100644 tests/browser/packages/support/reflections/getAllParentsOfClass.test.js create mode 100644 tests/browser/packages/support/reflections/getParentOfClass.test.js diff --git a/packages/support/src/reflections/getAllParentsOfClass.ts b/packages/support/src/reflections/getAllParentsOfClass.ts new file mode 100644 index 00000000..bb79372b --- /dev/null +++ b/packages/support/src/reflections/getAllParentsOfClass.ts @@ -0,0 +1,36 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { getParentOfClass } from "./getParentOfClass"; +import { isset } from "@aedart/support/misc"; + +/** + * Returns all parent classes of given target + * + * @see {getParentOfClass} + * + * @param {ConstructorOrAbstractConstructor} target The target class. + * @param {boolean} [includeTarget=false] If `true`, then given target is included in the output as the first element. + * + * @returns {ConstructorOrAbstractConstructor[]} List of parent classes, ordered by the top-most parent class first. + * + * @throws {TypeError} + */ +export function getAllParentsOfClass(target: ConstructorOrAbstractConstructor, includeTarget: boolean = false): ConstructorOrAbstractConstructor[] +{ + if (!isset(target)) { + throw new TypeError('getAllParentsOfClass() expects a target class as argument, undefined given'); + } + + const output: ConstructorOrAbstractConstructor[] = []; + if (includeTarget) { + output.push(target); + } + + let parent: ConstructorOrAbstractConstructor | null = getParentOfClass(target); + while (parent !== null) { + output.push(parent); + + parent = getParentOfClass(parent); + } + + return output; +} \ No newline at end of file diff --git a/packages/support/src/reflections/getParentOfClass.ts b/packages/support/src/reflections/getParentOfClass.ts new file mode 100644 index 00000000..c84acffb --- /dev/null +++ b/packages/support/src/reflections/getParentOfClass.ts @@ -0,0 +1,29 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; +import {isset} from "@aedart/support/misc"; + +/** + * Returns the parent class of given target class + * + * **Note**: _If target has a parent that matches + * [FUNCTION_PROTOTYPE]{@link import('@aedart/contracts/support/reflections').FUNCTION_PROTOTYPE}, then `null` is returned!_ + * + * @param {ConstructorOrAbstractConstructor} target The target class + * + * @returns {ConstructorOrAbstractConstructor | null} Parent class or `null`, if target has no parent class. + * + * @throws {TypeError} + */ +export function getParentOfClass(target: ConstructorOrAbstractConstructor): ConstructorOrAbstractConstructor | null +{ + if (!isset(target)) { + throw new TypeError('getParentOfClass() expects a target class as argument, undefined given'); + } + + const parent: object | null = Reflect.getPrototypeOf(target); + if (parent === FUNCTION_PROTOTYPE) { + return null; + } + + return parent as ConstructorOrAbstractConstructor; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index b2235ebc..6bd29b5e 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,6 +1,8 @@ export * from './assertHasPrototypeProperty'; +export * from './getAllParentsOfClass'; export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; +export * from './getParentOfClass'; export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; diff --git a/tests/browser/packages/support/reflections/getAllParentsOfClass.test.js b/tests/browser/packages/support/reflections/getAllParentsOfClass.test.js new file mode 100644 index 00000000..07d4aac3 --- /dev/null +++ b/tests/browser/packages/support/reflections/getAllParentsOfClass.test.js @@ -0,0 +1,78 @@ +import { getAllParentsOfClass } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('getAllParentsOfClass()', () => { + + it('fails when no arguments given', () => { + const callback = () => { + return getAllParentsOfClass(); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('returns all parent classes', () => { + + class A {} + class B extends A {} + class C extends B {} + + const parents = getAllParentsOfClass(C); + + // Debug + // console.log('parents of C', parents); + + expect(parents.length) + .withContext('Incorrect amount of parents returned') + .toEqual(2); + + expect(parents[0]) + .withContext('Incorrect parent of C') + .toEqual(B); + + expect(parents[1]) + .withContext('Incorrect parent of B') + .toEqual(A); + }); + + it('includes target in output', () => { + + class A {} + class B extends A {} + class C extends B {} + + const parents = getAllParentsOfClass(C, true); + + // Debug + // console.log('parents', parents); + + expect(parents.length) + .withContext('Incorrect amount of parents returned') + .toEqual(3); + + expect(parents[0]) + .withContext('First element should be C') + .toEqual(C); + + expect(parents[1]) + .withContext('Incorrect parent of C') + .toEqual(B); + + expect(parents[2]) + .withContext('Incorrect parent of B') + .toEqual(A); + }); + + it('returns empty array when target has no parents', () => { + + class A {} + + const parents = getAllParentsOfClass(A); + + expect(parents.length) + .withContext('A should not have any parents') + .toEqual(0); + }); + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/getParentOfClass.test.js b/tests/browser/packages/support/reflections/getParentOfClass.test.js new file mode 100644 index 00000000..d82f2eee --- /dev/null +++ b/tests/browser/packages/support/reflections/getParentOfClass.test.js @@ -0,0 +1,43 @@ +import { getParentOfClass } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('getParentOfClass()', () => { + + it('fails when no arguments given', () => { + const callback = () => { + return getParentOfClass(); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('can return parent class', () => { + + class A {} + class B extends A {} + class C extends B {} + + const parentOfC = getParentOfClass(C); + const parentOfB = getParentOfClass(B); + const parentOfA = getParentOfClass(A); + + // Debug + // console.log('Parent of C', parentOfC); + // console.log('Parent of B', parentOfB); + // console.log('Parent of A', parentOfA); + + expect(parentOfC) + .withContext('Incorrect parent of C') + .toEqual(B); + + expect(parentOfB) + .withContext('Incorrect parent of B') + .toEqual(A); + + expect(parentOfA) + .withContext('A should not have a parent') + .toBeNull() + }); + }); +}); \ No newline at end of file