Skip to content

Commit

Permalink
Add inject method
Browse files Browse the repository at this point in the history
  • Loading branch information
betula committed Jun 24, 2019
1 parent 5dbdace commit f2a8986
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 30 deletions.
107 changes: 107 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,110 @@
[![Build Status](https://travis-ci.org/betula/node-provide.svg?branch=master)](https://travis-ci.org/betula/node-provide)
[![Coverage Status](https://coveralls.io/repos/github/betula/node-provide/badge.svg?branch=master)](https://coveralls.io/github/betula/node-provide?branch=master)

Библиотека для внедрения зависимостей. Позволяет легко поставлять инстанцированные в единственном экземпляре зависимости в любую точку важего приложения без необходимости предварительного проектирования архитектуры приложения под использование привычных механизмов внедрения зависимостей.

Каждая зависимостей определяется как класс без параметров в конструкторе, либо функция без аргументов. Наиболее удобным является использование декоратора `provide` в TypeScript, но так же поддерживает приятный синтаксис и для JavaScript как с использованием декораторов, так и без них, в случае использования "чистой" Node.JS без каких-либо препроцессоров кода. В качестве идентификатора для зависимости используется класс или функция её определяющая.

Пример использования в TypeScript
```TypeScript
import { provide } from "node-provide";

class Server {
public configure(port: number = 80, hostname?: string) {
//...
}
public route(pattern: string, callback: (req: Request, res: Response) => void) {
//...
}
public start() {
//...
}
}

class IndexController {
// Использование декоратора `provide`,
// теперь инстанция класса Server доступна через `this.server`.
@provide server: Server;
public mount() {
this.server.route("/", this.index.bind(this));
}
public index(req: Request, res: Response) {
res.send("index");
}
}

class App {
@provide server: Server;
@provide indexController: IndexController;
public start() {
this.server.configure();
this.indexController.mount();
this.server.start();
}
}
```

Не забудьте включить опцию `emitDecoratorMetadata` в вашем `tsconfig.json` файле. Так же будет необходимо отключить опцию `strictPropertyInitialization`, так как TypeScript на данный момент не умеет вычислять преобразование поля класса в геттер через декоратор и считает такие поля не инициализированными и будет ошибочно "требовать" их инициализации в конструкторе.

На JavaScript с использованием декораторов использование `provide` будет выглядеть так:

```JavaScript
import { provide } from "node-provide";

class Server {
start() {
//...
}
}

class App {
// Здесь используется декоратор `provide`,
// где первым аргументом передан класс зависимости.
@provide(Server) server;
start() {
this.server.start();
}
}
```

Так же можно описать зависимость через функцию возвращающую объект с набором методов для работы с ней. Тут декораторы уже не потребуется, что значит можно использовать "чистый" Node.JS без каких-либо преобразований.

```JavaScript
// app.js
const { container } = require("node-provide");
const Server = require("./server");
const IndexController = require("./index-controller");

const services = container({
server: Server,
indexController: IndexController,
});

module.exports = function() {
//...
return {
start() {
services.server.configure();
services.indexController.mount();
}
}
}

// Если вам не нужно определять `App` как зависимость,
// то можете создать его инстанцию явно.
// index.js
const App = require("./app");
new App().start();

// Если же вы хотите, что бы `App` был полноценной зависимостью,
// доступной в любой точке приложения,
// то можете инстанцировать его через `resolve`.
// index.js
const { resolve } = require("node-provide");
const App = require("./app");
resolve(App).start();
```

Есть ещё несколько вариантов поставить зависимости, отличающиеся друг от друга синтаксически.


9 changes: 9 additions & 0 deletions examples/api-server-with-jest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ npm i
npm run start
```

For easy jest integration you need add some instruction to your `jest.config.js` file:

```JavaScript
module.exports = {
setupFilesAfterEnv: [
"node-provide/jest-cleanup-after-each"
]
}
```
9 changes: 0 additions & 9 deletions examples/api-server-with-jest/README.ru.md

This file was deleted.

Empty file removed examples/modules/README.ru.md
Empty file.
Empty file.
53 changes: 51 additions & 2 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
bind,
isolate,
assign,
inject,
reset,
getZoneId,
RootZoneId,
Expand Down Expand Up @@ -164,6 +165,17 @@ test("Should work resolve with multiple dependencies", () => {
expect(a).toBeInstanceOf(A);
expect(f).toBe(10);
expect(j).toBe(J);
expect(resolve()).toBeUndefined();
});

test("should work resolve with plain values", () => {
const d = new Date();
const c = {};
expect(resolve(null)).toBe(null);
expect(resolve("hello")).toBe("hello");
expect(resolve(10)).toBe(10);
expect(resolve(d)).toBe(d);
expect(resolve(c)).toBe(c);
});

test("Should work bind", () => {
Expand All @@ -184,13 +196,13 @@ test("Should work bind", () => {
expect(spy).toBeCalled();
});

test("Should work provide as class decorator", () => {
test("Should work inject with dependecies for class", () => {
const spyF = jest.fn().mockReturnValue({ v: 11 });
const F = () => spyF();
class A {}
const spyM = jest.fn();
const spyC = jest.fn();
const dec = provide({ a: A }, container({ f: F }));
const dec = inject({ a: A }, container({ f: F }));
expect(typeof dec).toBe("function");
class M {
f: any;
Expand All @@ -217,6 +229,43 @@ test("Should work provide as class decorator", () => {
expect(spyF).toBeCalledTimes(1);
});

test("Should work inject with dependencies for plain objects", () => {
class A { a = "a"; }
class B { b = "b"; }
const dec = inject({ a: A }, container({ b: B }));
const c = dec({
c: 10,
getA(this: any) {
return this.a.a;
},
});
expect(c.a.a).toBe("a");
expect(c.b.b).toBe("b");
expect(c.c).toBe(10);
expect(c.getA()).toBe("a");
});

test("Should work inject as decorator without parameters", () => {
class A { a = "a"; }
@inject()
class B {
constructor(public a: A) {}
}
@inject
class C {
constructor(public a: A, public b: B) {}
}
@inject
class Z {}
const b = resolve(B);
const c = resolve(C);
expect(b.a).toBeInstanceOf(A);
expect(c.a).toBe(b.a);
expect(c.b).toBe(b);
expect(c.b.a.a).toBe("a");
expect(new Z).toBeInstanceOf(Z);
});

test("Should work assign", () => {
class A {}
class B {}
Expand Down
75 changes: 56 additions & 19 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ type ObjectMap<T = any> = {
[key: string]: T;
};

type Dep<T = any> = (new () => T) | (() => T) | T;
type ClassType<T = any, K extends any[] = any> = new (...args: K) => T;
type Dep<T = any> = ClassType<T> | (() => T) | T;
enum DepResolvePhase {
Start,
Finish,
Expand Down Expand Up @@ -119,35 +120,64 @@ export function container(...configs: any[]) {
}

type PropertyKey = string | symbol;
type ClassType<T, K extends any[]> = new (...args: K) => T;
type ProvideClassDecRetFn<T> = <P, M extends any[]>(Class: ClassType<P, M>) => ClassType<P & T, M>;

export function provide(target: object, propertyKey: PropertyKey): any;
export function provide(dep: Dep): (target: object, propertyKey: PropertyKey) => any;
export function provide<T0 extends Deps>(...configs: [DepsConfig<T0>]): ProvideClassDecRetFn<T0>;
export function provide<T0 extends Deps, T1 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>]): ProvideClassDecRetFn<T0 & T1>;
export function provide<T0 extends Deps, T1 extends Deps, T2 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>]): ProvideClassDecRetFn<T0 & T1 & T2>;
export function provide<T0 extends Deps, T1 extends Deps, T2 extends Deps, T3 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>, DepsConfig<T3>]): ProvideClassDecRetFn<T0 & T1 & T2 & T3>;
export function provide<T0 extends Deps, T1 extends Deps, T2 extends Deps, T3 extends Deps, T4 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>, DepsConfig<T3>, DepsConfig<T4>, ...MoreDepsConfig[]]): ProvideClassDecRetFn<T0 & T1 & T2 & T3 & T4>;
export function provide(targetOrDepOrConfigs: any, propertyKey?: any): any {
if (typeof targetOrDepOrConfigs === "function") {
const dep = targetOrDepOrConfigs;
export function provide(targetOrDep: any, propertyKey?: any): any {
if (typeof propertyKey === "undefined") {
const dep: Dep = targetOrDep;
return (target: object, propertyKey: PropertyKey): any => (
createProvideDescriptor(dep as Dep, propertyKey)
createProvideDescriptor(dep, propertyKey)
);
}
if (typeof propertyKey === "undefined" || typeof propertyKey === "object") {
return (Class: any) => {
(attach as any)(Class.prototype, ...Array.prototype.slice.call(arguments));
return Class;
};
}
return createProvideDescriptor(
Reflect.getMetadata("design:type", targetOrDepOrConfigs, propertyKey!),
Reflect.getMetadata("design:type", targetOrDep, propertyKey!),
propertyKey!,
);
}

type InjectDecRetFn<T> = <P>(target: P) =>
P extends ClassType<infer M, infer K>
? ClassType<M & T, K>
: P & T;

export function inject(): <T extends any>(target: ClassType<T>) => ClassType<T, []>;
export function inject<T extends ClassType>(Class: T): T extends ClassType<infer U> ? ClassType<U, []> : never;
export function inject<T0 extends Deps>(...configs: [DepsConfig<T0>]): InjectDecRetFn<T0>;
export function inject<T0 extends Deps, T1 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>]): InjectDecRetFn<T0 & T1>;
export function inject<T0 extends Deps, T1 extends Deps, T2 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>]): InjectDecRetFn<T0 & T1 & T2>;
export function inject<T0 extends Deps, T1 extends Deps, T2 extends Deps, T3 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>, DepsConfig<T3>]): InjectDecRetFn<T0 & T1 & T2 & T3>;
export function inject<T0 extends Deps, T1 extends Deps, T2 extends Deps, T3 extends Deps, T4 extends Deps>(...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>, DepsConfig<T3>, DepsConfig<T4>, ...MoreDepsConfig[]]): InjectDecRetFn<T0 & T1 & T2 & T3 & T4>;
export function inject(...configsOrClass: any[]) {
if (configsOrClass.length === 0) {
return (Class: any) => {
const types = Reflect.getMetadata("design:paramtypes", Class);
if (!types || types.length === 0) {
return Class;
}
if (types.length === 1) {
return class extends Class {
constructor() {
super(resolve(types[0]));
}
};
}
return class extends Class {
constructor() {
super(...(resolve as any)(...types));
}
};
};
}
if (configsOrClass.length === 1 && typeof configsOrClass[0] === "function") {
return inject()(configsOrClass[0]);
}
return (target: any) => {
(attach as any)(target.prototype || target, ...configsOrClass);
return target;
};
}

export function attach<Y extends object, T0 extends Deps>(target: Y, ...configs: [DepsConfig<T0>]): Y & T0;
export function attach<Y extends object, T0 extends Deps, T1 extends Deps>(target: Y, ...configs: [DepsConfig<T0>, DepsConfig<T1>]): Y & T0 & T1;
export function attach<Y extends object, T0 extends Deps, T1 extends Deps, T2 extends Deps>(target: Y, ...configs: [DepsConfig<T0>, DepsConfig<T1>, DepsConfig<T2>]): Y & T0 & T1 & T2;
Expand Down Expand Up @@ -191,6 +221,7 @@ export function bind(...configs: any[]) {
};
}

export function resolve(): void;
export function resolve<T0>(...deps: [Dep<T0>]): T0;
export function resolve<T0, T1>(...deps: [Dep<T0>, Dep<T1>]): [T0, T1];
export function resolve<T0, T1, T2>(...deps: [Dep<T0>, Dep<T1>, Dep<T2>]): [T0, T1, T2];
Expand All @@ -202,11 +233,17 @@ export function resolve<T0, T1, T2, T3, T4, T5, T6, T7>(...deps: [Dep<T0>, Dep<T
export function resolve<T0, T1, T2, T3, T4, T5, T6, T7, T8>(...deps: [Dep<T0>, Dep<T1>, Dep<T2>, Dep<T3>, Dep<T4>, Dep<T5>, Dep<T6>, Dep<T7>, Dep<T8>]): [T0, T1, T2, T3, T4, T5, T6, T7, T8];
export function resolve<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(...deps: [Dep<T0>, Dep<T1>, Dep<T2>, Dep<T3>, Dep<T4>, Dep<T5>, Dep<T6>, Dep<T7>, Dep<T8>, Dep<T9>, ...Dep[]]): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9];
export function resolve(...deps: any[]) {
if (deps.length === 0) {
return;
}
if (deps.length > 1) {
return deps.map((dep) => resolve(dep));
}
let instance;
const dep = deps[0];
if (!dep) {
return dep;
}
instance = getInstance(dep);
if (!instance) {
const OverrideDep = getOverride(dep);
Expand Down

0 comments on commit f2a8986

Please sign in to comment.