Skip to content

Commit

Permalink
Support for metadata middleware (#507)
Browse files Browse the repository at this point in the history
* WIP support for metadata middleware

* Added unit tests and docs
  • Loading branch information
remojansen authored Mar 21, 2017
1 parent 96230ab commit d60a053
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 65 deletions.
2 changes: 0 additions & 2 deletions src/constants/error_msgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,3 @@ export const CONTAINER_OPTIONS_MUST_BE_AN_OBJECT = "Invalid Container constructo

export const CONTAINER_OPTIONS_INVALID_DEFAULT_SCOPE = "Invalid Container option. Default scope must " +
"be a string ('singleton' or 'transient').";

export const INVALID_BINDING_PROPERTY = "TODO";
8 changes: 8 additions & 0 deletions src/container/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { guid } from "../utils/guid";
import * as ERROR_MSGS from "../constants/error_msgs";
import * as METADATA_KEY from "../constants/metadata_keys";
import { BindingScopeEnum, TargetTypeEnum } from "../constants/literal_types";
import { MetadataReader } from "../planning/metadata_reader";

class Container implements interfaces.Container {

Expand All @@ -19,6 +20,7 @@ class Container implements interfaces.Container {
private _middleware: interfaces.Next | null;
private _bindingDictionary: interfaces.Lookup<interfaces.Binding<any>>;
private _snapshots: Array<interfaces.ContainerSnapshot>;
private _metadataReader: interfaces.MetadataReader;

public static merge(container1: interfaces.Container, container2: interfaces.Container): interfaces.Container {

Expand Down Expand Up @@ -77,6 +79,7 @@ class Container implements interfaces.Container {
this._snapshots = [];
this._middleware = null;
this.parent = null;
this._metadataReader = new MetadataReader();
}

public load(...modules: interfaces.ContainerModule[]): void {
Expand Down Expand Up @@ -218,6 +221,10 @@ class Container implements interfaces.Container {
}, initial);
}

public applyCustomMetadataReader(metadataReader: interfaces.MetadataReader) {
this._metadataReader = metadataReader;
}

// Resolves a dependency by its runtime identifier
// The runtime identifier must be associated with only one binding
// use getAll when the runtime identifier is associated with multiple bindings
Expand Down Expand Up @@ -291,6 +298,7 @@ class Container implements interfaces.Container {

// create a plan
let context = plan(
this._metadataReader,
this,
args.isMultiInject,
args.targetType,
Expand Down
23 changes: 18 additions & 5 deletions src/container/lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ class Lookup<T extends interfaces.Clonable<T>> implements interfaces.Lookup<T> {
// adds a new entry to _map
public add(serviceIdentifier: interfaces.ServiceIdentifier<any>, value: T): void {

if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); };
if (value === null || value === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); };
if (serviceIdentifier === null || serviceIdentifier === undefined) {
throw new Error(ERROR_MSGS.NULL_ARGUMENT);
};

if (value === null || value === undefined) {
throw new Error(ERROR_MSGS.NULL_ARGUMENT);
};

let entry = this._map.get(serviceIdentifier);
if (entry !== undefined) {
Expand All @@ -32,7 +37,9 @@ class Lookup<T extends interfaces.Clonable<T>> implements interfaces.Lookup<T> {
// gets the value of a entry by its key (serviceIdentifier)
public get(serviceIdentifier: interfaces.ServiceIdentifier<any>): T[] {

if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); }
if (serviceIdentifier === null || serviceIdentifier === undefined) {
throw new Error(ERROR_MSGS.NULL_ARGUMENT);
}

let entry = this._map.get(serviceIdentifier);

Expand All @@ -46,7 +53,9 @@ class Lookup<T extends interfaces.Clonable<T>> implements interfaces.Lookup<T> {
// removes a entry from _map by its key (serviceIdentifier)
public remove(serviceIdentifier: interfaces.ServiceIdentifier<any>): void {

if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); }
if (serviceIdentifier === null || serviceIdentifier === undefined) {
throw new Error(ERROR_MSGS.NULL_ARGUMENT);
}

if (!this._map.delete(serviceIdentifier)) {
throw new Error(ERROR_MSGS.KEY_NOT_FOUND);
Expand All @@ -67,7 +76,11 @@ class Lookup<T extends interfaces.Clonable<T>> implements interfaces.Lookup<T> {

// returns true if _map contains a key (serviceIdentifier)
public hasKey(serviceIdentifier: interfaces.ServiceIdentifier<any>): boolean {
if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); }

if (serviceIdentifier === null || serviceIdentifier === undefined) {
throw new Error(ERROR_MSGS.NULL_ARGUMENT);
}

return this._map.has(serviceIdentifier);
}

Expand Down
15 changes: 15 additions & 0 deletions src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ namespace interfaces {
getAll<T>(serviceIdentifier: ServiceIdentifier<T>): T[];
load(...modules: ContainerModule[]): void;
unload(...modules: ContainerModule[]): void;
applyCustomMetadataReader(metadataReader: MetadataReader): void;
applyMiddleware(...middleware: Middleware[]): void;
snapshot(): void;
restore(): void;
Expand Down Expand Up @@ -280,6 +281,20 @@ namespace interfaces {
(request: Request | null): boolean;
}

export interface MetadataReader {
getConstrucotorMetadata(constructorFunc: Function): ConstructorMetadata;
getPropertiesMetadata(constructorFunc: Function): MetadataMap;
}

export interface MetadataMap {
[propertyNameOrArgumentIndex: string]: Metadata[];
}

export interface ConstructorMetadata {
compilerGeneratedMetadata: Function[]|undefined;
userGeneratedMetadata: MetadataMap;
}

}

export { interfaces };
29 changes: 29 additions & 0 deletions src/planning/metadata_reader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { interfaces } from "../interfaces/interfaces";
import * as METADATA_KEY from "../constants/metadata_keys";

class MetadataReader implements interfaces.MetadataReader {

public getConstrucotorMetadata(constructorFunc: Function): interfaces.ConstructorMetadata {

// TypeScript compiler generated annotations
let compilerGeneratedMetadata = Reflect.getMetadata(METADATA_KEY.PARAM_TYPES, constructorFunc);

// User generated constructor annotations
let userGeneratedMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED, constructorFunc);

return {
compilerGeneratedMetadata: compilerGeneratedMetadata,
userGeneratedMetadata: userGeneratedMetadata || {}
};

}

public getPropertiesMetadata(constructorFunc: Function): interfaces.MetadataMap {
// User generated properties annotations
let userGeneratedMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED_PROP, constructorFunc) || [];
return userGeneratedMetadata;
}

}

export { MetadataReader };
8 changes: 5 additions & 3 deletions src/planning/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ function _validateActiveBindingCount(
}

function _createSubRequests(
metadataReader: interfaces.MetadataReader,
avoidConstraints: boolean,
serviceIdentifier: interfaces.ServiceIdentifier<any>,
context: interfaces.Context,
Expand Down Expand Up @@ -165,10 +166,10 @@ function _createSubRequests(

if (binding.type === BindingTypeEnum.Instance && binding.implementationType !== null) {

let dependencies = getDependencies(binding.implementationType);
let dependencies = getDependencies(metadataReader, binding.implementationType);

dependencies.forEach((dependency: interfaces.Target) => {
_createSubRequests(false, dependency.serviceIdentifier, context, subChildRequest, dependency);
_createSubRequests(metadataReader, false, dependency.serviceIdentifier, context, subChildRequest, dependency);
});

}
Expand Down Expand Up @@ -207,6 +208,7 @@ function getBindings<T>(
}

function plan(
metadataReader: interfaces.MetadataReader,
container: interfaces.Container,
isMultiInject: boolean,
targetType: interfaces.TargetType,
Expand All @@ -218,7 +220,7 @@ function plan(

let context = new Context(container);
let target = _createTarget(isMultiInject, targetType, serviceIdentifier, "", key, value);
_createSubRequests(avoidConstraints, serviceIdentifier, context, null, target);
_createSubRequests(metadataReader, avoidConstraints, serviceIdentifier, context, null, target);
return context;

}
Expand Down
34 changes: 20 additions & 14 deletions src/planning/reflection_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ import * as ERROR_MSGS from "../constants/error_msgs";
import * as METADATA_KEY from "../constants/metadata_keys";
import { TargetTypeEnum } from "../constants/literal_types";

function getDependencies(func: Function): interfaces.Target[] {
function getDependencies(
metadataReader: interfaces.MetadataReader, func: Function
): interfaces.Target[] {
let constructorName = getFunctionName(func);
let targets: interfaces.Target[] = getTargets(constructorName, func, false);
let targets: interfaces.Target[] = getTargets(metadataReader, constructorName, func, false);
return targets;
}

function getTargets(constructorName: string, func: Function, isBaseClass: boolean): interfaces.Target[] {
function getTargets(
metadataReader: interfaces.MetadataReader, constructorName: string, func: Function, isBaseClass: boolean
): interfaces.Target[] {

let metadata = metadataReader.getConstrucotorMetadata(func);

// TypeScript compiler generated annotations
let serviceIdentifiers = Reflect.getMetadata(METADATA_KEY.PARAM_TYPES, func);
let serviceIdentifiers = metadata.compilerGeneratedMetadata;

// All types resolved must be annotated with @injectable
if (serviceIdentifiers === undefined) {
Expand All @@ -23,7 +29,7 @@ function getTargets(constructorName: string, func: Function, isBaseClass: boolea
}

// User generated annotations
let constructorArgsMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED, func) || [];
let constructorArgsMetadata = metadata.userGeneratedMetadata;

let keys = Object.keys(constructorArgsMetadata);
let hasUserDeclaredUnknownInjections = (func.length === 0 && keys.length > 0);
Expand All @@ -39,7 +45,7 @@ function getTargets(constructorName: string, func: Function, isBaseClass: boolea
);

// Target instances that represent properties to be injected
let propertyTargets = getClassPropsAsTargets(func);
let propertyTargets = getClassPropsAsTargets(metadataReader, func);

let targets = [
...constructorTargets,
Expand All @@ -49,7 +55,7 @@ function getTargets(constructorName: string, func: Function, isBaseClass: boolea
// Throw if a derived class does not implement its constructor explicitly
// We do this to prevent errors when a base class (parent) has dependencies
// and one of the derived classes (children) has no dependencies
let baseClassDepencencyCount = getBaseClassDepencencyCount(func);
let baseClassDepencencyCount = getBaseClassDepencencyCount(metadataReader, func);

if (targets.length < baseClassDepencencyCount) {
let error = ERROR_MSGS.ARGUMENTS_LENGTH_MISMATCH_1 +
Expand Down Expand Up @@ -127,9 +133,9 @@ function getConstructorArgsAsTargets(
return targets;
}

function getClassPropsAsTargets(func: Function) {
function getClassPropsAsTargets(metadataReader: interfaces.MetadataReader, constructorFunc: Function) {

let classPropsMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED_PROP, func) || [];
let classPropsMetadata = metadataReader.getPropertiesMetadata(constructorFunc);
let targets: interfaces.Target[] = [];
let keys = Object.keys(classPropsMetadata);

Expand Down Expand Up @@ -157,11 +163,11 @@ function getClassPropsAsTargets(func: Function) {
}

// Check if base class has injected properties
let baseConstructor = Object.getPrototypeOf(func.prototype).constructor;
let baseConstructor = Object.getPrototypeOf(constructorFunc.prototype).constructor;

if (baseConstructor !== Object) {

let baseTargets = getClassPropsAsTargets(baseConstructor);
let baseTargets = getClassPropsAsTargets(metadataReader, baseConstructor);

targets = [
...targets,
Expand All @@ -173,15 +179,15 @@ function getClassPropsAsTargets(func: Function) {
return targets;
}

function getBaseClassDepencencyCount(func: Function): number {
function getBaseClassDepencencyCount(metadataReader: interfaces.MetadataReader, func: Function): number {

let baseConstructor = Object.getPrototypeOf(func.prototype).constructor;

if (baseConstructor !== Object) {

// get targets for base class
let baseConstructorName = getFunctionName(func);
let targets = getTargets(baseConstructorName, baseConstructor, true);
let targets = getTargets(metadataReader, baseConstructorName, baseConstructor, true);

// get unmanaged metadata
let metadata: any[] = targets.map((t: interfaces.Target) => {
Expand All @@ -198,7 +204,7 @@ function getBaseClassDepencencyCount(func: Function): number {
if (dependencyCount > 0 ) {
return dependencyCount;
} else {
return getBaseClassDepencencyCount(baseConstructor);
return getBaseClassDepencencyCount(metadataReader, baseConstructor);
}

} else {
Expand Down
3 changes: 2 additions & 1 deletion test/bugs/bugs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getFunctionName } from "../../src/utils/serialization";
import * as ERROR_MSGS from "../../src/constants/error_msgs";
import * as METADATA_KEY from "../../src/constants/metadata_keys";
import { getDependencies } from "../../src/planning/reflection_utils";
import { MetadataReader } from "../../src/planning/metadata_reader";
import {
Container,
injectable,
Expand Down Expand Up @@ -552,7 +553,7 @@ describe("Bugs", () => {
expect(serviceIdentifiers["0"][0].value.toString()).to.be.eql("Symbol(BAR)");

// is the plan correct?
let dependencies = getDependencies(Foo);
let dependencies = getDependencies(new MetadataReader(), Foo);
expect(dependencies.length).to.be.eql(1);
expect(dependencies[0].serviceIdentifier.toString()).to.be.eql("Symbol(BAR)");

Expand Down
Loading

0 comments on commit d60a053

Please sign in to comment.