Skip to content

Commit

Permalink
feat(infra): di container
Browse files Browse the repository at this point in the history
  • Loading branch information
EYHN committed Jan 10, 2024
1 parent d58d0be commit 416233d
Show file tree
Hide file tree
Showing 12 changed files with 772 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/common/infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"./command": "./src/command/index.ts",
"./atom": "./src/atom/index.ts",
"./app-config-storage": "./src/app-config-storage.ts",
"./di": "./src/di/index.ts",
"./livedata": "./src/livedata/index.ts",
".": "./src/index.ts"
},
Expand Down
287 changes: 287 additions & 0 deletions packages/common/infra/src/di/__tests__/di.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { describe, expect, test } from 'vitest';

import {
CircularDependencyError,
declareFactory,
declareService,
MissingDependencyError,
RecursionLimitError,
ServiceCollection,
ServiceNotFoundError,
} from '../';

describe('di', () => {
test('basic', () => {
const serviceCollection = new ServiceCollection();
serviceCollection.add(
declareService('test-service', { value: { a: 'b' } })
);

const provider = serviceCollection.provider();
expect(provider.resolve('test-service')).toEqual({ a: 'b' });
});

test('factory', () => {
const serviceCollection = new ServiceCollection();
serviceCollection.add(declareService('test', { value: { a: 'b' } }));

const provider = serviceCollection.provider();
expect(provider.resolve('test')).toEqual({ a: 'b' });
});

test('class type', () => {
class TestClass {
constructor(public readonly foo: string) {}
}

{
const serviceCollection = new ServiceCollection();
serviceCollection.add(
declareService(TestClass, { value: new TestClass('bar') })
);

const provider = serviceCollection.provider();
expect(provider.resolve(TestClass).foo).toEqual('bar');
}
});

test('dependency', () => {
const serviceCollection = new ServiceCollection();

// a depends on b
serviceCollection.add(
declareService('serviceA', {
factory: declareFactory(
['serviceB'],
b =>
new (class ServiceA {
b = b;
})()
),
})
);

// b depends on c
serviceCollection.add(
declareService('serviceB', {
factory: declareFactory(
['serviceC'],
c =>
new (class ServiceB {
c = c;
})()
),
})
);

serviceCollection.add(
declareService('serviceC', {
value: new (class ServiceC {
value = 'i am c';
})(),
})
);

const provider = serviceCollection.provider();

expect(provider.resolve<any>('serviceA').b.c.value).toEqual('i am c');
expect(provider.resolve<any>('serviceA').b.c.constructor.name).toEqual(
'ServiceC'
);
});

test('service not found', () => {
const serviceCollection = new ServiceCollection();

const provider = serviceCollection.provider();
expect(() => provider.resolve('serviceA')).toThrowError(
ServiceNotFoundError
);
});

test('missing dependency', () => {
const serviceCollection = new ServiceCollection();

// a depends on b
serviceCollection.add(
declareService('serviceA', {
factory: declareFactory(
['serviceB'],
b =>
new (class ServiceA {
b = b;
})()
),
})
);

const provider = serviceCollection.provider();
expect(() => provider.resolve('serviceA')).toThrowError(
MissingDependencyError
);
});

test('circular dependency', () => {
const serviceCollection = new ServiceCollection();

// a depends on b
serviceCollection.add(
declareService('serviceA', {
factory: declareFactory(
['serviceB'],
b =>
new (class ServiceA {
b = b;
})()
),
})
);

// b depends on c
serviceCollection.add(
declareService('serviceB', {
factory: declareFactory(
['serviceC'],
c =>
new (class ServiceB {
c = c;
})()
),
})
);

// c depends on a
serviceCollection.add(
declareService('serviceC', {
factory: declareFactory(
['serviceA'],
a =>
new (class ServiceC {
a = a;
})()
),
})
);

const provider = serviceCollection.provider();
expect(() => provider.resolve('serviceA')).toThrowError(
CircularDependencyError
);
expect(() => provider.resolve('serviceB')).toThrowError(
CircularDependencyError
);
expect(() => provider.resolve('serviceC')).toThrowError(
CircularDependencyError
);
});

test('recursion limit', () => {
// maxmium resolve depth is 100
const serviceCollection = new ServiceCollection();
let i = 0;
for (; i < 100; i++) {
const next = i + 1;
serviceCollection.add(
declareService('test', {
variant: i.toString(),
factory: declareFactory([], provider =>
provider.resolve('test', next.toString())
),
})
);
}
serviceCollection.add(
declareService('test', {
variant: i.toString(),
factory: declareFactory([], () => ({ a: 'b' })),
})
);
const provider = serviceCollection.provider();
expect(() => provider.resolve('test', '0')).toThrowError(
RecursionLimitError
);
});

test('variant', () => {
const serviceCollection = new ServiceCollection();
serviceCollection.add(
declareService('test-service', {
variant: 'typeA',
value: { a: 'i am A' },
})
);
serviceCollection.add(
declareService('test-service', {
variant: 'typeB',
value: { b: 'i am B' },
})
);

const provider = serviceCollection.provider();
expect(provider.resolve('test-service', 'typeA')).toEqual({ a: 'i am A' });
expect(provider.resolve('test-service', 'typeB')).toEqual({ b: 'i am B' });
});

test('duplicate, override', () => {
const serviceCollection = new ServiceCollection();
serviceCollection.add(
declareService('test-service', {
value: { a: 'i am A' },
})
);
serviceCollection.add(
declareService('test-service', {
value: { b: 'i am B' },
})
);

const provider = serviceCollection.provider();
expect(provider.resolve('test-service')).toEqual({ b: 'i am B' });
});

test('scope', () => {
const root = new ServiceCollection();
root.add(declareService('root', { value: 'from root' }));

root.add(
declareService('layer1', {
scope: ['layer1'],
factory: declareFactory(['root'], root => {
return root + ' from layer1';
}),
})
);

root.add(
declareService('layer2', {
scope: ['layer1', 'layer2'],
factory: declareFactory(['layer1'], root => {
return root + ' from layer2';
}),
})
);

const rootProvider = root.provider();
expect(rootProvider.resolve('root')).toEqual('from root');
expect(() => rootProvider.resolve('layer1')).toThrowError(
ServiceNotFoundError
);
expect(() => rootProvider.resolve('layer2')).toThrowError(
ServiceNotFoundError
);

const layer1Provider = root.provider(['layer1'], rootProvider);
expect(layer1Provider.resolve('root')).toEqual('from root');
expect(layer1Provider.resolve('layer1')).toEqual('from root from layer1');
expect(() => layer1Provider.resolve('layer2')).toThrowError(
ServiceNotFoundError
);

const layer2Provider = root.provider(['layer1', 'layer2'], layer1Provider);
expect(layer2Provider.resolve('root')).toEqual('from root');
expect(layer2Provider.resolve('layer1')).toEqual('from root from layer1');
expect(layer2Provider.resolve('layer2')).toEqual(
'from root from layer1 from layer2'
);
});
});
Loading

0 comments on commit 416233d

Please sign in to comment.