Skip to content

Commit

Permalink
refactor: boyscout rules
Browse files Browse the repository at this point in the history
  • Loading branch information
Evyweb committed Nov 10, 2024
1 parent e0b73f5 commit 6994bc2
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 112 deletions.
251 changes: 164 additions & 87 deletions specs/scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import {Container, createContainer, Scope} from "../src";
import {DI} from "./examples/DI";
import {MyService} from "./examples/MyService";
import {MyServiceInterface} from "./examples/MyServiceInterface";
import {vi} from "vitest";
import {sayHelloWorld} from "./examples/sayHelloWorld";
import {SayHelloType} from "./examples/SayHelloType";
import {Mock, vi} from "vitest";
import {MyServiceClass} from "./examples/MyServiceClass";
import {MyServiceClassInterface} from "./examples/MyServiceClassInterface";

describe('Scope', () => {

let container: Container;
let factoryCalls = vi.fn();
let factoryCalls: Mock;

beforeEach(() => {
container = createContainer();
Expand All @@ -18,109 +18,186 @@ describe('Scope', () => {
factoryCalls = vi.fn();
});

describe.each([
{scope: undefined},
{scope: 'singleton'},
])('When the scope is default or defined to "singleton"', ({scope}) => {
it('should return the same instance', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
describe('Factories', () => {

describe.each([
{scope: undefined},
{scope: 'singleton'},
])('When the scope is default or defined to "singleton"', ({scope}) => {
it('should return the same instance', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, scope as Scope);

const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);

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

// Assert
expect(myService1).toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(1);
});
});

describe('When the scope is defined to "transient"', () => {
it('should return a new instance each time', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, 'transient');

const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);

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

// Assert
expect(myService1).not.toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(2);
});
});

describe('When the scope is defined to "scoped"', () => {
it('should return the same instance within the same scope', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, 'scoped');

let myService1: MyServiceInterface | undefined;
let myService2: MyServiceInterface | undefined;

// Act
container.runInScope(() => {
myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);
myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);
});
}, scope as Scope);

const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);
// Assert
expect(myService1).toBeDefined();
expect(myService2).toBeDefined();
expect(myService1).toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(1);
});

it('should return different instances in different scopes', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, 'scoped');

let myService1: MyServiceInterface | undefined;
let myService2: MyServiceInterface | undefined;

container.runInScope(() => {
myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);
});

// Act
const myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);
// Act
container.runInScope(() => {
myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);
});

// Assert
expect(myService1).toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(1);
// Assert
expect(myService1).toBeDefined();
expect(myService2).toBeDefined();
expect(myService1).not.toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(2);
});
});
});

describe('When the scope is defined to "transient"', () => {
it('should return a new instance each time', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, 'transient');
describe('Classes', () => {

const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);
describe.each([
{scope: undefined},
{scope: 'singleton'},
])('When the scope is default or defined to "singleton"', ({scope}) => {
it('should return the same instance', () => {
// Arrange
container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], scope as Scope);

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

// Assert
expect(myService1).not.toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(2);
// Act
const myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);

// Assert
expect(myService1).toBe(myService2);
});
});
});

describe('When the scope is defined to "scoped"', () => {
it('should return the same instance within the same scope', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, 'scoped');
describe('When the scope is defined to "transient"', () => {
it('should return a new instance each time', () => {
// Arrange
container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'transient');

let myService1: MyServiceInterface | undefined;
let myService2: MyServiceInterface | undefined;
const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);

// Act
container.runInScope(() => {
myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);
myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);
});
// Act
const myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);

// Assert
expect(myService1).toBeDefined();
expect(myService2).toBeDefined();
expect(myService1).toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(1);
// Assert
expect(myService1).not.toBe(myService2);
});
});
});

it('should return different instances in different scopes', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
}, 'scoped');
describe('Higher order functions', () => {

let myService1: MyServiceInterface | undefined;
let myService2: MyServiceInterface | undefined;
describe.each([
{scope: undefined},
{scope: 'singleton'},
])('When the scope is default or defined to "singleton"', ({scope}) => {
it('should return the same instance', () => {
// Arrange
container.bind(DI.MY_SERVICE)
.toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, scope as Scope);

container.runInScope(() => {
myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);
});
const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);

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

// Act
container.runInScope(() => {
myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);
// Assert
expect(myService1).toBe(myService2);
});
});

describe('When the scope is defined to "transient"', () => {
it('should return a new instance each time', () => {
// Arrange
container.bind(DI.MY_SERVICE)
.toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'transient');

// Assert
expect(myService1).toBeDefined();
expect(myService2).toBeDefined();
expect(myService1).not.toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(2);
const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);

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

// Assert
expect(myService1).not.toBe(myService2);
});
});
});

Expand All @@ -139,10 +216,10 @@ describe('Scope', () => {
describe('When an unknown scope is used during binding', () => {
it('should throw an error', () => {
// Arrange
container.bind(DI.MY_SERVICE).toFunction(sayHelloWorld, 'unknown' as any);
container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [], 'unknown' as any);

// Act & Assert
expect(() => container.get<SayHelloType>(DI.MY_SERVICE))
expect(() => container.get<MyServiceClassInterface>(DI.MY_SERVICE))
.toThrowError('Unknown scope: unknown');
});
});
Expand Down
15 changes: 8 additions & 7 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {Binding, Container, Module} from "./types";
import {createModule} from "./module";
import { Binding, Container, Module } from "./types";
import { createModule } from "./module";

export function createContainer(): Container {
const modules = new Map<symbol, Module>();
const singletonInstances = new Map<symbol, unknown>();
const scopedInstances = new Map<symbol, Map<symbol, unknown>>();
let currentScopeId: symbol | undefined;

const DEFAULT_MODULE_KEY = Symbol("DEFAULT");
const defaultModule = createModule();
modules.set(Symbol('DEFAULT'), defaultModule);
modules.set(DEFAULT_MODULE_KEY, defaultModule);

const bind = (key: symbol) => defaultModule.bind(key);

Expand Down Expand Up @@ -37,18 +38,18 @@ export function createContainer(): Container {

const { factory, scope } = binding;

if (scope === 'singleton') {
if (scope === "singleton") {
if (!singletonInstances.has(key)) {
singletonInstances.set(key, factory(resolveDependency));
}
return singletonInstances.get(key) as T;
}

if (scope === 'transient') {
if (scope === "transient") {
return factory(resolveDependency) as T;
}

if (scope === 'scoped') {
if (scope === "scoped") {
if (!currentScopeId) throw new Error(`Cannot resolve scoped binding outside of a scope: ${key.toString()}`);

if (!scopedInstances.has(currentScopeId)) {
Expand All @@ -71,7 +72,7 @@ export function createContainer(): Container {

const runInScope = <T>(callback: () => T): T => {
const previousScopeId = currentScopeId;
currentScopeId = Symbol('scope');
currentScopeId = Symbol("scope");
try {
return callback();
} finally {
Expand Down
Loading

0 comments on commit 6994bc2

Please sign in to comment.