diff --git a/.travis.yml b/.travis.yml index a8658e7..176a1f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,8 @@ jobs: - npm run report-coverage - stage: release node_js: '8' + before_script: + - npm prune script: - npm run build - npm run semantic-release diff --git a/README.md b/README.md index f271ea7..b711b4f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Declarative and simple IoC container for node.js applications ### Usage ```javascript -import {Container} from '@ukitgroup/ioc'; +import { IoCContainer } from '@ukitgroup/ioc'; class ServiceA {} @@ -35,7 +35,7 @@ const moduleManifest = { // Then in your composition root just create container -const container = new Container(); +const container = new IoCContainer(); container.loadManifests([moduleManifest]); container.compile(); ``` @@ -186,7 +186,7 @@ const moduleManifest = { ], }; -const container = new Container(); +const container = new IoCContainer(); container.loadManifests([moduleManifest]); container.compile(); @@ -198,16 +198,22 @@ http.listen(port); ### Testing We provide a comfortable way for testing ```javascript -import {TestContainer} from '@ukitgroup/ioc'; +import { TestIoCContainer } from '@ukitgroup/ioc'; describe('Unit test', () => { const ctx = {} beforeEach(() => { - ctx.container = TestContainer.createTestModule([ + ctx.container = TestIoCContainer.createTestModule([ //... providers definition with mocks ]) ctx.container.compile(); }); + + it('test case', () => { + // Here you can just get provider by token + // You don't have to transmit module name + const provider = ctx.container.get('providerToken'); + }); }) ``` @@ -216,4 +222,5 @@ More examples you can find in [integration tests](https://github.com/Goodluckhf/ ### TODO: * support decorators with typescript -* TestContainer for integration tests +* TestIoCContainer for integration tests +* Get public providers by tag diff --git a/package-lock.json b/package-lock.json index 96bc5ca..9400fb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ukitgroup/ioc", - "version": "1.0.0", + "version": "0.0.0-dev", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1602,6 +1602,11 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "class-transformer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.2.3.tgz", + "integrity": "sha512-qsP+0xoavpOlJHuYsQJsN58HXSl8Jvveo+T37rEvCEeRfMWoytAyR0Ua/YsFgpM6AZYZ/og2PJwArwzJl1aXtQ==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -1625,6 +1630,15 @@ } } }, + "class-validator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.9.1.tgz", + "integrity": "sha512-3wApflrd3ywVZyx4jaasGoFt8pmo4aGLPPAEKCKCsTRWVGPilahD88q3jQjRQwja50rl9a7rsP5LAxJYwGK8/Q==", + "requires": { + "google-libphonenumber": "^3.1.6", + "validator": "10.4.0" + } + }, "clean-stack": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.1.0.tgz", @@ -3809,6 +3823,11 @@ } } }, + "google-libphonenumber": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.3.tgz", + "integrity": "sha512-8n4JyRptifaIRlHANKRlfqLR8fANm7+Q+1qvDuUsUeStSLtLGTVsZWe1llWDfgWTm1y07cEUyiRuNIv6cs2ovg==" + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -10174,6 +10193,11 @@ } } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "regenerator-runtime": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", @@ -11739,6 +11763,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.4.0.tgz", + "integrity": "sha512-Q/wBy3LB1uOyssgNlXSRmaf22NxjvDNZM2MtIQ4jaEOAB61xsh1TQxsq1CgzUMBV1lDrVMogIh8GjG1DYW0zLg==" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index 9c01d3a..bac2d16 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,10 @@ ] }, "dependencies": { - "eerror": "2.0.0" + "class-transformer": "^0.2.3", + "class-validator": "^0.9.1", + "eerror": "2.0.0", + "reflect-metadata": "^0.1.13" }, "husky": { "hooks": { diff --git a/src/Injector.spec.ts b/src/Injector.spec.ts deleted file mode 100644 index ea8025e..0000000 --- a/src/Injector.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Container } from './container'; -import { CircularDependencyError } from './errors/circular-dependency.error'; - -class ServiceA {} -class ServiceB {} -class ServiceC {} - -describe('Injector', () => { - it('should throw circular dependency', () => { - expect.assertions(1); - - const container = new Container(); - const diManifest = { - moduleName: 'tesModule', - providers: [ - { - token: 'ServiceA', - useClass: ServiceA, - dependencies: ['ServiceB'], - }, - { - token: 'ServiceB', - useClass: ServiceB, - dependencies: ['ServiceA'], - }, - ], - }; - - container.loadManifests([diManifest]); - try { - container.compile(); - } catch (e) { - expect(e).toBeInstanceOf(CircularDependencyError); - } - }); - - it('should throw circular dependency error with long chain', () => { - expect.assertions(1); - const container = new Container(); - const diManifestA = { - moduleName: 'ModuleA', - providers: [ - { - isPublic: true, - token: 'ServiceC', - useClass: ServiceC, - dependencies: [['ServiceA', { fromModule: 'ModuleB' }]], - }, - ], - }; - - const diManifestB = { - moduleName: 'ModuleB', - providers: [ - { - isPublic: true, - token: 'ServiceA', - useClass: ServiceA, - dependencies: ['ServiceB'], - }, - { - token: 'ServiceB', - useClass: ServiceB, - dependencies: [['ServiceC', { fromModule: 'ModuleA' }]], - }, - ], - }; - - // @ts-ignore - container.loadManifests([diManifestA, diManifestB]); - try { - container.compile(); - } catch (e) { - expect(e).toBeInstanceOf(CircularDependencyError); - } - }); -}); diff --git a/src/container.interface.ts b/src/container.interface.ts index f46b1a9..1342bd7 100644 --- a/src/container.interface.ts +++ b/src/container.interface.ts @@ -1,8 +1,8 @@ -import { ManifestInterface } from './manifest.interface'; import { Token } from './internal-types'; +import { ManifestInterface as publicManifestInterface } from './public-interfaces/manifest.interface'; export interface ContainerInterface { - loadManifests(manifests: ManifestInterface[]); + loadManifests(manifests: publicManifestInterface[]); compile(); get(moduleName: string, token: Token); } diff --git a/src/container.spec.ts b/src/container.spec.ts index fb9f968..2a1726d 100644 --- a/src/container.spec.ts +++ b/src/container.spec.ts @@ -1,50 +1,21 @@ +import 'reflect-metadata'; import { Container } from './container'; -import { Module } from './module'; import { AlreadyCompiledError } from './errors/already-compiled.error'; -class TestServiceA {} -class TestServiceB { - constructor(testServiceA) { +describe('IoC container', function() { + beforeEach(() => { // @ts-ignore - this.testServiceA = testServiceA; - } -} - -describe('IoC container', () => { - it('should parse manifest to module', () => { - const container = new Container(); - const diManifest = { - moduleName: 'tesModule', - providers: [ - { - token: 'testServiceA', - useClass: TestServiceA, - }, - ], - }; - - container.loadManifests([diManifest]); - // @ts-ignore - expect(container.modules.size).toEqual(1); - // @ts-ignore - expect([...container.modules.values()][0]).toBeInstanceOf(Module); + this.container = new Container({}, {}); }); it('Should throw error if has already compiled', () => { - const container = new Container(); - const diManifest = { - moduleName: 'tesModule', - providers: [ - { - token: 'testServiceA', - useClass: TestServiceA, - }, - ], - }; - container.loadManifests([diManifest]); - container.compile(); - expect(() => { - container.compile(); - }).toThrow(AlreadyCompiledError); + expect.assertions(1); + this.container.applyPublicProviders = () => {}; + this.container.compile(); + try { + this.container.compile(); + } catch (e) { + expect(e).toBeInstanceOf(AlreadyCompiledError); + } }); }); diff --git a/src/container.ts b/src/container.ts index fd9afa6..8856919 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,7 +1,5 @@ import { ContainerInterface } from './container.interface'; -import { ManifestInterface } from './manifest.interface'; import { buildPublicToken } from './helpers'; -import { InstanceWrapperFactory } from './instance-wrapper-factory'; import { Module } from './module'; import { Injector } from './injector'; import { ModuleHasAlreadyExists } from './errors/module-has-already-exists.error'; @@ -13,24 +11,41 @@ import { ModuleInterface, Token, } from './internal-types'; +import { InstanceWrapperFactoryInterface } from './instance-wrapper-factory.interface'; +import { ManifestTransformerInterface } from './manifest-transformer.interface'; +import { ManifestInterface as publicManifestInterface } from './public-interfaces/manifest.interface'; +import { ManifestInterface } from './dto/manifest.interface'; export class Container implements ContainerInterface { + private readonly instanceWrapperFactory: InstanceWrapperFactoryInterface; + + private readonly manifestTransformer: ManifestTransformerInterface; + private readonly publicProviders: Map; private readonly modules: Map; private compiled: boolean; - public constructor() { + public constructor( + instanceWrapperAbstractFactory: InstanceWrapperFactoryInterface, + manifestTransformer: ManifestTransformerInterface, + ) { + this.instanceWrapperFactory = instanceWrapperAbstractFactory; + this.manifestTransformer = manifestTransformer; + this.publicProviders = new Map(); this.modules = new Map(); this.compiled = false; } - public loadManifests(manifests: ManifestInterface[]) { - const instanceWrapperFactory = new InstanceWrapperFactory(); - manifests.forEach(manifest => { - const newModule = new Module(instanceWrapperFactory, manifest); + public loadManifests(manifestsData: publicManifestInterface[]) { + const parsedManifests: ManifestInterface[] = this.manifestTransformer.transform( + manifestsData, + ); + + parsedManifests.forEach(manifest => { + const newModule = new Module(this.instanceWrapperFactory, manifest); if (this.modules.has(newModule.name)) { throw new ModuleHasAlreadyExists().combine({ module: newModule.name }); } diff --git a/src/dependency.ts b/src/dependency.ts deleted file mode 100644 index 3739dd8..0000000 --- a/src/dependency.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { buildPublicToken } from './helpers'; -import { DependencyInterface, DependencyType, Token } from './internal-types'; - -export class Dependency implements DependencyInterface { - public readonly token: Token; - - public readonly fromModule: string; - - public readonly autoFactory: boolean; - - public constructor(config: DependencyType) { - if (Array.isArray(config)) { - const [token, dependencyOptions] = config; - this.token = token; - this.autoFactory = dependencyOptions && dependencyOptions.autoFactory; - this.fromModule = dependencyOptions && dependencyOptions.fromModule; - if (this.fromModule) { - this.token = buildPublicToken(this.fromModule, this.token); - } - } else { - this.token = config; - this.fromModule = null; - this.autoFactory = false; - } - } -} diff --git a/src/dto/dependency.dto.ts b/src/dto/dependency.dto.ts new file mode 100644 index 0000000..0642199 --- /dev/null +++ b/src/dto/dependency.dto.ts @@ -0,0 +1,32 @@ +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { buildPublicToken } from '../helpers'; +import { DependencyInterface, Token } from '../internal-types'; +import { DependencyType } from '../public-interfaces/dependency.interface'; + +export class DependencyDto implements DependencyInterface { + @IsString() + @IsNotEmpty() + public readonly token: Token; + + @IsOptional() + @IsString() + public readonly fromModule: string | null = null; + + @IsBoolean() + public readonly autoFactory: boolean = false; + + public constructor(config: DependencyType) { + if (Array.isArray(config)) { + const [token, dependencyOptions] = config; + this.token = token; + this.autoFactory = + (dependencyOptions && dependencyOptions.autoFactory) || false; + this.fromModule = dependencyOptions && dependencyOptions.fromModule; + if (this.fromModule) { + this.token = buildPublicToken(this.fromModule, this.token); + } + } else { + this.token = config; + } + } +} diff --git a/src/dto/manifest.dto.ts b/src/dto/manifest.dto.ts new file mode 100644 index 0000000..c2131bc --- /dev/null +++ b/src/dto/manifest.dto.ts @@ -0,0 +1,23 @@ +import { Type } from 'class-transformer'; +import { + ArrayNotEmpty, + IsArray, + IsNotEmpty, + IsString, + ValidateNested, +} from 'class-validator'; +import { ManifestInterface } from './manifest.interface'; +import { ProviderInterface } from './provider.interface'; +import { ProviderDto } from './provider.dto'; + +export class ManifestDto implements ManifestInterface { + @IsNotEmpty() + @IsString() + public moduleName: string; + + @IsArray() + @ArrayNotEmpty() + @ValidateNested() + @Type(() => ProviderDto) + public providers: ProviderInterface[]; +} diff --git a/src/manifest.interface.ts b/src/dto/manifest.interface.ts similarity index 100% rename from src/manifest.interface.ts rename to src/dto/manifest.interface.ts diff --git a/src/dto/provider.dto.ts b/src/dto/provider.dto.ts new file mode 100644 index 0000000..583534b --- /dev/null +++ b/src/dto/provider.dto.ts @@ -0,0 +1,54 @@ +import { Exclude, Transform } from 'class-transformer'; +import { + IsBoolean, + IsDefined, + IsNotEmpty, + IsOptional, + IsString, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { ProviderInterface } from './provider.interface'; +import { ClassType, DependencyInterface } from '../internal-types'; +import { DependencyDto } from './dependency.dto'; + +export class ProviderDto implements ProviderInterface { + @IsBoolean() + public autoFactory: boolean = false; + + @IsOptional() + @ValidateNested() + @Transform(array => array.map(value => new DependencyDto(value))) + public dependencies: DependencyInterface[]; + + @IsBoolean() + public isPublic: boolean = false; + + @IsString() + @IsNotEmpty() + public token: string; + + // @TODO: Убрать Exclude + @ValidateIf( + o => + typeof o.useFactory === 'undefined' && typeof o.useValue === 'undefined', + ) + @IsDefined() + @Exclude() + public useClass: ClassType; + + @ValidateIf( + o => typeof o.useClass === 'undefined' && typeof o.useValue === 'undefined', + ) + @IsDefined() + @Exclude() + public useFactory: (...any) => any; + + @ValidateIf( + o => + typeof o.useFactory === 'undefined' && typeof o.useClass === 'undefined', + ) + @IsDefined() + @Exclude() + public useValue: any; +} diff --git a/src/dto/provider.interface.ts b/src/dto/provider.interface.ts new file mode 100644 index 0000000..e0a0d26 --- /dev/null +++ b/src/dto/provider.interface.ts @@ -0,0 +1,29 @@ +import { ClassType, DependencyInterface } from '../internal-types'; + +export interface ProviderInterface { + token: string; + + isPublic?: boolean; + + autoFactory?: boolean; + + /** + * Arguments of constructor + */ + dependencies?: DependencyInterface[]; + + /** + * Provide by class + */ + useClass?: ClassType; + + /** + * provide by constant + */ + useValue?: any; + + /** + * provide dynamic value + */ + useFactory?: (...any) => any; +} diff --git a/src/errors/already-compiled.error.ts b/src/errors/already-compiled.error.ts index fd35f4d..9b0d36d 100644 --- a/src/errors/already-compiled.error.ts +++ b/src/errors/already-compiled.error.ts @@ -2,7 +2,7 @@ import EError from 'eerror'; const AlreadyCompiledError = EError.prepare({ name: 'AlreadyCompiledError', - message: 'Container must be compile only once', + message: 'IoCContainer must be compile only once', }); export { AlreadyCompiledError }; diff --git a/src/errors/validation.error.ts b/src/errors/validation.error.ts new file mode 100644 index 0000000..8b58e45 --- /dev/null +++ b/src/errors/validation.error.ts @@ -0,0 +1,8 @@ +import EError from 'eerror'; + +const ValidationError = EError.prepare({ + name: 'ValidationError', + message: 'Validation error during transform input manifest', +}); + +export { ValidationError }; diff --git a/src/index.ts b/src/index.ts index ab00fba..7173b79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,16 @@ -export { Container } from './container'; -export { TestContainer } from './test-container'; +import 'reflect-metadata'; +import { Container } from './container'; +import { InstanceWrapperFactory } from './instance-wrapper-factory'; +import { ManifestTransformer } from './manifest-transformer'; + +// export Facade +export class IoCContainer extends Container { + // @ts-ignore + public constructor() { + const instanceWrapperFactory = new InstanceWrapperFactory(); + const manifestTransformer = new ManifestTransformer(); + return new Container(instanceWrapperFactory, manifestTransformer); + } +} + +export { TestIoCContainer } from './test-ioc-container'; diff --git a/src/instance-wrapper-factory.interface.ts b/src/instance-wrapper-factory.interface.ts index 2f33bab..5f4474e 100644 --- a/src/instance-wrapper-factory.interface.ts +++ b/src/instance-wrapper-factory.interface.ts @@ -1,4 +1,4 @@ -import { ProviderInterface } from './provider.interface'; +import { ProviderInterface } from './dto/provider.interface'; import { InstanceWrapperInterface, ModuleInterface } from './internal-types'; export interface InstanceWrapperFactoryInterface { diff --git a/src/instance-wrapper-factory.ts b/src/instance-wrapper-factory.ts index 5d53b42..45af211 100644 --- a/src/instance-wrapper-factory.ts +++ b/src/instance-wrapper-factory.ts @@ -1,9 +1,8 @@ import { InstanceWrapperFactoryInterface } from './instance-wrapper-factory.interface'; -import { ProviderInterface } from './provider.interface'; +import { ProviderInterface } from './dto/provider.interface'; import { ValueInstanceWrapper } from './instance-wrappers/value-instance-wrapper'; import { ClassInstanceWrapper } from './instance-wrappers/class-instance-wrapper'; import { FactoryInstanceWrapper } from './instance-wrappers/factory-instance-wrapper'; -import { Dependency } from './dependency'; import { InstanceWrapperInterface, ModuleInterface } from './internal-types'; export class InstanceWrapperFactory implements InstanceWrapperFactoryInterface { @@ -16,10 +15,6 @@ export class InstanceWrapperFactory implements InstanceWrapperFactoryInterface { token: provider.token, }; - const dependenciesDefinition = Array.isArray(provider.dependencies) - ? provider.dependencies - : []; - if (typeof provider.useValue !== 'undefined') { return new ValueInstanceWrapper(moduleContext, { ...commonInstanceArgs, @@ -28,24 +23,18 @@ export class InstanceWrapperFactory implements InstanceWrapperFactoryInterface { } if (typeof provider.useClass !== 'undefined') { - const dependencies = dependenciesDefinition.map( - dependency => new Dependency(dependency), - ); return new ClassInstanceWrapper(moduleContext, { ...commonInstanceArgs, - dependencies, + dependencies: provider.dependencies, type: provider.useClass, autoFactory: provider.autoFactory, }); } if (typeof provider.useFactory !== 'undefined') { - const dependencies = dependenciesDefinition.map( - dependency => new Dependency(dependency), - ); return new FactoryInstanceWrapper(moduleContext, { ...commonInstanceArgs, - dependencies, + dependencies: provider.dependencies, factory: provider.useFactory, }); } diff --git a/src/integration-tests/multi-modules.spec.ts b/src/integration-tests/multi-modules.spec.ts index 93872ad..cc383fb 100644 --- a/src/integration-tests/multi-modules.spec.ts +++ b/src/integration-tests/multi-modules.spec.ts @@ -1,9 +1,11 @@ -// Module A -import { Container } from '../container'; -import { ManifestInterface } from '../manifest.interface'; +import 'reflect-metadata'; +import { IoCContainer } from '..'; import { NotFoundProviderDefinitionError } from '../errors/not-found-provider-definition.error'; import { ModuleHasAlreadyExists } from '../errors/module-has-already-exists.error'; +import { ManifestInterface } from '../public-interfaces/manifest.interface'; +import { CircularDependencyError } from '../errors/circular-dependency.error'; +// Module A class ServiceA {} class ServiceB { constructor(testServiceA) { @@ -54,7 +56,7 @@ describe('Inverse of Control: multi modules', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([moduleAManifest, moduleBManifest]); try { @@ -95,7 +97,7 @@ describe('Inverse of Control: multi modules', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([moduleAManifest, moduleBManifest]); container.compile(); @@ -130,7 +132,7 @@ describe('Inverse of Control: multi modules', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); try { container.loadManifests([manifestA, manifestB]); container.compile(); @@ -168,7 +170,7 @@ describe('Inverse of Control: multi modules', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([manifestA]); container.compile(); @@ -179,4 +181,44 @@ describe('Inverse of Control: multi modules', function() { expect(instanceOfService.arg2).toEqual(false); expect(instanceOfService.serviceA).toBeInstanceOf(ServiceA); }); + + it('should throw circular dependency error with long chain', () => { + expect.assertions(1); + const container = new IoCContainer(); + const diManifestA: ManifestInterface = { + moduleName: 'ModuleA', + providers: [ + { + isPublic: true, + token: 'ServiceC', + useClass: ServiceC, + dependencies: [['ServiceA', { fromModule: 'ModuleB' }]], + }, + ], + }; + + const diManifestB: ManifestInterface = { + moduleName: 'ModuleB', + providers: [ + { + isPublic: true, + token: 'ServiceA', + useClass: ServiceA, + dependencies: ['ServiceB'], + }, + { + token: 'ServiceB', + useClass: ServiceB, + dependencies: [['ServiceC', { fromModule: 'ModuleA' }]], + }, + ], + }; + + container.loadManifests([diManifestA, diManifestB]); + try { + container.compile(); + } catch (e) { + expect(e).toBeInstanceOf(CircularDependencyError); + } + }); }); diff --git a/src/integration-tests/one-module.spec.ts b/src/integration-tests/one-module.spec.ts index ed30c34..59853f6 100644 --- a/src/integration-tests/one-module.spec.ts +++ b/src/integration-tests/one-module.spec.ts @@ -1,7 +1,9 @@ -import { ManifestInterface } from '../manifest.interface'; -import { Container } from '../container'; +import 'reflect-metadata'; +import { IoCContainer } from '..'; import { NotFoundProviderDefinitionError } from '../errors/not-found-provider-definition.error'; import { RequiredAutoFactoryDefinitionError } from '../errors/required-autofactory-definition.error'; +import { ManifestInterface } from '../public-interfaces/manifest.interface'; +import { CircularDependencyError } from '../errors/circular-dependency.error'; class TestServiceA {} class TestServiceB { @@ -29,7 +31,7 @@ describe('Inverse of Control: one module', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([diManifest]); container.compile(); this.container = container; @@ -71,7 +73,7 @@ describe('Inverse of Control: one module', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([diManifest]); container.compile(); expect(serviceInjectedToFactory).toBeTruthy(); @@ -93,7 +95,7 @@ describe('Inverse of Control: one module', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([diManifest]); container.compile(); @@ -120,7 +122,7 @@ describe('Inverse of Control: one module', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([diManifest]); container.compile(); @@ -151,7 +153,7 @@ describe('Inverse of Control: one module', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([diManifest]); try { @@ -179,7 +181,7 @@ describe('Inverse of Control: one module', function() { ], }; - const container = new Container(); + const container = new IoCContainer(); container.loadManifests([diManifest]); container.compile(); @@ -188,4 +190,32 @@ describe('Inverse of Control: one module', function() { expect(serviceB).toBeInstanceOf(TestServiceB); expect(new serviceB.testServiceA()).toBeInstanceOf(TestServiceA); }); + + it('should throw circular dependency', () => { + expect.assertions(1); + + const container = new IoCContainer(); + const diManifest: ManifestInterface = { + moduleName: 'tesModule', + providers: [ + { + token: 'ServiceA', + useClass: TestServiceA, + dependencies: ['ServiceB'], + }, + { + token: 'ServiceB', + useClass: TestServiceB, + dependencies: ['ServiceA'], + }, + ], + }; + + container.loadManifests([diManifest]); + try { + container.compile(); + } catch (e) { + expect(e).toBeInstanceOf(CircularDependencyError); + } + }); }); diff --git a/src/integration-tests/test-container.spec.ts b/src/integration-tests/test-container.spec.ts index 4c0ca2c..1311dd8 100644 --- a/src/integration-tests/test-container.spec.ts +++ b/src/integration-tests/test-container.spec.ts @@ -1,10 +1,11 @@ -import { TestContainer } from '../test-container'; -import { ManifestInterface } from '../manifest.interface'; +import 'reflect-metadata'; +import { TestIoCContainer } from '../test-ioc-container'; +import { ManifestInterface } from '../dto/manifest.interface'; class ServiceA {} -describe.skip('TestContainer', () => { - it('should override providers', () => { +describe('TestContainer', () => { + it.skip('should override providers', () => { const diManifest: ManifestInterface = { moduleName: 'module', providers: [ @@ -16,7 +17,7 @@ describe.skip('TestContainer', () => { ], }; - const testContainerAdapter = new TestContainer([diManifest]); + const testContainerAdapter = new TestIoCContainer([diManifest]); // @ts-ignore testContainerAdapter.override([ { token: 'ServiceA', useValue: 10, isPublic: true }, @@ -26,4 +27,13 @@ describe.skip('TestContainer', () => { const serviceA = testContainerAdapter.get('ServiceA'); expect(serviceA).toEqual(10); }); + + it('can get provider without module', () => { + const testContainer = TestIoCContainer.createTestModule([ + { isPublic: true, token: 'testToken', useValue: 10 }, + ]); + + testContainer.compile(); + expect(testContainer.get('testToken')).toBe(10); + }); }); diff --git a/src/internal-types.ts b/src/internal-types.ts index acc3f43..64313f7 100644 --- a/src/internal-types.ts +++ b/src/internal-types.ts @@ -10,15 +10,6 @@ export interface ClassType { export type Token = string; -export interface DependencyOptions { - autoFactory?: boolean; - fromModule?: string; -} - -export type CustomDependencyType = [Token, DependencyOptions]; - -export type DependencyType = Token | CustomDependencyType; - // Due to circular dependency export interface ModuleInterface { readonly name: string; diff --git a/src/manifest-transformer.interface.ts b/src/manifest-transformer.interface.ts new file mode 100644 index 0000000..68adde1 --- /dev/null +++ b/src/manifest-transformer.interface.ts @@ -0,0 +1,6 @@ +import { ManifestInterface } from './dto/manifest.interface'; +import { ManifestInterface as PublicManifestInterface } from './public-interfaces/manifest.interface'; + +export interface ManifestTransformerInterface { + transform(manifestsData: PublicManifestInterface[]): ManifestInterface[]; +} diff --git a/src/manifest-transformer.spec.ts b/src/manifest-transformer.spec.ts new file mode 100644 index 0000000..6dc9b2d --- /dev/null +++ b/src/manifest-transformer.spec.ts @@ -0,0 +1,126 @@ +import 'reflect-metadata'; +import { ManifestTransformer } from './manifest-transformer'; +import { ManifestInterface } from './public-interfaces/manifest.interface'; +import { ManifestTransformerInterface } from './manifest-transformer.interface'; +import { ValidationError } from './errors/validation.error'; + +class A {} +describe('ManifestTransformer', () => { + let correctManifest: ManifestInterface; + + let manifestTransformer: ManifestTransformerInterface; + + beforeEach(() => { + manifestTransformer = new ManifestTransformer(); + correctManifest = { + moduleName: 'testModule', + providers: [ + { token: 'testToken', useClass: A }, + { token: 'testTokenB', useFactory: () => {}, isPublic: true }, + { token: 'testTokenE', useValue: 10 }, + { token: 'testTokenC', useClass: A, dependencies: ['testTokenB'] }, + { + token: 'testTokenD', + useClass: A, + dependencies: [['testTokenB', { fromModule: 'ModuleB' }]], + }, + { + token: 'testTokenD', + useFactory: () => {}, + dependencies: [ + ['testTokenB', { fromModule: 'ModuleB', autoFactory: true }], + ], + }, + ], + }; + }); + + it('should throw error if moduleName is not provided', () => { + expect.assertions(1); + delete correctManifest.moduleName; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if there is no providers', () => { + expect.assertions(1); + delete correctManifest.providers; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if there is no token in provider', () => { + expect.assertions(1); + delete correctManifest.providers[0].token; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if isPublic not boolean', () => { + expect.assertions(1); + // @ts-ignore + correctManifest.providers[0].isPublic = 'test'; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if autoFactory not boolean', () => { + expect.assertions(1); + // @ts-ignore + correctManifest.providers[0].autoFactory = 'test'; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if token is not string in simple dependency', () => { + expect.assertions(1); + // @ts-ignore + correctManifest.providers[3].dependencies[0] = 10; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if token is not string in custom dependency', () => { + expect.assertions(1); + // @ts-ignore + correctManifest.providers[4].dependencies[0] = {}; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should throw error if non of useClass, useValue, useFactory provided', () => { + expect.assertions(1); + // @ts-ignore + delete correctManifest.providers[0].useClass; + try { + manifestTransformer.transform([correctManifest]); + } catch (e) { + expect(e).toBeInstanceOf(ValidationError); + } + }); + + it('should not throw validation error', () => { + manifestTransformer.transform([correctManifest]); + }); +}); diff --git a/src/manifest-transformer.ts b/src/manifest-transformer.ts new file mode 100644 index 0000000..11829bf --- /dev/null +++ b/src/manifest-transformer.ts @@ -0,0 +1,45 @@ +import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; +import { ValidationError } from './errors/validation.error'; +import { ManifestTransformerInterface } from './manifest-transformer.interface'; +import { ManifestDto } from './dto/manifest.dto'; +import { ManifestInterface } from './dto/manifest.interface'; +import { ManifestInterface as PublicManifestInterface } from './public-interfaces/manifest.interface'; + +export class ManifestTransformer implements ManifestTransformerInterface { + public transform( + manifestsData: PublicManifestInterface[], + ): ManifestInterface[] { + return manifestsData.map(manifestData => { + const manifest = plainToClass(ManifestDto, manifestData); + // https://github.com/typestack/class-transformer/issues/276 + // @TODO: Переделать полностью на plainToClass + const providers = manifest.providers || []; + providers.forEach((provider, key) => { + if (manifestData.providers[key].useValue) { + // eslint-disable-next-line no-param-reassign + provider.useValue = manifestData.providers[key].useValue; + return; + } + if (manifestData.providers[key].useClass) { + // eslint-disable-next-line no-param-reassign + provider.useClass = manifestData.providers[key].useClass; + return; + } + if (manifestData.providers[key].useFactory) { + // eslint-disable-next-line no-param-reassign + provider.useFactory = manifestData.providers[key].useFactory; + } + }); + + const errors = validateSync(manifest, { + skipMissingProperties: false, + forbidNonWhitelisted: true, + }); + if (errors.length > 0) { + throw new ValidationError().combine({ errors }); + } + return manifest; + }); + } +} diff --git a/src/module.spec.ts b/src/module.spec.ts index ddb71dc..5a3f2a3 100644 --- a/src/module.spec.ts +++ b/src/module.spec.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import { Module } from './module'; import { TokenAlreadyUsedError } from './errors/token-already-used.error'; @@ -31,4 +32,38 @@ describe('Module', () => { expect(e).toBeInstanceOf(TokenAlreadyUsedError); } }); + + it('should get public providers', () => { + const diManifest = { + moduleName: 'tesModule', + providers: [ + { + isPublic: true, + token: 'ServiceA', + useValue: 10, + }, + { + token: 'ServiceB', + useValue: 11, + }, + ], + }; + + const newModule = new Module( + { + // @ts-ignore + create(value) { + return value; + }, + }, + diManifest, + ); + + const publicProviders = newModule.getPublicProviders(); + const publicProvider = publicProviders.find( + object => object.token === 'ServiceA', + ); + expect(publicProviders.length).toEqual(1); + expect(publicProvider).not.toBeNull(); + }); }); diff --git a/src/module.ts b/src/module.ts index d30f88a..5a0abcc 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,6 +1,6 @@ import { InstanceWrapperFactoryInterface } from './instance-wrapper-factory.interface'; -import { ManifestInterface } from './manifest.interface'; -import { ProviderInterface } from './provider.interface'; +import { ManifestInterface } from './dto/manifest.interface'; +import { ProviderInterface } from './dto/provider.interface'; import { TokenAlreadyUsedError } from './errors/token-already-used.error'; import { InstanceWrapperInterface, diff --git a/src/public-interfaces/dependency.interface.ts b/src/public-interfaces/dependency.interface.ts new file mode 100644 index 0000000..a134326 --- /dev/null +++ b/src/public-interfaces/dependency.interface.ts @@ -0,0 +1,10 @@ +import { Token } from '../internal-types'; + +export interface DependencyOptions { + autoFactory?: boolean; + fromModule?: string; +} + +export type CustomDependencyType = [Token, DependencyOptions]; + +export type DependencyType = Token | CustomDependencyType; diff --git a/src/public-interfaces/manifest.interface.ts b/src/public-interfaces/manifest.interface.ts new file mode 100644 index 0000000..eddbc76 --- /dev/null +++ b/src/public-interfaces/manifest.interface.ts @@ -0,0 +1,6 @@ +import { ProviderInterface } from './provider.interface'; + +export interface ManifestInterface { + moduleName: string; + providers: ProviderInterface[]; +} diff --git a/src/provider.interface.ts b/src/public-interfaces/provider.interface.ts similarity index 78% rename from src/provider.interface.ts rename to src/public-interfaces/provider.interface.ts index ed29358..507c162 100644 --- a/src/provider.interface.ts +++ b/src/public-interfaces/provider.interface.ts @@ -1,4 +1,5 @@ -import { ClassType, DependencyType } from './internal-types'; +import { DependencyType } from './dependency.interface'; +import { ClassType } from '../internal-types'; export interface ProviderInterface { token: string; diff --git a/src/test-container.ts b/src/test-ioc-container.ts similarity index 57% rename from src/test-container.ts rename to src/test-ioc-container.ts index 9fa1e13..07b3cd9 100644 --- a/src/test-container.ts +++ b/src/test-ioc-container.ts @@ -1,14 +1,18 @@ import { Container } from './container'; -import { ProviderInterface } from './provider.interface'; +import { ProviderInterface } from './dto/provider.interface'; import { Token } from './internal-types'; +import { InstanceWrapperFactory } from './instance-wrapper-factory'; +import { ManifestTransformer } from './manifest-transformer'; const testModuleName = 'testModule'; -class TestContainer { +class TestIoCContainer { private container: Container; public constructor(manifests) { - this.container = new Container(); + const instanceWrapperFactory = new InstanceWrapperFactory(); + const manifestTransformer = new ManifestTransformer(); + this.container = new Container(instanceWrapperFactory, manifestTransformer); this.container.loadManifests(manifests); } @@ -33,4 +37,4 @@ class TestContainer { } } -export { TestContainer }; +export { TestIoCContainer }; diff --git a/tsconfig.json b/tsconfig.json index 2eae6e6..132096e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "module": "commonjs", "declaration": true, "removeComments": true, - "emitDecoratorMetadata": false, - "experimentalDecorators": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, "target": "es2017", "sourceMap": true, "outDir": "./dist",