diff --git a/README.md b/README.md index 08e9799..cfda549 100644 --- a/README.md +++ b/README.md @@ -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(DI.DEP1), + dep2: container.get(DI.DEP2) + }); +}); + +// You can register a factory so myService will be injected +container.bind(DI.MY_USE_CASE).toFactory(() => { + return MyUseCase({ + myService: container.get(DI.MY_SERVICE) + }); +}); +``` + +### Resolve the dependencies + +```typescript +import { DI } from './di'; + +// Call the container to resolve the dependencies +const myUseCase = container.get(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. \ No newline at end of file diff --git a/package.json b/package.json index baeb00a..d111d96 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/specs/Container.spec.ts b/specs/Container.spec.ts new file mode 100644 index 0000000..a2514f3 --- /dev/null +++ b/specs/Container.spec.ts @@ -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(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(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }); + + // Act + const myService = container.get(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(DI.DEP1), + dep2: container.get(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(DI.MY_SERVICE), + logger: container.get(DI.LOGGER), + sayHello: container.get(DI.HELLO_WORLD) + }); + }); + + // Act + const myUseCase = container.get(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'); + }); + }); + }); +}); \ No newline at end of file diff --git a/specs/DI.ts b/specs/DI.ts new file mode 100644 index 0000000..741d8c9 --- /dev/null +++ b/specs/DI.ts @@ -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'), +} \ No newline at end of file diff --git a/specs/LoggerInterface.ts b/specs/LoggerInterface.ts new file mode 100644 index 0000000..527ecf9 --- /dev/null +++ b/specs/LoggerInterface.ts @@ -0,0 +1,3 @@ +export interface LoggerInterface { + log: (message: string) => void; +} \ No newline at end of file diff --git a/specs/MyService.ts b/specs/MyService.ts new file mode 100644 index 0000000..74aae26 --- /dev/null +++ b/specs/MyService.ts @@ -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}`; + } +}); \ No newline at end of file diff --git a/specs/MyServiceInterface.ts b/specs/MyServiceInterface.ts new file mode 100644 index 0000000..2e556ef --- /dev/null +++ b/specs/MyServiceInterface.ts @@ -0,0 +1,3 @@ +export interface MyServiceInterface { + runTask: () => string; +} \ No newline at end of file diff --git a/specs/MyUseCase.ts b/specs/MyUseCase.ts new file mode 100644 index 0000000..a6482f2 --- /dev/null +++ b/specs/MyUseCase.ts @@ -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; + } + }; +} \ No newline at end of file diff --git a/specs/MyUseCaseInterface.ts b/specs/MyUseCaseInterface.ts new file mode 100644 index 0000000..49974ec --- /dev/null +++ b/specs/MyUseCaseInterface.ts @@ -0,0 +1,3 @@ +export interface MyUseCaseInterface { + execute: () => string; +} \ No newline at end of file diff --git a/specs/SayHelloType.ts b/specs/SayHelloType.ts new file mode 100644 index 0000000..7755a38 --- /dev/null +++ b/specs/SayHelloType.ts @@ -0,0 +1 @@ +export type SayHelloType = () => string; \ No newline at end of file diff --git a/specs/hello.spec.ts b/specs/hello.spec.ts deleted file mode 100644 index fa303a5..0000000 --- a/specs/hello.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {hello} from "../src/hello"; - -describe('[Hello World]', () => { - it('should work', () => { - expect(hello()).toBe('world'); - }); -}); \ No newline at end of file diff --git a/specs/sayHelloWorld.ts b/specs/sayHelloWorld.ts new file mode 100644 index 0000000..71c40a7 --- /dev/null +++ b/specs/sayHelloWorld.ts @@ -0,0 +1,3 @@ +import {SayHelloType} from "./SayHelloType"; + +export const sayHelloWorld: SayHelloType = () => 'hello world'; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts deleted file mode 100644 index efa8f17..0000000 --- a/src/app.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {hello} from "./hello"; - -console.log(hello()); \ No newline at end of file diff --git a/src/container.ts b/src/container.ts new file mode 100644 index 0000000..df1e21a --- /dev/null +++ b/src/container.ts @@ -0,0 +1,32 @@ +export interface Container { + bind(key: symbol): { + toValue: (value: any) => void; + toFunction: (fn: CallableFunction) => void; + toFactory: (factory: CallableFunction) => void; + }; + + get(key: symbol): T; +} + +export function createContainer(): Container { + const functionsOrValues = new Map(); + const factories = new Map(); + + 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(key: symbol): T { + if (factories.has(key)) { + const factory = factories.get(key)!; + return factory(); + } + return functionsOrValues.get(key); + } + + return {bind, get}; +} \ No newline at end of file diff --git a/src/hello.ts b/src/hello.ts deleted file mode 100644 index 1569f81..0000000 --- a/src/hello.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const hello = () => { - return 'world'; -}; \ No newline at end of file