diff --git a/.changeset/green-beans-own.md b/.changeset/green-beans-own.md new file mode 100644 index 0000000..4bfd246 --- /dev/null +++ b/.changeset/green-beans-own.md @@ -0,0 +1,5 @@ +--- +"@inversifyjs/container": minor +--- + +Added `Container`. diff --git a/.changeset/itchy-coins-agree.md b/.changeset/itchy-coins-agree.md new file mode 100644 index 0000000..d2093de --- /dev/null +++ b/.changeset/itchy-coins-agree.md @@ -0,0 +1,5 @@ +--- +"@inversifyjs/container": minor +--- + +Added `InversifyContainerError`. diff --git a/.changeset/rare-ghosts-rule.md b/.changeset/rare-ghosts-rule.md new file mode 100644 index 0000000..ea32c00 --- /dev/null +++ b/.changeset/rare-ghosts-rule.md @@ -0,0 +1,5 @@ +--- +"@inversifyjs/container": minor +--- + +Added `ContainerModule`. diff --git a/.changeset/witty-worms-wash.md b/.changeset/witty-worms-wash.md new file mode 100644 index 0000000..0fb0d5b --- /dev/null +++ b/.changeset/witty-worms-wash.md @@ -0,0 +1,5 @@ +--- +"@inversifyjs/container": minor +--- + +Added `BindToFluentSyntax`. diff --git a/packages/container/libraries/container/package.json b/packages/container/libraries/container/package.json index 76078f5..f680eed 100644 --- a/packages/container/libraries/container/package.json +++ b/packages/container/libraries/container/package.json @@ -29,6 +29,9 @@ "tslib": "2.8.1", "typescript": "5.7.2" }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + }, "devEngines": { "node": "^20.18.0", "pnpm": "^9.12.1" @@ -78,5 +81,5 @@ "test:unit:js": "pnpm run test:js --selectProjects Unit" }, "sideEffects": false, - "version": "1.4.0" + "version": "1.0.0" } diff --git a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntax.ts b/packages/container/libraries/container/src/binding/models/BindingFluentSyntax.ts similarity index 100% rename from packages/container/libraries/container/src/container/binding/models/BindingFluentSyntax.ts rename to packages/container/libraries/container/src/binding/models/BindingFluentSyntax.ts diff --git a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.spec.ts b/packages/container/libraries/container/src/binding/models/BindingFluentSyntaxImplementation.spec.ts similarity index 99% rename from packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.spec.ts rename to packages/container/libraries/container/src/binding/models/BindingFluentSyntaxImplementation.spec.ts index 01aa772..2a7cf5d 100644 --- a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.spec.ts +++ b/packages/container/libraries/container/src/binding/models/BindingFluentSyntaxImplementation.spec.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; -jest.mock('../../../binding/actions/getBindingId'); +jest.mock('../actions/getBindingId'); import { ServiceIdentifier } from '@inversifyjs/common'; import { @@ -22,8 +22,8 @@ import { ServiceRedirectionBinding, } from '@inversifyjs/core'; -import { getBindingId } from '../../../binding/actions/getBindingId'; -import { Writable } from '../../../common/models/Writable'; +import { Writable } from '../../common/models/Writable'; +import { getBindingId } from '../actions/getBindingId'; import { BindInFluentSyntaxImplementation, BindInWhenOnFluentSyntaxImplementation, diff --git a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.ts b/packages/container/libraries/container/src/binding/models/BindingFluentSyntaxImplementation.ts similarity index 97% rename from packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.ts rename to packages/container/libraries/container/src/binding/models/BindingFluentSyntaxImplementation.ts index c720d26..9b16240 100644 --- a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.ts +++ b/packages/container/libraries/container/src/binding/models/BindingFluentSyntaxImplementation.ts @@ -22,9 +22,9 @@ import { ServiceRedirectionBinding, } from '@inversifyjs/core'; -import { getBindingId } from '../../../binding/actions/getBindingId'; -import { Writable } from '../../../common/models/Writable'; -import { BindingConstraintUtils } from '../utils/BindingConstraintUtils'; +import { Writable } from '../../common/models/Writable'; +import { BindingConstraintUtils } from '../../container/binding/utils/BindingConstraintUtils'; +import { getBindingId } from '../actions/getBindingId'; import { BindInFluentSyntax, BindInWhenOnFluentSyntax, diff --git a/packages/container/libraries/container/src/container/actions/getContainerModuleId.spec.ts b/packages/container/libraries/container/src/container/actions/getContainerModuleId.spec.ts new file mode 100644 index 0000000..38aae10 --- /dev/null +++ b/packages/container/libraries/container/src/container/actions/getContainerModuleId.spec.ts @@ -0,0 +1,88 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('@inversifyjs/reflect-metadata-utils'); + +import { + getReflectMetadata, + setReflectMetadata, + updateReflectMetadata, +} from '@inversifyjs/reflect-metadata-utils'; + +import { getContainerModuleId } from './getContainerModuleId'; + +describe(getContainerModuleId.name, () => { + describe('when called, and getReflectMetadata() returns undefined', () => { + let result: unknown; + + beforeAll(() => { + ( + getReflectMetadata as jest.Mock + ).mockReturnValueOnce(0); + + result = getContainerModuleId(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call getReflectMetadata()', () => { + expect(getReflectMetadata).toHaveBeenCalledTimes(1); + expect(getReflectMetadata).toHaveBeenCalledWith( + Object, + '@inversifyjs/container/bindingId', + ); + }); + + it('should call updateReflectMetadata()', () => { + expect(updateReflectMetadata).toHaveBeenCalledTimes(1); + expect(updateReflectMetadata).toHaveBeenCalledWith( + Object, + '@inversifyjs/container/bindingId', + expect.any(Function), + expect.any(Function), + ); + }); + + it('should return default id', () => { + expect(result).toBe(0); + }); + }); + + describe('when called, and getReflectMetadata() returns Number.MAX_SAFE_INTEGER', () => { + let result: unknown; + + beforeAll(() => { + ( + getReflectMetadata as jest.Mock + ).mockReturnValueOnce(Number.MAX_SAFE_INTEGER); + + result = getContainerModuleId(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call getReflectMetadata()', () => { + expect(getReflectMetadata).toHaveBeenCalledTimes(1); + expect(getReflectMetadata).toHaveBeenCalledWith( + Object, + '@inversifyjs/container/bindingId', + ); + }); + + it('should call setReflectMetadata()', () => { + expect(setReflectMetadata).toHaveBeenCalledTimes(1); + expect(setReflectMetadata).toHaveBeenCalledWith( + Object, + '@inversifyjs/container/bindingId', + Number.MIN_SAFE_INTEGER, + ); + }); + + it('should return expected result', () => { + expect(result).toBe(Number.MAX_SAFE_INTEGER); + }); + }); +}); diff --git a/packages/container/libraries/container/src/container/actions/getContainerModuleId.ts b/packages/container/libraries/container/src/container/actions/getContainerModuleId.ts new file mode 100644 index 0000000..00b14ae --- /dev/null +++ b/packages/container/libraries/container/src/container/actions/getContainerModuleId.ts @@ -0,0 +1,25 @@ +import { + getReflectMetadata, + setReflectMetadata, + updateReflectMetadata, +} from '@inversifyjs/reflect-metadata-utils'; + +const ID_METADATA: string = '@inversifyjs/container/bindingId'; + +export function getContainerModuleId(): number { + const bindingId: number = + getReflectMetadata(Object, ID_METADATA) ?? 0; + + if (bindingId === Number.MAX_SAFE_INTEGER) { + setReflectMetadata(Object, ID_METADATA, Number.MIN_SAFE_INTEGER); + } else { + updateReflectMetadata( + Object, + ID_METADATA, + () => bindingId, + (id: number) => id + 1, + ); + } + + return bindingId; +} diff --git a/packages/container/libraries/container/src/container/models/ContainerModule.spec.ts b/packages/container/libraries/container/src/container/models/ContainerModule.spec.ts new file mode 100644 index 0000000..70e06db --- /dev/null +++ b/packages/container/libraries/container/src/container/models/ContainerModule.spec.ts @@ -0,0 +1,50 @@ +import { beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('../actions/getContainerModuleId'); + +import { getContainerModuleId } from '../actions/getContainerModuleId'; +import { ContainerModule, ContainerModuleLoadOptions } from './ContainerModule'; + +describe(ContainerModule.name, () => { + let containerModuleIdfixture: number; + let loadMock: jest.Mock< + (options: ContainerModuleLoadOptions) => Promise + >; + + beforeAll(() => { + containerModuleIdfixture = 1; + loadMock = jest.fn(); + + ( + getContainerModuleId as jest.Mock + ).mockReturnValue(containerModuleIdfixture); + }); + + describe('.id', () => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = new ContainerModule(loadMock).id; + }); + + it('should return expected value', () => { + expect(result).toBe(containerModuleIdfixture); + }); + }); + }); + + describe('.load', () => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = new ContainerModule(loadMock).load; + }); + + it('should return expected value', () => { + expect(result).toBe(loadMock); + }); + }); + }); +}); diff --git a/packages/container/libraries/container/src/container/models/ContainerModule.ts b/packages/container/libraries/container/src/container/models/ContainerModule.ts new file mode 100644 index 0000000..efa32e4 --- /dev/null +++ b/packages/container/libraries/container/src/container/models/ContainerModule.ts @@ -0,0 +1,41 @@ +import { ServiceIdentifier } from '@inversifyjs/common'; +import { BindingActivation, BindingDeactivation } from '@inversifyjs/core'; + +import { BindToFluentSyntax } from '../../binding/models/BindingFluentSyntax'; +import { getContainerModuleId } from '../actions/getContainerModuleId'; +import { IsBoundOptions } from './isBoundOptions'; + +export interface ContainerModuleLoadOptions { + bind(serviceIdentifier: ServiceIdentifier): BindToFluentSyntax; + isBound( + serviceIdentifier: ServiceIdentifier, + options?: IsBoundOptions, + ): boolean; + onActivation( + serviceIdentifier: ServiceIdentifier, + activation: BindingActivation, + ): void; + onDeactivation( + serviceIdentifier: ServiceIdentifier, + deactivation: BindingDeactivation, + ): void; + unbind(serviceIdentifier: ServiceIdentifier): Promise; +} + +export class ContainerModule { + readonly #id: number; + readonly #load: (options: ContainerModuleLoadOptions) => Promise; + + constructor(load: (options: ContainerModuleLoadOptions) => Promise) { + this.#id = getContainerModuleId(); + this.#load = load; + } + + public get id(): number { + return this.#id; + } + + public get load(): (options: ContainerModuleLoadOptions) => Promise { + return this.#load; + } +} diff --git a/packages/container/libraries/container/src/container/models/isBoundOptions.ts b/packages/container/libraries/container/src/container/models/isBoundOptions.ts new file mode 100644 index 0000000..3c6cf11 --- /dev/null +++ b/packages/container/libraries/container/src/container/models/isBoundOptions.ts @@ -0,0 +1,6 @@ +import { GetOptionsTagConstraint, MetadataName } from '@inversifyjs/core'; + +export interface IsBoundOptions { + name?: MetadataName; + tag?: GetOptionsTagConstraint; +} diff --git a/packages/container/libraries/container/src/container/services/Container.spec.ts b/packages/container/libraries/container/src/container/services/Container.spec.ts new file mode 100644 index 0000000..0478849 --- /dev/null +++ b/packages/container/libraries/container/src/container/services/Container.spec.ts @@ -0,0 +1,1020 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('@inversifyjs/core'); + +import { Newable, ServiceIdentifier } from '@inversifyjs/common'; +import { + ActivationsService, + Binding, + BindingActivation, + BindingActivationRelation, + BindingDeactivation, + BindingDeactivationRelation, + BindingMetadata, + BindingScope, + bindingScopeValues, + BindingService, + bindingTypeValues, + ClassMetadata, + DeactivationParams, + DeactivationsService, + getClassMetadata, + GetOptions, + GetOptionsTagConstraint, + plan, + PlanParams, + PlanResult, + ResolutionContext, + ResolutionParams, + resolve, + resolveServiceDeactivations, +} from '@inversifyjs/core'; + +import { BindToFluentSyntax } from '../../binding/models/BindingFluentSyntax'; +import { BindToFluentSyntaxImplementation } from '../../binding/models/BindingFluentSyntaxImplementation'; +import { InversifyContainerError } from '../../error/models/InversifyContainerError'; +import { InversifyContainerErrorKind } from '../../error/models/InversifyContainerErrorKind'; +import { + ContainerModule, + ContainerModuleLoadOptions, +} from '../models/ContainerModule'; +import { IsBoundOptions } from '../models/isBoundOptions'; +import { Container } from './Container'; + +describe(Container.name, () => { + let activationServiceMock: jest.Mocked; + let bindingServiceMock: jest.Mocked; + let deactivationServiceMock: jest.Mocked; + + beforeAll(() => { + activationServiceMock = { + add: jest.fn(), + removeAllByServiceId: jest.fn(), + } as Partial< + jest.Mocked + > as jest.Mocked; + bindingServiceMock = { + get: jest.fn(), + remove: jest.fn(), + set: jest.fn(), + } as Partial> as jest.Mocked; + deactivationServiceMock = { + add: jest.fn(), + removeAllByServiceId: jest.fn(), + } as Partial< + jest.Mocked + > as jest.Mocked; + + (ActivationsService as jest.Mock<() => ActivationsService>).mockReturnValue( + activationServiceMock, + ); + (BindingService as jest.Mock<() => BindingService>).mockReturnValue( + bindingServiceMock, + ); + ( + DeactivationsService as jest.Mock<() => DeactivationsService> + ).mockReturnValue(deactivationServiceMock); + }); + + describe('.bind', () => { + describe('when called', () => { + let bindingScopeFixture: BindingScope; + let serviceIdentifierFixture: ServiceIdentifier; + + let result: unknown; + + beforeAll(() => { + bindingScopeFixture = bindingScopeValues.Singleton; + serviceIdentifierFixture = 'service-id'; + + result = new Container().bind(serviceIdentifierFixture); + }); + + it('should return BindToFluentSyntax', () => { + const expected: BindToFluentSyntax = + new BindToFluentSyntaxImplementation( + expect.any(Function) as unknown as ( + binding: Binding, + ) => void, + undefined, + bindingScopeFixture, + serviceIdentifierFixture, + ); + + expect(result).toStrictEqual(expected); + }); + }); + }); + + describe('.get', () => { + describe('when called', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let getOptionsFixture: GetOptions; + + let planResultFixture: PlanResult; + + let resolvedValueFixture: unknown; + + let result: unknown; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + getOptionsFixture = { + name: 'name', + optional: true, + tag: { + key: 'tag-key', + value: Symbol(), + }, + }; + + planResultFixture = Symbol() as unknown as PlanResult; + + resolvedValueFixture = Symbol(); + + (plan as jest.Mock).mockReturnValueOnce(planResultFixture); + + (resolve as jest.Mock).mockReturnValueOnce( + resolvedValueFixture, + ); + + result = new Container().get( + serviceIdentifierFixture, + getOptionsFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call plan()', () => { + const expectedPlanParams: PlanParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata, + rootConstraints: { + isMultiple: false, + isOptional: getOptionsFixture.optional as true, + name: getOptionsFixture.name as string, + serviceIdentifier: serviceIdentifierFixture, + tag: getOptionsFixture.tag as GetOptionsTagConstraint, + }, + servicesBranch: new Set(), + }; + + expect(plan).toHaveBeenCalledTimes(1); + expect(plan).toHaveBeenCalledWith(expectedPlanParams); + }); + + it('should call resolve()', () => { + const expectedResolveParams: ResolutionParams = { + context: { + get: expect.any(Function), + getAll: expect.any(Function), + getAllAsync: expect.any(Function), + getAsync: expect.any(Function), + } as unknown as ResolutionContext, + getActivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + planResult: planResultFixture, + requestScopeCache: new Map(), + }; + + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expectedResolveParams); + }); + + it('should return expected value', () => { + expect(result).toBe(resolvedValueFixture); + }); + }); + + describe('when called, and resolve returns Promise', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let getOptionsFixture: GetOptions; + + let planResultFixture: PlanResult; + + let resolvedValueFixture: unknown; + + let result: unknown; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + getOptionsFixture = { + name: 'name', + optional: true, + tag: { + key: 'tag-key', + value: Symbol(), + }, + }; + + planResultFixture = Symbol() as unknown as PlanResult; + + resolvedValueFixture = Symbol(); + + (plan as jest.Mock).mockReturnValueOnce(planResultFixture); + + (resolve as jest.Mock).mockReturnValueOnce( + Promise.resolve(resolvedValueFixture), + ); + + try { + new Container().get(serviceIdentifierFixture, getOptionsFixture); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call plan()', () => { + const expectedPlanParams: PlanParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata, + rootConstraints: { + isMultiple: false, + isOptional: getOptionsFixture.optional as true, + name: getOptionsFixture.name as string, + serviceIdentifier: serviceIdentifierFixture, + tag: getOptionsFixture.tag as GetOptionsTagConstraint, + }, + servicesBranch: new Set(), + }; + + expect(plan).toHaveBeenCalledTimes(1); + expect(plan).toHaveBeenCalledWith(expectedPlanParams); + }); + + it('should call resolve()', () => { + const expectedResolveParams: ResolutionParams = { + context: { + get: expect.any(Function), + getAll: expect.any(Function), + getAllAsync: expect.any(Function), + getAsync: expect.any(Function), + } as unknown as ResolutionContext, + getActivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + planResult: planResultFixture, + requestScopeCache: new Map(), + }; + + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expectedResolveParams); + }); + + it('should throw an InversifyContainerError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyContainerErrorKind.invalidOperation, + message: `Unexpected asyncronous service when resolving service "${serviceIdentifierFixture as string}"`, + }; + + expect(result).toBeInstanceOf(InversifyContainerError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); + + describe('.getAll', () => { + describe('when called', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let getOptionsFixture: GetOptions; + + let planResultFixture: PlanResult; + + let resolvedValueFixture: unknown; + + let result: unknown; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + getOptionsFixture = { + name: 'name', + optional: true, + tag: { + key: 'tag-key', + value: Symbol(), + }, + }; + + planResultFixture = Symbol() as unknown as PlanResult; + + resolvedValueFixture = Symbol(); + + (plan as jest.Mock).mockReturnValueOnce(planResultFixture); + + (resolve as jest.Mock).mockReturnValueOnce([ + resolvedValueFixture, + ]); + + result = new Container().getAll( + serviceIdentifierFixture, + getOptionsFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call plan()', () => { + const expectedPlanParams: PlanParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata, + rootConstraints: { + isMultiple: true, + isOptional: getOptionsFixture.optional as true, + name: getOptionsFixture.name as string, + serviceIdentifier: serviceIdentifierFixture, + tag: getOptionsFixture.tag as GetOptionsTagConstraint, + }, + servicesBranch: new Set(), + }; + + expect(plan).toHaveBeenCalledTimes(1); + expect(plan).toHaveBeenCalledWith(expectedPlanParams); + }); + + it('should call resolve()', () => { + const expectedResolveParams: ResolutionParams = { + context: { + get: expect.any(Function), + getAll: expect.any(Function), + getAllAsync: expect.any(Function), + getAsync: expect.any(Function), + } as unknown as ResolutionContext, + getActivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + planResult: planResultFixture, + requestScopeCache: new Map(), + }; + + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expectedResolveParams); + }); + + it('should return expected value', () => { + expect(result).toStrictEqual([resolvedValueFixture]); + }); + }); + + describe('when called, and resolve returns Promise', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let getOptionsFixture: GetOptions; + + let planResultFixture: PlanResult; + + let resolvedValueFixture: unknown; + + let result: unknown; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + getOptionsFixture = { + name: 'name', + optional: true, + tag: { + key: 'tag-key', + value: Symbol(), + }, + }; + + planResultFixture = Symbol() as unknown as PlanResult; + + resolvedValueFixture = Symbol(); + + (plan as jest.Mock).mockReturnValueOnce(planResultFixture); + + (resolve as jest.Mock).mockReturnValueOnce( + Promise.resolve([resolvedValueFixture]), + ); + + try { + new Container().getAll(serviceIdentifierFixture, getOptionsFixture); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call plan()', () => { + const expectedPlanParams: PlanParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata, + rootConstraints: { + isMultiple: true, + isOptional: getOptionsFixture.optional as true, + name: getOptionsFixture.name as string, + serviceIdentifier: serviceIdentifierFixture, + tag: getOptionsFixture.tag as GetOptionsTagConstraint, + }, + servicesBranch: new Set(), + }; + + expect(plan).toHaveBeenCalledTimes(1); + expect(plan).toHaveBeenCalledWith(expectedPlanParams); + }); + + it('should call resolve()', () => { + const expectedResolveParams: ResolutionParams = { + context: { + get: expect.any(Function), + getAll: expect.any(Function), + getAllAsync: expect.any(Function), + getAsync: expect.any(Function), + } as unknown as ResolutionContext, + getActivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + planResult: planResultFixture, + requestScopeCache: new Map(), + }; + + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expectedResolveParams); + }); + + it('should throw an InversifyContainerError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyContainerErrorKind.invalidOperation, + message: `Unexpected asyncronous service when resolving service "${serviceIdentifierFixture as string}"`, + }; + + expect(result).toBeInstanceOf(InversifyContainerError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); + + describe('.getAllAsync', () => { + describe('when called', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let getOptionsFixture: GetOptions; + + let planResultFixture: PlanResult; + + let resolvedValueFixture: unknown; + + let result: unknown; + + beforeAll(async () => { + serviceIdentifierFixture = 'service-id'; + getOptionsFixture = { + name: 'name', + optional: true, + tag: { + key: 'tag-key', + value: Symbol(), + }, + }; + + planResultFixture = Symbol() as unknown as PlanResult; + + resolvedValueFixture = Symbol(); + + (plan as jest.Mock).mockReturnValueOnce(planResultFixture); + + (resolve as jest.Mock).mockReturnValueOnce([ + resolvedValueFixture, + ]); + + result = await new Container().getAllAsync( + serviceIdentifierFixture, + getOptionsFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call plan()', () => { + const expectedPlanParams: PlanParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata, + rootConstraints: { + isMultiple: true, + isOptional: getOptionsFixture.optional as true, + name: getOptionsFixture.name as string, + serviceIdentifier: serviceIdentifierFixture, + tag: getOptionsFixture.tag as GetOptionsTagConstraint, + }, + servicesBranch: new Set(), + }; + + expect(plan).toHaveBeenCalledTimes(1); + expect(plan).toHaveBeenCalledWith(expectedPlanParams); + }); + + it('should call resolve()', () => { + const expectedResolveParams: ResolutionParams = { + context: { + get: expect.any(Function), + getAll: expect.any(Function), + getAllAsync: expect.any(Function), + getAsync: expect.any(Function), + } as unknown as ResolutionContext, + getActivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + planResult: planResultFixture, + requestScopeCache: new Map(), + }; + + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expectedResolveParams); + }); + + it('should return expected value', () => { + expect(result).toStrictEqual([resolvedValueFixture]); + }); + }); + }); + + describe('.getAsync', () => { + describe('when called', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let getOptionsFixture: GetOptions; + + let planResultFixture: PlanResult; + + let resolvedValueFixture: unknown; + + let result: unknown; + + beforeAll(async () => { + serviceIdentifierFixture = 'service-id'; + getOptionsFixture = { + name: 'name', + optional: true, + tag: { + key: 'tag-key', + value: Symbol(), + }, + }; + + planResultFixture = Symbol() as unknown as PlanResult; + + resolvedValueFixture = Symbol(); + + (plan as jest.Mock).mockReturnValueOnce(planResultFixture); + + (resolve as jest.Mock).mockReturnValueOnce( + resolvedValueFixture, + ); + + result = await new Container().getAsync( + serviceIdentifierFixture, + getOptionsFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call plan()', () => { + const expectedPlanParams: PlanParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata, + rootConstraints: { + isMultiple: false, + isOptional: getOptionsFixture.optional as true, + name: getOptionsFixture.name as string, + serviceIdentifier: serviceIdentifierFixture, + tag: getOptionsFixture.tag as GetOptionsTagConstraint, + }, + servicesBranch: new Set(), + }; + + expect(plan).toHaveBeenCalledTimes(1); + expect(plan).toHaveBeenCalledWith(expectedPlanParams); + }); + + it('should call resolve()', () => { + const expectedResolveParams: ResolutionParams = { + context: { + get: expect.any(Function), + getAll: expect.any(Function), + getAllAsync: expect.any(Function), + getAsync: expect.any(Function), + } as unknown as ResolutionContext, + getActivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + planResult: planResultFixture, + requestScopeCache: new Map(), + }; + + expect(resolve).toHaveBeenCalledTimes(1); + expect(resolve).toHaveBeenCalledWith(expectedResolveParams); + }); + + it('should return expected value', () => { + expect(result).toBe(resolvedValueFixture); + }); + }); + }); + + describe('.isBound', () => { + let serviceIdentifierFixture: ServiceIdentifier; + + let nameFixture: string; + let tagFixture: GetOptionsTagConstraint; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + + nameFixture = 'name-fixture'; + tagFixture = { + key: 'tag-key-fixture', + value: Symbol(), + }; + }); + + describe('when called, and bindingService.get() returns undefined', () => { + let result: unknown; + + beforeAll(() => { + result = new Container().isBound(serviceIdentifierFixture, { + name: nameFixture, + tag: tagFixture, + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call bindingService.get()', () => { + expect(bindingServiceMock.get).toHaveBeenCalledTimes(1); + expect(bindingServiceMock.get).toHaveBeenCalledWith( + serviceIdentifierFixture, + ); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + + describe('when called, and bindingService.get() returns binding ann binding.isSatisfiedBy() returns false', () => { + let bindingMock: jest.Mocked; + + let result: unknown; + + beforeAll(() => { + bindingMock = { + cache: { + isRight: false, + value: undefined, + }, + id: 1, + isSatisfiedBy: jest.fn(), + moduleId: undefined, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'service-id', + type: bindingTypeValues.ConstantValue, + value: Symbol.for('constant-value-binding-fixture-value'), + }; + + bindingServiceMock.get.mockReturnValueOnce([bindingMock]); + + bindingMock.isSatisfiedBy.mockReturnValueOnce(false); + + result = new Container().isBound(serviceIdentifierFixture, { + name: nameFixture, + tag: tagFixture, + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call bindingService.get()', () => { + expect(bindingServiceMock.get).toHaveBeenCalledTimes(1); + expect(bindingServiceMock.get).toHaveBeenCalledWith( + serviceIdentifierFixture, + ); + }); + + it('should call binding.isSatisfiedBy()', () => { + const expectedBindingMetadata: BindingMetadata = { + getAncestor: expect.any(Function) as unknown as () => + | BindingMetadata + | undefined, + name: nameFixture, + serviceIdentifier: serviceIdentifierFixture, + tags: new Map([[tagFixture.key, tagFixture.value]]), + }; + + expect(bindingMock.isSatisfiedBy).toHaveBeenCalledTimes(1); + expect(bindingMock.isSatisfiedBy).toHaveBeenCalledWith( + expectedBindingMetadata, + ); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + + describe('when called, and bindingService.get() returns binding ann binding.isSatisfiedBy() returns true', () => { + let bindingMock: jest.Mocked; + + let result: unknown; + + beforeAll(() => { + bindingMock = { + cache: { + isRight: false, + value: undefined, + }, + id: 1, + isSatisfiedBy: jest.fn(), + moduleId: undefined, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'service-id', + type: bindingTypeValues.ConstantValue, + value: Symbol.for('constant-value-binding-fixture-value'), + }; + + bindingServiceMock.get.mockReturnValueOnce([bindingMock]); + + bindingMock.isSatisfiedBy.mockReturnValueOnce(true); + + result = new Container().isBound(serviceIdentifierFixture, { + name: nameFixture, + tag: tagFixture, + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call bindingService.get()', () => { + expect(bindingServiceMock.get).toHaveBeenCalledTimes(1); + expect(bindingServiceMock.get).toHaveBeenCalledWith( + serviceIdentifierFixture, + ); + }); + + it('should call binding.isSatisfiedBy()', () => { + const expectedBindingMetadata: BindingMetadata = { + getAncestor: expect.any(Function) as unknown as () => + | BindingMetadata + | undefined, + name: nameFixture, + serviceIdentifier: serviceIdentifierFixture, + tags: new Map([[tagFixture.key, tagFixture.value]]), + }; + + expect(bindingMock.isSatisfiedBy).toHaveBeenCalledTimes(1); + expect(bindingMock.isSatisfiedBy).toHaveBeenCalledWith( + expectedBindingMetadata, + ); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + }); + + describe('.load', () => { + let containerModuleMock: jest.Mocked; + + beforeAll(() => { + containerModuleMock = { + load: jest.fn(), + } as Partial< + jest.Mocked + > as jest.Mocked; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(async () => { + result = await new Container().load(containerModuleMock); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call containerModuler.load', () => { + const options: ContainerModuleLoadOptions = { + bind: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => BindToFluentSyntax, + isBound: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + options?: IsBoundOptions, + ) => boolean, + onActivation: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + activation: BindingActivation, + ) => void, + onDeactivation: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + activation: BindingDeactivation, + ) => void, + unbind: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Promise, + }; + + expect(containerModuleMock.load).toHaveBeenCalledTimes(1); + expect(containerModuleMock.load).toHaveBeenCalledWith(options); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('.onActivation', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let activationMock: jest.Mock; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + activationMock = jest.fn(); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = new Container().onActivation( + serviceIdentifierFixture, + activationMock, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call activationService.add()', () => { + const bindingActivationRelation: BindingActivationRelation = { + serviceId: serviceIdentifierFixture, + }; + + expect(activationServiceMock.add).toHaveBeenCalledTimes(1); + expect(activationServiceMock.add).toHaveBeenCalledWith( + activationMock, + bindingActivationRelation, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('.onDeactivation', () => { + let serviceIdentifierFixture: ServiceIdentifier; + let deactivationMock: jest.Mock; + + beforeAll(() => { + serviceIdentifierFixture = 'service-id'; + deactivationMock = jest.fn(); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = new Container().onDeactivation( + serviceIdentifierFixture, + deactivationMock, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call deactivationService.add()', () => { + const bindingDeactivationRelation: BindingDeactivationRelation = { + serviceId: serviceIdentifierFixture, + }; + + expect(deactivationServiceMock.add).toHaveBeenCalledTimes(1); + expect(deactivationServiceMock.add).toHaveBeenCalledWith( + deactivationMock, + bindingDeactivationRelation, + ); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('.unbind', () => { + let serviceIdentifierFixture: ServiceIdentifier; + + beforeAll(() => { + serviceIdentifierFixture = 'serviceId'; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(async () => { + result = await new Container().unbind(serviceIdentifierFixture); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call resolveServiceDeactivations', () => { + const expectedParams: DeactivationParams = { + getBindings: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Binding[] | undefined, + getClassMetadata: expect.any(Function) as unknown as ( + type: Newable, + ) => ClassMetadata, + getDeactivations: expect.any(Function) as unknown as ( + serviceIdentifier: ServiceIdentifier, + ) => Iterable> | undefined, + }; + + expect(resolveServiceDeactivations).toHaveBeenCalledTimes(1); + expect(resolveServiceDeactivations).toHaveBeenCalledWith( + expectedParams, + serviceIdentifierFixture, + ); + }); + + it('should call activationService.removeAllByServiceId()', () => { + expect( + activationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledTimes(1); + expect(activationServiceMock.removeAllByServiceId).toHaveBeenCalledWith( + serviceIdentifierFixture, + ); + }); + + it('should call bindingService.remove()', () => { + expect(bindingServiceMock.remove).toHaveBeenCalledTimes(1); + expect(bindingServiceMock.remove).toHaveBeenCalledWith( + serviceIdentifierFixture, + ); + }); + + it('should call deactivationService.removeAllByServiceId()', () => { + expect( + deactivationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledTimes(1); + expect( + deactivationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledWith(serviceIdentifierFixture); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/packages/container/libraries/container/src/container/services/Container.ts b/packages/container/libraries/container/src/container/services/Container.ts new file mode 100644 index 0000000..06121e8 --- /dev/null +++ b/packages/container/libraries/container/src/container/services/Container.ts @@ -0,0 +1,350 @@ +import { + isPromise, + ServiceIdentifier, + stringifyServiceIdentifier, +} from '@inversifyjs/common'; +import { + ActivationsService, + Binding, + BindingActivation, + BindingDeactivation, + BindingMetadata, + BindingScope, + bindingScopeValues, + BindingService, + DeactivationsService, + getClassMetadata, + GetOptions, + OptionalGetOptions, + plan, + PlanParams, + PlanResult, + resolve, + resolveServiceDeactivations, +} from '@inversifyjs/core'; + +import { BindToFluentSyntax } from '../../binding/models/BindingFluentSyntax'; +import { BindToFluentSyntaxImplementation } from '../../binding/models/BindingFluentSyntaxImplementation'; +import { InversifyContainerError } from '../../error/models/InversifyContainerError'; +import { InversifyContainerErrorKind } from '../../error/models/InversifyContainerErrorKind'; +import { + ContainerModule, + ContainerModuleLoadOptions, +} from '../models/ContainerModule'; +import { IsBoundOptions } from '../models/isBoundOptions'; + +export interface ContainerOptions { + defaultScope?: BindingScope | undefined; + parent?: Container | undefined; +} + +interface InternalContainerOptions { + defaultScope: BindingScope; +} + +const DEFAULT_DEFAULT_SCOPE: BindingScope = bindingScopeValues.Transient; + +export class Container { + readonly #activationService: ActivationsService; + readonly #bindingService: BindingService; + readonly #deactivationService: DeactivationsService; + readonly #options: InternalContainerOptions; + + constructor(options?: ContainerOptions) { + if (options?.parent !== undefined) { + this.#activationService = new ActivationsService( + options.parent.#activationService, + ); + this.#bindingService = new BindingService(options.parent.#bindingService); + this.#deactivationService = new DeactivationsService( + options.parent.#deactivationService, + ); + } else { + this.#activationService = new ActivationsService(undefined); + this.#bindingService = new BindingService(undefined); + this.#deactivationService = new DeactivationsService(undefined); + } + + this.#options = { + defaultScope: options?.defaultScope ?? DEFAULT_DEFAULT_SCOPE, + }; + } + + public bind( + serviceIdentifier: ServiceIdentifier, + ): BindToFluentSyntax { + return new BindToFluentSyntaxImplementation( + (binding: Binding): void => { + this.#bindingService.set(binding); + }, + undefined, + this.#options.defaultScope, + serviceIdentifier, + ); + } + + public get( + serviceIdentifier: ServiceIdentifier, + options: OptionalGetOptions, + ): T | undefined; + public get( + serviceIdentifier: ServiceIdentifier, + options?: GetOptions, + ): T; + public get( + serviceIdentifier: ServiceIdentifier, + options?: GetOptions, + ): T | undefined { + const planResult: PlanResult = this.#buildPlanResult( + false, + serviceIdentifier, + options, + ); + + const resolvedValue: T | Promise | undefined = + this.#getFromPlanResult(planResult); + + if (isPromise(resolvedValue)) { + throw new InversifyContainerError( + InversifyContainerErrorKind.invalidOperation, + `Unexpected asyncronous service when resolving service "${stringifyServiceIdentifier(serviceIdentifier)}"`, + ); + } + + return resolvedValue; + } + + public getAll( + serviceIdentifier: ServiceIdentifier, + options?: GetOptions, + ): T[] { + const planResult: PlanResult = this.#buildPlanResult( + true, + serviceIdentifier, + options, + ); + + const resolvedValue: T[] | Promise = + this.#getFromPlanResult(planResult); + + if (isPromise(resolvedValue)) { + throw new InversifyContainerError( + InversifyContainerErrorKind.invalidOperation, + `Unexpected asyncronous service when resolving service "${stringifyServiceIdentifier(serviceIdentifier)}"`, + ); + } + + return resolvedValue; + } + + public async getAllAsync( + serviceIdentifier: ServiceIdentifier, + options?: GetOptions, + ): Promise { + const planResult: PlanResult = this.#buildPlanResult( + true, + serviceIdentifier, + options, + ); + + return this.#getFromPlanResult(planResult); + } + + public async getAsync( + serviceIdentifier: ServiceIdentifier, + options: OptionalGetOptions, + ): Promise; + public async getAsync( + serviceIdentifier: ServiceIdentifier, + options?: GetOptions, + ): Promise; + public async getAsync( + serviceIdentifier: ServiceIdentifier, + options?: GetOptions, + ): Promise { + const planResult: PlanResult = this.#buildPlanResult( + false, + serviceIdentifier, + options, + ); + + return this.#getFromPlanResult(planResult); + } + + public isBound( + serviceIdentifier: ServiceIdentifier, + options?: IsBoundOptions, + ): boolean { + const bindings: Binding[] | undefined = + this.#bindingService.get(serviceIdentifier); + + if (bindings === undefined) { + return false; + } + + const bindingMetadata: BindingMetadata = { + getAncestor: () => undefined, + name: options?.name, + serviceIdentifier, + tags: new Map(), + }; + + if (options?.tag !== undefined) { + bindingMetadata.tags.set(options.tag.key, options.tag.value); + } + + return bindings.some((binding: Binding): boolean => + binding.isSatisfiedBy(bindingMetadata), + ); + } + + public async load(...modules: ContainerModule[]): Promise { + await Promise.all( + modules.map( + async (module: ContainerModule): Promise => + module.load(this.#buildContainerModuleLoadOptions(module.id)), + ), + ); + } + + public onActivation( + serviceIdentifier: ServiceIdentifier, + activation: BindingActivation, + ): void { + this.#activationService.add(activation as BindingActivation, { + serviceId: serviceIdentifier, + }); + } + + public onDeactivation( + serviceIdentifier: ServiceIdentifier, + deactivation: BindingDeactivation, + ): void { + this.#deactivationService.add(deactivation as BindingDeactivation, { + serviceId: serviceIdentifier, + }); + } + + public async unbind(serviceIdentifier: ServiceIdentifier): Promise { + await resolveServiceDeactivations( + { + getBindings: ( + serviceIdentifier: ServiceIdentifier, + ): Binding[] | undefined => + this.#bindingService.get(serviceIdentifier), + getClassMetadata, + getDeactivations: ( + serviceIdentifier: ServiceIdentifier, + ) => this.#deactivationService.get(serviceIdentifier), + }, + serviceIdentifier, + ); + + this.#activationService.removeAllByServiceId(serviceIdentifier); + this.#bindingService.remove(serviceIdentifier); + this.#deactivationService.removeAllByServiceId(serviceIdentifier); + } + + #buildContainerModuleLoadOptions( + moduleId: number, + ): ContainerModuleLoadOptions { + return { + bind: ( + serviceIdentifier: ServiceIdentifier, + ): BindToFluentSyntax => { + return new BindToFluentSyntaxImplementation( + (binding: Binding): void => { + this.#bindingService.set(binding); + }, + moduleId, + this.#options.defaultScope, + serviceIdentifier, + ); + }, + isBound: this.isBound.bind(this), + onActivation: ( + serviceIdentifier: ServiceIdentifier, + activation: BindingActivation, + ): void => { + this.#activationService.add(activation as BindingActivation, { + moduleId, + serviceId: serviceIdentifier, + }); + }, + onDeactivation: ( + serviceIdentifier: ServiceIdentifier, + deactivation: BindingDeactivation, + ): void => { + this.#deactivationService.add(deactivation as BindingDeactivation, { + moduleId, + serviceId: serviceIdentifier, + }); + }, + unbind: this.unbind.bind(this), + }; + } + + #buildPlanResult( + isMultiple: boolean, + serviceIdentifier: ServiceIdentifier, + options: GetOptions | undefined, + ): PlanResult { + const planParams: PlanParams = { + getBindings: this.#bindingService.get.bind(this.#bindingService), + getClassMetadata, + rootConstraints: { + isMultiple, + serviceIdentifier, + }, + servicesBranch: new Set(), + }; + + this.#handlePlanParamsRootConstraints(planParams, options); + + return plan(planParams); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + #getFromPlanResult(planResult: PlanResult): T { + return resolve({ + context: { + get: this.get.bind(this), + getAll: this.getAll.bind(this), + getAllAsync: this.getAllAsync.bind(this), + getAsync: this.getAsync.bind(this), + }, + getActivations: ( + serviceIdentifier: ServiceIdentifier, + ): BindingActivation[] | undefined => + this.#activationService.get(serviceIdentifier) as + | BindingActivation[] + | undefined, + planResult, + requestScopeCache: new Map(), + }) as T; + } + + #handlePlanParamsRootConstraints( + planParams: PlanParams, + options: GetOptions | undefined, + ): void { + if (options === undefined) { + return; + } + + if (options.name !== undefined) { + planParams.rootConstraints.name = options.name; + } + + if (options.optional === true) { + planParams.rootConstraints.isOptional = true; + } + + if (options.tag !== undefined) { + planParams.rootConstraints.tag = { + key: options.tag.key, + value: options.tag.value, + }; + } + } +} diff --git a/packages/container/libraries/container/src/error/models/InversifyContainerError.ts b/packages/container/libraries/container/src/error/models/InversifyContainerError.ts new file mode 100644 index 0000000..b1c1a2e --- /dev/null +++ b/packages/container/libraries/container/src/error/models/InversifyContainerError.ts @@ -0,0 +1,37 @@ +import { InversifyContainerErrorKind } from './InversifyContainerErrorKind'; + +export const isAppErrorSymbol: unique symbol = Symbol.for( + '@inversifyjs/container/InversifyContainerError', +); + +export class InversifyContainerError extends Error { + public [isAppErrorSymbol]: true; + + public kind: InversifyContainerErrorKind; + + constructor( + kind: InversifyContainerErrorKind, + message?: string, + options?: ErrorOptions, + ) { + super(message, options); + + this[isAppErrorSymbol] = true; + this.kind = kind; + } + + public static is(value: unknown): value is InversifyContainerError { + return ( + typeof value === 'object' && + value !== null && + (value as Record)[isAppErrorSymbol] === true + ); + } + + public static isErrorOfKind( + value: unknown, + kind: InversifyContainerErrorKind, + ): value is InversifyContainerError { + return InversifyContainerError.is(value) && value.kind === kind; + } +} diff --git a/packages/container/libraries/container/src/error/models/InversifyContainerErrorKind.ts b/packages/container/libraries/container/src/error/models/InversifyContainerErrorKind.ts new file mode 100644 index 0000000..5b55980 --- /dev/null +++ b/packages/container/libraries/container/src/error/models/InversifyContainerErrorKind.ts @@ -0,0 +1,4 @@ +export enum InversifyContainerErrorKind { + invalidOperation, + unknown, +} diff --git a/packages/container/libraries/container/src/index.ts b/packages/container/libraries/container/src/index.ts index cb0ff5c..7dc3a5f 100644 --- a/packages/container/libraries/container/src/index.ts +++ b/packages/container/libraries/container/src/index.ts @@ -1 +1,37 @@ -export {}; +import 'reflect-metadata'; + +import { + BindInFluentSyntax, + BindInWhenOnFluentSyntax, + BindOnFluentSyntax, + BindToFluentSyntax, + BindWhenFluentSyntax, + BindWhenOnFluentSyntax, +} from './binding/models/BindingFluentSyntax'; +import { + ContainerModule, + ContainerModuleLoadOptions, +} from './container/models/ContainerModule'; +import { IsBoundOptions } from './container/models/isBoundOptions'; +import { Container, ContainerOptions } from './container/services/Container'; +import { InversifyContainerError } from './error/models/InversifyContainerError'; +import { InversifyContainerErrorKind } from './error/models/InversifyContainerErrorKind'; + +export type { + BindInFluentSyntax, + BindInWhenOnFluentSyntax, + BindOnFluentSyntax, + BindToFluentSyntax, + BindWhenFluentSyntax, + BindWhenOnFluentSyntax, + ContainerModuleLoadOptions, + ContainerOptions, + IsBoundOptions, +}; + +export { + Container, + ContainerModule, + InversifyContainerError, + InversifyContainerErrorKind, +}; diff --git a/packages/container/libraries/core/src/index.ts b/packages/container/libraries/core/src/index.ts index a07ceb6..a1a9ba1 100644 --- a/packages/container/libraries/core/src/index.ts +++ b/packages/container/libraries/core/src/index.ts @@ -23,7 +23,10 @@ import { BindingActivationRelation, } from './binding/services/ActivationsService'; import { BindingService } from './binding/services/BindingService'; -import { DeactivationsService } from './binding/services/DeactivationsService'; +import { + BindingDeactivationRelation, + DeactivationsService, +} from './binding/services/DeactivationsService'; import { getClassMetadata } from './metadata/calculations/getClassMetadata'; import { inject } from './metadata/decorators/inject'; import { injectable } from './metadata/decorators/injectable'; @@ -76,6 +79,7 @@ export type { BindingActivation, BindingActivationRelation, BindingDeactivation, + BindingDeactivationRelation, BindingMetadata, BindingScope, BindingType,