From ea0bf64e00ef8bb41064f672f384ee50f970e0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= <roberto.pintos.lopez@gmail.com> Date: Sun, 17 Nov 2024 23:55:27 +0100 Subject: [PATCH 1/2] refactor(core): add updateMetadataTag --- .../actions/updateMetadataTag.spec.ts | 96 +++++++++++++++++++ .../src/metadata/actions/updateMetadataTag.ts | 27 ++++++ 2 files changed, 123 insertions(+) create mode 100644 packages/container/libraries/core/src/metadata/actions/updateMetadataTag.spec.ts create mode 100644 packages/container/libraries/core/src/metadata/actions/updateMetadataTag.ts diff --git a/packages/container/libraries/core/src/metadata/actions/updateMetadataTag.spec.ts b/packages/container/libraries/core/src/metadata/actions/updateMetadataTag.spec.ts new file mode 100644 index 00000000..c7aae687 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/actions/updateMetadataTag.spec.ts @@ -0,0 +1,96 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { ClassElementMetadataKind } from '../models/ClassElementMetadataKind'; +import { ManagedClassElementMetadata } from '../models/ManagedClassElementMetadata'; +import { MaybeManagedClassElementMetadata } from '../models/MaybeManagedClassElementMetadata'; +import { MetadataTag } from '../models/MetadataTag'; +import { updateMetadataTag } from './updateMetadataTag'; + +describe(updateMetadataTag.name, () => { + describe('having metadata with missing tag', () => { + let metadataFixture: + | ManagedClassElementMetadata + | MaybeManagedClassElementMetadata; + let keyFixture: MetadataTag; + let valueFixture: unknown; + + beforeAll(() => { + metadataFixture = { + kind: ClassElementMetadataKind.singleInjection, + name: undefined, + optional: false, + tags: new Map(), + targetName: undefined, + value: 'service-id', + }; + keyFixture = 'tag-fixture'; + valueFixture = Symbol(); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = updateMetadataTag(keyFixture, valueFixture)(metadataFixture); + }); + + it('should return metadata', () => { + const expected: + | ManagedClassElementMetadata + | MaybeManagedClassElementMetadata = { + ...metadataFixture, + tags: new Map([[keyFixture, valueFixture]]), + }; + + expect(result).toStrictEqual(expected); + }); + }); + }); + + describe('having metadata with existing tag', () => { + let metadataFixture: + | ManagedClassElementMetadata + | MaybeManagedClassElementMetadata; + let keyFixture: MetadataTag; + let valueFixture: unknown; + + beforeAll(() => { + metadataFixture = { + kind: ClassElementMetadataKind.singleInjection, + name: undefined, + optional: false, + tags: new Map([['tag-fixture', Symbol()]]), + targetName: undefined, + value: 'service-id', + }; + keyFixture = 'tag-fixture'; + valueFixture = Symbol(); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + try { + updateMetadataTag(keyFixture, valueFixture)(metadataFixture); + } catch (error: unknown) { + result = error; + } + }); + + it('should throw InversifyCoreError', () => { + const expectedErrorProperties: Partial<InversifyCoreError> = { + kind: InversifyCoreErrorKind.injectionDecoratorConflict, + message: 'Unexpected duplicated tag decorator with existing tag', + }; + + expect(result).toBeInstanceOf(InversifyCoreError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/metadata/actions/updateMetadataTag.ts b/packages/container/libraries/core/src/metadata/actions/updateMetadataTag.ts new file mode 100644 index 00000000..baf1c627 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/actions/updateMetadataTag.ts @@ -0,0 +1,27 @@ +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { ManagedClassElementMetadata } from '../models/ManagedClassElementMetadata'; +import { MaybeManagedClassElementMetadata } from '../models/MaybeManagedClassElementMetadata'; +import { MetadataTag } from '../models/MetadataTag'; + +export function updateMetadataTag( + key: MetadataTag, + value: unknown, +): ( + metadata: ManagedClassElementMetadata | MaybeManagedClassElementMetadata, +) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata { + return ( + metadata: ManagedClassElementMetadata | MaybeManagedClassElementMetadata, + ): ManagedClassElementMetadata | MaybeManagedClassElementMetadata => { + if (metadata.tags.has(key)) { + throw new InversifyCoreError( + InversifyCoreErrorKind.injectionDecoratorConflict, + 'Unexpected duplicated tag decorator with existing tag', + ); + } + + metadata.tags.set(key, value); + + return metadata; + }; +} From 9fa2ee1ea95fedfbf0b5e61e473a1d1ee1b236ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= <roberto.pintos.lopez@gmail.com> Date: Sun, 17 Nov 2024 23:55:43 +0100 Subject: [PATCH 2/2] refactor(core): add tagged decorator --- .../metadata/decorators/tagged.int.spec.ts | 84 ++++ .../src/metadata/decorators/tagged.spec.ts | 376 ++++++++++++++++++ .../core/src/metadata/decorators/tagged.ts | 36 ++ 3 files changed, 496 insertions(+) create mode 100644 packages/container/libraries/core/src/metadata/decorators/tagged.int.spec.ts create mode 100644 packages/container/libraries/core/src/metadata/decorators/tagged.spec.ts create mode 100644 packages/container/libraries/core/src/metadata/decorators/tagged.ts diff --git a/packages/container/libraries/core/src/metadata/decorators/tagged.int.spec.ts b/packages/container/libraries/core/src/metadata/decorators/tagged.int.spec.ts new file mode 100644 index 00000000..2ce68970 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/decorators/tagged.int.spec.ts @@ -0,0 +1,84 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import 'reflect-metadata'; + +import { getReflectMetadata } from '@inversifyjs/reflect-metadata-utils'; + +import { classMetadataReflectKey } from '../../reflectMetadata/data/classMetadataReflectKey'; +import { MaybeClassElementMetadataKind } from '../models/MaybeClassElementMetadataKind'; +import { MaybeClassMetadata } from '../models/MaybeClassMetadata'; +import { tagged } from './tagged'; + +describe(tagged.name, () => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + class Foo { + @tagged('bar', 'bar-value') + public readonly bar!: string; + + @tagged('baz', 'baz-value') + public readonly baz!: string; + + constructor( + @tagged('firstParam', 'firstParam-value') + public firstParam: number, + @tagged('secondParam', 'secondParam-value') + public secondParam: number, + ) {} + } + + result = getReflectMetadata(Foo, classMetadataReflectKey); + }); + + it('should return expected metadata', () => { + const expected: MaybeClassMetadata = { + constructorArguments: [ + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: false, + tags: new Map([['firstParam', 'firstParam-value']]), + targetName: undefined, + }, + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: false, + tags: new Map([['secondParam', 'secondParam-value']]), + targetName: undefined, + }, + ], + lifecycle: { + postConstructMethodName: undefined, + preDestroyMethodName: undefined, + }, + properties: new Map([ + [ + 'bar', + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: false, + tags: new Map([['bar', 'bar-value']]), + targetName: undefined, + }, + ], + [ + 'baz', + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: false, + tags: new Map([['baz', 'baz-value']]), + targetName: undefined, + }, + ], + ]), + }; + + expect(result).toStrictEqual(expected); + }); + }); +}); diff --git a/packages/container/libraries/core/src/metadata/decorators/tagged.spec.ts b/packages/container/libraries/core/src/metadata/decorators/tagged.spec.ts new file mode 100644 index 00000000..4554f0ae --- /dev/null +++ b/packages/container/libraries/core/src/metadata/decorators/tagged.spec.ts @@ -0,0 +1,376 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('../actions/updateMetadataName'); +jest.mock( + '../calculations/buildMaybeClassElementMetadataFromMaybeClassElementMetadata', +); +jest.mock('../calculations/handleInjectionError'); +jest.mock('./injectBase'); + +import { updateMetadataName } from '../actions/updateMetadataName'; +import { buildMaybeClassElementMetadataFromMaybeClassElementMetadata } from '../calculations/buildMaybeClassElementMetadataFromMaybeClassElementMetadata'; +import { handleInjectionError } from '../calculations/handleInjectionError'; +import { ManagedClassElementMetadata } from '../models/ManagedClassElementMetadata'; +import { MaybeClassElementMetadata } from '../models/MaybeClassElementMetadata'; +import { MaybeManagedClassElementMetadata } from '../models/MaybeManagedClassElementMetadata'; +import { MetadataTag } from '../models/MetadataTag'; +import { injectBase } from './injectBase'; +import { tagged } from './tagged'; + +describe(tagged.name, () => { + let updateMetadataNameResultMock: jest.Mock< + ( + metadata: ManagedClassElementMetadata | MaybeManagedClassElementMetadata, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + beforeAll(() => { + updateMetadataNameResultMock = jest.fn(); + + ( + updateMetadataName as jest.Mock<typeof updateMetadataName> + ).mockReturnValue(updateMetadataNameResultMock); + }); + + describe('having a non undefined propertyKey and an undefined parameterIndex', () => { + let targetFixture: object; + let propertyKeyFixture: string | symbol; + + beforeAll(() => { + targetFixture = class {}; + propertyKeyFixture = 'property-key'; + }); + + describe('when called', () => { + let keyFixture: MetadataTag; + let valueFixture: unknown; + let injectBaseDecoratorMock: jest.Mock< + ParameterDecorator & PropertyDecorator + > & + ParameterDecorator & + PropertyDecorator; + + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + keyFixture = 'tag-fixture'; + valueFixture = Symbol(); + injectBaseDecoratorMock = jest.fn() as jest.Mock< + ParameterDecorator & PropertyDecorator + > & + ParameterDecorator & + PropertyDecorator; + + updateMetadataMock = jest.fn(); + + ( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata as jest.Mock< + typeof buildMaybeClassElementMetadataFromMaybeClassElementMetadata + > + ).mockReturnValueOnce(updateMetadataMock); + + (injectBase as jest.Mock<typeof injectBase>).mockReturnValueOnce( + injectBaseDecoratorMock, + ); + + result = tagged(keyFixture, valueFixture)( + targetFixture, + propertyKeyFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should call injectBase()', () => { + expect(injectBase).toHaveBeenCalledTimes(1); + expect(injectBase).toHaveBeenCalledWith(updateMetadataMock); + }); + + it('should call injectBaseDecorator()', () => { + expect(injectBaseDecoratorMock).toHaveBeenCalledTimes(1); + expect(injectBaseDecoratorMock).toHaveBeenCalledWith( + targetFixture, + propertyKeyFixture, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + + describe('when called, and injectBase throws an Error', () => { + let keyFixture: MetadataTag; + let valueFixture: unknown; + let errorFixture: Error; + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + keyFixture = 'tag-fixture'; + valueFixture = Symbol(); + errorFixture = new Error('message-error-fixture'); + updateMetadataMock = jest.fn(); + + ( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata as jest.Mock< + typeof buildMaybeClassElementMetadataFromMaybeClassElementMetadata + > + ).mockReturnValueOnce(updateMetadataMock); + + (injectBase as jest.Mock<typeof injectBase>).mockImplementation( + (): never => { + throw errorFixture; + }, + ); + + ( + handleInjectionError as jest.Mock<typeof handleInjectionError> + ).mockImplementation( + ( + _target: object, + _propertyKey: string | symbol | undefined, + _parameterIndex: number | undefined, + error: unknown, + ): never => { + throw error; + }, + ); + + try { + tagged(keyFixture, valueFixture)(targetFixture, propertyKeyFixture); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should call injectBase()', () => { + expect(injectBase).toHaveBeenCalledTimes(1); + expect(injectBase).toHaveBeenCalledWith(updateMetadataMock); + }); + + it('should throw handleInjectionError()', () => { + expect(handleInjectionError).toHaveBeenCalledTimes(1); + expect(handleInjectionError).toHaveBeenCalledWith( + targetFixture, + propertyKeyFixture, + undefined, + errorFixture, + ); + }); + + it('should throw an Error', () => { + expect(result).toBe(errorFixture); + }); + }); + }); + + describe('having an undefined propertyKey and an non undefined parameterIndex', () => { + let targetFixture: object; + let paramIndexFixture: number; + + beforeAll(() => { + targetFixture = class {}; + paramIndexFixture = 0; + }); + + describe('when called', () => { + let keyFixture: MetadataTag; + let valueFixture: unknown; + let injectBaseDecoratorMock: jest.Mock< + ParameterDecorator & PropertyDecorator + > & + ParameterDecorator & + PropertyDecorator; + + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + keyFixture = 'name-fixture'; + valueFixture = Symbol(); + injectBaseDecoratorMock = jest.fn() as jest.Mock< + ParameterDecorator & PropertyDecorator + > & + ParameterDecorator & + PropertyDecorator; + + updateMetadataMock = jest.fn(); + + ( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata as jest.Mock< + typeof buildMaybeClassElementMetadataFromMaybeClassElementMetadata + > + ).mockReturnValueOnce(updateMetadataMock); + + (injectBase as jest.Mock<typeof injectBase>).mockReturnValueOnce( + injectBaseDecoratorMock, + ); + + result = tagged(keyFixture, valueFixture)( + targetFixture, + undefined, + paramIndexFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should call injectBase()', () => { + expect(injectBase).toHaveBeenCalledTimes(1); + expect(injectBase).toHaveBeenCalledWith(updateMetadataMock); + }); + + it('should call injectBaseDecorator()', () => { + expect(injectBaseDecoratorMock).toHaveBeenCalledTimes(1); + expect(injectBaseDecoratorMock).toHaveBeenCalledWith( + targetFixture, + undefined, + paramIndexFixture, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + + describe('when called, and injectBase throws an Error', () => { + let keyFixture: MetadataTag; + let valueFixture: unknown; + let errorFixture: Error; + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + keyFixture = 'tag-fixture'; + valueFixture = Symbol(); + errorFixture = new Error('message-error-fixture'); + updateMetadataMock = jest.fn(); + + ( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata as jest.Mock< + typeof buildMaybeClassElementMetadataFromMaybeClassElementMetadata + > + ).mockReturnValueOnce(updateMetadataMock); + + (injectBase as jest.Mock<typeof injectBase>).mockImplementation( + (): never => { + throw errorFixture; + }, + ); + + ( + handleInjectionError as jest.Mock<typeof handleInjectionError> + ).mockImplementation( + ( + _target: object, + _propertyKey: string | symbol | undefined, + _parameterIndex: number | undefined, + error: unknown, + ): never => { + throw error; + }, + ); + + try { + tagged(keyFixture, valueFixture)( + targetFixture, + undefined, + paramIndexFixture, + ); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should call injectBase()', () => { + expect(injectBase).toHaveBeenCalledTimes(1); + expect(injectBase).toHaveBeenCalledWith(updateMetadataMock); + }); + + it('should throw handleInjectionError()', () => { + expect(handleInjectionError).toHaveBeenCalledTimes(1); + expect(handleInjectionError).toHaveBeenCalledWith( + targetFixture, + undefined, + paramIndexFixture, + errorFixture, + ); + }); + + it('should throw an Error', () => { + expect(result).toBe(errorFixture); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/metadata/decorators/tagged.ts b/packages/container/libraries/core/src/metadata/decorators/tagged.ts new file mode 100644 index 00000000..69b04a4d --- /dev/null +++ b/packages/container/libraries/core/src/metadata/decorators/tagged.ts @@ -0,0 +1,36 @@ +import { updateMetadataTag } from '../actions/updateMetadataTag'; +import { buildMaybeClassElementMetadataFromMaybeClassElementMetadata } from '../calculations/buildMaybeClassElementMetadataFromMaybeClassElementMetadata'; +import { handleInjectionError } from '../calculations/handleInjectionError'; +import { ManagedClassElementMetadata } from '../models/ManagedClassElementMetadata'; +import { MaybeClassElementMetadata } from '../models/MaybeClassElementMetadata'; +import { MaybeManagedClassElementMetadata } from '../models/MaybeManagedClassElementMetadata'; +import { MetadataTag } from '../models/MetadataTag'; +import { injectBase } from './injectBase'; + +export function tagged( + key: MetadataTag, + value: unknown, +): ParameterDecorator & PropertyDecorator { + return ( + target: object, + propertyKey: string | symbol | undefined, + parameterIndex?: number, + ): void => { + const updateMetadata: ( + metadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata = + buildMaybeClassElementMetadataFromMaybeClassElementMetadata( + updateMetadataTag(key, value), + ); + + try { + if (parameterIndex === undefined) { + injectBase(updateMetadata)(target, propertyKey as string | symbol); + } else { + injectBase(updateMetadata)(target, propertyKey, parameterIndex); + } + } catch (error: unknown) { + handleInjectionError(target, propertyKey, parameterIndex, error); + } + }; +}