diff --git a/packages/container/libraries/container/package.json b/packages/container/libraries/container/package.json index ee097c4..4b585c5 100644 --- a/packages/container/libraries/container/package.json +++ b/packages/container/libraries/container/package.json @@ -4,6 +4,11 @@ "url": "https://github.com/inversify/monorepo/issues" }, "description": "InversifyJs container", + "dependencies": { + "@inversifyjs/common": "workspace:*", + "@inversifyjs/core": "workspace:*", + "@inversifyjs/reflect-metadata-utils": "workspace:*" + }, "devDependencies": { "@eslint/js": "9.17.0", "@jest/globals": "29.7.0", diff --git a/packages/container/libraries/container/src/binding/actions/getBindingId.spec.ts b/packages/container/libraries/container/src/binding/actions/getBindingId.spec.ts new file mode 100644 index 0000000..1d9e18f --- /dev/null +++ b/packages/container/libraries/container/src/binding/actions/getBindingId.spec.ts @@ -0,0 +1,88 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('@inversifyjs/reflect-metadata-utils'); + +import { + getReflectMetadata, + updateReflectMetadata, +} from '@inversifyjs/reflect-metadata-utils'; + +import { getBindingId } from './getBindingId'; + +describe(getBindingId.name, () => { + describe('when called, and getReflectMetadata() returns undefined', () => { + let result: unknown; + + beforeAll(() => { + ( + getReflectMetadata as jest.Mock + ).mockReturnValueOnce(0); + + result = getBindingId(); + }); + + 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', + 0, + 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 = getBindingId(); + }); + + 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', + Number.MAX_SAFE_INTEGER, + expect.any(Function), + ); + }); + + it('should return default id', () => { + expect(result).toBe(Number.MAX_SAFE_INTEGER); + }); + }); +}); diff --git a/packages/container/libraries/container/src/binding/actions/getBindingId.ts b/packages/container/libraries/container/src/binding/actions/getBindingId.ts new file mode 100644 index 0000000..0c0ab57 --- /dev/null +++ b/packages/container/libraries/container/src/binding/actions/getBindingId.ts @@ -0,0 +1,29 @@ +import { + getReflectMetadata, + updateReflectMetadata, +} from '@inversifyjs/reflect-metadata-utils'; + +const ID_METADATA: string = '@inversifyjs/container/bindingId'; + +export function getBindingId(): number { + const bindingId: number = + getReflectMetadata(Object, ID_METADATA) ?? 0; + + if (bindingId === Number.MAX_SAFE_INTEGER) { + updateReflectMetadata( + Object, + ID_METADATA, + bindingId, + () => Number.MIN_SAFE_INTEGER, + ); + } else { + updateReflectMetadata( + Object, + ID_METADATA, + bindingId, + (id: number) => id + 1, + ); + } + + return bindingId; +} diff --git a/packages/container/libraries/container/src/common/models/Writable.ts b/packages/container/libraries/container/src/common/models/Writable.ts new file mode 100644 index 0000000..0a15572 --- /dev/null +++ b/packages/container/libraries/container/src/common/models/Writable.ts @@ -0,0 +1,3 @@ +export type Writable = { + -readonly [TKey in keyof T]: T[TKey]; +}; diff --git a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntax.ts b/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntax.ts new file mode 100644 index 0000000..f7ef4b0 --- /dev/null +++ b/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntax.ts @@ -0,0 +1,53 @@ +import { Newable, ServiceIdentifier } from '@inversifyjs/common'; +import { + BindingActivation, + BindingDeactivation, + BindingMetadata, + DynamicValueBuilder, + Factory, + Provider, + ResolutionContext, +} from '@inversifyjs/core'; + +export interface BindToFluentSyntax { + to(type: Newable): BindInWhenOnFluentSyntax; + toSelf(): BindInWhenOnFluentSyntax; + toConstantValue(value: T): BindWhenOnFluentSyntax; + toDynamicValue(builder: DynamicValueBuilder): BindInWhenOnFluentSyntax; + toFactory( + factory: T extends Factory + ? (context: ResolutionContext) => T + : never, + ): BindWhenOnFluentSyntax; + toProvider( + provider: T extends Provider + ? (context: ResolutionContext) => T + : never, + ): BindWhenOnFluentSyntax; + toService(service: ServiceIdentifier): void; +} + +export interface BindInFluentSyntax { + inSingletonScope(): BindWhenOnFluentSyntax; + inTransientScope(): BindWhenOnFluentSyntax; + inRequestScope(): BindWhenOnFluentSyntax; +} + +export interface BindInWhenOnFluentSyntax + extends BindInFluentSyntax, + BindWhenOnFluentSyntax {} + +export interface BindOnFluentSyntax { + onActivation(activation: BindingActivation): BindWhenFluentSyntax; + onDeactivation(deactivation: BindingDeactivation): BindWhenFluentSyntax; +} + +export interface BindWhenOnFluentSyntax + extends BindWhenFluentSyntax, + BindOnFluentSyntax {} + +export interface BindWhenFluentSyntax { + when( + constraint: (metadata: BindingMetadata) => boolean, + ): BindOnFluentSyntax; +} diff --git a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.spec.ts b/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.spec.ts new file mode 100644 index 0000000..01aa772 --- /dev/null +++ b/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.spec.ts @@ -0,0 +1,872 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('../../../binding/actions/getBindingId'); + +import { ServiceIdentifier } from '@inversifyjs/common'; +import { + Binding, + BindingActivation, + BindingDeactivation, + BindingMetadata, + BindingScope, + bindingScopeValues, + BindingType, + bindingTypeValues, + ConstantValueBinding, + DynamicValueBuilder, + Factory, + InstanceBinding, + Provider, + ResolutionContext, + ScopedBinding, + ServiceRedirectionBinding, +} from '@inversifyjs/core'; + +import { getBindingId } from '../../../binding/actions/getBindingId'; +import { Writable } from '../../../common/models/Writable'; +import { + BindInFluentSyntaxImplementation, + BindInWhenOnFluentSyntaxImplementation, + BindOnFluentSyntaxImplementation, + BindToFluentSyntaxImplementation, + BindWhenFluentSyntaxImplementation, + BindWhenOnFluentSyntaxImplementation, +} from './BindingFluentSyntaxImplementation'; + +describe(BindInFluentSyntaxImplementation.name, () => { + let bindingMock: jest.Mocked< + ScopedBinding + >; + + let bindingMockSetScopeMock: jest.Mock<(value: BindingScope) => void>; + + let bindInFluentSyntaxImplementation: BindInFluentSyntaxImplementation; + + beforeAll(() => { + let bindingScope: BindingScope = bindingScopeValues.Singleton; + + bindingMockSetScopeMock = jest.fn(); + + bindingMock = { + get scope(): BindingScope { + return bindingScope; + }, + set scope(value: BindingScope) { + bindingMockSetScopeMock(value); + + bindingScope = value; + }, + } as Partial< + jest.Mocked> + > as jest.Mocked>; + + bindInFluentSyntaxImplementation = new BindInFluentSyntaxImplementation( + bindingMock, + ); + }); + + describe.each< + [ + string, + ( + bindInFluentSyntaxImplementation: BindInFluentSyntaxImplementation, + ) => unknown, + BindingScope, + ] + >([ + [ + '.inRequestScope()', + ( + bindInFluentSyntaxImplementation: BindInFluentSyntaxImplementation, + ) => bindInFluentSyntaxImplementation.inRequestScope(), + bindingScopeValues.Request, + ], + [ + '.inSingletonScope()', + ( + bindInFluentSyntaxImplementation: BindInFluentSyntaxImplementation, + ) => bindInFluentSyntaxImplementation.inSingletonScope(), + bindingScopeValues.Singleton, + ], + [ + '.inTransientScope()', + ( + bindInFluentSyntaxImplementation: BindInFluentSyntaxImplementation, + ) => bindInFluentSyntaxImplementation.inTransientScope(), + bindingScopeValues.Transient, + ], + ])( + '%s', + ( + _: string, + buildResult: ( + bindInFluentSyntaxImplementation: BindInFluentSyntaxImplementation, + ) => unknown, + expectedScope: BindingScope, + ) => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = buildResult(bindInFluentSyntaxImplementation); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should set binding scope', () => { + expect(bindingMockSetScopeMock).toHaveBeenCalledTimes(1); + expect(bindingMockSetScopeMock).toHaveBeenCalledWith(expectedScope); + }); + + it('should return BindWhenOnFluentSyntax', () => { + expect(result).toBeInstanceOf(BindWhenOnFluentSyntaxImplementation); + }); + }); + }, + ); +}); + +describe(BindToFluentSyntaxImplementation.name, () => { + class Foo {} + + let bindingIdFixture: number; + + let dynamicValueBuilderfixture: DynamicValueBuilder; + let factoryBuilderFixture: (context: ResolutionContext) => Factory; + let providerBuilderFixture: (context: ResolutionContext) => Provider; + + let callbackMock: jest.Mock<(binding: Binding) => void>; + let containerModuleIdFixture: number; + let defaultScopeFixture: BindingScope; + let serviceIdentifierFixture: ServiceIdentifier; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation; + + beforeAll(() => { + bindingIdFixture = 1; + + dynamicValueBuilderfixture = () => Symbol.for('dynamic-value'); + factoryBuilderFixture = () => () => Symbol.for('value-from-factory'); + providerBuilderFixture = () => async () => + Symbol.for('value-from-provider'); + + (getBindingId as jest.Mock).mockReturnValue( + bindingIdFixture, + ); + + callbackMock = jest.fn(); + containerModuleIdFixture = 1; + defaultScopeFixture = bindingScopeValues.Singleton; + serviceIdentifierFixture = 'service-id'; + + bindToFluentSyntaxImplementation = new BindToFluentSyntaxImplementation( + callbackMock, + containerModuleIdFixture, + defaultScopeFixture, + serviceIdentifierFixture, + ); + }); + + describe.each< + [ + string, + ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation, + ) => unknown, + () => Binding, + NewableFunction, + ] + >([ + [ + '.to()', + ( + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation, + ): unknown => bindToFluentSyntaxImplementation.to(Foo), + (): Binding => ({ + cache: { + isRight: false, + value: undefined, + }, + id: bindingIdFixture, + implementationType: Foo, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + onActivation: undefined, + onDeactivation: undefined, + scope: defaultScopeFixture, + serviceIdentifier: serviceIdentifierFixture, + type: bindingTypeValues.Instance, + }), + BindWhenOnFluentSyntaxImplementation, + ], + [ + '.toConstantValue()', + ( + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation, + ): unknown => + bindToFluentSyntaxImplementation.toConstantValue( + Symbol.for('constant-value'), + ), + (): Binding => ({ + cache: { + isRight: false, + value: undefined, + }, + id: bindingIdFixture, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: serviceIdentifierFixture, + type: bindingTypeValues.ConstantValue, + value: Symbol.for('constant-value'), + }), + BindWhenOnFluentSyntaxImplementation, + ], + [ + '.toDynamicValue()', + ( + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation, + ): unknown => + bindToFluentSyntaxImplementation.toDynamicValue( + dynamicValueBuilderfixture, + ), + (): Binding => ({ + cache: { + isRight: false, + value: undefined, + }, + id: bindingIdFixture, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + onActivation: undefined, + onDeactivation: undefined, + scope: defaultScopeFixture, + serviceIdentifier: serviceIdentifierFixture, + type: bindingTypeValues.DynamicValue, + value: dynamicValueBuilderfixture, + }), + BindWhenOnFluentSyntaxImplementation, + ], + [ + '.toFactory()', + ( + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation< + Factory + >, + ): unknown => + bindToFluentSyntaxImplementation.toFactory(factoryBuilderFixture), + (): Binding => ({ + cache: { + isRight: false, + value: undefined, + }, + factory: factoryBuilderFixture, + id: bindingIdFixture, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: serviceIdentifierFixture, + type: bindingTypeValues.Factory, + }), + BindWhenOnFluentSyntaxImplementation, + ], + [ + '.toProvider()', + ( + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation< + Provider + >, + ): unknown => + bindToFluentSyntaxImplementation.toProvider(providerBuilderFixture), + (): Binding => ({ + cache: { + isRight: false, + value: undefined, + }, + id: bindingIdFixture, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + onActivation: undefined, + onDeactivation: undefined, + provider: providerBuilderFixture, + scope: bindingScopeValues.Singleton, + serviceIdentifier: serviceIdentifierFixture, + type: bindingTypeValues.Provider, + }), + BindWhenOnFluentSyntaxImplementation, + ], + ])( + '%s', + ( + _: string, + buildResult: ( + bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation, + ) => unknown, + buildExpectedBinding: () => Binding, + expectedResultType: NewableFunction, + ) => { + describe('when called', () => { + let expectedBinding: Binding; + let result: unknown; + + beforeAll(() => { + expectedBinding = buildExpectedBinding(); + result = buildResult(bindToFluentSyntaxImplementation); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call getBindingId', () => { + expect(getBindingId).toHaveBeenCalledTimes(1); + expect(getBindingId).toHaveBeenCalledWith(); + }); + + it('should call callback', () => { + expect(callbackMock).toHaveBeenCalledTimes(1); + expect(callbackMock).toHaveBeenCalledWith(expectedBinding); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(expectedResultType); + }); + }); + }, + ); + + describe('.toSelf', () => { + describe('having a non function service identifier', () => { + let callbackMock: jest.Mock<(binding: Binding) => void>; + let containerModuleIdFixture: number; + let defaultScopeFixture: BindingScope; + let serviceIdentifierFixture: ServiceIdentifier; + + let bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation; + + beforeAll(() => { + callbackMock = jest.fn(); + containerModuleIdFixture = 1; + defaultScopeFixture = bindingScopeValues.Singleton; + serviceIdentifierFixture = 'service-id'; + + bindToFluentSyntaxImplementation = new BindToFluentSyntaxImplementation( + callbackMock, + containerModuleIdFixture, + defaultScopeFixture, + serviceIdentifierFixture, + ); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + try { + bindToFluentSyntaxImplementation.toSelf(); + } catch (error: unknown) { + result = error; + } + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should trow an Error', () => { + const expectedErrorProperties: Partial = { + message: + '"toSelf" function can only be applied when a newable function is used as service identifier', + }; + + expect(result).toBeInstanceOf(Error); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); + + describe('having a function service identifier', () => { + class Foo {} + + let callbackMock: jest.Mock<(binding: Binding) => void>; + let containerModuleIdFixture: number; + let defaultScopeFixture: BindingScope; + let serviceIdentifierFixture: ServiceIdentifier; + + let bindToFluentSyntaxImplementation: BindToFluentSyntaxImplementation; + + beforeAll(() => { + callbackMock = jest.fn(); + containerModuleIdFixture = 1; + defaultScopeFixture = bindingScopeValues.Singleton; + serviceIdentifierFixture = Foo; + + bindToFluentSyntaxImplementation = new BindToFluentSyntaxImplementation( + callbackMock, + containerModuleIdFixture, + defaultScopeFixture, + serviceIdentifierFixture, + ); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = bindToFluentSyntaxImplementation.toSelf(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call callback()', () => { + const expectedBinding: InstanceBinding = { + cache: { + isRight: false, + value: undefined, + }, + id: getBindingId(), + implementationType: Foo, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + onActivation: undefined, + onDeactivation: undefined, + scope: defaultScopeFixture, + serviceIdentifier: serviceIdentifierFixture, + type: bindingTypeValues.Instance, + }; + + expect(callbackMock).toHaveBeenCalledTimes(1); + expect(callbackMock).toHaveBeenCalledWith(expectedBinding); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(BindInWhenOnFluentSyntaxImplementation); + }); + }); + }); + }); + + describe('.toService', () => { + describe('when called', () => { + let targetServiceFixture: ServiceIdentifier; + + let result: unknown; + + beforeAll(() => { + targetServiceFixture = 'another-service-id'; + + result = + bindToFluentSyntaxImplementation.toService(targetServiceFixture); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call callback()', () => { + const expectedBinding: ServiceRedirectionBinding = { + id: bindingIdFixture, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: containerModuleIdFixture, + serviceIdentifier: serviceIdentifierFixture, + targetServiceIdentifier: targetServiceFixture, + type: bindingTypeValues.ServiceRedirection, + }; + + expect(callbackMock).toHaveBeenCalledTimes(1); + expect(callbackMock).toHaveBeenCalledWith(expectedBinding); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); +}); + +describe(BindOnFluentSyntaxImplementation.name, () => { + let bindingFixture: Writable>; + let bindingActivationSetterMock: jest.Mock< + (value: BindingActivation | undefined) => undefined + >; + let bindingDeactivationSetterMock: jest.Mock< + (value: BindingDeactivation | undefined) => undefined + >; + + let bindOnFluentSyntaxImplementation: BindOnFluentSyntaxImplementation; + + beforeAll(() => { + bindingActivationSetterMock = jest.fn(); + bindingDeactivationSetterMock = jest.fn(); + + bindingFixture = { + cache: { + isRight: false, + value: undefined, + }, + id: 1, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: undefined, + get onActivation(): BindingActivation | undefined { + return undefined; + }, + set onActivation(value: BindingActivation | undefined) { + bindingActivationSetterMock(value); + }, + get onDeactivation(): BindingDeactivation | undefined { + return undefined; + }, + set onDeactivation(value: BindingDeactivation | undefined) { + bindingDeactivationSetterMock(value); + }, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'service-id', + type: bindingTypeValues.ConstantValue, + value: Symbol.for('constant-value'), + }; + + bindOnFluentSyntaxImplementation = new BindOnFluentSyntaxImplementation( + bindingFixture, + ); + }); + + describe('.onActivation', () => { + describe('when called', () => { + let activationFixture: BindingActivation; + + let result: unknown; + + beforeAll(() => { + activationFixture = (value: unknown) => value; + + result = + bindOnFluentSyntaxImplementation.onActivation(activationFixture); + }); + + it('should set binding activation', () => { + expect(bindingActivationSetterMock).toHaveBeenCalledTimes(1); + expect(bindingActivationSetterMock).toHaveBeenCalledWith( + activationFixture, + ); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(BindWhenFluentSyntaxImplementation); + }); + }); + }); + + describe('.onDeactivation', () => { + describe('when called', () => { + let deactivationFixture: BindingDeactivation; + + let result: unknown; + + beforeAll(() => { + deactivationFixture = () => undefined; + + result = + bindOnFluentSyntaxImplementation.onDeactivation(deactivationFixture); + }); + + it('should set binding deactivation', () => { + expect(bindingDeactivationSetterMock).toHaveBeenCalledTimes(1); + expect(bindingDeactivationSetterMock).toHaveBeenCalledWith( + deactivationFixture, + ); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(BindWhenFluentSyntaxImplementation); + }); + }); + }); +}); + +describe(BindWhenFluentSyntaxImplementation.name, () => { + let bindingFixture: ConstantValueBinding; + + let isSatisfiedBySetterMock: jest.Mock< + (value: (metadata: BindingMetadata) => boolean) => void + >; + + let bindWhenFluentSyntaxImplementation: BindWhenFluentSyntaxImplementation; + + beforeAll(() => { + isSatisfiedBySetterMock = jest.fn(); + + bindingFixture = { + cache: { + isRight: false, + value: undefined, + }, + id: 1, + get isSatisfiedBy() { + return () => true; + }, + set isSatisfiedBy(value: (metadata: BindingMetadata) => boolean) { + isSatisfiedBySetterMock(value); + }, + moduleId: undefined, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'service-id', + type: bindingTypeValues.ConstantValue, + value: Symbol(), + }; + + bindWhenFluentSyntaxImplementation = new BindWhenFluentSyntaxImplementation( + bindingFixture, + ); + }); + + describe('.when', () => { + let constraintFixture: (metadata: BindingMetadata) => boolean; + + let result: unknown; + + beforeAll(() => { + constraintFixture = () => true; + + result = bindWhenFluentSyntaxImplementation.when(constraintFixture); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should set constraint', () => { + expect(isSatisfiedBySetterMock).toHaveBeenCalledTimes(1); + expect(isSatisfiedBySetterMock).toHaveBeenCalledWith(constraintFixture); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(BindOnFluentSyntaxImplementation); + }); + }); +}); + +describe(BindWhenOnFluentSyntaxImplementation.name, () => { + let bindingFixture: Writable>; + let bindingActivationSetterMock: jest.Mock< + (value: BindingActivation | undefined) => undefined + >; + let bindingDeactivationSetterMock: jest.Mock< + (value: BindingDeactivation | undefined) => undefined + >; + + let bindWhenOnFluentSyntaxImplementation: BindWhenOnFluentSyntaxImplementation; + + beforeAll(() => { + bindingActivationSetterMock = jest.fn(); + bindingDeactivationSetterMock = jest.fn(); + + bindingFixture = { + cache: { + isRight: false, + value: undefined, + }, + id: 1, + isSatisfiedBy: expect.any(Function) as unknown as ( + metadata: BindingMetadata, + ) => boolean, + moduleId: undefined, + get onActivation(): BindingActivation | undefined { + return undefined; + }, + set onActivation(value: BindingActivation | undefined) { + bindingActivationSetterMock(value); + }, + get onDeactivation(): BindingDeactivation | undefined { + return undefined; + }, + set onDeactivation(value: BindingDeactivation | undefined) { + bindingDeactivationSetterMock(value); + }, + scope: bindingScopeValues.Singleton, + serviceIdentifier: 'service-id', + type: bindingTypeValues.ConstantValue, + value: Symbol.for('constant-value'), + }; + + bindWhenOnFluentSyntaxImplementation = + new BindWhenOnFluentSyntaxImplementation(bindingFixture); + }); + + describe('.onActivation', () => { + describe('when called', () => { + let activationFixture: BindingActivation; + + let result: unknown; + + beforeAll(() => { + activationFixture = (value: unknown) => value; + + result = + bindWhenOnFluentSyntaxImplementation.onActivation(activationFixture); + }); + + it('should set binding activation', () => { + expect(bindingActivationSetterMock).toHaveBeenCalledTimes(1); + expect(bindingActivationSetterMock).toHaveBeenCalledWith( + activationFixture, + ); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(BindWhenFluentSyntaxImplementation); + }); + }); + }); + + describe('.onDeactivation', () => { + describe('when called', () => { + let deactivationFixture: BindingDeactivation; + + let result: unknown; + + beforeAll(() => { + deactivationFixture = () => undefined; + + result = + bindWhenOnFluentSyntaxImplementation.onDeactivation( + deactivationFixture, + ); + }); + + it('should set binding deactivation', () => { + expect(bindingDeactivationSetterMock).toHaveBeenCalledTimes(1); + expect(bindingDeactivationSetterMock).toHaveBeenCalledWith( + deactivationFixture, + ); + }); + + it('should return expected result', () => { + expect(result).toBeInstanceOf(BindWhenFluentSyntaxImplementation); + }); + }); + }); +}); + +describe(BindInWhenOnFluentSyntaxImplementation.name, () => { + let bindingMock: jest.Mocked< + ScopedBinding + >; + + let bindingMockSetScopeMock: jest.Mock<(value: BindingScope) => void>; + + let bindInWhenOnFluentSyntaxImplementation: BindInWhenOnFluentSyntaxImplementation; + + beforeAll(() => { + let bindingScope: BindingScope = bindingScopeValues.Singleton; + + bindingMockSetScopeMock = jest.fn(); + + bindingMock = { + get scope(): BindingScope { + return bindingScope; + }, + set scope(value: BindingScope) { + bindingMockSetScopeMock(value); + + bindingScope = value; + }, + } as Partial< + jest.Mocked> + > as jest.Mocked>; + + bindInWhenOnFluentSyntaxImplementation = + new BindInWhenOnFluentSyntaxImplementation(bindingMock); + }); + + describe.each< + [ + string, + ( + bindInFluentSyntaxImplementation: BindInWhenOnFluentSyntaxImplementation, + ) => unknown, + BindingScope, + ] + >([ + [ + '.inRequestScope()', + ( + bindInFluentSyntaxImplementation: BindInWhenOnFluentSyntaxImplementation, + ) => bindInFluentSyntaxImplementation.inRequestScope(), + bindingScopeValues.Request, + ], + [ + '.inSingletonScope()', + ( + bindInFluentSyntaxImplementation: BindInWhenOnFluentSyntaxImplementation, + ) => bindInFluentSyntaxImplementation.inSingletonScope(), + bindingScopeValues.Singleton, + ], + [ + '.inTransientScope()', + ( + bindInFluentSyntaxImplementation: BindInWhenOnFluentSyntaxImplementation, + ) => bindInFluentSyntaxImplementation.inTransientScope(), + bindingScopeValues.Transient, + ], + ])( + '%s', + ( + _: string, + buildResult: ( + bindInFluentSyntaxImplementation: BindInWhenOnFluentSyntaxImplementation, + ) => unknown, + expectedScope: BindingScope, + ) => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = buildResult(bindInWhenOnFluentSyntaxImplementation); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should set binding scope', () => { + expect(bindingMockSetScopeMock).toHaveBeenCalledTimes(1); + expect(bindingMockSetScopeMock).toHaveBeenCalledWith(expectedScope); + }); + + it('should return BindWhenOnFluentSyntax', () => { + expect(result).toBeInstanceOf(BindWhenOnFluentSyntaxImplementation); + }); + }); + }, + ); +}); diff --git a/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.ts b/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.ts new file mode 100644 index 0000000..c720d26 --- /dev/null +++ b/packages/container/libraries/container/src/container/binding/models/BindingFluentSyntaxImplementation.ts @@ -0,0 +1,343 @@ +import { Newable, ServiceIdentifier } from '@inversifyjs/common'; +import { + Binding, + BindingActivation, + BindingDeactivation, + BindingMetadata, + BindingScope, + bindingScopeValues, + BindingType, + bindingTypeValues, + ConstantValueBinding, + DynamicValueBinding, + DynamicValueBuilder, + Factory, + FactoryBinding, + InstanceBinding, + Provider, + ProviderBinding, + ResolutionContext, + Resolved, + ScopedBinding, + ServiceRedirectionBinding, +} from '@inversifyjs/core'; + +import { getBindingId } from '../../../binding/actions/getBindingId'; +import { Writable } from '../../../common/models/Writable'; +import { BindingConstraintUtils } from '../utils/BindingConstraintUtils'; +import { + BindInFluentSyntax, + BindInWhenOnFluentSyntax, + BindOnFluentSyntax, + BindToFluentSyntax, + BindWhenFluentSyntax, + BindWhenOnFluentSyntax, +} from './BindingFluentSyntax'; + +export class BindInFluentSyntaxImplementation + implements BindInFluentSyntax +{ + readonly #binding: Writable>; + + constructor(binding: Writable>) { + this.#binding = binding; + } + + public inRequestScope(): BindWhenOnFluentSyntax { + this.#binding.scope = bindingScopeValues.Request; + + return new BindWhenOnFluentSyntaxImplementation(this.#binding); + } + + public inSingletonScope(): BindWhenOnFluentSyntax { + this.#binding.scope = bindingScopeValues.Singleton; + + return new BindWhenOnFluentSyntaxImplementation(this.#binding); + } + + public inTransientScope(): BindWhenOnFluentSyntax { + this.#binding.scope = bindingScopeValues.Transient; + + return new BindWhenOnFluentSyntaxImplementation(this.#binding); + } +} + +export class BindToFluentSyntaxImplementation + implements BindToFluentSyntax +{ + readonly #callback: (binding: Binding) => void; + readonly #containerModuleId: number | undefined; + readonly #defaultScope: BindingScope; + readonly #serviceIdentifier: ServiceIdentifier; + + constructor( + callback: (binding: Binding) => void, + containerModuleId: number | undefined, + defaultScope: BindingScope, + serviceIdentifier: ServiceIdentifier, + ) { + this.#callback = callback; + this.#containerModuleId = containerModuleId; + this.#defaultScope = defaultScope; + this.#serviceIdentifier = serviceIdentifier; + } + + public to(type: Newable): BindInWhenOnFluentSyntax { + const binding: InstanceBinding = { + cache: { + isRight: false, + value: undefined, + }, + id: getBindingId(), + implementationType: type, + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + onActivation: undefined, + onDeactivation: undefined, + scope: this.#defaultScope, + serviceIdentifier: this.#serviceIdentifier, + type: bindingTypeValues.Instance, + }; + + this.#callback(binding); + + return new BindInWhenOnFluentSyntaxImplementation(binding); + } + + public toSelf(): BindInWhenOnFluentSyntax { + if (typeof this.#serviceIdentifier !== 'function') { + throw new Error( + '"toSelf" function can only be applied when a newable function is used as service identifier', + ); + } + + const binding: InstanceBinding = { + cache: { + isRight: false, + value: undefined, + }, + id: getBindingId(), + implementationType: this.#serviceIdentifier as Newable, + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + onActivation: undefined, + onDeactivation: undefined, + scope: this.#defaultScope, + serviceIdentifier: this.#serviceIdentifier, + type: bindingTypeValues.Instance, + }; + + this.#callback(binding); + + return new BindInWhenOnFluentSyntaxImplementation(binding); + } + + public toConstantValue(value: T): BindWhenOnFluentSyntax { + const binding: ConstantValueBinding = { + cache: { + isRight: false, + value: undefined, + }, + id: getBindingId(), + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: this.#serviceIdentifier, + type: bindingTypeValues.ConstantValue, + value: value as Resolved, + }; + + this.#callback(binding); + + return new BindWhenOnFluentSyntaxImplementation(binding); + } + + public toDynamicValue( + builder: DynamicValueBuilder, + ): BindInWhenOnFluentSyntax { + const binding: DynamicValueBinding = { + cache: { + isRight: false, + value: undefined, + }, + id: getBindingId(), + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: this.#serviceIdentifier, + type: bindingTypeValues.DynamicValue, + value: builder, + }; + + this.#callback(binding); + + return new BindInWhenOnFluentSyntaxImplementation(binding); + } + + public toFactory( + builder: T extends Factory + ? (context: ResolutionContext) => T + : never, + ): BindWhenOnFluentSyntax { + const binding: FactoryBinding> = { + cache: { + isRight: false, + value: undefined, + }, + factory: builder, + id: getBindingId(), + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + onActivation: undefined, + onDeactivation: undefined, + scope: bindingScopeValues.Singleton, + serviceIdentifier: this.#serviceIdentifier, + type: bindingTypeValues.Factory, + }; + + this.#callback(binding as Binding as Binding); + + return new BindWhenOnFluentSyntaxImplementation( + binding as Writable>, + ); + } + + public toProvider( + provider: T extends Provider + ? (context: ResolutionContext) => T + : never, + ): BindWhenOnFluentSyntax { + const binding: ProviderBinding> = { + cache: { + isRight: false, + value: undefined, + }, + id: getBindingId(), + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + onActivation: undefined, + onDeactivation: undefined, + provider, + scope: bindingScopeValues.Singleton, + serviceIdentifier: this.#serviceIdentifier, + type: bindingTypeValues.Provider, + }; + + this.#callback(binding as Binding as Binding); + + return new BindWhenOnFluentSyntaxImplementation( + binding as Writable>, + ); + } + + public toService(service: ServiceIdentifier): void { + const binding: ServiceRedirectionBinding = { + id: getBindingId(), + isSatisfiedBy: BindingConstraintUtils.always, + moduleId: this.#containerModuleId, + serviceIdentifier: this.#serviceIdentifier, + targetServiceIdentifier: service, + type: bindingTypeValues.ServiceRedirection, + }; + + this.#callback(binding); + } +} + +export class BindOnFluentSyntaxImplementation + implements BindOnFluentSyntax +{ + readonly #binding: Writable>; + + constructor(binding: Writable>) { + this.#binding = binding; + } + + public onActivation( + activation: BindingActivation, + ): BindWhenFluentSyntax { + this.#binding.onActivation = activation; + + return new BindWhenFluentSyntaxImplementation(this.#binding); + } + + public onDeactivation( + deactivation: BindingDeactivation, + ): BindWhenFluentSyntax { + this.#binding.onDeactivation = deactivation; + + return new BindWhenFluentSyntaxImplementation(this.#binding); + } +} + +export class BindWhenFluentSyntaxImplementation + implements BindWhenFluentSyntax +{ + readonly #binding: Writable>; + + constructor(binding: Writable>) { + this.#binding = binding; + } + + public when( + constraint: (metadata: BindingMetadata) => boolean, + ): BindOnFluentSyntax { + this.#binding.isSatisfiedBy = constraint; + + return new BindOnFluentSyntaxImplementation(this.#binding); + } +} + +export class BindWhenOnFluentSyntaxImplementation + extends BindWhenFluentSyntaxImplementation + implements BindWhenOnFluentSyntax +{ + readonly #bindOnFluentSyntax: BindOnFluentSyntax; + + constructor(binding: Writable>) { + super(binding); + + this.#bindOnFluentSyntax = new BindOnFluentSyntaxImplementation(binding); + } + + public onActivation( + activation: BindingActivation, + ): BindWhenFluentSyntax { + return this.#bindOnFluentSyntax.onActivation(activation); + } + + public onDeactivation( + deactivation: BindingDeactivation, + ): BindWhenFluentSyntax { + return this.#bindOnFluentSyntax.onDeactivation(deactivation); + } +} + +export class BindInWhenOnFluentSyntaxImplementation + extends BindWhenOnFluentSyntaxImplementation + implements BindInWhenOnFluentSyntax +{ + readonly #bindInFluentSyntax: BindInFluentSyntax; + + constructor(binding: Writable>) { + super(binding); + + this.#bindInFluentSyntax = new BindInFluentSyntaxImplementation(binding); + } + + public inRequestScope(): BindWhenOnFluentSyntax { + return this.#bindInFluentSyntax.inRequestScope(); + } + + public inSingletonScope(): BindWhenOnFluentSyntax { + return this.#bindInFluentSyntax.inSingletonScope(); + } + + public inTransientScope(): BindWhenOnFluentSyntax { + return this.#bindInFluentSyntax.inTransientScope(); + } +} diff --git a/packages/container/libraries/container/src/container/binding/utils/BindingConstraintUtils.spec.ts b/packages/container/libraries/container/src/container/binding/utils/BindingConstraintUtils.spec.ts new file mode 100644 index 0000000..086ae94 --- /dev/null +++ b/packages/container/libraries/container/src/container/binding/utils/BindingConstraintUtils.spec.ts @@ -0,0 +1,23 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import { BindingMetadata } from '@inversifyjs/core'; + +import { BindingConstraintUtils } from './BindingConstraintUtils'; + +describe(BindingConstraintUtils.name, () => { + describe('.allways', () => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = BindingConstraintUtils.always( + Symbol() as unknown as BindingMetadata, + ); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + }); +}); diff --git a/packages/container/libraries/container/src/container/binding/utils/BindingConstraintUtils.ts b/packages/container/libraries/container/src/container/binding/utils/BindingConstraintUtils.ts new file mode 100644 index 0000000..c244e00 --- /dev/null +++ b/packages/container/libraries/container/src/container/binding/utils/BindingConstraintUtils.ts @@ -0,0 +1,8 @@ +import { BindingMetadata } from '@inversifyjs/core'; + +export class BindingConstraintUtils { + public static readonly always: (bindingMetadata: BindingMetadata) => boolean = + (_bindingMetadata: BindingMetadata): boolean => { + return true; + }; +}