diff --git a/packages/container/libraries/core/src/error/models/InversifyCoreErrorKind.ts b/packages/container/libraries/core/src/error/models/InversifyCoreErrorKind.ts index 698687e9..98f507e3 100644 --- a/packages/container/libraries/core/src/error/models/InversifyCoreErrorKind.ts +++ b/packages/container/libraries/core/src/error/models/InversifyCoreErrorKind.ts @@ -1,5 +1,6 @@ export enum InversifyCoreErrorKind { injectionDecoratorConflict, missingInjectionDecorator, + planning, unknown, } diff --git a/packages/container/libraries/core/src/planning/actions/addBranchService.spec.ts b/packages/container/libraries/core/src/planning/actions/addBranchService.spec.ts new file mode 100644 index 00000000..ff1d3687 --- /dev/null +++ b/packages/container/libraries/core/src/planning/actions/addBranchService.spec.ts @@ -0,0 +1,94 @@ +import { beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('@inversifyjs/common'); + +import { + ServiceIdentifier, + stringifyServiceIdentifier, +} from '@inversifyjs/common'; + +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { BasePlanParams } from '../models/BasePlanParams'; +import { addBranchService } from './addBranchService'; + +describe(addBranchService.name, () => { + describe('having BasePlanParams with empty servicesBranch', () => { + let basePlanParamsFixture: BasePlanParams; + let serviceIdentifierFixture: ServiceIdentifier; + + beforeAll(() => { + basePlanParamsFixture = { + servicesBranch: new Set(), + } as Partial as BasePlanParams; + serviceIdentifierFixture = 'service-id'; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = addBranchService( + basePlanParamsFixture, + serviceIdentifierFixture, + ); + }); + + it('should add seriveIdentifier to basePlanParams.serviceBranch', () => { + expect(basePlanParamsFixture.servicesBranch).toContain( + serviceIdentifierFixture, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('having BasePlanParams with servicesBranch with serviceIdentifier', () => { + let basePlanParamsFixture: BasePlanParams; + let serviceIdentifierFixture: ServiceIdentifier; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + basePlanParamsFixture = { + servicesBranch: new Set([serviceIdentifierFixture]), + } as Partial as BasePlanParams; + }); + + describe('when called', () => { + let stringifiedServiceIdentifier: string; + + let result: unknown; + + beforeAll(() => { + stringifiedServiceIdentifier = 'stringified-service-id'; + + ( + stringifyServiceIdentifier as jest.Mock< + typeof stringifyServiceIdentifier + > + ) + .mockReturnValueOnce(stringifiedServiceIdentifier) + .mockReturnValueOnce(stringifiedServiceIdentifier); + + try { + addBranchService(basePlanParamsFixture, serviceIdentifierFixture); + } catch (error: unknown) { + result = error; + } + }); + + it('should throw an InversifyCoreError', () => { + const expected: Partial = { + kind: InversifyCoreErrorKind.planning, + message: `Circular dependency found: ${stringifiedServiceIdentifier} -> ${stringifiedServiceIdentifier}`, + }; + + expect(result).toBeInstanceOf(InversifyCoreError); + expect(result).toStrictEqual(expect.objectContaining(expected)); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/planning/actions/addBranchService.ts b/packages/container/libraries/core/src/planning/actions/addBranchService.ts new file mode 100644 index 00000000..0475db32 --- /dev/null +++ b/packages/container/libraries/core/src/planning/actions/addBranchService.ts @@ -0,0 +1,41 @@ +import { + ServiceIdentifier, + stringifyServiceIdentifier, +} from '@inversifyjs/common'; + +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { BasePlanParams } from '../models/BasePlanParams'; + +export function addBranchService( + params: BasePlanParams, + serviceIdentifier: ServiceIdentifier, +): void { + if (params.servicesBranch.has(serviceIdentifier)) { + throwError(params, serviceIdentifier); + } + + params.servicesBranch.add(serviceIdentifier); +} + +function stringifyServiceIdentifierTrace( + serviceIdentifiers: Iterable, +): string { + return [...serviceIdentifiers].map(stringifyServiceIdentifier).join(' -> '); +} + +function throwError( + params: BasePlanParams, + serviceIdentifier: ServiceIdentifier, +): never { + const stringifiedCircularDependencies: string = + stringifyServiceIdentifierTrace([ + ...params.servicesBranch, + serviceIdentifier, + ]); + + throw new InversifyCoreError( + InversifyCoreErrorKind.planning, + `Circular dependency found: ${stringifiedCircularDependencies}`, + ); +} diff --git a/packages/container/libraries/core/src/planning/calculations/checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.spec.ts b/packages/container/libraries/core/src/planning/calculations/checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.spec.ts new file mode 100644 index 00000000..b9a4193e --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.spec.ts @@ -0,0 +1,198 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('./isPlanServiceRedirectionBindingNode'); +jest.mock('./throwErrorWhenUnexpectedBindingsAmountFound'); + +import { bindingScopeValues } from '../../binding/models/BindingScope'; +import { bindingTypeValues } from '../../binding/models/BindingType'; +import { ServiceRedirectionBinding } from '../../binding/models/ServiceRedirectionBinding'; +import { BindingNodeParent } from '../models/BindingNodeParent'; +import { PlanServiceRedirectionBindingNode } from '../models/PlanServiceRedirectionBindingNode'; +import { checkPlanServiceRedirectionBindingNodeSingleInjectionBindings } from './checkPlanServiceRedirectionBindingNodeSingleInjectionBindings'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; +import { throwErrorWhenUnexpectedBindingsAmountFound } from './throwErrorWhenUnexpectedBindingsAmountFound'; + +describe( + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.name, + () => { + describe('having a PlanServiceRedirectionBindingNode with no redirections', () => { + let planServiceRedirectionBindingNodeFixture: PlanServiceRedirectionBindingNode; + let isOptionalFixture: boolean; + + beforeAll(() => { + planServiceRedirectionBindingNodeFixture = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + binding: Symbol() as unknown as ServiceRedirectionBinding, + parent: Symbol() as unknown as BindingNodeParent, + redirections: [], + }; + isOptionalFixture = false; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings( + planServiceRedirectionBindingNodeFixture, + isOptionalFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call throwErrorWhenUnexpectedBindingsAmountFound()', () => { + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).toHaveBeenCalledTimes(1); + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).toHaveBeenCalledWith( + planServiceRedirectionBindingNodeFixture.redirections, + isOptionalFixture, + planServiceRedirectionBindingNodeFixture, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('having a PlanServiceRedirectionBindingNode with a single redirection to a leaf node', () => { + let planServiceRedirectionBindingNodeFixture: PlanServiceRedirectionBindingNode; + let isOptionalFixture: boolean; + + beforeAll(() => { + planServiceRedirectionBindingNodeFixture = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + binding: Symbol() as unknown as ServiceRedirectionBinding, + parent: Symbol() as unknown as BindingNodeParent, + redirections: [ + { + binding: { + cache: { + isRight: true, + value: Symbol(), + }, + id: 1, + isSatisfiedBy: () => true, + moduleId: undefined, + onActivation: { + isRight: false, + value: undefined, + }, + onDeactivation: { + isRight: false, + value: undefined, + }, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'service-id', + type: bindingTypeValues.ConstantValue, + }, + parent: Symbol() as unknown as BindingNodeParent, + }, + ], + }; + isOptionalFixture = false; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns false', () => { + let result: unknown; + + beforeAll(() => { + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(false); + + result = + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings( + planServiceRedirectionBindingNodeFixture, + isOptionalFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should not call throwErrorWhenUnexpectedBindingsAmountFound()', () => { + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).not.toHaveBeenCalled(); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('having a PlanServiceRedirectionBindingNode with a single redirection to a PlanServiceRedirectionBindingNode with no redirections', () => { + let planServiceRedirectionBindingNodeRedirectionFixture: PlanServiceRedirectionBindingNode; + let planServiceRedirectionBindingNodeFixture: PlanServiceRedirectionBindingNode; + let isOptionalFixture: boolean; + + beforeAll(() => { + planServiceRedirectionBindingNodeRedirectionFixture = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + binding: Symbol() as unknown as ServiceRedirectionBinding, + parent: Symbol() as unknown as BindingNodeParent, + redirections: [], + }; + planServiceRedirectionBindingNodeFixture = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + binding: Symbol() as unknown as ServiceRedirectionBinding, + parent: Symbol() as unknown as BindingNodeParent, + redirections: [planServiceRedirectionBindingNodeRedirectionFixture], + }; + isOptionalFixture = false; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns true', () => { + let result: unknown; + + beforeAll(() => { + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(true); + + result = + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings( + planServiceRedirectionBindingNodeFixture, + isOptionalFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call throwErrorWhenUnexpectedBindingsAmountFound()', () => { + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).toHaveBeenCalledTimes(1); + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).toHaveBeenCalledWith( + planServiceRedirectionBindingNodeRedirectionFixture.redirections, + isOptionalFixture, + planServiceRedirectionBindingNodeRedirectionFixture, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + }, +); diff --git a/packages/container/libraries/core/src/planning/calculations/checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.ts b/packages/container/libraries/core/src/planning/calculations/checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.ts new file mode 100644 index 00000000..863b1d2a --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/checkPlanServiceRedirectionBindingNodeSingleInjectionBindings.ts @@ -0,0 +1,34 @@ +import { PlanBindingNode } from '../models/PlanBindingNode'; +import { PlanServiceRedirectionBindingNode } from '../models/PlanServiceRedirectionBindingNode'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; +import { throwErrorWhenUnexpectedBindingsAmountFound } from './throwErrorWhenUnexpectedBindingsAmountFound'; + +const SINGLE_INJECTION_BINDINGS: number = 1; + +export function checkPlanServiceRedirectionBindingNodeSingleInjectionBindings( + serviceRedirectionBindingNode: PlanServiceRedirectionBindingNode, + isOptional: boolean, +): void { + if ( + serviceRedirectionBindingNode.redirections.length === + SINGLE_INJECTION_BINDINGS + ) { + const [planBindingNode]: [PlanBindingNode] = + serviceRedirectionBindingNode.redirections as [PlanBindingNode]; + + if (isPlanServiceRedirectionBindingNode(planBindingNode)) { + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings( + planBindingNode, + isOptional, + ); + } + + return; + } + + throwErrorWhenUnexpectedBindingsAmountFound( + serviceRedirectionBindingNode.redirections, + isOptional, + serviceRedirectionBindingNode, + ); +} diff --git a/packages/container/libraries/core/src/planning/calculations/checkServiceNodeSingleInjectionBindings.spec.ts b/packages/container/libraries/core/src/planning/calculations/checkServiceNodeSingleInjectionBindings.spec.ts new file mode 100644 index 00000000..6137c62b --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/checkServiceNodeSingleInjectionBindings.spec.ts @@ -0,0 +1,148 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('./checkPlanServiceRedirectionBindingNodeSingleInjectionBindings'); +jest.mock('./isPlanServiceRedirectionBindingNode'); +jest.mock('./throwErrorWhenUnexpectedBindingsAmountFound'); + +import { PlanBindingNode } from '../models/PlanBindingNode'; +import { PlanServiceNode } from '../models/PlanServiceNode'; +import { PlanServiceNodeParent } from '../models/PlanServiceNodeParent'; +import { checkPlanServiceRedirectionBindingNodeSingleInjectionBindings } from './checkPlanServiceRedirectionBindingNodeSingleInjectionBindings'; +import { checkServiceNodeSingleInjectionBindings } from './checkServiceNodeSingleInjectionBindings'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; +import { throwErrorWhenUnexpectedBindingsAmountFound } from './throwErrorWhenUnexpectedBindingsAmountFound'; + +describe(checkServiceNodeSingleInjectionBindings.name, () => { + describe('having a PlanServiceNode with no bindings', () => { + let nodeFixture: PlanServiceNode; + let isOptionalFixture: boolean; + + beforeAll(() => { + nodeFixture = { + bindings: [], + parent: Symbol() as unknown as PlanServiceNodeParent, + serviceIdentifier: 'service-id', + }; + isOptionalFixture = false; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = checkServiceNodeSingleInjectionBindings( + nodeFixture, + isOptionalFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call throwErrorWhenUnexpectedBindingsAmountFound()', () => { + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).toHaveBeenCalledTimes(1); + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).toHaveBeenCalledWith( + nodeFixture.bindings, + isOptionalFixture, + nodeFixture, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('having a PlanServiceNode with single binding', () => { + let nodeFixtureBinding: PlanBindingNode; + let nodeFixture: PlanServiceNode; + let isOptionalFixture: boolean; + + beforeAll(() => { + nodeFixtureBinding = Symbol() as unknown as PlanBindingNode; + nodeFixture = { + bindings: [nodeFixtureBinding], + parent: Symbol() as unknown as PlanServiceNodeParent, + serviceIdentifier: 'service-id', + }; + isOptionalFixture = false; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns false', () => { + let result: unknown; + + beforeAll(() => { + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(false); + + result = checkServiceNodeSingleInjectionBindings( + nodeFixture, + isOptionalFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should not call throwErrorWhenUnexpectedBindingsAmountFound()', () => { + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).not.toHaveBeenCalled(); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns true', () => { + let result: unknown; + + beforeAll(() => { + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(true); + + result = checkServiceNodeSingleInjectionBindings( + nodeFixture, + isOptionalFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call checkPlanServiceRedirectionBindingNodeSingleInjectionBindings()', () => { + expect( + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings, + ).toHaveBeenCalledTimes(1); + expect( + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings, + ).toHaveBeenCalledWith(nodeFixtureBinding, isOptionalFixture); + }); + + it('should not call throwErrorWhenUnexpectedBindingsAmountFound()', () => { + expect( + throwErrorWhenUnexpectedBindingsAmountFound, + ).not.toHaveBeenCalled(); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/planning/calculations/checkServiceNodeSingleInjectionBindings.ts b/packages/container/libraries/core/src/planning/calculations/checkServiceNodeSingleInjectionBindings.ts new file mode 100644 index 00000000..b9bb63d2 --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/checkServiceNodeSingleInjectionBindings.ts @@ -0,0 +1,33 @@ +import { PlanBindingNode } from '../models/PlanBindingNode'; +import { PlanServiceNode } from '../models/PlanServiceNode'; +import { checkPlanServiceRedirectionBindingNodeSingleInjectionBindings } from './checkPlanServiceRedirectionBindingNodeSingleInjectionBindings'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; +import { throwErrorWhenUnexpectedBindingsAmountFound } from './throwErrorWhenUnexpectedBindingsAmountFound'; + +const SINGLE_INJECTION_BINDINGS: number = 1; + +export function checkServiceNodeSingleInjectionBindings( + serviceNode: PlanServiceNode, + isOptional: boolean, +): void { + if (serviceNode.bindings.length === SINGLE_INJECTION_BINDINGS) { + const [planBindingNode]: [PlanBindingNode] = serviceNode.bindings as [ + PlanBindingNode, + ]; + + if (isPlanServiceRedirectionBindingNode(planBindingNode)) { + checkPlanServiceRedirectionBindingNodeSingleInjectionBindings( + planBindingNode, + isOptional, + ); + } + + return; + } + + throwErrorWhenUnexpectedBindingsAmountFound( + serviceNode.bindings, + isOptional, + serviceNode, + ); +} diff --git a/packages/container/libraries/core/src/planning/calculations/isPlanServiceRedirectionBindingNode.spec.ts b/packages/container/libraries/core/src/planning/calculations/isPlanServiceRedirectionBindingNode.spec.ts new file mode 100644 index 00000000..f4040640 --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/isPlanServiceRedirectionBindingNode.spec.ts @@ -0,0 +1,60 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import { ServiceRedirectionBinding } from '../../binding/models/ServiceRedirectionBinding'; +import { BindingNodeParent } from '../models/BindingNodeParent'; +import { PlanServiceNode } from '../models/PlanServiceNode'; +import { PlanServiceRedirectionBindingNode } from '../models/PlanServiceRedirectionBindingNode'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; + +describe(isPlanServiceRedirectionBindingNode.name, () => { + describe('having a PlanServiceRedirectionBindingNode', () => { + let planServiceRedirectionBindingNodeFixture: PlanServiceRedirectionBindingNode; + + beforeAll(() => { + planServiceRedirectionBindingNodeFixture = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + binding: Symbol() as unknown as ServiceRedirectionBinding, + parent: Symbol() as unknown as BindingNodeParent, + redirections: [], + }; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = isPlanServiceRedirectionBindingNode( + planServiceRedirectionBindingNodeFixture, + ); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + }); + + describe('having a PlanServiceNode', () => { + let planServiceNodeFixture: PlanServiceNode; + + beforeAll(() => { + planServiceNodeFixture = { + bindings: [], + parent: undefined, + serviceIdentifier: 'service-id', + }; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = isPlanServiceRedirectionBindingNode(planServiceNodeFixture); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/planning/calculations/isPlanServiceRedirectionBindingNode.ts b/packages/container/libraries/core/src/planning/calculations/isPlanServiceRedirectionBindingNode.ts new file mode 100644 index 00000000..a2d3bcee --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/isPlanServiceRedirectionBindingNode.ts @@ -0,0 +1,12 @@ +import { BindingNodeParent } from '../models/BindingNodeParent'; +import { PlanBindingNode } from '../models/PlanBindingNode'; +import { PlanServiceRedirectionBindingNode } from '../models/PlanServiceRedirectionBindingNode'; + +export function isPlanServiceRedirectionBindingNode( + node: PlanBindingNode | BindingNodeParent, +): node is PlanServiceRedirectionBindingNode { + return ( + (node as Partial).redirections !== + undefined + ); +} diff --git a/packages/container/libraries/core/src/planning/calculations/throwErrorWhenUnexpectedBindingsAmountFound.spec.ts b/packages/container/libraries/core/src/planning/calculations/throwErrorWhenUnexpectedBindingsAmountFound.spec.ts new file mode 100644 index 00000000..1d2d193c --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/throwErrorWhenUnexpectedBindingsAmountFound.spec.ts @@ -0,0 +1,360 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('@inversifyjs/common'); + +import { stringifyServiceIdentifier } from '@inversifyjs/common'; + +jest.mock('../../binding/calculations/stringifyBinding'); +jest.mock('./isPlanServiceRedirectionBindingNode'); + +import { stringifyBinding } from '../../binding/calculations/stringifyBinding'; +import { bindingScopeValues } from '../../binding/models/BindingScope'; +import { bindingTypeValues } from '../../binding/models/BindingType'; +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { PlanBindingNode } from '../models/PlanBindingNode'; +import { PlanServiceNode } from '../models/PlanServiceNode'; +import { PlanServiceRedirectionBindingNode } from '../models/PlanServiceRedirectionBindingNode'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; +import { throwErrorWhenUnexpectedBindingsAmountFound } from './throwErrorWhenUnexpectedBindingsAmountFound'; + +describe(throwErrorWhenUnexpectedBindingsAmountFound.name, () => { + describe('having bindings empty and isOptional false and node PlanServiceRedirectionBindingNode', () => { + let bindingsFixture: []; + let isOptionalFixture: false; + let nodeFixture: PlanServiceRedirectionBindingNode; + + beforeAll(() => { + bindingsFixture = []; + isOptionalFixture = false; + nodeFixture = { + binding: { + id: 1, + isSatisfiedBy: () => true, + moduleId: undefined, + serviceIdentifier: 'service-id', + targetServiceIdentifier: 'target-service-id', + type: bindingTypeValues.ServiceRedirection, + }, + parent: { + bindings: [], + parent: undefined, + serviceIdentifier: 'service-id', + }, + redirections: [], + }; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns true', () => { + let stringifiedServiceIdentifier: string; + let stringifiedTargetServiceIdentifier: string; + + let result: unknown; + + beforeAll(() => { + stringifiedServiceIdentifier = 'stringified-service-id'; + stringifiedTargetServiceIdentifier = 'stringified-target-service-id'; + + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(true); + + ( + stringifyServiceIdentifier as jest.Mock< + typeof stringifyServiceIdentifier + > + ) + .mockReturnValueOnce(stringifiedServiceIdentifier) + .mockReturnValueOnce(stringifiedTargetServiceIdentifier); + + try { + result = throwErrorWhenUnexpectedBindingsAmountFound( + bindingsFixture, + isOptionalFixture, + nodeFixture, + ); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should throw InversifyCoreError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyCoreErrorKind.planning, + message: `No bindings found for service: "${stringifiedTargetServiceIdentifier}". + +Trying to resolve bindings for "${stringifiedServiceIdentifier}".`, + }; + + expect(result).toBeInstanceOf(InversifyCoreError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); + + describe('having bindings empty and isOptional false and node PlanServiceNode', () => { + let bindingsFixture: []; + let isOptionalFixture: false; + let nodeFixture: PlanServiceNode; + + beforeAll(() => { + bindingsFixture = []; + isOptionalFixture = false; + nodeFixture = { + bindings: [], + parent: undefined, + serviceIdentifier: 'service-identifier', + }; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns true', () => { + let stringifiedServiceIdentifier: string; + + let result: unknown; + + beforeAll(() => { + stringifiedServiceIdentifier = 'stringified-service-id'; + + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(false); + + ( + stringifyServiceIdentifier as jest.Mock< + typeof stringifyServiceIdentifier + > + ) + .mockReturnValueOnce(stringifiedServiceIdentifier) + .mockReturnValueOnce(stringifiedServiceIdentifier); + + try { + throwErrorWhenUnexpectedBindingsAmountFound( + bindingsFixture, + isOptionalFixture, + nodeFixture, + ); + } catch (error) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should throw InversifyCoreError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyCoreErrorKind.planning, + message: `No bindings found for service: "${stringifiedServiceIdentifier}". + +Trying to resolve bindings for "${stringifiedServiceIdentifier} (Root service)".`, + }; + + expect(result).toBeInstanceOf(InversifyCoreError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); + + describe('having bindings empty and isOptional true and node PlanServiceNode', () => { + let bindingsFixture: []; + let isOptionalFixture: true; + let nodeFixture: PlanServiceNode; + + beforeAll(() => { + bindingsFixture = []; + isOptionalFixture = true; + nodeFixture = { + bindings: [], + parent: undefined, + serviceIdentifier: 'service-identifier', + }; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns true', () => { + let result: unknown; + + beforeAll(() => { + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(false); + + result = throwErrorWhenUnexpectedBindingsAmountFound( + bindingsFixture, + isOptionalFixture, + nodeFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('having multiple bindings and node PlanServiceRedirectionBindingNode', () => { + let bindingsFixture: PlanBindingNode[]; + let isOptionalFixture: boolean; + let nodeFixture: PlanServiceRedirectionBindingNode; + + beforeAll(() => { + const parentNode: PlanServiceNode = { + bindings: [], + parent: undefined, + serviceIdentifier: 'target-service-id', + }; + + bindingsFixture = [ + { + binding: { + cache: { + isRight: true, + value: Symbol(), + }, + id: 0, + isSatisfiedBy: () => true, + moduleId: undefined, + onActivation: { + isRight: false, + value: undefined, + }, + onDeactivation: { + isRight: false, + value: undefined, + }, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'target-service-id', + type: bindingTypeValues.ConstantValue, + }, + parent: parentNode, + }, + { + binding: { + cache: { + isRight: true, + value: Symbol(), + }, + id: 0, + isSatisfiedBy: () => true, + moduleId: undefined, + onActivation: { + isRight: false, + value: undefined, + }, + onDeactivation: { + isRight: false, + value: undefined, + }, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'target-service-id', + type: bindingTypeValues.ConstantValue, + }, + parent: parentNode, + }, + ]; + isOptionalFixture = false; + nodeFixture = { + binding: { + id: 1, + isSatisfiedBy: () => true, + moduleId: undefined, + serviceIdentifier: 'service-id', + targetServiceIdentifier: 'target-service-id', + type: bindingTypeValues.ServiceRedirection, + }, + parent: { + bindings: [], + parent: undefined, + serviceIdentifier: 'service-id', + }, + redirections: [], + }; + }); + + describe('when called, and isPlanServiceRedirectionBindingNode() returns true', () => { + let stringifiedTargetServiceIdentifierFixture: string; + let stringifiedServiceIdentifierFixture: string; + + let stringifiedBindingFixture: string; + + let result: unknown; + + beforeAll(() => { + stringifiedTargetServiceIdentifierFixture = + 'stringified-target-service-id'; + stringifiedServiceIdentifierFixture = 'stringified-service-id'; + + stringifiedBindingFixture = 'stringified-binding'; + + ( + isPlanServiceRedirectionBindingNode as unknown as jest.Mock< + typeof isPlanServiceRedirectionBindingNode + > + ).mockReturnValueOnce(true); + + ( + stringifyServiceIdentifier as jest.Mock< + typeof stringifyServiceIdentifier + > + ) + .mockReturnValueOnce(stringifiedServiceIdentifierFixture) + .mockReturnValueOnce(stringifiedTargetServiceIdentifierFixture); + + (stringifyBinding as jest.Mock) + .mockReturnValueOnce(stringifiedBindingFixture) + .mockReturnValueOnce(stringifiedBindingFixture); + + try { + result = throwErrorWhenUnexpectedBindingsAmountFound( + bindingsFixture, + isOptionalFixture, + nodeFixture, + ); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should throw InversifyCoreError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyCoreErrorKind.planning, + message: `Ambiguous bindings found for service: "${stringifiedTargetServiceIdentifierFixture}". + +Registered bindings: + +${stringifiedBindingFixture} +${stringifiedBindingFixture} + +Trying to resolve bindings for "${stringifiedServiceIdentifierFixture}".`, + }; + + expect(result).toBeInstanceOf(InversifyCoreError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/planning/calculations/throwErrorWhenUnexpectedBindingsAmountFound.ts b/packages/container/libraries/core/src/planning/calculations/throwErrorWhenUnexpectedBindingsAmountFound.ts new file mode 100644 index 00000000..c0f2afba --- /dev/null +++ b/packages/container/libraries/core/src/planning/calculations/throwErrorWhenUnexpectedBindingsAmountFound.ts @@ -0,0 +1,61 @@ +import { + ServiceIdentifier, + stringifyServiceIdentifier, +} from '@inversifyjs/common'; + +import { stringifyBinding } from '../../binding/calculations/stringifyBinding'; +import { InversifyCoreError } from '../../error/models/InversifyCoreError'; +import { InversifyCoreErrorKind } from '../../error/models/InversifyCoreErrorKind'; +import { BindingNodeParent } from '../models/BindingNodeParent'; +import { PlanBindingNode } from '../models/PlanBindingNode'; +import { isPlanServiceRedirectionBindingNode } from './isPlanServiceRedirectionBindingNode'; + +export function throwErrorWhenUnexpectedBindingsAmountFound( + bindings: PlanBindingNode[], + isOptional: boolean, + node: BindingNodeParent, +): void { + let serviceIdentifier: ServiceIdentifier; + let parentServiceIdentifier: ServiceIdentifier | undefined; + + if (isPlanServiceRedirectionBindingNode(node)) { + serviceIdentifier = node.binding.targetServiceIdentifier; + parentServiceIdentifier = node.binding.serviceIdentifier; + } else { + serviceIdentifier = node.serviceIdentifier; + parentServiceIdentifier = node.parent?.binding.serviceIdentifier; + } + + if (bindings.length === 0) { + if (!isOptional) { + const stringifiedParentServiceIdentifier: string = + parentServiceIdentifier === undefined + ? `${stringifyServiceIdentifier(serviceIdentifier)} (Root service)` + : stringifyServiceIdentifier(parentServiceIdentifier); + + const errorMessage: string = `No bindings found for service: "${stringifyServiceIdentifier(serviceIdentifier)}". + +Trying to resolve bindings for "${stringifiedParentServiceIdentifier}".`; + + throw new InversifyCoreError( + InversifyCoreErrorKind.planning, + errorMessage, + ); + } + } else { + const stringifiedParentServiceIdentifier: string = + parentServiceIdentifier === undefined + ? `${stringifyServiceIdentifier(serviceIdentifier)} (Root service)` + : stringifyServiceIdentifier(parentServiceIdentifier); + + const errorMessage: string = `Ambiguous bindings found for service: "${stringifyServiceIdentifier(serviceIdentifier)}". + +Registered bindings: + +${bindings.map((binding: PlanBindingNode): string => stringifyBinding(binding.binding)).join('\n')} + +Trying to resolve bindings for "${stringifiedParentServiceIdentifier}".`; + + throw new InversifyCoreError(InversifyCoreErrorKind.planning, errorMessage); + } +}