Skip to content

Commit 37c3d26

Browse files
committed
Make services replacable
1 parent a338dd3 commit 37c3d26

5 files changed

Lines changed: 102 additions & 28 deletions

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ jobs:
99
- uses: actions/checkout@v2
1010
- name: Install modules
1111
run: npm install
12+
- name: Run typecheck
13+
run: npm run typecheck
1214
- name: Run coverage
1315
run: npm run test:cov

package-lock.json

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"name": "@timesplinter/pimple",
33
"license": "LGPL-3.0-or-later",
4-
"version": "2.0.0",
4+
"version": "2.1.0",
55
"module": "./lib/esm/index.js",
66
"main": "./lib/cjs/index.js",
77
"scripts": {
88
"test": "jest",
99
"test:cov": "jest --collect-coverage",
10+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig-cjs.json",
1011
"transpile": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
1112
"prepublish": "npm run transpile"
1213
},
@@ -17,10 +18,10 @@
1718
"@babel/core": "^7.23.9",
1819
"@babel/preset-env": "^7.23.9",
1920
"@babel/preset-typescript": "^7.23.3",
20-
"@types/jest": "^29.5.11",
21+
"@types/jest": "^29.5.14",
2122
"babel-jest": "^29.7.0",
2223
"jest": "^29.7.0",
23-
"typescript": "^4.9.5",
24-
"ts-node": "^10.9.2"
24+
"ts-node": "^10.9.2",
25+
"typescript": "^4.9.5"
2526
}
2627
}

src/pimple.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,18 @@ const reservedProperties: string[] = [
2828
* @copyright 2016 SerafimArts <nesk@xakep.ru>
2929
* @copyright 2021 TiMESPLiNTER <dev@timesplinter.ch>
3030
* @license LGPL
31-
* @version 3.0.0
31+
* @version 2.1.0
3232
*/
3333
export default class Pimple<T> implements Container<T>
3434
{
35-
/**
36-
* @type {string}
37-
*/
38-
static get VERSION() { return '3.0.0'; }
35+
static get VERSION() { return '2.1.0'; }
3936

40-
/**
41-
* @type {{}}
42-
* @private
43-
*/
4437
private _definitions: Partial<ServiceMap<T>> = {};
4538

46-
/**
47-
* @type {{}}
48-
* @private
49-
*/
5039
private _raw: Partial<ServiceMap<T>> = {};
5140

41+
private _resolved: Set<string | number | symbol> = new Set();
42+
5243
constructor(services: Partial<ServiceMap<T>> = {}) {
5344
Object.keys(services).forEach((service) => {
5445
const serviceKey = service as ServiceKey<T>;
@@ -57,9 +48,33 @@ export default class Pimple<T> implements Container<T>
5748
}
5849

5950
/**
60-
* Define a service
51+
* Define a service (first-time registration only)
6152
*/
6253
public set<K extends ServiceKey<T>>(name: K, service: ServiceDefinition<T,T[K]>): Pimple<T>
54+
{
55+
if (this.has(name)) {
56+
throw new Error(`Service "${name.toString()}" is already defined. Use replace() to overwrite it.`);
57+
}
58+
59+
return this.define(name, service);
60+
}
61+
62+
/**
63+
* Replace an existing service definition before it has been resolved
64+
*/
65+
public replace<K extends ServiceKey<T>>(name: K, service: ServiceDefinition<T,T[K]>): Pimple<T>
66+
{
67+
if (!this.has(name)) {
68+
throw new RangeError(`Service "${name.toString()}" is not defined in the container.`);
69+
}
70+
if (this._resolved.has(name)) {
71+
throw new Error(`Service "${name.toString()}" has already been resolved and cannot be replaced.`);
72+
}
73+
74+
return this.define(name, service);
75+
}
76+
77+
private define<K extends ServiceKey<T>>(name: K, service: ServiceDefinition<T,T[K]>): Pimple<T>
6378
{
6479
this._raw[name] = service;
6580

@@ -76,6 +91,7 @@ export default class Pimple<T> implements Container<T>
7691

7792
if (reservedProperties.indexOf(name.toString()) === -1) {
7893
Object.defineProperty(this, name, {
94+
configurable: true,
7995
get: () => {
8096
return this.get(name);
8197
}
@@ -94,6 +110,7 @@ export default class Pimple<T> implements Container<T>
94110

95111
if (reservedProperties.indexOf(name.toString()) === -1) {
96112
Object.defineProperty(this, name, {
113+
configurable: true,
97114
get: () => {
98115
return this.get(name);
99116
}
@@ -108,6 +125,7 @@ export default class Pimple<T> implements Container<T>
108125
*/
109126
public get<K extends ServiceKey<T>>(name: K): T[K] {
110127
if (this._definitions[name] instanceof Function) {
128+
this._resolved.add(name);
111129
return (this._definitions[name] as LazyServiceDefinition<T, T[K]>)(this);
112130
}
113131
return this._definitions[name] as T[K];

tests/pimple.spec.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import Pimple from '../src/pimple';
22
import {ServiceProvider} from "../src/index.js";
3+
import {describe, expect, it} from "@jest/globals";
34

45
describe('pimple container', () => {
56
it('returns container version', async () => {
6-
expect(Pimple.VERSION).toBe('3.0.0');
7+
expect(Pimple.VERSION).toBe('2.1.0');
78
});
89

910
it('stores values', async () => {
@@ -174,6 +175,57 @@ describe('pimple container', () => {
174175
}).toThrow('Definition with "foo" not defined in container.');
175176
});
176177

178+
it('throws when calling set on an already defined service', () => {
179+
type ServiceMap = {
180+
foo: string,
181+
}
182+
183+
const container = new Pimple<ServiceMap>();
184+
container.set('foo', () => 'first');
185+
186+
expect(() => {
187+
container.set('foo', () => 'second');
188+
}).toThrow('Service "foo" is already defined. Use replace() to overwrite it.');
189+
});
190+
191+
it('replaces a service before it is resolved', () => {
192+
type ServiceMap = {
193+
foo: string,
194+
}
195+
196+
const container = new Pimple<ServiceMap>();
197+
container.set('foo', () => 'original');
198+
container.replace('foo', () => 'replaced');
199+
200+
expect(container.get('foo')).toBe('replaced');
201+
});
202+
203+
it('throws when replacing a service that has already been resolved', () => {
204+
type ServiceMap = {
205+
foo: string,
206+
}
207+
208+
const container = new Pimple<ServiceMap>();
209+
container.set('foo', () => 'original');
210+
container.get('foo');
211+
212+
expect(() => {
213+
container.replace('foo', () => 'replaced');
214+
}).toThrow('Service "foo" has already been resolved and cannot be replaced.');
215+
});
216+
217+
it('throws when replacing a service that is not defined', () => {
218+
type ServiceMap = {
219+
foo: string,
220+
}
221+
222+
const container = new Pimple<ServiceMap>();
223+
224+
expect(() => {
225+
container.replace('foo', () => 'replaced');
226+
}).toThrow('Service "foo" is not defined in the container.');
227+
});
228+
177229
it('extends service', () => {
178230
type ServiceMap = {
179231
foo: string,

0 commit comments

Comments
 (0)