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);
+    }
+  };
+}