Skip to content

Commit

Permalink
feat: add basic implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Evyweb committed Sep 5, 2024
1 parent efa897f commit 4dc8c91
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 16 deletions.
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,66 @@
# Project name
# Draft of a simple IOC container in Typescript

## Introduction
This is just a draft for an attempt to create a simple IOC (Inversion of Control) container in Typescript. The idea is to create a simple container that can be used to register and resolve dependencies working with functions (no class) and without reflect metadata.

## How to use

### List the dependencies
Create a symbol for each dependency you want to register. It will be used to identify the dependency.

```typescript
export const DI = {
HELLO_WORLD: Symbol('HELLO_WORLD'),
DEP1: Symbol('dep1'),
DEP2: Symbol('dep2'),
MY_SERVICE: Symbol('MyService'),
MY_USE_CASE: Symbol('MyUseCase'),
LOGGER: Symbol('LOGGER'),
};
```

### Register the dependencies

```typescript
import { DI } from './di';

const container: Container = createContainer();

// You can register primitives
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);

// You can register functions without dependencies
container.bind(DI.HELLO_WORLD).toFunction(sayHelloWorld);

// You can register a factory so dep1 and dep2 will be injected
container.bind(DI.MY_SERVICE).toFactory(() => {
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
});

// You can register a factory so myService will be injected
container.bind(DI.MY_USE_CASE).toFactory(() => {
return MyUseCase({
myService: container.get<MyService>(DI.MY_SERVICE)
});
});
```

### Resolve the dependencies

```typescript
import { DI } from './di';

// Call the container to resolve the dependencies
const myUseCase = container.get<MyUseCaseInterface>(DI.MY_USE_CASE);

myUseCase.execute();
```

Code used in the examples can be found in the specs folder.

This is just a draft and it is not ready for production.
Can be improved in many ways.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"dist/"
],
"scripts": {
"build": "tsup src/app.ts --format cjs,esm --dts",
"build": "tsup src/container.ts --format cjs,esm --dts",
"lint": "tsc --noEmit",
"start": "npm run build && node dist/app.js",
"start": "npm run build && node dist/Container.js",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
Expand Down
88 changes: 88 additions & 0 deletions specs/Container.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {MyService} from "./MyService";
import {MyServiceInterface} from "./MyServiceInterface";
import {SayHelloType} from "./SayHelloType";
import {sayHelloWorld} from "./sayHelloWorld";
import {MyUseCase} from "./MyUseCase";
import {MyUseCaseInterface} from "./MyUseCaseInterface";
import {LoggerInterface} from "./LoggerInterface";
import {DI} from "./DI";
import {Container, createContainer} from "../src/container";

describe('Container', () => {

let container: Container;

beforeEach(() => {
container = createContainer();
});

describe('When the function is registered using a symbol', () => {
it('should return the associated function', () => {
// Arrange
container.bind(DI.HELLO_WORLD).toFunction(sayHelloWorld);

// Act
const sayHello = container.get<SayHelloType>(DI.HELLO_WORLD);

// Assert
expect(sayHello()).toBe('hello world');
});

it('should resolve all its dependencies using the factory', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);

container.bind(DI.MY_SERVICE).toFactory(() => {
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
});

// Act
const myService = container.get<MyServiceInterface>(DI.MY_SERVICE);

// Assert
expect(myService.runTask()).toBe('Executing with dep1: dependency1 and dep2: 42');
});

describe('When the dependency has dependencies', () => {
it('should resolve all its dependencies using the factory', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.HELLO_WORLD).toFunction(sayHelloWorld);

container.bind(DI.MY_SERVICE).toFactory(() => {
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
});

const fakeLogger: LoggerInterface = {
log: vi.fn()
}
container.bind(DI.LOGGER).toValue(fakeLogger);

container.bind(DI.MY_USE_CASE).toFactory(() => {
return MyUseCase({
myService: container.get<MyServiceInterface>(DI.MY_SERVICE),
logger: container.get<LoggerInterface>(DI.LOGGER),
sayHello: container.get<SayHelloType>(DI.HELLO_WORLD)
});
});

// Act
const myUseCase = container.get<MyUseCaseInterface>(DI.MY_USE_CASE);

// Assert
expect(myUseCase.execute()).toBe('Executing with dep1: dependency1 and dep2: 42');
expect(fakeLogger.log).toHaveBeenCalledTimes(2);
expect(fakeLogger.log).toHaveBeenCalledWith('Executing with dep1: dependency1 and dep2: 42');
expect(fakeLogger.log).toHaveBeenCalledWith('hello world');
});
});
});
});
8 changes: 8 additions & 0 deletions specs/DI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const DI = {
HELLO_WORLD: Symbol('HELLO_WORLD'),
DEP1: Symbol('dep1'),
DEP2: Symbol('dep2'),
MY_SERVICE: Symbol('MyService'),
MY_USE_CASE: Symbol('MyUseCase'),
LOGGER: Symbol('LOGGER'),
}
3 changes: 3 additions & 0 deletions specs/LoggerInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface LoggerInterface {
log: (message: string) => void;
}
12 changes: 12 additions & 0 deletions specs/MyService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {MyServiceInterface} from "./MyServiceInterface";

