From 18ad2deada49cde3ea9dcfbb5ff56cf4e8173b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Sun, 17 Nov 2024 20:15:08 +0100 Subject: [PATCH 1/2] refactor(core): add updateMetadataOptional --- .../actions/updateMetadataOptional.spec.ts | 87 +++++++++++++++++++ .../actions/updateMetadataOptional.ts | 19 ++++ 2 files changed, 106 insertions(+) create mode 100644 packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.spec.ts create mode 100644 packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.ts diff --git a/packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.spec.ts b/packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.spec.ts new file mode 100644 index 00000000..bb0c6599 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.spec.ts @@ -0,0 +1,87 @@ +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 { updateMetadataOptional } from './updateMetadataOptional'; + +describe(updateMetadataOptional.name, () => { + describe('having metadata with no optional', () => { + let metadataFixture: + | ManagedClassElementMetadata + | MaybeManagedClassElementMetadata; + + beforeAll(() => { + metadataFixture = { + kind: ClassElementMetadataKind.singleInjection, + name: undefined, + optional: false, + tags: new Map(), + targetName: undefined, + value: 'service-id', + }; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = updateMetadataOptional(metadataFixture); + }); + + it('should return metadata', () => { + const expected: + | ManagedClassElementMetadata + | MaybeManagedClassElementMetadata = { + ...metadataFixture, + optional: true, + }; + + expect(result).toStrictEqual(expected); + }); + }); + }); + + describe('having metadata with optional', () => { + let metadataFixture: + | ManagedClassElementMetadata + | MaybeManagedClassElementMetadata; + + beforeAll(() => { + metadataFixture = { + kind: ClassElementMetadataKind.singleInjection, + name: undefined, + optional: true, + tags: new Map(), + targetName: undefined, + value: 'service-id', + }; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + try { + updateMetadataOptional(metadataFixture); + } catch (error: unknown) { + result = error; + } + }); + + it('should throw InversifyCoreError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyCoreErrorKind.injectionDecoratorConflict, + message: 'Unexpected duplicated optional decorator', + }; + + expect(result).toBeInstanceOf(InversifyCoreError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.ts b/packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.ts new file mode 100644 index 00000000..7bf0712a --- /dev/null +++ b/packages/container/libraries/core/src/metadata/actions/updateMetadataOptional.ts @@ -0,0 +1,19 @@ +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { ManagedClassElementMetadata } from '../models/ManagedClassElementMetadata'; +import { MaybeManagedClassElementMetadata } from '../models/MaybeManagedClassElementMetadata'; + +export function updateMetadataOptional( + metadata: ManagedClassElementMetadata | MaybeManagedClassElementMetadata, +): ManagedClassElementMetadata | MaybeManagedClassElementMetadata { + if (metadata.optional) { + throw new InversifyCoreError( + InversifyCoreErrorKind.injectionDecoratorConflict, + 'Unexpected duplicated optional decorator', + ); + } + + metadata.optional = true; + + return metadata; +} From a8966d8a2b4da69a28d5c53aafc2facefa3f57af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Sun, 17 Nov 2024 20:15:24 +0100 Subject: [PATCH 2/2] refactor(core): add optional decorator --- .../metadata/decorators/optional.int.spec.ts | 84 +++++ .../src/metadata/decorators/optional.spec.ts | 334 ++++++++++++++++++ .../core/src/metadata/decorators/optional.ts | 32 ++ 3 files changed, 450 insertions(+) create mode 100644 packages/container/libraries/core/src/metadata/decorators/optional.int.spec.ts create mode 100644 packages/container/libraries/core/src/metadata/decorators/optional.spec.ts create mode 100644 packages/container/libraries/core/src/metadata/decorators/optional.ts diff --git a/packages/container/libraries/core/src/metadata/decorators/optional.int.spec.ts b/packages/container/libraries/core/src/metadata/decorators/optional.int.spec.ts new file mode 100644 index 00000000..36662569 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/decorators/optional.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 { optional } from './optional'; + +describe(optional.name, () => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + class Foo { + @optional() + public readonly bar!: string; + + @optional() + public readonly baz!: string; + + constructor( + @optional() + public firstParam: number, + @optional() + public secondParam: number, + ) {} + } + + result = getReflectMetadata(Foo, classMetadataReflectKey); + }); + + it('should return expected metadata', () => { + const expected: MaybeClassMetadata = { + constructorArguments: [ + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: true, + tags: new Map(), + targetName: undefined, + }, + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: true, + tags: new Map(), + targetName: undefined, + }, + ], + lifecycle: { + postConstructMethodName: undefined, + preDestroyMethodName: undefined, + }, + properties: new Map([ + [ + 'bar', + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: true, + tags: new Map(), + targetName: undefined, + }, + ], + [ + 'baz', + { + kind: MaybeClassElementMetadataKind.unknown, + name: undefined, + optional: true, + tags: new Map(), + targetName: undefined, + }, + ], + ]), + }; + + expect(result).toStrictEqual(expected); + }); + }); +}); diff --git a/packages/container/libraries/core/src/metadata/decorators/optional.spec.ts b/packages/container/libraries/core/src/metadata/decorators/optional.spec.ts new file mode 100644 index 00000000..04f8bf89 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/decorators/optional.spec.ts @@ -0,0 +1,334 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('../actions/updateMetadataOptional'); +jest.mock( + '../calculations/buildMaybeClassElementMetadataFromMaybeClassElementMetadata', +); +jest.mock('../calculations/handleInjectionError'); +jest.mock('./injectBase'); + +import { updateMetadataOptional } from '../actions/updateMetadataOptional'; +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 { injectBase } from './injectBase'; +import { optional } from './optional'; + +describe(optional.name, () => { + 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 injectBaseDecoratorMock: jest.Mock< + ParameterDecorator & PropertyDecorator + > & + ParameterDecorator & + PropertyDecorator; + + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + 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).mockReturnValueOnce( + injectBaseDecoratorMock, + ); + + result = optional()(targetFixture, propertyKeyFixture); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(updateMetadataOptional); + }); + + 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 errorFixture: Error; + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + errorFixture = new Error('message-error-fixture'); + updateMetadataMock = jest.fn(); + + ( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata as jest.Mock< + typeof buildMaybeClassElementMetadataFromMaybeClassElementMetadata + > + ).mockReturnValueOnce(updateMetadataMock); + + (injectBase as jest.Mock).mockImplementation( + (): never => { + throw errorFixture; + }, + ); + + ( + handleInjectionError as jest.Mock + ).mockImplementation( + ( + _target: object, + _propertyKey: string | symbol | undefined, + _parameterIndex: number | undefined, + error: unknown, + ): never => { + throw error; + }, + ); + + try { + optional()(targetFixture, propertyKeyFixture); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(updateMetadataOptional); + }); + + 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 injectBaseDecoratorMock: jest.Mock< + ParameterDecorator & PropertyDecorator + > & + ParameterDecorator & + PropertyDecorator; + + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + 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).mockReturnValueOnce( + injectBaseDecoratorMock, + ); + + result = optional()(targetFixture, undefined, paramIndexFixture); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(updateMetadataOptional); + }); + + 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 errorFixture: Error; + let updateMetadataMock: jest.Mock< + ( + classElementMetadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata + >; + + let result: unknown; + + beforeAll(() => { + errorFixture = new Error('message-error-fixture'); + updateMetadataMock = jest.fn(); + + ( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata as jest.Mock< + typeof buildMaybeClassElementMetadataFromMaybeClassElementMetadata + > + ).mockReturnValueOnce(updateMetadataMock); + + (injectBase as jest.Mock).mockImplementation( + (): never => { + throw errorFixture; + }, + ); + + ( + handleInjectionError as jest.Mock + ).mockImplementation( + ( + _target: object, + _propertyKey: string | symbol | undefined, + _parameterIndex: number | undefined, + error: unknown, + ): never => { + throw error; + }, + ); + + try { + optional()(targetFixture, undefined, paramIndexFixture); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call buildMaybeClassElementMetadataFromMaybeClassElementMetadata()', () => { + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledTimes(1); + expect( + buildMaybeClassElementMetadataFromMaybeClassElementMetadata, + ).toHaveBeenCalledWith(updateMetadataOptional); + }); + + 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/optional.ts b/packages/container/libraries/core/src/metadata/decorators/optional.ts new file mode 100644 index 00000000..3758b897 --- /dev/null +++ b/packages/container/libraries/core/src/metadata/decorators/optional.ts @@ -0,0 +1,32 @@ +import { updateMetadataOptional } from '../actions/updateMetadataOptional'; +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 { injectBase } from './injectBase'; + +export function optional(): ParameterDecorator & PropertyDecorator { + return ( + target: object, + propertyKey: string | symbol | undefined, + parameterIndex?: number, + ): void => { + const updateMetadata: ( + metadata: MaybeClassElementMetadata | undefined, + ) => ManagedClassElementMetadata | MaybeManagedClassElementMetadata = + buildMaybeClassElementMetadataFromMaybeClassElementMetadata( + updateMetadataOptional, + ); + + 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); + } + }; +}