diff --git a/CHANGELOG.md b/CHANGELOG.md index d6191525a..91e41a865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- createTaggedDecorator #1343 - Async bindings #1132 - Async binding resolution (getAllAsync, getAllNamedAsync, getAllTaggedAsync, getAsync, getNamedAsync, getTaggedAsync, rebindAsync, unbindAsync, unbindAllAsync, unloadAsync) #1132 - Global onActivation / onDeactivation #1132 @@ -17,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - @postConstruct can target an asyncronous function #1132 ### Fixed +- only inject decorator can be applied to setters #1342 - Container.resolve should resolve in that container #1338 ## [5.1.1] - 2021-04-25 -Fix pre-publish for build artifacts diff --git a/src/annotation/decorator_utils.ts b/src/annotation/decorator_utils.ts index eda3573e7..95856fea7 100644 --- a/src/annotation/decorator_utils.ts +++ b/src/annotation/decorator_utils.ts @@ -1,68 +1,117 @@ import * as ERROR_MSGS from "../constants/error_msgs"; import * as METADATA_KEY from "../constants/metadata_keys"; import { interfaces } from "../interfaces/interfaces"; +import { getFirstArrayDuplicate } from "../utils/js"; + +function targetIsConstructorFunction(target:DecoratorTarget): target is ConstructorFunction{ + return (target as ConstructorFunction).prototype !== undefined; +} + +type Prototype = { + [Property in keyof T ]: + T[Property] extends Function? + T[Property] : + T[Property] | undefined +} & {constructor:Function} + +interface ConstructorFunction{ + new (...args:unknown[]): T, + prototype:Prototype +} + +export type DecoratorTarget = ConstructorFunction | Prototype + +function _throwIfMethodParameter(parameterName:string | symbol | undefined):void { + if(parameterName !== undefined) { + throw new Error(ERROR_MSGS.INVALID_DECORATOR_OPERATION); + } +} + function tagParameter( - annotationTarget: any, - propertyName: string, + annotationTarget: DecoratorTarget, + parameterName: string | symbol | undefined, parameterIndex: number, - metadata: interfaces.Metadata + metadata: interfaces.MetadataOrMetadataArray ) { - const metadataKey = METADATA_KEY.TAGGED; - _tagParameterOrProperty(metadataKey, annotationTarget, propertyName, metadata, parameterIndex); + _throwIfMethodParameter(parameterName); + _tagParameterOrProperty(METADATA_KEY.TAGGED, annotationTarget as ConstructorFunction, parameterIndex.toString(), metadata); } function tagProperty( - annotationTarget: any, - propertyName: string, - metadata: interfaces.Metadata + annotationTarget: DecoratorTarget, + propertyName: string | symbol, + metadata: interfaces.MetadataOrMetadataArray ) { - const metadataKey = METADATA_KEY.TAGGED_PROP; - _tagParameterOrProperty(metadataKey, annotationTarget.constructor, propertyName, metadata); + if(targetIsConstructorFunction(annotationTarget)) { + throw new Error(ERROR_MSGS.INVALID_DECORATOR_OPERATION); + } + _tagParameterOrProperty(METADATA_KEY.TAGGED_PROP, annotationTarget.constructor, propertyName, metadata); +} + +function _ensureNoMetadataKeyDuplicates(metadata: interfaces.MetadataOrMetadataArray):interfaces.Metadata[]{ + let metadatas: interfaces.Metadata[] = []; + if(Array.isArray(metadata)){ + metadatas = metadata; + const duplicate = getFirstArrayDuplicate(metadatas.map(md => md.key)); + if(duplicate !== undefined) { + throw new Error(`${ERROR_MSGS.DUPLICATED_METADATA} ${duplicate.toString()}`); + } + }else{ + metadatas = [metadata]; + } + return metadatas; } function _tagParameterOrProperty( metadataKey: string, - annotationTarget: any, - propertyName: string, - metadata: interfaces.Metadata, - parameterIndex?: number + annotationTarget: Function, + key: string | symbol, + metadata: interfaces.MetadataOrMetadataArray, ) { + const metadatas: interfaces.Metadata[] = _ensureNoMetadataKeyDuplicates(metadata); - let paramsOrPropertiesMetadata: interfaces.ReflectResult = {}; - const isParameterDecorator = (typeof parameterIndex === "number"); - const key: string = (parameterIndex !== undefined && isParameterDecorator) ? parameterIndex.toString() : propertyName; - - // if the decorator is used as a parameter decorator, the property name must be provided - if (isParameterDecorator && propertyName !== undefined) { - throw new Error(ERROR_MSGS.INVALID_DECORATOR_OPERATION); - } - + let paramsOrPropertiesMetadata:Record = {}; // read metadata if available if (Reflect.hasOwnMetadata(metadataKey, annotationTarget)) { paramsOrPropertiesMetadata = Reflect.getMetadata(metadataKey, annotationTarget); } - // get metadata for the decorated parameter by its index - let paramOrPropertyMetadata: interfaces.Metadata[] = paramsOrPropertiesMetadata[key]; + let paramOrPropertyMetadata: interfaces.Metadata[] | undefined = paramsOrPropertiesMetadata[key as any]; - if (!Array.isArray(paramOrPropertyMetadata)) { + if (paramOrPropertyMetadata === undefined) { paramOrPropertyMetadata = []; } else { for (const m of paramOrPropertyMetadata) { - if (m.key === metadata.key) { + if (metadatas.some(md => md.key === m.key)) { throw new Error(`${ERROR_MSGS.DUPLICATED_METADATA} ${m.key.toString()}`); } } } // set metadata - paramOrPropertyMetadata.push(metadata); - paramsOrPropertiesMetadata[key] = paramOrPropertyMetadata; + paramOrPropertyMetadata.push(...metadatas); + paramsOrPropertiesMetadata[key as any] = paramOrPropertyMetadata; Reflect.defineMetadata(metadataKey, paramsOrPropertiesMetadata, annotationTarget); } +function createTaggedDecorator( + metadata: interfaces.MetadataOrMetadataArray, +) { + return ( + target: DecoratorTarget, + targetKey?: string | symbol, + indexOrPropertyDescriptor?: number | TypedPropertyDescriptor, + ) => { + if (typeof indexOrPropertyDescriptor === "number") { + tagParameter(target, targetKey, indexOrPropertyDescriptor, metadata); + } else { + tagProperty(target, targetKey as string | symbol, metadata); + } + }; +} + function _decorate(decorators: any[], target: any): void { Reflect.decorate(decorators, target); } @@ -72,22 +121,22 @@ function _param(paramIndex: number, decorator: ParameterDecorator) { } // Allows VanillaJS developers to use decorators: -// decorate(injectable("Foo", "Bar"), FooBar); +// decorate(injectable(), FooBar); // decorate(targetName("foo", "bar"), FooBar); // decorate(named("foo"), FooBar, 0); // decorate(tagged("bar"), FooBar, 1); function decorate( decorator: (ClassDecorator | ParameterDecorator | MethodDecorator), - target: any, - parameterIndex?: number | string): void { + target: object, + parameterIndexOrProperty?: number | string): void { - if (typeof parameterIndex === "number") { - _decorate([_param(parameterIndex, decorator as ParameterDecorator)], target); - } else if (typeof parameterIndex === "string") { - Reflect.decorate([decorator as MethodDecorator], target, parameterIndex); + if (typeof parameterIndexOrProperty === "number") { + _decorate([_param(parameterIndexOrProperty, decorator as ParameterDecorator)], target); + } else if (typeof parameterIndexOrProperty === "string") { + Reflect.decorate([decorator as MethodDecorator], target, parameterIndexOrProperty); } else { _decorate([decorator as ClassDecorator], target); } } -export { decorate, tagParameter, tagProperty }; +export { decorate, tagParameter, tagProperty, createTaggedDecorator }; diff --git a/src/annotation/inject.ts b/src/annotation/inject.ts index 9e67fa30a..7f56543d2 100644 --- a/src/annotation/inject.ts +++ b/src/annotation/inject.ts @@ -1,37 +1,6 @@ -import { UNDEFINED_INJECT_ANNOTATION } from "../constants/error_msgs"; import * as METADATA_KEY from "../constants/metadata_keys"; -import { interfaces } from "../interfaces/interfaces"; -import { Metadata } from "../planning/metadata"; -import { tagParameter, tagProperty } from "./decorator_utils"; +import { injectBase } from "./inject_base"; -export type ServiceIdentifierOrFunc = interfaces.ServiceIdentifier | LazyServiceIdentifer; - -export class LazyServiceIdentifer { - private _cb: () => interfaces.ServiceIdentifier; - public constructor(cb: () => interfaces.ServiceIdentifier) { - this._cb = cb; - } - - public unwrap() { - return this._cb(); - } -} - -function inject(serviceIdentifier: ServiceIdentifierOrFunc) { - return function(target: any, targetKey: string, index?: number | PropertyDescriptor): void { - if (serviceIdentifier === undefined) { - throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name)); - } - - const metadata = new Metadata(METADATA_KEY.INJECT_TAG, serviceIdentifier); - - if (typeof index === "number") { - tagParameter(target, targetKey, index, metadata); - } else { - tagProperty(target, targetKey, metadata); - } - - }; -} +const inject = injectBase(METADATA_KEY.INJECT_TAG); export { inject }; diff --git a/src/annotation/inject_base.ts b/src/annotation/inject_base.ts new file mode 100644 index 000000000..0f2456f11 --- /dev/null +++ b/src/annotation/inject_base.ts @@ -0,0 +1,23 @@ +import { UNDEFINED_INJECT_ANNOTATION } from "../constants/error_msgs"; +import { Metadata } from "../planning/metadata"; +import { createTaggedDecorator, DecoratorTarget } from "./decorator_utils"; +import { ServiceIdentifierOrFunc } from "./lazy_service_identifier"; + +export function injectBase(metadataKey: string) { + return (serviceIdentifier: ServiceIdentifierOrFunc) => { + return ( + target: DecoratorTarget, + targetKey?: string | symbol, + indexOrPropertyDescriptor?: number | TypedPropertyDescriptor, + ) => { + if (serviceIdentifier === undefined) { + const className = typeof target === "function" ? target.name : target.constructor.name; + + throw new Error(UNDEFINED_INJECT_ANNOTATION(className)); + } + return createTaggedDecorator( + new Metadata(metadataKey, serviceIdentifier) + )(target, targetKey, indexOrPropertyDescriptor); + }; + } +} diff --git a/src/annotation/lazy_service_identifier.ts b/src/annotation/lazy_service_identifier.ts new file mode 100644 index 000000000..8b75dad59 --- /dev/null +++ b/src/annotation/lazy_service_identifier.ts @@ -0,0 +1,14 @@ +import { interfaces } from "../interfaces/interfaces"; + +export type ServiceIdentifierOrFunc = interfaces.ServiceIdentifier | LazyServiceIdentifer; + +export class LazyServiceIdentifer { + private _cb: () => interfaces.ServiceIdentifier; + public constructor(cb: () => interfaces.ServiceIdentifier) { + this._cb = cb; + } + + public unwrap() { + return this._cb(); + } +} diff --git a/src/annotation/multi_inject.ts b/src/annotation/multi_inject.ts index c479e28c8..d874ae3ef 100644 --- a/src/annotation/multi_inject.ts +++ b/src/annotation/multi_inject.ts @@ -1,20 +1,6 @@ import * as METADATA_KEY from "../constants/metadata_keys"; -import { interfaces } from "../interfaces/interfaces"; -import { Metadata } from "../planning/metadata"; -import { tagParameter, tagProperty } from "./decorator_utils"; +import { injectBase } from "./inject_base"; -function multiInject(serviceIdentifier: interfaces.ServiceIdentifier) { - return function(target: any, targetKey: string, index?: number) { - - const metadata = new Metadata(METADATA_KEY.MULTI_INJECT_TAG, serviceIdentifier); - - if (typeof index === "number") { - tagParameter(target, targetKey, index, metadata); - } else { - tagProperty(target, targetKey, metadata); - } - - }; -} +const multiInject = injectBase(METADATA_KEY.MULTI_INJECT_TAG); export { multiInject }; diff --git a/src/annotation/named.ts b/src/annotation/named.ts index 004776efe..a13a83780 100644 --- a/src/annotation/named.ts +++ b/src/annotation/named.ts @@ -1,17 +1,10 @@ import * as METADATA_KEY from "../constants/metadata_keys"; import { Metadata } from "../planning/metadata"; -import { tagParameter, tagProperty } from "./decorator_utils"; +import { createTaggedDecorator } from "./decorator_utils"; // Used to add named metadata which is used to resolve name-based contextual bindings. function named(name: string | number | symbol) { - return function(target: any, targetKey: string, index?: number) { - const metadata = new Metadata(METADATA_KEY.NAMED_TAG, name); - if (typeof index === "number") { - tagParameter(target, targetKey, index, metadata); - } else { - tagProperty(target, targetKey, metadata); - } - }; + return createTaggedDecorator(new Metadata(METADATA_KEY.NAMED_TAG, name)); } export { named }; diff --git a/src/annotation/optional.ts b/src/annotation/optional.ts index 25a766a89..31de6d5ef 100644 --- a/src/annotation/optional.ts +++ b/src/annotation/optional.ts @@ -1,19 +1,9 @@ import * as METADATA_KEY from "../constants/metadata_keys"; import { Metadata } from "../planning/metadata"; -import { tagParameter, tagProperty } from "./decorator_utils"; +import { createTaggedDecorator } from "./decorator_utils"; function optional() { - return function(target: any, targetKey: string, index?: number) { - - const metadata = new Metadata(METADATA_KEY.OPTIONAL_TAG, true); - - if (typeof index === "number") { - tagParameter(target, targetKey, index, metadata); - } else { - tagProperty(target, targetKey, metadata); - } - - }; + return createTaggedDecorator(new Metadata(METADATA_KEY.OPTIONAL_TAG, true)); } export { optional }; diff --git a/src/annotation/tagged.ts b/src/annotation/tagged.ts index e5055e731..c5a8786ff 100644 --- a/src/annotation/tagged.ts +++ b/src/annotation/tagged.ts @@ -1,16 +1,9 @@ import { Metadata } from "../planning/metadata"; -import { tagParameter, tagProperty } from "./decorator_utils"; +import { createTaggedDecorator } from "./decorator_utils"; // Used to add custom metadata which is used to resolve metadata-based contextual bindings. function tagged(metadataKey: string | number | symbol, metadataValue: any) { - return function(target: any, targetKey: string, index?: number) { - const metadata = new Metadata(metadataKey, metadataValue); - if (typeof index === "number") { - tagParameter(target, targetKey, index, metadata); - } else { - tagProperty(target, targetKey, metadata); - } - }; + return createTaggedDecorator(new Metadata(metadataKey, metadataValue)); } export { tagged }; diff --git a/src/annotation/target_name.ts b/src/annotation/target_name.ts index 032a1a3f9..3e36bc3a0 100644 --- a/src/annotation/target_name.ts +++ b/src/annotation/target_name.ts @@ -1,9 +1,9 @@ import * as METADATA_KEY from "../constants/metadata_keys"; import { Metadata } from "../planning/metadata"; -import { tagParameter } from "./decorator_utils"; +import { tagParameter, DecoratorTarget } from "./decorator_utils"; function targetName(name: string) { - return function(target: any, targetKey: string, index: number) { + return function(target: DecoratorTarget, targetKey: string, index: number) { const metadata = new Metadata(METADATA_KEY.NAME_TAG, name); tagParameter(target, targetKey, index, metadata); }; diff --git a/src/annotation/unmanaged.ts b/src/annotation/unmanaged.ts index 798484568..becb37854 100644 --- a/src/annotation/unmanaged.ts +++ b/src/annotation/unmanaged.ts @@ -1,9 +1,9 @@ import * as METADATA_KEY from "../constants/metadata_keys"; import { Metadata } from "../planning/metadata"; -import { tagParameter } from "./decorator_utils"; +import { tagParameter, DecoratorTarget } from "./decorator_utils"; function unmanaged() { - return function(target: any, targetKey: string, index: number) { + return function(target: DecoratorTarget, targetKey: string, index: number) { const metadata = new Metadata(METADATA_KEY.UNMANAGED_TAG, true); tagParameter(target, targetKey, index, metadata); }; diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 5a92dada7..3b6a3da7d 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -111,9 +111,7 @@ namespace interfaces { setCurrentRequest(request: Request): void; } - export interface ReflectResult { - [key: string]: Metadata[]; - } + export type MetadataOrMetadataArray = Metadata | Metadata[]; export interface Metadata { key: string | number | symbol; @@ -160,6 +158,7 @@ namespace interfaces { serviceIdentifier: ServiceIdentifier; type: TargetType; name: QueryableString; + identifier: string | symbol; metadata: Metadata[]; getNamedTag(): interfaces.Metadata | null; getCustomTags(): interfaces.Metadata[] | null; diff --git a/src/inversify.ts b/src/inversify.ts index b23403e52..8307f4f99 100644 --- a/src/inversify.ts +++ b/src/inversify.ts @@ -3,10 +3,12 @@ export const METADATA_KEY = keys; export { Container } from "./container/container"; export { BindingScopeEnum, BindingTypeEnum, TargetTypeEnum } from "./constants/literal_types"; export { AsyncContainerModule, ContainerModule } from "./container/container_module"; +export { createTaggedDecorator } from "./annotation/decorator_utils" export { injectable } from "./annotation/injectable"; export { tagged } from "./annotation/tagged"; export { named } from "./annotation/named"; -export { inject, LazyServiceIdentifer } from "./annotation/inject"; +export { inject } from "./annotation/inject"; +export { LazyServiceIdentifer } from "./annotation/lazy_service_identifier" export { optional } from "./annotation/optional"; export { unmanaged } from "./annotation/unmanaged"; export { multiInject } from "./annotation/multi_inject"; diff --git a/src/planning/reflection_utils.ts b/src/planning/reflection_utils.ts index f538f98d1..8643187a8 100644 --- a/src/planning/reflection_utils.ts +++ b/src/planning/reflection_utils.ts @@ -1,4 +1,4 @@ -import { LazyServiceIdentifer } from "../annotation/inject"; +import { LazyServiceIdentifer } from "../annotation/lazy_service_identifier"; import * as ERROR_MSGS from "../constants/error_msgs"; import { TargetTypeEnum } from "../constants/literal_types"; import * as METADATA_KEY from "../constants/metadata_keys"; @@ -48,7 +48,7 @@ function getTargets( ); // Target instances that represent properties to be injected - const propertyTargets = getClassPropsAsTargets(metadataReader, func); + const propertyTargets = getClassPropsAsTargets(metadataReader, func, constructorName); const targets = [ ...constructorTargets, @@ -130,11 +130,22 @@ function getConstructorArgsAsTargets( return targets; } -function getClassPropsAsTargets(metadataReader: interfaces.MetadataReader, constructorFunc: Function) { +function _getServiceIdentifierForProperty(inject:any,multiInject:any,propertyName:string | symbol, className: string):any { + const serviceIdentifier = (inject || multiInject); + if(serviceIdentifier === undefined) { + const msg = `${ERROR_MSGS.MISSING_INJECTABLE_ANNOTATION} for property ${String(propertyName)} in class ${className}.`; + throw new Error(msg); + } + return serviceIdentifier; +} + +function getClassPropsAsTargets(metadataReader: interfaces.MetadataReader, constructorFunc: Function, constructorName:string) { - const classPropsMetadata = metadataReader.getPropertiesMetadata(constructorFunc); + const classPropsMetadata:any = metadataReader.getPropertiesMetadata(constructorFunc); let targets: interfaces.Target[] = []; - const keys = Object.keys(classPropsMetadata); + const symbolKeys = Object.getOwnPropertySymbols(classPropsMetadata); + const stringKeys:(string | symbol)[] = Object.keys(classPropsMetadata); + const keys:(string | symbol)[] = stringKeys.concat(symbolKeys); for (const key of keys) { @@ -144,14 +155,13 @@ function getClassPropsAsTargets(metadataReader: interfaces.MetadataReader, const // the metadata formatted for easier access const metadata = formatTargetMetadata(classPropsMetadata[key]); - // the name of the property being injected - const targetName = metadata.targetName || key; + const identifier = metadata.targetName || key; // Take types to be injected from user-generated metadata - const serviceIdentifier = (metadata.inject || metadata.multiInject); + const serviceIdentifier = _getServiceIdentifierForProperty(metadata.inject,metadata.multiInject,key,constructorName); // The property target - const target = new Target(TargetTypeEnum.ClassProperty, targetName, serviceIdentifier); + const target = new Target(TargetTypeEnum.ClassProperty, identifier, serviceIdentifier); target.metadata = targetMetadata; targets.push(target); } @@ -161,7 +171,7 @@ function getClassPropsAsTargets(metadataReader: interfaces.MetadataReader, const if (baseConstructor !== Object) { - const baseTargets = getClassPropsAsTargets(metadataReader, baseConstructor); + const baseTargets = getClassPropsAsTargets(metadataReader, baseConstructor,constructorName); targets = [ ...targets, diff --git a/src/planning/target.ts b/src/planning/target.ts index bd9d90b1f..39e1fbb04 100644 --- a/src/planning/target.ts +++ b/src/planning/target.ts @@ -1,6 +1,7 @@ import * as METADATA_KEY from "../constants/metadata_keys"; import { interfaces } from "../interfaces/interfaces"; import { id } from "../utils/id"; +import { getSymbolDescription } from "../utils/serialization"; import { Metadata } from "./metadata"; import { QueryableString } from "./queryable_string"; @@ -10,11 +11,13 @@ class Target implements interfaces.Target { public type: interfaces.TargetType; public serviceIdentifier: interfaces.ServiceIdentifier; public name: interfaces.QueryableString; + public identifier: string | symbol; + public key:string | symbol public metadata: Metadata[]; public constructor( type: interfaces.TargetType, - name: string, + identifier: string | symbol, serviceIdentifier: interfaces.ServiceIdentifier, namedOrTagged?: (string | Metadata) ) { @@ -22,7 +25,9 @@ class Target implements interfaces.Target { this.id = id(); this.type = type; this.serviceIdentifier = serviceIdentifier; - this.name = new QueryableString(name || ""); + const queryableName = typeof identifier === 'symbol' ? getSymbolDescription(identifier): identifier; + this.name = new QueryableString(queryableName || ""); + this.identifier = identifier; this.metadata = new Array(); let metadataItem: interfaces.Metadata | null = null; diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 8610a2043..639069bd1 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -66,9 +66,9 @@ function createInstanceWithInjections( ): T { const instance = new args.constr(...args.constructorInjections); args.propertyRequests.forEach((r: interfaces.Request, index: number) => { - const propertyName = r.target.name.value(); + const property = r.target.identifier; const injection = args.propertyInjections[index]; - (instance as Record)[propertyName] = injection; + (instance as any)[property] = injection; }); return instance } diff --git a/src/utils/js.ts b/src/utils/js.ts new file mode 100644 index 000000000..75a03c22c --- /dev/null +++ b/src/utils/js.ts @@ -0,0 +1,11 @@ +export function getFirstArrayDuplicate(array:T[]):T | undefined { + const seenValues = new Set() + + for (const entry of array) { + if (seenValues.has(entry)) { + return entry; + } else { + seenValues.add(entry); + } + } +} diff --git a/src/utils/serialization.ts b/src/utils/serialization.ts index 206c9f349..de51db0d5 100644 --- a/src/utils/serialization.ts +++ b/src/utils/serialization.ts @@ -134,10 +134,15 @@ function getFunctionName(v: any): string { } } +function getSymbolDescription(symbol:Symbol) { + return symbol.toString().slice(7,-1); +} + export { getFunctionName, getServiceIdentifierAsString, listRegisteredBindingsForServiceIdentifier, listMetadataForTarget, - circularDependencyToException + circularDependencyToException, + getSymbolDescription }; diff --git a/test/annotation/decorator_utils.test.ts b/test/annotation/decorator_utils.test.ts new file mode 100644 index 000000000..9d74cc71a --- /dev/null +++ b/test/annotation/decorator_utils.test.ts @@ -0,0 +1,99 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { createTaggedDecorator, tagParameter, tagProperty } from "../../src/annotation/decorator_utils" +import * as ERROR_MSGS from "../../src/constants/error_msgs"; +import { Container, inject, injectable } from "../../src/inversify"; +describe("createTaggedDecorator", () => { + let sandbox:sinon.SinonSandbox + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it("should pass to tagParameter for parameter decorators", () => { + class Target {} + const metadata = {key:"1",value:"2"}; + const decorator = createTaggedDecorator(metadata); + const spiedTagParameter = sandbox.spy(tagParameter); + decorator(Target,undefined as any,1); + expect(spiedTagParameter.calledWithExactly(Target, undefined as any, 1, metadata)); + }); + + it("should pass to tagProperty for property decorators", () => { + class Target {} + const metadata = {key:"2",value:"2"}; + const decorator = createTaggedDecorator(metadata); + const spiedTagProperty = sandbox.spy(tagProperty); + decorator(Target.prototype,"PropertyName"); + expect(spiedTagProperty.calledWithExactly(Target, "PropertyName", metadata)); + }); + + it("should enable constraining to multiple metadata with a single decorator", () => { + function multipleMetadataDecorator(key1Value:string, key2Value: string) { + return createTaggedDecorator([{key:"key1",value:key1Value},{key:"key2",value:key2Value}]); + } + + interface Thing{ + type:string + } + + @injectable() + class Thing1 implements Thing{ + type = "Thing1" + } + + @injectable() + class Root { + public thingyType:string; + @multipleMetadataDecorator("Key1Value","Key2Value") + @inject("Thing") + set thingy(thingy:Thing) { + this.thingyType = thingy.type + } + } + + const container = new Container(); + container.bind("Thing").to(Thing1).when(request => { + const metadatas = request.target.metadata; + const key1Metadata = metadatas[1]; + const key2Metadata = metadatas[2]; + return key1Metadata.value === "Key1Value" && key2Metadata.value === "Key2Value"; + }); + container.resolve(Root); + }); + +}); + +describe("tagParameter", () => { + it("should throw if multiple metadata with same key", () => { + class Target {} + expect( + () => tagParameter(Target,undefined as any, 1, [{key:"Duplicate",value:"1"},{key:"Duplicate",value:"2"}]) + ).to.throw(`${ERROR_MSGS.DUPLICATED_METADATA} Duplicate`); + }); +}); + +describe("tagProperty", () => { + it("should throw if multiple metadata with same key", () => { + class Target {} + expect( + () => tagProperty(Target.prototype,"Property", [{key:"Duplicate",value:"1"},{key:"Duplicate",value:"2"}]) + ).to.throw(`${ERROR_MSGS.DUPLICATED_METADATA} Duplicate`); + }); + + it("should throw for static properties", () => { + class Target {} + + // does not throw + tagProperty(Target.prototype,"Property", {key:"key",value:"value"}) + + expect( + () => tagProperty(Target,"StaticProperty", {key:"key",value:"value"}) + ).to.throw(ERROR_MSGS.INVALID_DECORATOR_OPERATION); + + }); + +}); diff --git a/test/annotation/inject.test.ts b/test/annotation/inject.test.ts index 2f1f4f304..2ca491c90 100644 --- a/test/annotation/inject.test.ts +++ b/test/annotation/inject.test.ts @@ -3,10 +3,12 @@ declare function __param(paramIndex: number, decorator: ParameterDecorator): Cla import { expect } from "chai"; import { decorate } from "../../src/annotation/decorator_utils"; -import { inject, LazyServiceIdentifer } from "../../src/annotation/inject"; +import { inject } from "../../src/annotation/inject"; +import { LazyServiceIdentifer } from "../../src/annotation/lazy_service_identifier"; import * as ERROR_MSGS from "../../src/constants/error_msgs"; import * as METADATA_KEY from "../../src/constants/metadata_keys"; import { interfaces } from "../../src/interfaces/interfaces"; +import { multiInject } from "../../src/inversify"; interface Katana {} interface Shuriken {} @@ -113,7 +115,7 @@ describe("@inject", () => { }); - it("Should throw when not applayed to a constructor", () => { + it("Should throw when not applied to a constructor", () => { const useDecoratorOnMethodThatIsNotAConstructor = function() { __decorate([ __param(0, inject("Katana")) ], @@ -175,4 +177,23 @@ describe("@inject", () => { }); + it("should throw when applied inject decorator with undefined service identifier to a property", () => { + expect(() => { + //@ts-ignore + class WithUndefinedInject{ + @inject(undefined as any) + property:string + } + }).to.throw(`${ERROR_MSGS.UNDEFINED_INJECT_ANNOTATION("WithUndefinedInject")}`) + }); + + it("should throw when applied multiInject decorator with undefined service identifier to a constructor parameter", () => { + expect(() => { + //@ts-ignore + class WithUndefinedInject{ + constructor(@multiInject(undefined as any) readonly dependency:string[]){} + } + }).to.throw(`${ERROR_MSGS.UNDEFINED_INJECT_ANNOTATION("WithUndefinedInject")}`) + }); + }); diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 2afb4e54f..b3d836bda 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -1,5 +1,6 @@ import { assert, expect } from "chai"; import * as sinon from "sinon"; +import { inject } from "../../src/annotation/inject"; import { injectable } from "../../src/annotation/injectable"; import { postConstruct } from "../../src/annotation/post_construct"; import * as ERROR_MSGS from "../../src/constants/error_msgs"; @@ -1088,6 +1089,42 @@ describe("Container", () => { ).to.throw(ERROR_MSGS.CONTAINER_OPTIONS_INVALID_SKIP_BASE_CHECK); }); + it("Should be able to inject when symbol property key ", () => { + const weaponProperty = Symbol(); + interface Weapon {} + @injectable() + class Shuriken implements Weapon { } + @injectable() + class Ninja{ + @inject("Weapon") + [weaponProperty]: Weapon + } + const container = new Container(); + container.bind("Weapon").to(Shuriken); + const myNinja = container.resolve(Ninja); + const weapon = myNinja[weaponProperty]; + expect(weapon).to.be.instanceOf(Shuriken); + }); + + it("Should be possible to constrain to a symbol description", () => { + const throwableWeapon = Symbol("throwable"); + interface Weapon {} + @injectable() + class Shuriken implements Weapon { } + @injectable() + class Ninja{ + @inject("Weapon") + [throwableWeapon]: Weapon + } + const container = new Container(); + container.bind("Weapon").to(Shuriken).when(request => { + return request.target.name.equals("throwable"); + }) + const myNinja = container.resolve(Ninja); + const weapon = myNinja[throwableWeapon]; + expect(weapon).to.be.instanceOf(Shuriken); + }); + it("container resolve should come from the same container", () => { @injectable() class CompositionRoot{} @@ -1114,4 +1151,5 @@ describe("Container", () => { // tslint:disable-next-line: no-unused-expression expect(() => myContainer.resolve(CompositionRoot)).not.to.throw; }) + }); diff --git a/test/inversify.test.ts b/test/inversify.test.ts index c28fdb891..24bd296af 100644 --- a/test/inversify.test.ts +++ b/test/inversify.test.ts @@ -72,7 +72,7 @@ describe("InversifyJS", () => { }); - it("Should be able to to do setter injection and property injection", () => { + it("Should be able to do setter injection and property injection", () => { @injectable() class Shuriken { public throw() { @@ -110,14 +110,22 @@ describe("InversifyJS", () => { expect(ninja.sneak()).to.eql("hit!"); expect(ninja.fight()).to.eql("cut!"); }); + it("Should be able to resolve and inject dependencies in VanillaJS", () => { const TYPES = { Katana: "Katana", Ninja: "Ninja", - Shuriken: "Shuriken" + Shuriken: "Shuriken", + Blowgun: "Blowgun" }; + class Blowgun { + public blow() { + return "poison!"; + } + } + class Katana { public hit() { return "cut!"; @@ -134,6 +142,7 @@ describe("InversifyJS", () => { public _katana: Katana; public _shuriken: Shuriken; + public _blowgun: Blowgun; public constructor(katana: Katana, shuriken: Shuriken) { this._katana = katana; @@ -141,23 +150,32 @@ describe("InversifyJS", () => { } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } + public poisonDart() { return this._blowgun.blow();} + + public set blowgun(blowgun:Blowgun) { + this._blowgun = blowgun; + } } decorate(injectable(), Katana); decorate(injectable(), Shuriken); decorate(injectable(), Ninja); + decorate(injectable(), Blowgun); decorate(inject(TYPES.Katana), Ninja, 0); decorate(inject(TYPES.Shuriken), Ninja, 1); + decorate(inject(TYPES.Blowgun), Ninja.prototype, "blowgun"); const container = new Container(); container.bind(TYPES.Ninja).to(Ninja); container.bind(TYPES.Katana).to(Katana); container.bind(TYPES.Shuriken).to(Shuriken); + container.bind(TYPES.Blowgun).to(Blowgun); const ninja = container.get(TYPES.Ninja); expect(ninja.fight()).eql("cut!"); expect(ninja.sneak()).eql("hit!"); + expect(ninja.poisonDart()).eql("poison!"); }); diff --git a/test/planning/planner.test.ts b/test/planning/planner.test.ts index 21adb0d32..d8e60898a 100644 --- a/test/planning/planner.test.ts +++ b/test/planning/planner.test.ts @@ -9,6 +9,7 @@ import * as ERROR_MSGS from "../../src/constants/error_msgs"; import { TargetTypeEnum } from "../../src/constants/literal_types"; import { Container } from "../../src/container/container"; import { interfaces } from "../../src/interfaces/interfaces"; +import { named } from "../../src/inversify"; import { MetadataReader } from "../../src/planning/metadata_reader"; import { plan } from "../../src/planning/planner"; @@ -465,7 +466,7 @@ describe("Planner", () => { }); - it("Should be throw when a class has a missing @injectable annotation", () => { + it("Should throw when a class has a missing @injectable annotation", () => { interface Weapon { } @@ -482,6 +483,30 @@ describe("Planner", () => { }); + it("Should throw when apply a metadata decorator without @inject or @multiInject", () => { + @injectable() + class Ninja { + @named("name") + // tslint:disable-next-line: no-empty + set weapon(weapon : Weapon){ + + } + } + interface Weapon { } + + class Katana implements Weapon { } + + const container = new Container(); + container.bind("Weapon").to(Katana); + container.bind(Ninja).toSelf(); + + const throwFunction = () => { + plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, Ninja); + }; + + expect(throwFunction).to.throw(`${ERROR_MSGS.MISSING_INJECTABLE_ANNOTATION} for property weapon in class Ninja.`); + }); + it("Should ignore checking base classes for @injectable when skipBaseClassChecks is set on the container", () => { class Test { } diff --git a/test/utils/serialization.test.ts b/test/utils/serialization.test.ts index 18179085b..e7de8865f 100644 --- a/test/utils/serialization.test.ts +++ b/test/utils/serialization.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { TargetTypeEnum } from "../../src/constants/literal_types"; import { Target } from "../../src/planning/target"; -import { getFunctionName, listMetadataForTarget } from "../../src/utils/serialization"; +import { getFunctionName, getSymbolDescription, listMetadataForTarget } from "../../src/utils/serialization"; describe("Serialization", () => { @@ -32,4 +32,12 @@ describe("Serialization", () => { expect(list).to.eql(` ${serviceIdentifier}`); }); + it("Should extract symbol description", () => { + const symbolWithDescription = Symbol("description"); + expect(getSymbolDescription(symbolWithDescription)).to.equal("description"); + + const symbolWithoutDescription = Symbol(); + expect(getSymbolDescription(symbolWithoutDescription)).to.equal(""); + }); + }); diff --git a/wiki/custom_tag_decorators.md b/wiki/custom_tag_decorators.md index bdfd69e76..e3a1e7cb4 100644 --- a/wiki/custom_tag_decorators.md +++ b/wiki/custom_tag_decorators.md @@ -19,3 +19,23 @@ class Ninja implements Ninja { } } ``` + +If you need to create a reusable decorator for multiple tags: + +```ts +function moodReason(mood:string, reason: string) { + return createTaggedDecorator([{key:"mood",value:mood},{key:"reason",value:reason}]); +} +const happyAndIKnowIt = moodReason("happy","I know it"); +const dontLikeMondays = moodReason("miserable","I don't like Mondays"); + +@injectable() +class MoodyNinja { + public constructor( + @inject("Response") @happyAndIKnowIt clapHands: Response, + @inject("Response") @dontLikeMondays shootWholeDayDown: Response + ) { + //.... + } +} +```