interface Dependencies {
dep1: string,
dep2: number
}

export const MyService = ({ dep1, dep2 }: Dependencies): MyServiceInterface => ({
runTask() {
return `Executing with dep1: ${dep1} and dep2: ${dep2}`;
}
});
3 changes: 3 additions & 0 deletions specs/MyServiceInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface MyServiceInterface {
runTask: () => string;
}
21 changes: 21 additions & 0 deletions specs/MyUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {MyServiceInterface} from "./MyServiceInterface";
import {MyUseCaseInterface} from "./MyUseCaseInterface";
import {LoggerInterface} from "./LoggerInterface";
import {SayHelloType} from "./SayHelloType";

interface Dependencies {
myService: MyServiceInterface,
logger: LoggerInterface,
sayHello: SayHelloType
}

export function MyUseCase({myService, logger, sayHello}: Dependencies): MyUseCaseInterface {
return {
execute() {
const message = myService.runTask();
logger.log(message);
logger.log(sayHello());
return message;
}
};
}
3 changes: 3 additions & 0 deletions specs/MyUseCaseInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface MyUseCaseInterface {
execute: () => string;
}
1 change: 1 addition & 0 deletions specs/SayHelloType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type SayHelloType = () => string;
7 changes: 0 additions & 7 deletions specs/hello.spec.ts

This file was deleted.

3 changes: 3 additions & 0 deletions specs/sayHelloWorld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {SayHelloType} from "./SayHelloType";

export const sayHelloWorld: SayHelloType = () => 'hello world';
3 changes: 0 additions & 3 deletions src/app.ts

This file was deleted.

32 changes: 32 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export interface Container {
bind(key: symbol): {
toValue: (value: any) => void;
toFunction: (fn: CallableFunction) => void;
toFactory: (factory: CallableFunction) => void;
};

get<T>(key: symbol): T;
}

export function createContainer(): Container {
const functionsOrValues = new Map<symbol, any>();
const factories = new Map<symbol, CallableFunction>();

function bind(key: symbol) {
return {
toValue: (value: any) => functionsOrValues.set(key, value),
toFunction: (fn: CallableFunction) => functionsOrValues.set(key, fn),
toFactory: (factory: CallableFunction) => factories.set(key, factory)
};
}

function get<T>(key: symbol): T {
if (factories.has(key)) {
const factory = factories.get(key)!;
return factory();
}
return functionsOrValues.get(key);
}

return {bind, get};
}
3 changes: 0 additions & 3 deletions src/hello.ts

This file was deleted.

0 comments on commit 4dc8c91

Please sign in to comment.